@the-portland-company/shell 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 — 2026-05-13
4
+
5
+ ### Added
6
+ - `<ShellChrome>` component — drops chrome data (organization, linkedAccounts, support, devNotesMenu, myProfileHref) into the shell from a position deep in the tree. Use this when the data lives in app contexts nested BELOW `<ShellProvider>`. Apps that have the data at the top can keep using `<ShellProvider>` props directly.
7
+ - Re-exported chrome context objects (`ShellOrgContext`, `ShellLinkedAccountsContext`, `ShellSupportProviderContext`, `ShellChromeContext`) so power users can write their own bindings.
8
+
9
+ ## 0.2.0 — 2026-05-13
10
+
11
+ ### Added
12
+ - Shared header chrome rendered by `<Header />` automatically when supplied via `ShellProvider`:
13
+ - `<UserMenu />` — avatar dropdown with My Profile, color-mode toggle, Sign out, and optional Switch-account section.
14
+ - `<OrgSelector />` — current org badge + dropdown of other orgs.
15
+ - `<SupportRequestButton />` — life-buoy icon that opens a Support modal; submits via the host-supplied handler.
16
+ - `ShellProvider` accepts four new optional prop-driven contexts:
17
+ - `organization?: ShellOrganizationValue` — drives `<OrgSelector />`.
18
+ - `linkedAccounts?: ShellLinkedAccountsValue` — drives the UserMenu "Switch account" section.
19
+ - `support?: ShellSupportValue` — drives `<SupportRequestButton />` and modal submission.
20
+ - `devNotesMenu?: ReactNode` — rendered in the header (typically `<DevNotesMenu />` from `@the-portland-company/devnotes`).
21
+ - `myProfileHref?: string` — UserMenu profile link target. Defaults to `/my-profile`.
22
+ - New hooks: `useShellOrganization`, `useShellLinkedAccounts`, `useShellSupport`, `useShellChrome`. Each returns `null` when the corresponding prop wasn't supplied, so chrome components opt out cleanly.
23
+ - New types: `ShellOrganization`, `ShellOrganizationValue`, `ShellLinkedAccount`, `ShellLinkedAccountsValue`, `ShellSupportContext`, `ShellSupportValue`.
24
+
25
+ ### Changed
26
+ - `<Header />` now renders `<OrgSelector />`, `<SupportRequestButton />`, `devNotesMenu`, and `<UserMenu />` by default. The old hard-coded avatar menu inside Header was removed (its behavior is now inside `<UserMenu />`).
27
+
28
+ ### Notes
29
+ - Backwards compatible: existing consumers passing only `currentApp` + `supabaseClient` continue to work; the new chrome simply renders nothing when its props aren't supplied.
30
+
3
31
  ## 0.1.0 — 2026-05-12
4
32
 
5
33
  Initial public release.
package/dist/index.cjs CHANGED
@@ -1121,6 +1121,10 @@ function GlobalUiIdProvider() {
1121
1121
  }
1122
1122
  var ShellAuthContext = react$1.createContext(null);
1123
1123
  var ShellMetaContext = react$1.createContext(null);
