@the-portland-company/shell 0.18.1 → 0.19.1
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/dist/app-chrome.cjs +919 -0
- package/dist/app-chrome.cjs.map +1 -0
- package/dist/app-chrome.d.cts +71 -0
- package/dist/app-chrome.d.ts +71 -0
- package/dist/app-chrome.js +193 -0
- package/dist/app-chrome.js.map +1 -0
- package/dist/chunk-XLOQNVP7.js +1655 -0
- package/dist/chunk-XLOQNVP7.js.map +1 -0
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +2 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +3 -1653
- package/dist/native.js.map +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var fi = require('react-icons/fi');
|
|
6
|
+
var reactDom = require('react-dom');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var devnotes = require('@the-portland-company/devnotes');
|
|
9
|
+
|
|
10
|
+
function useTopBarIconSlotGlyph(slot) {
|
|
11
|
+
const [name, setName] = react.useState(null);
|
|
12
|
+
react.useEffect(() => {
|
|
13
|
+
if (typeof window === "undefined") return;
|
|
14
|
+
const read = () => {
|
|
15
|
+
const raw = window.getComputedStyle(document.documentElement).getPropertyValue(`--topbar-icon-${slot}`).trim().replace(/^['"]|['"]$/g, "");
|
|
16
|
+
setName(raw.length > 0 ? raw : null);
|
|
17
|
+
};
|
|
18
|
+
read();
|
|
19
|
+
const observer = new MutationObserver(read);
|
|
20
|
+
observer.observe(document.head, { childList: true, subtree: true });
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, [slot]);
|
|
23
|
+
return name;
|
|
24
|
+
}
|
|
25
|
+
var GLYPHS = {
|
|
26
|
+
// Communications Fuel reads as a LEVEL, so its glyph is a battery. `bolt`/
|
|
27
|
+
// `zap` stay mapped for older Brand values.
|
|
28
|
+
"battery-charging": fi.FiBatteryCharging,
|
|
29
|
+
battery: fi.FiBattery,
|
|
30
|
+
fuel: fi.FiBatteryCharging,
|
|
31
|
+
bolt: fi.FiZap,
|
|
32
|
+
zap: fi.FiZap,
|
|
33
|
+
grid: fi.FiGrid,
|
|
34
|
+
apps: fi.FiGrid,
|
|
35
|
+
bell: fi.FiBell,
|
|
36
|
+
notification: fi.FiBell,
|
|
37
|
+
"alert-triangle": fi.FiAlertTriangle,
|
|
38
|
+
alert: fi.FiAlertTriangle,
|
|
39
|
+
"alert-circle": fi.FiAlertCircle,
|
|
40
|
+
"life-buoy": fi.FiLifeBuoy,
|
|
41
|
+
"life-ring": fi.FiLifeBuoy,
|
|
42
|
+
support: fi.FiLifeBuoy,
|
|
43
|
+
"help-circle": fi.FiHelpCircle,
|
|
44
|
+
moon: fi.FiMoon,
|
|
45
|
+
sun: fi.FiSun,
|
|
46
|
+
info: fi.FiInfo,
|
|
47
|
+
mail: fi.FiMail,
|
|
48
|
+
"message-square": fi.FiMessageSquare,
|
|
49
|
+
settings: fi.FiSettings,
|
|
50
|
+
user: fi.FiUser
|
|
51
|
+
};
|
|
52
|
+
function resolveBrandGlyph(name) {
|
|
53
|
+
if (!name) return null;
|
|
54
|
+
return GLYPHS[name.trim().toLowerCase()] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/theme-preference.ts
|
|
58
|
+
var THEME_STORAGE_KEY = "politogy_theme";
|
|
59
|
+
var THEME_COOKIE = "politogy_theme";
|
|
60
|
+
var VALID = ["light", "dark", "system"];
|
|
61
|
+
function isThemePreference(v) {
|
|
62
|
+
return typeof v === "string" && VALID.includes(v);
|
|
63
|
+
}
|
|
64
|
+
function readThemeCookie() {
|
|
65
|
+
if (typeof document === "undefined") return null;
|
|
66
|
+
try {
|
|
67
|
+
const m = document.cookie.match(
|
|
68
|
+
new RegExp(`(?:^|;\\s*)${THEME_COOKIE}=([^;]+)`)
|
|
69
|
+
);
|
|
70
|
+
const v = m ? decodeURIComponent(m[1]) : null;
|
|
71
|
+
return isThemePreference(v) ? v : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function writeThemeCookie(pref) {
|
|
77
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
78
|
+
const host = window.location.hostname;
|
|
79
|
+
const onParentDomain = /(^|\.)politogyvrm\.com$/i.test(host);
|
|
80
|
+
const domainAttr = onParentDomain ? "; Domain=.politogyvrm.com" : "";
|
|
81
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
82
|
+
document.cookie = `${THEME_COOKIE}=${encodeURIComponent(
|
|
83
|
+
pref
|
|
84
|
+
)}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
|
|
85
|
+
}
|
|
86
|
+
function resolveTheme(pref) {
|
|
87
|
+
if (pref === "system") {
|
|
88
|
+
try {
|
|
89
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
90
|
+
} catch {
|
|
91
|
+
return "light";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return pref;
|
|
95
|
+
}
|
|
96
|
+
function applyThemePreference(pref) {
|
|
97
|
+
if (typeof document === "undefined") return;
|
|
98
|
+
const resolved = resolveTheme(pref);
|
|
99
|
+
const d = document.documentElement;
|
|
100
|
+
d.classList.toggle("dark", resolved === "dark");
|
|
101
|
+
try {
|
|
102
|
+
d.style.colorScheme = resolved;
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`(function(){try{
|
|
107
|
+
var KEY=${JSON.stringify(THEME_STORAGE_KEY)};
|
|
108
|
+
var VALID=["light","dark","system"];
|
|
109
|
+
function fromCookie(){try{var m=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE}=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;}}
|
|
110
|
+
var cookie=fromCookie();
|
|
111
|
+
var ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}
|
|
112
|
+
var pref=cookie||ls||"system";
|
|
113
|
+
if(VALID.indexOf(pref)===-1)pref="system";
|
|
114
|
+
if(cookie&&cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}
|
|
115
|
+
var resolved=pref;
|
|
116
|
+
if(pref==="system"){try{resolved=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";}catch(e){resolved="light";}}
|
|
117
|
+
var d=document.documentElement;
|
|
118
|
+
if(resolved==="dark"){d.classList.add("dark");}else{d.classList.remove("dark");}
|
|
119
|
+
try{d.style.colorScheme=resolved;}catch(e){}
|
|
120
|
+
}catch(e){}})();`;
|
|
121
|
+
function topBarIconButtonClass(tone = "default") {
|
|
122
|
+
const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--color-race-red)] hover:text-[color:var(--color-race-red)]" : "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)]";
|
|
123
|
+
return [
|
|
124
|
+
// `group/tbtip` powers the shared <TopBarTooltip> rendered inside the trigger.
|
|
125
|
+
"group/tbtip relative flex items-center justify-center rounded-full border border-[color:var(--line)] transition",
|
|
126
|
+
"h-[var(--topbar-icon-button-size,32px)] w-[var(--topbar-icon-button-size,32px)]",
|
|
127
|
+
toneClass
|
|
128
|
+
].join(" ");
|
|
129
|
+
}
|
|
130
|
+
function TopBarCountBadge({ count }) {
|
|
131
|
+
if (!(count > 0)) return null;
|
|
132
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--color-race-red)] px-1 text-[10px] text-white", children: count > 99 ? "99+" : count });
|
|
133
|
+
}
|
|
134
|
+
function TopBarTooltip({
|
|
135
|
+
title,
|
|
136
|
+
description
|
|
137
|
+
}) {
|
|
138
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
139
|
+
"span",
|
|
140
|
+
{
|
|
141
|
+
role: "tooltip",
|
|
142
|
+
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",
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
145
|
+
"span",
|
|
146
|
+
{
|
|
147
|
+
"aria-hidden": true,
|
|
148
|
+
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)]"
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-[color:var(--foreground)]", children: title }),
|
|
152
|
+
description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 text-[11px] leading-snug text-[color:var(--muted)]", children: description }) : null
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
function TopBarPanelArrow() {
|
|
158
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
159
|
+
"span",
|
|
160
|
+
{
|
|
161
|
+
"aria-hidden": true,
|
|
162
|
+
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)]"
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function TopBarWidgetSlot({
|
|
167
|
+
title,
|
|
168
|
+
description,
|
|
169
|
+
children
|
|
170
|
+
}) {
|
|
171
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "topbar-widget-slot group/tbtip relative inline-flex", children: [
|
|
172
|
+
children,
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title, description })
|
|
174
|
+
] });
|
|
175
|
+
}
|
|
176
|
+
var NavChromeContext = react.createContext(null);
|
|
177
|
+
function NavChromeProvider({
|
|
178
|
+
fetchJson,
|
|
179
|
+
children
|
|
180
|
+
}) {
|
|
181
|
+
const [me, setMe] = react.useState(null);
|
|
182
|
+
const [meLoaded, setMeLoaded] = react.useState(false);
|
|
183
|
+
react.useEffect(() => {
|
|
184
|
+
let cancelled = false;
|
|
185
|
+
fetchJson("me").then((data) => {
|
|
186
|
+
if (!cancelled) setMe(data);
|
|
187
|
+
}).catch(() => {
|
|
188
|
+
}).finally(() => {
|
|
189
|
+
if (!cancelled) setMeLoaded(true);
|
|
190
|
+
});
|
|
191
|
+
return () => {
|
|
192
|
+
cancelled = true;
|
|
193
|
+
};
|
|
194
|
+
}, [fetchJson]);
|
|
195
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, me, meLoaded }, children });
|
|
196
|
+
}
|
|
197
|
+
function useNavChrome(component) {
|
|
198
|
+
const ctx = react.useContext(NavChromeContext);
|
|
199
|
+
if (!ctx) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${component} must be rendered inside <NavChromeProvider> (inject the chrome API fetcher there).`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return ctx;
|
|
205
|
+
}
|
|
206
|
+
function useChromeOrgs() {
|
|
207
|
+
const { fetchJson } = useNavChrome("useChromeOrgs");
|
|
208
|
+
const [orgs, setOrgs] = react.useState(null);
|
|
209
|
+
const [loading, setLoading] = react.useState(true);
|
|
210
|
+
const [error, setError] = react.useState(false);
|
|
211
|
+
react.useEffect(() => {
|
|
212
|
+
let cancelled = false;
|
|
213
|
+
setLoading(true);
|
|
214
|
+
setError(false);
|
|
215
|
+
fetchJson("orgs").then((data) => {
|
|
216
|
+
if (cancelled) return;
|
|
217
|
+
const list = data?.orgs ?? [];
|
|
218
|
+
setOrgs(list);
|
|
219
|
+
}).catch(() => {
|
|
220
|
+
if (!cancelled) setError(true);
|
|
221
|
+
}).finally(() => {
|
|
222
|
+
if (!cancelled) setLoading(false);
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
cancelled = true;
|
|
226
|
+
};
|
|
227
|
+
}, [fetchJson]);
|
|
228
|
+
return { orgs, loading, error };
|
|
229
|
+
}
|
|
230
|
+
function ValueSkeleton({ className }) {
|
|
231
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
232
|
+
"span",
|
|
233
|
+
{
|
|
234
|
+
"aria-hidden": true,
|
|
235
|
+
className: [
|
|
236
|
+
"inline-block animate-pulse rounded bg-[color:var(--panel-elevated)]",
|
|
237
|
+
className ?? "h-4 w-16"
|
|
238
|
+
].join(" ")
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
function SlotGlyph({
|
|
243
|
+
slot,
|
|
244
|
+
fallback: Fallback,
|
|
245
|
+
size = 20
|
|
246
|
+
}) {
|
|
247
|
+
const glyphName = useTopBarIconSlotGlyph(slot ?? "apps");
|
|
248
|
+
const BrandGlyph = slot ? resolveBrandGlyph(glyphName) : null;
|
|
249
|
+
const Glyph = BrandGlyph ?? Fallback;
|
|
250
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Glyph, { size, className: "text-current" });
|
|
251
|
+
}
|
|
252
|
+
function TopBarPopover({
|
|
253
|
+
label,
|
|
254
|
+
description,
|
|
255
|
+
slot,
|
|
256
|
+
fallbackIcon,
|
|
257
|
+
badge,
|
|
258
|
+
tone,
|
|
259
|
+
onOpen,
|
|
260
|
+
children,
|
|
261
|
+
panelClassName
|
|
262
|
+
}) {
|
|
263
|
+
const [open, setOpen] = react.useState(false);
|
|
264
|
+
const rootRef = react.useRef(null);
|
|
265
|
+
react.useEffect(() => {
|
|
266
|
+
if (!open) return;
|
|
267
|
+
const onDocClick = (e) => {
|
|
268
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
269
|
+
};
|
|
270
|
+
const onKey = (e) => {
|
|
271
|
+
if (e.key === "Escape") setOpen(false);
|
|
272
|
+
};
|
|
273
|
+
document.addEventListener("mousedown", onDocClick);
|
|
274
|
+
document.addEventListener("keydown", onKey);
|
|
275
|
+
return () => {
|
|
276
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
277
|
+
document.removeEventListener("keydown", onKey);
|
|
278
|
+
};
|
|
279
|
+
}, [open]);
|
|
280
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: "relative", children: [
|
|
281
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
type: "button",
|
|
285
|
+
"aria-label": label,
|
|
286
|
+
"aria-expanded": open,
|
|
287
|
+
className: topBarIconButtonClass(tone),
|
|
288
|
+
onClick: () => {
|
|
289
|
+
setOpen((v) => {
|
|
290
|
+
const next = !v;
|
|
291
|
+
if (next) onOpen?.();
|
|
292
|
+
return next;
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
children: [
|
|
296
|
+
/* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot, fallback: fallbackIcon }),
|
|
297
|
+
typeof badge === "number" ? /* @__PURE__ */ jsxRuntime.jsx(TopBarCountBadge, { count: badge }) : null,
|
|
298
|
+
!open ? /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description }) : null
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
open ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
className: [
|
|
306
|
+
"absolute right-0 z-40 mt-2 rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] shadow-[var(--shadow-soft)]",
|
|
307
|
+
panelClassName ?? "w-80"
|
|
308
|
+
].join(" "),
|
|
309
|
+
children: [
|
|
310
|
+
/* @__PURE__ */ jsxRuntime.jsx(TopBarPanelArrow, {}),
|
|
311
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-xl", children })
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
) : null
|
|
315
|
+
] });
|
|
316
|
+
}
|
|
317
|
+
function BatteryGlyph({ size = 20, className }) {
|
|
318
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
|
|
319
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "1", y: "6", width: "18", height: "12", rx: "2", ry: "2" }),
|
|
320
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "23", y1: "10", x2: "23", y2: "14" }),
|
|
321
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "10", x2: "5", y2: "14" }),
|
|
322
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "10", x2: "9", y2: "14" })
|
|
323
|
+
] });
|
|
324
|
+
}
|
|
325
|
+
function BellGlyph({ size = 20, className }) {
|
|
326
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
|
|
327
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" }),
|
|
328
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13.73 21a2 2 0 0 1-3.46 0" })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
function LifeBuoyGlyph({ size = 20, className }) {
|
|
332
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
|
|
333
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
334
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4" }),
|
|
335
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "4.93", x2: "9.17", y2: "9.17" }),
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "14.83", x2: "19.07", y2: "19.07" }),
|
|
337
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "9.17", x2: "19.07", y2: "4.93" }),
|
|
338
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "19.07", x2: "9.17", y2: "14.83" })
|
|
339
|
+
] });
|
|
340
|
+
}
|
|
341
|
+
function LayoutGlyph({ size = 20, className }) {
|
|
342
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
|
|
343
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
|
|
344
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }),
|
|
345
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "21", x2: "9", y2: "9" })
|
|
346
|
+
] });
|
|
347
|
+
}
|
|
348
|
+
function FuelRow({
|
|
349
|
+
label,
|
|
350
|
+
channel,
|
|
351
|
+
loading
|
|
352
|
+
}) {
|
|
353
|
+
const remaining = channel?.remainingUnits ?? 0;
|
|
354
|
+
const total = channel ? Math.max(channel.rawBalanceUnits, channel.remainingUnits) : 0;
|
|
355
|
+
const ratio = total > 0 ? Math.min(100, Math.max(0, remaining / total * 100)) : 0;
|
|
356
|
+
const low = channel?.lowFuel ?? false;
|
|
357
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
358
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center justify-between", children: [
|
|
359
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-[color:var(--foreground)]", children: label }),
|
|
360
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-3.5 w-14" }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
361
|
+
"span",
|
|
362
|
+
{
|
|
363
|
+
className: [
|
|
364
|
+
"text-xs font-semibold tabular-nums",
|
|
365
|
+
low ? "text-[color:var(--color-race-red)]" : "text-[color:var(--muted)]"
|
|
366
|
+
].join(" "),
|
|
367
|
+
children: [
|
|
368
|
+
remaining.toLocaleString(),
|
|
369
|
+
" left"
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
] }),
|
|
374
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel)]", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-full animate-pulse bg-[color:var(--line)]" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
375
|
+
"div",
|
|
376
|
+
{
|
|
377
|
+
className: [
|
|
378
|
+
"h-full rounded-full transition-[width]",
|
|
379
|
+
low ? "bg-[color:var(--color-race-red)]" : "bg-[color:var(--color-ok,#00A651)]"
|
|
380
|
+
].join(" "),
|
|
381
|
+
style: { width: `${ratio}%` }
|
|
382
|
+
}
|
|
383
|
+
) })
|
|
384
|
+
] });
|
|
385
|
+
}
|
|
386
|
+
function FuelAction({ orgId }) {
|
|
387
|
+
const { fetchJson } = useNavChrome("FuelAction");
|
|
388
|
+
const [fuel, setFuel] = react.useState(null);
|
|
389
|
+
const [loading, setLoading] = react.useState(true);
|
|
390
|
+
const [error, setError] = react.useState(false);
|
|
391
|
+
react.useEffect(() => {
|
|
392
|
+
let cancelled = false;
|
|
393
|
+
setLoading(true);
|
|
394
|
+
setError(false);
|
|
395
|
+
setFuel(null);
|
|
396
|
+
fetchJson("communications/fuel-status", {
|
|
397
|
+
headers: orgId ? { "x-contacts-org": orgId } : void 0
|
|
398
|
+
}).then((data) => {
|
|
399
|
+
if (cancelled) return;
|
|
400
|
+
setFuel(data?.fuel ?? null);
|
|
401
|
+
}).catch(() => {
|
|
402
|
+
if (!cancelled) setError(true);
|
|
403
|
+
}).finally(() => {
|
|
404
|
+
if (!cancelled) setLoading(false);
|
|
405
|
+
});
|
|
406
|
+
return () => {
|
|
407
|
+
cancelled = true;
|
|
408
|
+
};
|
|
409
|
+
}, [fetchJson, orgId]);
|
|
410
|
+
const low = !!fuel && (fuel.signal.lowFuel || fuel.broadcast.lowFuel);
|
|
411
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
412
|
+
TopBarPopover,
|
|
413
|
+
{
|
|
414
|
+
label: "Communications Fuel",
|
|
415
|
+
description: "Remaining SMS and email sending balance for the active organization.",
|
|
416
|
+
slot: "comms-fuel",
|
|
417
|
+
fallbackIcon: BatteryGlyph,
|
|
418
|
+
tone: low ? "warning" : "default",
|
|
419
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Communications Fuel" }),
|
|
421
|
+
error || !loading && !fuel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Fuel balance is unavailable right now." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
422
|
+
/* @__PURE__ */ jsxRuntime.jsx(FuelRow, { label: "SMS Signal", channel: fuel?.signal ?? null, loading }),
|
|
423
|
+
/* @__PURE__ */ jsxRuntime.jsx(FuelRow, { label: "Email Broadcast", channel: fuel?.broadcast ?? null, loading })
|
|
424
|
+
] })
|
|
425
|
+
] })
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function NotificationsAction({ orgId }) {
|
|
430
|
+
const { fetchJson } = useNavChrome("NotificationsAction");
|
|
431
|
+
const [payload, setPayload] = react.useState(null);
|
|
432
|
+
const [loading, setLoading] = react.useState(true);
|
|
433
|
+
const [error, setError] = react.useState(false);
|
|
434
|
+
react.useEffect(() => {
|
|
435
|
+
let cancelled = false;
|
|
436
|
+
setLoading(true);
|
|
437
|
+
setError(false);
|
|
438
|
+
setPayload(null);
|
|
439
|
+
fetchJson("notifications", {
|
|
440
|
+
headers: orgId ? { "x-contacts-org": orgId } : void 0
|
|
441
|
+
}).then((data) => {
|
|
442
|
+
if (!cancelled) setPayload(data);
|
|
443
|
+
}).catch(() => {
|
|
444
|
+
if (!cancelled) setError(true);
|
|
445
|
+
}).finally(() => {
|
|
446
|
+
if (!cancelled) setLoading(false);
|
|
447
|
+
});
|
|
448
|
+
return () => {
|
|
449
|
+
cancelled = true;
|
|
450
|
+
};
|
|
451
|
+
}, [fetchJson, orgId]);
|
|
452
|
+
const count = (payload?.alerts.length ?? 0) + (payload?.replies.length ?? 0);
|
|
453
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
454
|
+
TopBarPopover,
|
|
455
|
+
{
|
|
456
|
+
label: "Alerts & Notifications",
|
|
457
|
+
description: "System alerts and recent activity for the active organization.",
|
|
458
|
+
slot: "notifications",
|
|
459
|
+
fallbackIcon: BellGlyph,
|
|
460
|
+
badge: loading ? 0 : count,
|
|
461
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
462
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Notifications" }),
|
|
463
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 py-1", children: [
|
|
464
|
+
/* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-4 w-full" }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-4 w-3/4" })
|
|
466
|
+
] }) : error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Notifications are unavailable right now." }) : count === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-1 text-sm text-[color:var(--muted)]", children: "You\u2019re all caught up." }) : /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "flex max-h-80 flex-col gap-1 overflow-y-auto", children: [
|
|
467
|
+
payload?.alerts.map((a) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
468
|
+
"a",
|
|
469
|
+
{
|
|
470
|
+
href: a.href ?? "#",
|
|
471
|
+
className: "block rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]",
|
|
472
|
+
children: [
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: a.title }),
|
|
474
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-[color:var(--muted)]", children: a.description })
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
) }, a.id)),
|
|
478
|
+
payload?.replies.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]", children: [
|
|
479
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: r.name }),
|
|
480
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-xs text-[color:var(--muted)]", children: r.preview })
|
|
481
|
+
] }, r.id))
|
|
482
|
+
] })
|
|
483
|
+
] })
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
function SupportAction() {
|
|
488
|
+
const { fetchJson } = useNavChrome("SupportAction");
|
|
489
|
+
const [open, setOpen] = react.useState(false);
|
|
490
|
+
const [message, setMessage] = react.useState("");
|
|
491
|
+
const [state, setState] = react.useState("idle");
|
|
492
|
+
const close = react.useCallback(() => {
|
|
493
|
+
setOpen(false);
|
|
494
|
+
setState("idle");
|
|
495
|
+
setMessage("");
|
|
496
|
+
}, []);
|
|
497
|
+
react.useEffect(() => {
|
|
498
|
+
if (!open) return;
|
|
499
|
+
const onKey = (e) => {
|
|
500
|
+
if (e.key === "Escape") close();
|
|
501
|
+
};
|
|
502
|
+
document.addEventListener("keydown", onKey);
|
|
503
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
504
|
+
}, [open, close]);
|
|
505
|
+
const submit = async () => {
|
|
506
|
+
if (!message.trim() || state === "sending") return;
|
|
507
|
+
setState("sending");
|
|
508
|
+
try {
|
|
509
|
+
await fetchJson("support", {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "Content-Type": "application/json" },
|
|
512
|
+
body: JSON.stringify({ message: message.trim() })
|
|
513
|
+
});
|
|
514
|
+
setState("sent");
|
|
515
|
+
} catch {
|
|
516
|
+
setState("error");
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
520
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
521
|
+
"button",
|
|
522
|
+
{
|
|
523
|
+
type: "button",
|
|
524
|
+
"aria-label": "Support",
|
|
525
|
+
className: topBarIconButtonClass(),
|
|
526
|
+
onClick: () => setOpen(true),
|
|
527
|
+
children: [
|
|
528
|
+
/* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot: "support", fallback: LifeBuoyGlyph }),
|
|
529
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
530
|
+
TopBarTooltip,
|
|
531
|
+
{
|
|
532
|
+
title: "Support",
|
|
533
|
+
description: "Send a support request to the Politogy team."
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
),
|
|
539
|
+
open && typeof document !== "undefined" ? reactDom.createPortal(
|
|
540
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
541
|
+
"div",
|
|
542
|
+
{
|
|
543
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4",
|
|
544
|
+
role: "dialog",
|
|
545
|
+
"aria-modal": "true",
|
|
546
|
+
"aria-label": "Support request",
|
|
547
|
+
onMouseDown: (e) => {
|
|
548
|
+
if (e.target === e.currentTarget) close();
|
|
549
|
+
},
|
|
550
|
+
children: /* @__PURE__ */ jsxRuntime.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: [
|
|
551
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: "Contact Support" }),
|
|
552
|
+
state === "sent" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
553
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 text-sm text-[color:var(--muted)]", children: "Your request was sent. The team will reply by email." }),
|
|
554
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-5 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
555
|
+
"button",
|
|
556
|
+
{
|
|
557
|
+
type: "button",
|
|
558
|
+
onClick: close,
|
|
559
|
+
className: "rounded-full bg-[color:var(--color-race-red)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90",
|
|
560
|
+
children: "Done"
|
|
561
|
+
}
|
|
562
|
+
) })
|
|
563
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
564
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-[color:var(--muted)]", children: "Describe the problem or question. Your account details ride along automatically." }),
|
|
565
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
566
|
+
"textarea",
|
|
567
|
+
{
|
|
568
|
+
value: message,
|
|
569
|
+
onChange: (e) => setMessage(e.target.value),
|
|
570
|
+
rows: 5,
|
|
571
|
+
placeholder: "How can we help?",
|
|
572
|
+
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-race-red)]"
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-[color:var(--color-race-red)]", children: "Sending failed \u2014 try again." }) : null,
|
|
576
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
|
|
577
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
578
|
+
"button",
|
|
579
|
+
{
|
|
580
|
+
type: "button",
|
|
581
|
+
onClick: close,
|
|
582
|
+
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)]",
|
|
583
|
+
children: "Cancel"
|
|
584
|
+
}
|
|
585
|
+
),
|
|
586
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
587
|
+
"button",
|
|
588
|
+
{
|
|
589
|
+
type: "button",
|
|
590
|
+
onClick: submit,
|
|
591
|
+
disabled: !message.trim() || state === "sending",
|
|
592
|
+
className: "rounded-full bg-[color:var(--color-race-red)] px-4 py-2 text-sm font-semibold text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
593
|
+
children: state === "sending" ? "Sending\u2026" : "Send request"
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
] })
|
|
597
|
+
] })
|
|
598
|
+
] })
|
|
599
|
+
}
|
|
600
|
+
),
|
|
601
|
+
document.body
|
|
602
|
+
) : null
|
|
603
|
+
] });
|
|
604
|
+
}
|
|
605
|
+
var NAV_DISPLAY_OPTIONS = [
|
|
606
|
+
{ value: "menu", label: "Sub Menu" },
|
|
607
|
+
{ value: "tabs", label: "Tabs Only" },
|
|
608
|
+
{ value: "both", label: "Both" }
|
|
609
|
+
];
|
|
610
|
+
function NavDisplayAction({
|
|
611
|
+
value,
|
|
612
|
+
onChange
|
|
613
|
+
}) {
|
|
614
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
615
|
+
TopBarPopover,
|
|
616
|
+
{
|
|
617
|
+
label: "Navigation Display",
|
|
618
|
+
description: "Show navigation as a sub-menu, tabs, or both.",
|
|
619
|
+
fallbackIcon: LayoutGlyph,
|
|
620
|
+
panelClassName: "w-44",
|
|
621
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-1", role: "radiogroup", "aria-label": "Navigation display", children: NAV_DISPLAY_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
622
|
+
"button",
|
|
623
|
+
{
|
|
624
|
+
type: "button",
|
|
625
|
+
role: "radio",
|
|
626
|
+
"aria-checked": value === opt.value,
|
|
627
|
+
onClick: () => onChange(opt.value),
|
|
628
|
+
className: [
|
|
629
|
+
"flex w-full items-center justify-between px-4 py-2 text-left text-sm font-medium",
|
|
630
|
+
value === opt.value ? "text-[color:var(--color-race-red)]" : "text-[color:var(--foreground)] hover:bg-[color:var(--panel)]"
|
|
631
|
+
].join(" "),
|
|
632
|
+
children: [
|
|
633
|
+
opt.label,
|
|
634
|
+
value === opt.value ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: "\u2713" }) : null
|
|
635
|
+
]
|
|
636
|
+
},
|
|
637
|
+
opt.value
|
|
638
|
+
)) })
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
function useThemePreference() {
|
|
643
|
+
const ctx = react.useContext(NavChromeContext);
|
|
644
|
+
const fetchJson = ctx?.fetchJson ?? null;
|
|
645
|
+
const [value, setValueState] = react.useState("system");
|
|
646
|
+
const reconciled = react.useRef(false);
|
|
647
|
+
react.useEffect(() => {
|
|
648
|
+
const cookie = readThemeCookie();
|
|
649
|
+
let ls = null;
|
|
650
|
+
try {
|
|
651
|
+
ls = localStorage.getItem(THEME_STORAGE_KEY);
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
const pref = cookie ?? (isThemePreference(ls) ? ls : null) ?? "system";
|
|
655
|
+
setValueState(pref);
|
|
656
|
+
applyThemePreference(pref);
|
|
657
|
+
}, []);
|
|
658
|
+
const meCtx = react.useContext(NavChromeContext);
|
|
659
|
+
react.useEffect(() => {
|
|
660
|
+
if (reconciled.current) return;
|
|
661
|
+
const pref = meCtx?.me?.themePreference;
|
|
662
|
+
if (isThemePreference(pref)) {
|
|
663
|
+
reconciled.current = true;
|
|
664
|
+
setValueState(pref);
|
|
665
|
+
applyThemePreference(pref);
|
|
666
|
+
writeThemeCookie(pref);
|
|
667
|
+
try {
|
|
668
|
+
localStorage.setItem(THEME_STORAGE_KEY, pref);
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}, [meCtx?.me]);
|
|
673
|
+
react.useEffect(() => {
|
|
674
|
+
const onStorage = (e) => {
|
|
675
|
+
if (e.key === THEME_STORAGE_KEY && isThemePreference(e.newValue)) {
|
|
676
|
+
setValueState(e.newValue);
|
|
677
|
+
applyThemePreference(e.newValue);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
const onWake = () => {
|
|
681
|
+
const cookie = readThemeCookie();
|
|
682
|
+
if (cookie) {
|
|
683
|
+
setValueState((prev) => {
|
|
684
|
+
if (cookie !== prev) applyThemePreference(cookie);
|
|
685
|
+
return cookie;
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
window.addEventListener("storage", onStorage);
|
|
690
|
+
window.addEventListener("focus", onWake);
|
|
691
|
+
document.addEventListener("visibilitychange", onWake);
|
|
692
|
+
return () => {
|
|
693
|
+
window.removeEventListener("storage", onStorage);
|
|
694
|
+
window.removeEventListener("focus", onWake);
|
|
695
|
+
document.removeEventListener("visibilitychange", onWake);
|
|
696
|
+
};
|
|
697
|
+
}, []);
|
|
698
|
+
react.useEffect(() => {
|
|
699
|
+
if (value !== "system") return;
|
|
700
|
+
let mq;
|
|
701
|
+
try {
|
|
702
|
+
mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
703
|
+
} catch {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const onChange = () => applyThemePreference("system");
|
|
707
|
+
mq.addEventListener("change", onChange);
|
|
708
|
+
return () => mq.removeEventListener("change", onChange);
|
|
709
|
+
}, [value]);
|
|
710
|
+
const setValue = react.useCallback(
|
|
711
|
+
(next) => {
|
|
712
|
+
setValueState(next);
|
|
713
|
+
applyThemePreference(next);
|
|
714
|
+
writeThemeCookie(next);
|
|
715
|
+
try {
|
|
716
|
+
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
fetchJson?.("me/preferences", {
|
|
720
|
+
method: "PATCH",
|
|
721
|
+
headers: { "Content-Type": "application/json" },
|
|
722
|
+
body: JSON.stringify({ themePreference: next })
|
|
723
|
+
}).catch(() => {
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
[fetchJson]
|
|
727
|
+
);
|
|
728
|
+
return { value, setValue };
|
|
729
|
+
}
|
|
730
|
+
var ORG_STORAGE_KEY = "vrm.activeOrg";
|
|
731
|
+
var NAV_DISPLAY_KEY = "vrm.navDisplay";
|
|
732
|
+
var CORE_BASE_URL = "https://app.politogyvrm.com";
|
|
733
|
+
var ChromeStateContext = react.createContext(null);
|
|
734
|
+
function useChromeState() {
|
|
735
|
+
return react.useContext(ChromeStateContext);
|
|
736
|
+
}
|
|
737
|
+
function ChromeStateProvider({ children }) {
|
|
738
|
+
const { orgs } = useChromeOrgs();
|
|
739
|
+
const [orgId, setOrgIdState] = react.useState(null);
|
|
740
|
+
const [navDisplay, setNavDisplayState] = react.useState("both");
|
|
741
|
+
react.useEffect(() => {
|
|
742
|
+
try {
|
|
743
|
+
const storedOrg = localStorage.getItem(ORG_STORAGE_KEY);
|
|
744
|
+
if (storedOrg) setOrgIdState(storedOrg);
|
|
745
|
+
const storedNav = localStorage.getItem(NAV_DISPLAY_KEY);
|
|
746
|
+
if (storedNav === "menu" || storedNav === "tabs" || storedNav === "both") {
|
|
747
|
+
setNavDisplayState(storedNav);
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
}, []);
|
|
752
|
+
react.useEffect(() => {
|
|
753
|
+
if (!orgs || orgs.length === 0) return;
|
|
754
|
+
setOrgIdState((prev) => prev && orgs.some((o) => o.id === prev) ? prev : orgs[0].id);
|
|
755
|
+
}, [orgs]);
|
|
756
|
+
const setOrgId = react.useCallback((id) => {
|
|
757
|
+
setOrgIdState(id);
|
|
758
|
+
try {
|
|
759
|
+
localStorage.setItem(ORG_STORAGE_KEY, id);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}, []);
|
|
763
|
+
const setNavDisplay = react.useCallback((m) => {
|
|
764
|
+
setNavDisplayState(m);
|
|
765
|
+
try {
|
|
766
|
+
localStorage.setItem(NAV_DISPLAY_KEY, m);
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
}, []);
|
|
770
|
+
const value = react.useMemo(
|
|
771
|
+
() => ({
|
|
772
|
+
orgId,
|
|
773
|
+
setOrgId,
|
|
774
|
+
orgs: (orgs ?? []).map((o) => ({
|
|
775
|
+
id: o.id,
|
|
776
|
+
name: o.name,
|
|
777
|
+
logoUrl: o.logoUrl,
|
|
778
|
+
brandColor: o.brandColor,
|
|
779
|
+
role: o.role,
|
|
780
|
+
rawRole: o.rawRole ?? null
|
|
781
|
+
})),
|
|
782
|
+
navDisplay,
|
|
783
|
+
setNavDisplay
|
|
784
|
+
}),
|
|
785
|
+
[orgId, setOrgId, orgs, navDisplay, setNavDisplay]
|
|
786
|
+
);
|
|
787
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChromeStateContext.Provider, { value, children });
|
|
788
|
+
}
|
|
789
|
+
function AppDevNotes({
|
|
790
|
+
storagePrefix,
|
|
791
|
+
proxyBasePath,
|
|
792
|
+
identity,
|
|
793
|
+
children
|
|
794
|
+
}) {
|
|
795
|
+
const adapter = react.useMemo(
|
|
796
|
+
() => devnotes.createDevNotesClient({
|
|
797
|
+
basePath: `${proxyBasePath}/devnotes`,
|
|
798
|
+
// The proxy attaches the session's Bearer server-side.
|
|
799
|
+
getAuthToken: async () => ""
|
|
800
|
+
}),
|
|
801
|
+
[proxyBasePath]
|
|
802
|
+
);
|
|
803
|
+
const user = react.useMemo(
|
|
804
|
+
() => ({
|
|
805
|
+
id: identity.userId,
|
|
806
|
+
email: identity.email ?? "",
|
|
807
|
+
fullName: identity.userName ?? void 0
|
|
808
|
+
}),
|
|
809
|
+
[identity.userId, identity.email, identity.userName]
|
|
810
|
+
);
|
|
811
|
+
const config = react.useMemo(
|
|
812
|
+
() => ({
|
|
813
|
+
storagePrefix,
|
|
814
|
+
role: identity.role.toLowerCase() === "admin" ? "admin" : "reporter",
|
|
815
|
+
disableAi: true,
|
|
816
|
+
getPagePath: () => window.location.pathname
|
|
817
|
+
}),
|
|
818
|
+
[storagePrefix, identity.role]
|
|
819
|
+
);
|
|
820
|
+
return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesProvider, { adapter, user, config, children });
|
|
821
|
+
}
|
|
822
|
+
function PolitogyAppChrome({
|
|
823
|
+
storagePrefix,
|
|
824
|
+
proxyBasePath = "/api/chrome",
|
|
825
|
+
fetchJson: fetchJsonOverride,
|
|
826
|
+
identity,
|
|
827
|
+
children
|
|
828
|
+
}) {
|
|
829
|
+
const proxyFetchJson = react.useMemo(
|
|
830
|
+
() => async (path, init) => {
|
|
831
|
+
const res = await fetch(`${proxyBasePath}/${path}`, {
|
|
832
|
+
method: init?.method ?? "GET",
|
|
833
|
+
headers: init?.headers,
|
|
834
|
+
body: init?.body
|
|
835
|
+
});
|
|
836
|
+
if (!res.ok) throw new Error(`chrome ${path}: ${res.status}`);
|
|
837
|
+
if (res.status === 204) return null;
|
|
838
|
+
const text = await res.text();
|
|
839
|
+
return text ? JSON.parse(text) : null;
|
|
840
|
+
},
|
|
841
|
+
[proxyBasePath]
|
|
842
|
+
);
|
|
843
|
+
const fetchJson = fetchJsonOverride ?? proxyFetchJson;
|
|
844
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, children: /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
845
|
+
AppDevNotes,
|
|
846
|
+
{
|
|
847
|
+
storagePrefix,
|
|
848
|
+
proxyBasePath,
|
|
849
|
+
identity,
|
|
850
|
+
children
|
|
851
|
+
}
|
|
852
|
+
) }) });
|
|
853
|
+
}
|
|
854
|
+
function ChromeTopBarActions() {
|
|
855
|
+
const state = useChromeState();
|
|
856
|
+
if (!state) return null;
|
|
857
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
858
|
+
/* @__PURE__ */ jsxRuntime.jsx(FuelAction, { orgId: state.orgId }),
|
|
859
|
+
/* @__PURE__ */ jsxRuntime.jsx(NavDisplayAction, { value: state.navDisplay, onChange: state.setNavDisplay }),
|
|
860
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
861
|
+
TopBarWidgetSlot,
|
|
862
|
+
{
|
|
863
|
+
title: "DevNotes",
|
|
864
|
+
description: "Capture bugs and notes, filed straight to Forge.",
|
|
865
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, {})
|
|
866
|
+
}
|
|
867
|
+
),
|
|
868
|
+
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, {}),
|
|
869
|
+
/* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
|
|
870
|
+
] });
|
|
871
|
+
}
|
|
872
|
+
function useIsSuperAdmin() {
|
|
873
|
+
const state = useChromeState();
|
|
874
|
+
return (state?.orgs ?? []).some((o) => /super/i.test(o.rawRole ?? ""));
|
|
875
|
+
}
|
|
876
|
+
var FOOTER_LINKS = [
|
|
877
|
+
{ label: "Privacy", href: `${CORE_BASE_URL}/privacy-policy`, external: true },
|
|
878
|
+
{ label: "Terms", href: `${CORE_BASE_URL}/terms-and-conditions`, external: true },
|
|
879
|
+
{
|
|
880
|
+
label: "Subscriptions",
|
|
881
|
+
href: "https://politogyvrm.com/subscription-cancellation-policy",
|
|
882
|
+
external: true
|
|
883
|
+
}
|
|
884
|
+
];
|
|
885
|
+
function useChromeFooter(meta) {
|
|
886
|
+
const isSuperAdmin = useIsSuperAdmin();
|
|
887
|
+
return react.useMemo(
|
|
888
|
+
() => isSuperAdmin ? { links: FOOTER_LINKS, ...meta } : { links: FOOTER_LINKS },
|
|
889
|
+
[isSuperAdmin, meta.envLabel, meta.version, meta.commit, meta.commitDate, meta.commitAuthor]
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
function useChromeUserMenu({
|
|
893
|
+
userId,
|
|
894
|
+
userName,
|
|
895
|
+
email
|
|
896
|
+
}) {
|
|
897
|
+
const state = useChromeState();
|
|
898
|
+
const theme = useThemePreference();
|
|
899
|
+
const activeOrg = state?.orgs.find((o) => o.id === state.orgId) ?? null;
|
|
900
|
+
const orgSuffix = activeOrg ? `?org=${activeOrg.id.slice(-6)}` : "";
|
|
901
|
+
return {
|
|
902
|
+
// My Profile is global — NOT org-suffixed (main-app ShellChromeBridge).
|
|
903
|
+
myProfileHref: `${CORE_BASE_URL}/my-profile`,
|
|
904
|
+
organizationSettingsHref: `${CORE_BASE_URL}/organization-settings${orgSuffix}`,
|
|
905
|
+
roles: (state?.orgs ?? []).filter((o) => o.rawRole || o.role).map((o) => ({ role: o.rawRole ?? o.role, orgName: o.name })),
|
|
906
|
+
accounts: [{ id: userId, name: userName, email, avatarUrl: null, active: true }],
|
|
907
|
+
addAccountHref: `${CORE_BASE_URL}/my-profile#accounts`,
|
|
908
|
+
theme: { value: theme.value, onChange: theme.setValue }
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
exports.ChromeTopBarActions = ChromeTopBarActions;
|
|
913
|
+
exports.PolitogyAppChrome = PolitogyAppChrome;
|
|
914
|
+
exports.useChromeFooter = useChromeFooter;
|
|
915
|
+
exports.useChromeState = useChromeState;
|
|
916
|
+
exports.useChromeUserMenu = useChromeUserMenu;
|
|
917
|
+
exports.useIsSuperAdmin = useIsSuperAdmin;
|
|
918
|
+
//# sourceMappingURL=app-chrome.cjs.map
|
|
919
|
+
//# sourceMappingURL=app-chrome.cjs.map
|