1124
+ var ShellOrgContext = react$1.createContext(null);
1125
+ var ShellLinkedAccountsContext = react$1.createContext(null);
1126
+ var ShellSupportProviderContext = react$1.createContext(null);
1127
+ var ShellChromeContext = react$1.createContext(null);
1124
1128
  function toShellUser(session) {
1125
1129
  if (!session?.user) return null;
1126
1130
  const su = session.user;
@@ -1137,7 +1141,12 @@ function ShellProvider({
1137
1141
  supabaseClient,
1138
1142
  currentApp,
1139
1143
  appRegistry,
1140
- basename
1144
+ basename,
1145
+ organization,
1146
+ linkedAccounts,
1147
+ support,
1148
+ devNotesMenu,
1149
+ myProfileHref
1141
1150
  }) {
1142
1151
  const [status, setStatus] = react$1.useState("loading");
1143
1152
  const [session, setSession] = react$1.useState(null);
@@ -1176,7 +1185,14 @@ function ShellProvider({
1176
1185
  }),
1177
1186
  [currentApp, appRegistry, basename]
1178
1187
  );
1179
- return /* @__PURE__ */ jsxRuntime.jsx(ShellAuthContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(ShellMetaContext.Provider, { value: meta, children: /* @__PURE__ */ jsxRuntime.jsx(react.ChakraProvider, { theme: politogyTheme, children }) }) });
1188
+ const chrome = react$1.useMemo(
1189
+ () => ({
1190
+ devNotesMenu: devNotesMenu ?? null,
1191
+ myProfileHref: myProfileHref ?? "/my-profile"
1192
+ }),
1193
+ [devNotesMenu, myProfileHref]
1194
+ );
1195
+ return /* @__PURE__ */ jsxRuntime.jsx(ShellAuthContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(ShellMetaContext.Provider, { value: meta, children: /* @__PURE__ */ jsxRuntime.jsx(ShellOrgContext.Provider, { value: organization ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellLinkedAccountsContext.Provider, { value: linkedAccounts ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellSupportProviderContext.Provider, { value: support ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellChromeContext.Provider, { value: chrome, children: /* @__PURE__ */ jsxRuntime.jsx(react.ChakraProvider, { theme: politogyTheme, children }) }) }) }) }) }) });
1180
1196
  }
1181
1197
  function useShellAuth() {
1182
1198
  const ctx = react$1.useContext(ShellAuthContext);
@@ -1199,8 +1215,472 @@ function useCurrentApp() {
1199
1215
  }
1200
1216
  return meta.currentApp;
1201
1217
  }
1218
+ function useShellOrganization() {
1219
+ return react$1.useContext(ShellOrgContext);
1220
+ }
1221
+ function useShellLinkedAccounts() {
1222
+ return react$1.useContext(ShellLinkedAccountsContext);
1223
+ }
1224
+ function useShellSupport() {
1225
+ return react$1.useContext(ShellSupportProviderContext);
1226
+ }
1227
+ function useShellChrome() {
1228
+ const ctx = react$1.useContext(ShellChromeContext);
1229
+ if (!ctx) {
1230
+ throw new Error("useShellChrome must be used inside <ShellProvider>");
1231
+ }
1232
+ return ctx;
1233
+ }
1234
+ var HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
1235
+ function normalizeHexColor(value) {
1236
+ const trimmed = value?.trim();
1237
+ if (!trimmed || !HEX_COLOR_PATTERN.test(trimmed)) return null;
1238
+ if (trimmed.length === 7) return trimmed;
1239
+ const [r, g, b] = trimmed.slice(1).split("");
1240
+ return `#${r}${r}${g}${g}${b}${b}`;
1241
+ }
1242
+ function getOrganizationBadgeLetters(name) {
1243
+ const normalizedName = String(name || "").trim().replace(/[^a-zA-Z0-9\s]+/g, " ");
1244
+ const parts = normalizedName.split(/\s+/).filter(Boolean);
1245
+ if (parts.length >= 2) {
1246
+ return `${parts[0][0] || ""}${parts[1][0] || ""}`.toUpperCase();
1247
+ }
1248
+ const compact = parts[0] || normalizedName.replace(/\s+/g, "");
1249
+ return compact.slice(0, 2).toUpperCase() || "OR";
1250
+ }
1251
+ function OrgBadge({ name, logoUrl, brandColor, size }) {
1252
+ const normalizedColor = normalizeHexColor(brandColor);
1253
+ const badgeColor = normalizedColor || "#5a5a5a";
1254
+ const badgeLetters = getOrganizationBadgeLetters(name);
1255
+ return /* @__PURE__ */ jsxRuntime.jsx(
1256
+ react.Box,
1257
+ {
1258
+ boxSize: size,
1259
+ borderRadius: "full",
1260
+ overflow: "hidden",
1261
+ border: "1px solid white",
1262
+ flexShrink: 0,
1263
+ bg: logoUrl ? "whiteAlpha.300" : badgeColor,
1264
+ color: "white",
1265
+ display: "inline-flex",
1266
+ alignItems: "center",
1267
+ justifyContent: "center",
1268
+ fontSize: "xs",
1269
+ fontWeight: "bold",
1270
+ letterSpacing: "0.08em",
1271
+ textTransform: "uppercase",
1272
+ children: logoUrl ? /* @__PURE__ */ jsxRuntime.jsx(react.Image, { src: logoUrl, alt: `${name ?? "Organization"} logo`, boxSize: "full", objectFit: "cover" }) : badgeLetters
1273
+ }
1274
+ );
1275
+ }
1276
+ function OrgSelector() {
1277
+ const org = useShellOrganization();
1278
+ if (!org) return null;
1279
+ const { currentOrg, organizations, switchOrganization, createOrgHref } = org;
1280
+ const others = organizations.filter((o) => o.id !== currentOrg?.id);
1281
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Menu, { placement: "bottom-start", children: [
1282
+ /* @__PURE__ */ jsxRuntime.jsx(
1283
+ react.MenuButton,
1284
+ {
1285
+ as: react.Box,
1286
+ cursor: "pointer",
1287
+ px: 2,
1288
+ py: 1,
1289
+ borderRadius: "md",
1290
+ _hover: { bg: "gray.100" },
1291
+ _active: { bg: "gray.200" },
1292
+ "aria-label": "Switch organization",
1293
+ children: /* @__PURE__ */ jsxRuntime.jsxs(react.HStack, { spacing: 2, align: "center", children: [
1294
+ currentOrg ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1295
+ /* @__PURE__ */ jsxRuntime.jsx(
1296
+ OrgBadge,
1297
+ {
1298
+ name: currentOrg.name,
1299
+ logoUrl: currentOrg.logoUrl,
1300
+ brandColor: currentOrg.brandColor,
1301
+ size: "24px"
1302
+ }
1303
+ ),
1304
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "600", color: "gray.800", noOfLines: 1, children: currentOrg.name })
1305
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "gray.500", children: "Select organization" }),
1306
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { as: "span", fontSize: "xs", color: "gray.500", "aria-hidden": "true", children: "\u25BE" })
1307
+ ] })
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ jsxRuntime.jsx(react.Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(react.MenuList, { zIndex: 1500, children: [
1311
+ others.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { isDisabled: true, children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "gray.500", children: "No other organizations" }) }),
1312
+ others.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
1313
+ react.MenuItem,
1314
+ {
1315
+ onClick: () => {
1316
+ void switchOrganization(o.id);
1317
+ },
1318
+ children: /* @__PURE__ */ jsxRuntime.jsxs(react.HStack, { spacing: 2, align: "center", children: [
1319
+ /* @__PURE__ */ jsxRuntime.jsx(OrgBadge, { name: o.name, logoUrl: o.logoUrl, brandColor: o.brandColor, size: "20px" }),
1320
+ /* @__PURE__ */ jsxRuntime.jsx(react.Flex, { direction: "column", children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "600", children: o.name }) })
1321
+ ] })
1322
+ },
1323
+ o.id
1324
+ )),
1325
+ createOrgHref && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1326
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuDivider, {}),
1327
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { as: "a", href: createOrgHref, children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "brand.500", fontWeight: "600", children: "Create organization" }) })
1328
+ ] })
1329
+ ] }) })
1330
+ ] });
1331
+ }
1332
+ var SUPPORT_REQUEST_SUCCESS_MESSAGE = "Your support request was sent to Politogy leadership.";
1333
+ function splitName(value) {
1334
+ const trimmed = String(value ?? "").trim();
1335
+ if (!trimmed) return { firstName: "", lastName: "" };
1336
+ const [first, ...rest] = trimmed.split(/\s+/);
1337
+ return { firstName: first ?? "", lastName: rest.join(" ") };
1338
+ }
1339
+ function resolveSupportProfile(user) {
1340
+ if (!user) {
1341
+ return { firstName: "", lastName: "", email: "", phone: "", missingEmail: true, missingPhone: true };
1342
+ }
1343
+ const meta = user.raw.user_metadata ?? {};
1344
+ const resolvedName = String(meta.full_name ?? meta.name ?? "").trim() || String(user.email ?? "").split("@")[0] || "";
1345
+ const { firstName, lastName } = splitName(resolvedName);
1346
+ const email = String(user.email ?? "").trim();
1347
+ const phone = String(meta.phone ?? "").trim();
1348
+ return {
1349
+ firstName,
1350
+ lastName,
1351
+ email,
1352
+ phone,
1353
+ missingEmail: email.length === 0,
1354
+ missingPhone: phone.length === 0
1355
+ };
1356
+ }
1357
+ function formatTimestamp(date) {
1358
+ return date.toLocaleString("en-US", {
1359
+ year: "numeric",
1360
+ month: "short",
1361
+ day: "numeric",
1362
+ hour: "numeric",
1363
+ minute: "2-digit",
1364
+ second: "2-digit",
1365
+ hour12: true
1366
+ });
1367
+ }
1368
+ function buildSupportRequestMessage(input) {
1369
+ const submittedAt = input.submittedAt ?? /* @__PURE__ */ new Date();
1370
+ const iso = submittedAt.toISOString();
1371
+ const display = formatTimestamp(submittedAt);
1372
+ const trimmedSearch = String(input.search ?? "").trim();
1373
+ return [
1374
+ "A user requested support.",
1375
+ "",
1376
+ `First Name: ${input.firstName.trim() || "Unknown"}`,
1377
+ `Last Name: ${input.lastName.trim() || "Unknown"}`,
1378
+ `Organization: ${(input.organizationName ?? "").trim() || "Unknown"}`,
1379
+ `Email: ${input.email.trim()}`,
1380
+ `Phone: ${input.phone.trim()}`,
1381
+ `Submitted At (ISO): ${iso}`,
1382
+ `Submitted At (Local): ${display}`,
1383
+ `Page Path: ${input.pathname.trim() || "/"}`,
1384
+ `Page Title: ${(input.pageTitle ?? "").trim() || "Unknown"}`,
1385
+ `Query String: ${trimmedSearch || "None"}`
1386
+ ].join("\n");
1387
+ }
1388
+ function SupportRequestButton() {
1389
+ const support = useShellSupport();
1390
+ const { user } = useShellAuth();
1391
+ const org = useShellOrganization();
1392
+ const { isOpen, onOpen, onClose } = react.useDisclosure();
1393
+ const toast = react.useToast();
1394
+ const profile = react$1.useMemo(() => resolveSupportProfile(user), [user]);
1395
+ const [email, setEmail] = react$1.useState("");
1396
+ const [phone, setPhone] = react$1.useState("");
1397
+ const [message, setMessage] = react$1.useState("");
1398
+ const [isSubmitting, setIsSubmitting] = react$1.useState(false);
1399
+ react$1.useEffect(() => {
1400
+ if (!isOpen) return;
1401
+ setEmail(profile.email);
1402
+ setPhone(profile.phone);
1403
+ setMessage("");
1404
+ setIsSubmitting(false);
1405
+ }, [isOpen, profile.email, profile.phone]);
1406
+ const handleClose = react$1.useCallback(() => {
1407
+ if (isSubmitting) return;
1408
+ onClose();
1409
+ }, [isSubmitting, onClose]);
1410
+ const handleSubmit = react$1.useCallback(async () => {
1411
+ if (!support) return;
1412
+ const resolvedEmail = (profile.missingEmail ? email : profile.email).trim();
1413
+ const resolvedPhone = (profile.missingPhone ? phone : profile.phone).trim();
1414
+ if (!resolvedEmail) {
1415
+ toast({ title: "Email required", status: "error", duration: 4e3, isClosable: true });
1416
+ return;
1417
+ }
1418
+ if (!resolvedPhone) {
1419
+ toast({ title: "Phone required", status: "error", duration: 4e3, isClosable: true });
1420
+ return;
1421
+ }
1422
+ if (!user?.id) {
1423
+ toast({
1424
+ title: "Unable to send support request",
1425
+ description: "You must be signed in to request support.",
1426
+ status: "error",
1427
+ duration: 4e3,
1428
+ isClosable: true
1429
+ });
1430
+ return;
1431
+ }
1432
+ const context = {
1433
+ pathname: typeof window !== "undefined" ? window.location.pathname : "/",
1434
+ search: typeof window !== "undefined" ? window.location.search : "",
1435
+ pageTitle: typeof document !== "undefined" ? document.title : void 0,
1436
+ organizationName: org?.currentOrg?.name ?? void 0
1437
+ };
1438
+ const formatted = buildSupportRequestMessage({
1439
+ firstName: profile.firstName,
1440
+ lastName: profile.lastName,
1441
+ organizationName: context.organizationName,
1442
+ email: resolvedEmail,
1443
+ phone: resolvedPhone,
1444
+ pathname: context.pathname,
1445
+ search: context.search,
1446
+ pageTitle: context.pageTitle
1447
+ });
1448
+ try {
1449
+ setIsSubmitting(true);
1450
+ await support.submitSupportRequest({
1451
+ email: resolvedEmail,
1452
+ phone: resolvedPhone,
1453
+ message: message.trim() ? `${message.trim()}
1454
+
1455
+ ---
1456
+
1457
+ ${formatted}` : formatted,
1458
+ context
1459
+ });
1460
+ toast({
1461
+ title: "Support request sent",
1462
+ description: SUPPORT_REQUEST_SUCCESS_MESSAGE,
1463
+ status: "success",
1464
+ duration: 5e3,
1465
+ isClosable: true
1466
+ });
1467
+ onClose();
1468
+ } catch (err) {
1469
+ const description = err instanceof Error ? err.message : "Unable to send your support request.";
1470
+ toast({
1471
+ title: "Failed to request support",
1472
+ description,
1473
+ status: "error",
1474
+ duration: 5e3,
1475
+ isClosable: true
1476
+ });
1477
+ } finally {
1478
+ setIsSubmitting(false);
1479
+ }
1480
+ }, [support, profile, email, phone, message, toast, user?.id, onClose, org?.currentOrg?.name]);
1481
+ if (!support) return null;
1482
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1483
+ /* @__PURE__ */ jsxRuntime.jsx(
1484
+ react.IconButton,
1485
+ {
1486
+ "aria-label": "Request support",
1487
+ icon: /* @__PURE__ */ jsxRuntime.jsx(LifebuoyIcon, {}),
1488
+ variant: "ghost",
1489
+ size: "sm",
1490
+ onClick: onOpen
1491
+ }
1492
+ ),
1493
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Modal, { isOpen, onClose: handleClose, isCentered: true, children: [
1494
+ /* @__PURE__ */ jsxRuntime.jsx(react.ModalOverlay, {}),
1495
+ /* @__PURE__ */ jsxRuntime.jsxs(react.ModalContent, { children: [
1496
+ /* @__PURE__ */ jsxRuntime.jsx(react.ModalHeader, { children: "Support" }),
1497
+ /* @__PURE__ */ jsxRuntime.jsx(react.ModalCloseButton, { isDisabled: isSubmitting }),
1498
+ /* @__PURE__ */ jsxRuntime.jsx(react.ModalBody, { children: /* @__PURE__ */ jsxRuntime.jsxs(react.VStack, { align: "stretch", spacing: 4, children: [
1499
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "gray.600", children: "A message will be texted and emailed with your contact information and support will reply as soon as possible. If you are a beta customer that is typically within minutes." }),
1500
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { rounded: "md", borderWidth: "1px", borderColor: "gray.200", bg: "gray.50", p: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(react.VStack, { align: "stretch", spacing: 1, children: [
1501
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "sm", children: [
1502
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Name:" }),
1503
+ " ",
1504
+ [profile.firstName, profile.lastName].filter(Boolean).join(" ") || "Unknown"
1505
+ ] }),
1506
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "sm", children: [
1507
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Organization:" }),
1508
+ " ",
1509
+ org?.currentOrg?.name ?? "Unknown"
1510
+ ] }),
1511
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "sm", children: [
1512
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Email:" }),
1513
+ " ",
1514
+ (profile.missingEmail ? email : profile.email) || "Missing"
1515
+ ] }),
1516
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "sm", children: [
1517
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Phone:" }),
1518
+ " ",
1519
+ (profile.missingPhone ? phone : profile.phone) || "Missing"
1520
+ ] })
1521
+ ] }) }),
1522
+ profile.missingEmail && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
1523
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", fontWeight: "600", color: "gray.700", mb: 1, children: "Email" }),
1524
+ /* @__PURE__ */ jsxRuntime.jsx(
1525
+ "input",
1526
+ {
1527
+ type: "email",
1528
+ "aria-label": "Email",
1529
+ value: email,
1530
+ onChange: (e) => setEmail(e.target.value),
1531
+ placeholder: "name@example.com",
1532
+ style: {
1533
+ width: "100%",
1534
+ padding: "8px 12px",
1535
+ border: "1px solid #e2e8f0",
1536
+ borderRadius: "6px",
1537
+ fontSize: "14px"
1538
+ }
1539
+ }
1540
+ )
1541
+ ] }),
1542
+ profile.missingPhone && /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
1543
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", fontWeight: "600", color: "gray.700", mb: 1, children: "Phone" }),
1544
+ /* @__PURE__ */ jsxRuntime.jsx(
1545
+ "input",
1546
+ {
1547
+ "aria-label": "Phone",
1548
+ value: phone,
1549
+ onChange: (e) => setPhone(e.target.value),
1550
+ placeholder: "(503) 555-1212",
1551
+ style: {
1552
+ width: "100%",
1553
+ padding: "8px 12px",
1554
+ border: "1px solid #e2e8f0",
1555
+ borderRadius: "6px",
1556
+ fontSize: "14px"
1557
+ }
1558
+ }
1559
+ )
1560
+ ] }),
1561
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { children: [
1562
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", fontWeight: "600", color: "gray.700", mb: 1, children: "Optional message" }),
1563
+ /* @__PURE__ */ jsxRuntime.jsx(
1564
+ react.Textarea,
1565
+ {
1566
+ value: message,
1567
+ onChange: (e) => setMessage(e.target.value),
1568
+ placeholder: "What can we help you with?",
1569
+ rows: 3
1570
+ }
1571
+ )
1572
+ ] })
1573
+ ] }) }),
1574
+ /* @__PURE__ */ jsxRuntime.jsxs(react.ModalFooter, { children: [
1575
+ /* @__PURE__ */ jsxRuntime.jsx(react.Button, { variant: "ghost", mr: 3, onClick: handleClose, isDisabled: isSubmitting, children: "Cancel" }),
1576
+ /* @__PURE__ */ jsxRuntime.jsx(
1577
+ react.Button,
1578
+ {
1579
+ colorScheme: "orange",
1580
+ onClick: () => void handleSubmit(),
1581
+ isLoading: isSubmitting,
1582
+ loadingText: "Sending",
1583
+ children: "Request Support"
1584
+ }
1585
+ )
1586
+ ] })
1587
+ ] })
1588
+ ] })
1589
+ ] });
1590
+ }
1591
+ function LifebuoyIcon() {
1592
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1593
+ "svg",
1594
+ {
1595
+ width: "18",
1596
+ height: "18",
1597
+ viewBox: "0 0 24 24",
1598
+ fill: "none",
1599
+ stroke: "currentColor",
1600
+ strokeWidth: "2",
1601
+ strokeLinecap: "round",
1602
+ strokeLinejoin: "round",
1603
+ "aria-hidden": "true",
1604
+ children: [
1605
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
1606
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4" }),
1607
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "4.93", x2: "9.17", y2: "9.17" }),
1608
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "14.83", x2: "19.07", y2: "19.07" }),
1609
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "9.17", x2: "19.07", y2: "4.93" }),
1610
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "9.17", x2: "18.36", y2: "5.64" }),
1611
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "19.07", x2: "9.17", y2: "14.83" })
1612
+ ]
1613
+ }
1614
+ );
1615
+ }
1616
+ function avatarUrlFromUser(user) {
1617
+ if (!user) return void 0;
1618
+ const meta = user.raw.user_metadata;
1619
+ return meta?.avatar_url ?? void 0;
1620
+ }
1621
+ function UserMenu() {
1622
+ const { user, status, signOut } = useShellAuth();
1623
+ const { myProfileHref } = useShellChrome();
1624
+ const linked = useShellLinkedAccounts();
1625
+ const { colorMode, toggleColorMode } = react.useColorMode();
1626
+ if (status !== "authenticated" || !user) return null;
1627
+ const displayName = user.displayName ?? user.email ?? "User";
1628
+ const avatarUrl = avatarUrlFromUser(user);
1629
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Menu, { placement: "bottom-end", children: [
1630
+ /* @__PURE__ */ jsxRuntime.jsx(
1631
+ react.MenuButton,
1632
+ {
1633
+ "aria-label": "Open account menu",
1634
+ as: react.Box,
1635
+ cursor: "pointer",
1636
+ borderRadius: "full",
1637
+ _hover: { opacity: 0.85 },
1638
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Avatar, { size: "sm", name: displayName, src: avatarUrl })
1639
+ }
1640
+ ),
1641
+ /* @__PURE__ */ jsxRuntime.jsx(react.Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(react.MenuList, { zIndex: 1500, children: [
1642
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { px: 3, py: 2, children: [
1643
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "600", noOfLines: 1, children: displayName }),
1644
+ user.email && /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "gray.500", noOfLines: 1, children: user.email })
1645
+ ] }),
1646
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuDivider, {}),
1647
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { as: "a", href: myProfileHref, children: "My Profile" }),
1648
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { onClick: toggleColorMode, children: colorMode === "light" ? "Switch to dark mode" : "Switch to light mode" }),
1649
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { onClick: () => void signOut(), children: "Sign out" }),
1650
+ linked && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1651
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuDivider, {}),
1652
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { px: 3, pt: 1, pb: 1, children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", textTransform: "uppercase", letterSpacing: "0.08em", color: "gray.500", children: "Switch account" }) }),
1653
+ linked.linkedAccounts.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { isDisabled: true, children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "gray.500", children: "No linked accounts" }) }),
1654
+ linked.linkedAccounts.map((acct) => /* @__PURE__ */ jsxRuntime.jsx(
1655
+ react.MenuItem,
1656
+ {
1657
+ onClick: () => {
1658
+ void linked.switchToAccount(acct.id);
1659
+ },
1660
+ children: /* @__PURE__ */ jsxRuntime.jsxs(react.HStack, { spacing: 2, align: "center", children: [
1661
+ /* @__PURE__ */ jsxRuntime.jsx(
1662
+ react.Avatar,
1663
+ {
1664
+ size: "xs",
1665
+ name: acct.displayName ?? acct.email,
1666
+ src: acct.avatarUrl ?? void 0
1667
+ }
1668
+ ),
1669
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Flex, { direction: "column", minW: 0, children: [
1670
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "600", noOfLines: 1, children: acct.displayName ?? acct.email }),
1671
+ acct.displayName && /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "gray.500", noOfLines: 1, children: acct.email })
1672
+ ] })
1673
+ ] })
1674
+ },
1675
+ acct.id
1676
+ )),
1677
+ /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { onClick: linked.onAddAccount, children: "Add account" })
1678
+ ] })
1679
+ ] }) })
1680
+ ] });
1681
+ }
1202
1682
  function Header({ extrasSlot }) {
1203
- const { user, signOut, status } = useShellAuth();
1683
+ const { devNotesMenu } = useShellChrome();
1204
1684
  return /* @__PURE__ */ jsxRuntime.jsxs(
1205
1685
  react.Flex,
1206
1686
  {
@@ -1211,14 +1691,15 @@ function Header({ extrasSlot }) {
1211
1691
  bg: "white",
1212
1692
  borderBottom: "1px solid",
1213
1693
  borderColor: "gray.200",
1694
+ gap: 3,
1214
1695
  children: [
1215
1696
  /* @__PURE__ */ jsxRuntime.jsx(react.Heading, { size: "md", color: "brand.500", children: "politogy" }),
1697
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(OrgSelector, {}) }),
1216
1698
  /* @__PURE__ */ jsxRuntime.jsx(react.Spacer, {}),
1217
- extrasSlot && /* @__PURE__ */ jsxRuntime.jsx(react.Box, { mr: 3, children: extrasSlot }),
1218
- status === "authenticated" && user && /* @__PURE__ */ jsxRuntime.jsxs(react.Menu, { children: [
1219
- /* @__PURE__ */ jsxRuntime.jsx(react.MenuButton, { children: /* @__PURE__ */ jsxRuntime.jsx(react.Avatar, { size: "sm", name: user.displayName ?? user.email ?? "" }) }),
1220
- /* @__PURE__ */ jsxRuntime.jsx(react.MenuList, { children: /* @__PURE__ */ jsxRuntime.jsx(react.MenuItem, { onClick: () => signOut(), children: "Sign out" }) })
1221
- ] })
1699
+ extrasSlot && /* @__PURE__ */ jsxRuntime.jsx(react.Box, { children: extrasSlot }),
1700
+ /* @__PURE__ */ jsxRuntime.jsx(SupportRequestButton, {}),
1701
+ devNotesMenu,
1702
+ /* @__PURE__ */ jsxRuntime.jsx(UserMenu, {})
1222
1703
  ]
1223
1704
  }
1224
1705
  );
@@ -1301,6 +1782,23 @@ function AppLayout({
1301
1782
  overlaySlot
1302
1783
  ] });
1303
1784
  }
1785
+ function ShellChrome({
1786
+ children,
1787
+ organization,
1788
+ linkedAccounts,
1789
+ support,
1790
+ devNotesMenu,
1791
+ myProfileHref
1792
+ }) {
1793
+ const chrome = react$1.useMemo(
1794
+ () => ({
1795
+ devNotesMenu: devNotesMenu ?? null,
1796
+ myProfileHref: myProfileHref ?? "/my-profile"
1797
+ }),
1798
+ [devNotesMenu, myProfileHref]
1799
+ );
1800
+ return /* @__PURE__ */ jsxRuntime.jsx(ShellOrgContext.Provider, { value: organization ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellLinkedAccountsContext.Provider, { value: linkedAccounts ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellSupportProviderContext.Provider, { value: support ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellChromeContext.Provider, { value: chrome, children }) }) }) });
1801
+ }
1304
1802
 
1305
1803
  // src/sw/registerShellPrecache.ts
1306
1804
  async function registerShellPrecache(opts = {}) {
@@ -1339,15 +1837,27 @@ exports.Footer = Footer;
1339
1837
  exports.GlobalUiIdProvider = GlobalUiIdProvider;
1340
1838
  exports.Header = Header;
1341
1839
  exports.HeaderTabBar = HeaderTabBar;
1840
+ exports.OrgSelector = OrgSelector;
1342
1841
  exports.PageContainer = PageContainer;
1343
1842
  exports.PageHeader = PageHeader;
1843
+ exports.ShellChrome = ShellChrome;
1844
+ exports.ShellChromeContext = ShellChromeContext;
1845
+ exports.ShellLinkedAccountsContext = ShellLinkedAccountsContext;
1846
+ exports.ShellOrgContext = ShellOrgContext;
1344
1847
  exports.ShellProvider = ShellProvider;
1848
+ exports.ShellSupportProviderContext = ShellSupportProviderContext;
1345
1849
  exports.Sidebar = Sidebar;
1850
+ exports.SupportRequestButton = SupportRequestButton;
1851
+ exports.UserMenu = UserMenu;
1346
1852
  exports.buildSpeculationRules = buildSpeculationRules;
1347
1853
  exports.politogyTheme = politogyTheme;
1348
1854
  exports.registerShellPrecache = registerShellPrecache;
1349
1855
  exports.useAppRegistry = useAppRegistry;
1350
1856
  exports.useCurrentApp = useCurrentApp;
1351
1857
  exports.useShellAuth = useShellAuth;
1858
+ exports.useShellChrome = useShellChrome;
1859
+ exports.useShellLinkedAccounts = useShellLinkedAccounts;
1860
+ exports.useShellOrganization = useShellOrganization;
1861
+ exports.useShellSupport = useShellSupport;
1352
1862
  //# sourceMappingURL=index.cjs.map
1353
1863
  //# sourceMappingURL=index.cjs.map