@the-portland-company/shell 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ function resolveBrandGlyph(name) {
57
57
  // src/theme-preference.ts
58
58
  var THEME_STORAGE_KEY = "politogy_theme";
59
59
  var THEME_COOKIE = "politogy_theme";
60
+ var THEME_PENDING_COOKIE = "politogy_theme_pending";
60
61
  var VALID = ["light", "dark", "system"];
61
62
  function isThemePreference(v) {
62
63
  return typeof v === "string" && VALID.includes(v);
@@ -83,6 +84,33 @@ function writeThemeCookie(pref) {
83
84
  pref
84
85
  )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
85
86
  }
87
+ function writeSharedCookie(name, value, maxAgeSeconds) {
88
+ if (typeof document === "undefined" || typeof window === "undefined") return;
89
+ const host = window.location.hostname;
90
+ const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
91
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
92
+ document.cookie = `${name}=${encodeURIComponent(
93
+ value
94
+ )}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${domainAttr}${secure}`;
95
+ }
96
+ function markThemeChoicePending(pref) {
97
+ writeSharedCookie(THEME_PENDING_COOKIE, pref, 31536e3);
98
+ }
99
+ function readPendingThemeChoice() {
100
+ if (typeof document === "undefined") return null;
101
+ try {
102
+ const m = document.cookie.match(
103
+ new RegExp(`(?:^|;\\s*)${THEME_PENDING_COOKIE}=([^;]+)`)
104
+ );
105
+ const v = m ? decodeURIComponent(m[1]) : null;
106
+ return isThemePreference(v) ? v : null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ function clearPendingThemeChoice() {
112
+ writeSharedCookie(THEME_PENDING_COOKIE, "", 0);
113
+ }
86
114
  function resolveTheme(pref) {
87
115
  if (pref === "system") {
88
116
  try {
@@ -173,6 +201,42 @@ function TopBarWidgetSlot({
173
201
  /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title, description })
174
202
  ] });
175
203
  }
204
+ var THEME_OPTIONS = [
205
+ { value: "light", label: "Light" },
206
+ { value: "system", label: "System" },
207
+ { value: "dark", label: "Dark" }
208
+ ];
209
+ function NavThemeControl({
210
+ value,
211
+ onChange
212
+ }) {
213
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
214
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Appearance" }),
215
+ /* @__PURE__ */ jsxRuntime.jsx(
216
+ "div",
217
+ {
218
+ role: "radiogroup",
219
+ "aria-label": "Appearance",
220
+ className: "flex overflow-hidden rounded-full border border-[color:var(--line)]",
221
+ children: THEME_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ role: "radio",
226
+ "aria-checked": value === opt.value,
227
+ onClick: () => onChange(opt.value),
228
+ className: [
229
+ "flex-1 px-3 py-1.5 text-xs font-semibold transition",
230
+ value === opt.value ? "bg-[color:var(--color-race-red)] text-white" : "text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]"
231
+ ].join(" "),
232
+ children: opt.label
233
+ },
234
+ opt.value
235
+ ))
236
+ }
237
+ )
238
+ ] });
239
+ }
176
240
  var NavChromeContext = react.createContext(null);
177
241
  function NavChromeProvider({
178
242
  fetchJson,
@@ -194,6 +258,10 @@ function NavChromeProvider({
194
258
  }, [fetchJson]);
195
259
  return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, me, meLoaded }, children });
196
260
  }
261
+ function useChromeMe() {
262
+ const ctx = react.useContext(NavChromeContext);
263
+ return { me: ctx?.me ?? null, loaded: ctx?.meLoaded ?? false };
264
+ }
197
265
  function useNavChromeFetcher() {
198
266
  return react.useContext(NavChromeContext)?.fetchJson ?? null;
199
267
  }
@@ -661,6 +729,25 @@ function useThemePreference() {
661
729
  const meCtx = react.useContext(NavChromeContext);
662
730
  react.useEffect(() => {
663
731
  if (reconciled.current) return;
732
+ const pending = readPendingThemeChoice();
733
+ if (pending) {
734
+ if (meCtx?.me) reconciled.current = true;
735
+ setValueState(pending);
736
+ applyThemePreference(pending);
737
+ writeThemeCookie(pending);
738
+ try {
739
+ localStorage.setItem(THEME_STORAGE_KEY, pending);
740
+ } catch {
741
+ }
742
+ if (!meCtx?.me) return;
743
+ fetchJson?.("me/preferences", {
744
+ method: "PATCH",
745
+ headers: { "Content-Type": "application/json" },
746
+ body: JSON.stringify({ themePreference: pending })
747
+ }).then(() => clearPendingThemeChoice()).catch(() => {
748
+ });
749
+ return;
750
+ }
664
751
  const pref = meCtx?.me?.themePreference;
665
752
  if (isThemePreference(pref)) {
666
753
  reconciled.current = true;
@@ -719,17 +806,1281 @@ function useThemePreference() {
719
806
  localStorage.setItem(THEME_STORAGE_KEY, next);
720
807
  } catch {
721
808
  }
809
+ markThemeChoicePending(next);
722
810
  fetchJson?.("me/preferences", {
723
811
  method: "PATCH",
724
812
  headers: { "Content-Type": "application/json" },
725
813
  body: JSON.stringify({ themePreference: next })
726
- }).catch(() => {
814
+ }).then(() => clearPendingThemeChoice()).catch(() => {
727
815
  });
728
816
  },
729
817
  [fetchJson]
730
818
  );
731
819
  return { value, setValue };
732
820
  }
821
+
822
+ // src/native/standardNav.ts
823
+ var CONTACTS_DEFAULT_CHILDREN = [
824
+ { label: "Dashboard", icon: "grid", path: "/relationship/contacts/dashboard" },
825
+ { label: "All Contacts", icon: "list", path: "/relationship/contacts/all" },
826
+ { label: "Tribes", icon: "users", path: "/relationship/contacts/groups" },
827
+ { label: "Import", icon: "upload", path: "/relationship/contacts/import" },
828
+ { label: "Import History", icon: "fileText", path: "/relationship/contacts/import-history" },
829
+ { label: "Merge Rules", icon: "layout", path: "/relationship/contacts/merge-rules" }
830
+ ];
831
+ var EMAIL_CHILDREN = [
832
+ { label: "Dashboard", icon: "grid", path: "/relationship/email/dashboard" },
833
+ { label: "Create & Edit", icon: "edit", path: "/relationship/email/create-and-edit" },
834
+ { label: "Sender Information", icon: "mail", path: "/relationship/email/inbox" },
835
+ { label: "Recipients", icon: "user", path: "/relationship/email/send-to" },
836
+ { label: "Send & Automate", icon: "clock", path: "/relationship/email/send-and-automate" },
837
+ { label: "Statistics", icon: "barChart", path: "/relationship/email/statistics" }
838
+ ];
839
+ var SMS_CHILDREN = [
840
+ { label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
841
+ { label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
842
+ { label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
843
+ ];
844
+ var CTA_CHILDREN = [
845
+ { label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
846
+ { label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
847
+ { label: "Submissions", icon: "fileText", path: "/relationship/cta-submissions/submissions" }
848
+ ];
849
+ var ORG_SETTINGS_CHILDREN = [
850
+ { label: "General", icon: "settings", path: "/organization-settings" },
851
+ {
852
+ label: "Subscription & Billing",
853
+ icon: "creditCard",
854
+ path: "/organization-settings/subscription-billing",
855
+ children: [
856
+ { label: "Current Plan", icon: "fileText", path: "/organization-settings/subscription-billing/current-plan" },
857
+ { label: "SMS, MMS & Email Usage", icon: "activity", path: "/organization-settings/subscription-billing/text-email" },
858
+ { label: "Wallet", icon: "dollarSign", path: "/organization-settings/subscription-billing/wallet" },
859
+ { label: "10DLC", icon: "messageSquare", path: "/organization-settings/subscription-billing/10dlc" },
860
+ { label: "Billing History", icon: "clock", path: "/organization-settings/subscription-billing/billing-history" }
861
+ ]
862
+ },
863
+ { label: "Email", icon: "mail", path: "/organization-settings/email-settings" },
864
+ { label: "Users", icon: "users", path: "/organization-settings/users" },
865
+ { label: "Shared Links", icon: "share", path: "/organization-settings/shared-links" }
866
+ ];
867
+ var APP_FORMS_CHILDREN = [
868
+ { label: "Forms", icon: "list", href: "https://forms.politogyvrm.com/?tab=forms" },
869
+ { label: "Canvas", icon: "edit", href: "https://forms.politogyvrm.com/?tab=canvas" },
870
+ { label: "Submissions", icon: "fileText", href: "https://forms.politogyvrm.com/?tab=submissions" }
871
+ ];
872
+ var APP_SURVEYS_CHILDREN = [
873
+ { label: "Surveys", icon: "list", href: "https://surveys.politogyvrm.com/surveys" }
874
+ ];
875
+ var APP_POLLS_CHILDREN = [
876
+ { label: "Command Center", icon: "grid", href: "https://polls.politogyvrm.com/" },
877
+ { label: "New Poll", icon: "edit", href: "https://polls.politogyvrm.com/polls/new" }
878
+ ];
879
+ function standardPolitogyNav(opts) {
880
+ const base = opts.coreBaseUrl.replace(/\/+$/, "");
881
+ const abs = (path) => `${base}${path.startsWith("/") ? path : `/${path}`}`;
882
+ const icon = (key) => opts.icons?.[key];
883
+ const leaves = (children) => children.map((c) => ({ label: c.label, icon: icon(c.icon), href: abs(c.path) }));
884
+ const branches = (children) => children.map((c) => ({
885
+ label: c.label,
886
+ icon: icon(c.icon),
887
+ href: abs(c.path),
888
+ children: c.children ? leaves(c.children) : void 0
889
+ }));
890
+ const contacts = opts.contactsItem ? {
891
+ label: "Contacts",
892
+ icon: icon("users"),
893
+ href: opts.contactsItem.href,
894
+ children: opts.contactsItem.children
895
+ } : {
896
+ label: "Contacts",
897
+ icon: icon("users"),
898
+ href: abs("/relationship/contacts/dashboard"),
899
+ children: leaves(CONTACTS_DEFAULT_CHILDREN)
900
+ };
901
+ const items = [
902
+ { label: "Dashboard", icon: icon("home"), href: abs("/relationship/dashboard") },
903
+ contacts,
904
+ {
905
+ label: "Email Broadcast",
906
+ icon: icon("mail"),
907
+ href: abs("/relationship/email/dashboard"),
908
+ children: leaves(EMAIL_CHILDREN)
909
+ },
910
+ {
911
+ label: "SMS/MMS Blast",
912
+ icon: icon("messageSquare"),
913
+ href: abs("/relationship/sms-mms-blast/dashboard"),
914
+ children: leaves(SMS_CHILDREN)
915
+ },
916
+ // These two carry no top-level href in VRM core (parent row only toggles).
917
+ { label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
918
+ {
919
+ label: "Forms",
920
+ icon: icon("activity"),
921
+ href: "https://forms.politogyvrm.com/",
922
+ children: APP_FORMS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: c.href }))
923
+ },
924
+ {
925
+ label: "Surveys",
926
+ icon: icon("fileText"),
927
+ href: "https://surveys.politogyvrm.com/surveys",
928
+ children: APP_SURVEYS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: c.href }))
929
+ },
930
+ {
931
+ label: "Polls",
932
+ icon: icon("barChart"),
933
+ href: "https://polls.politogyvrm.com/",
934
+ children: APP_POLLS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: c.href }))
935
+ },
936
+ {
937
+ label: "Organization Settings",
938
+ icon: icon("briefcase"),
939
+ href: abs("/organization-settings"),
940
+ children: branches(ORG_SETTINGS_CHILDREN)
941
+ }
942
+ ];
943
+ if (opts.supportItem) {
944
+ items.push({
945
+ label: "Support",
946
+ icon: icon("lifeBuoy"),
947
+ href: opts.supportItem.href,
948
+ onClick: opts.supportItem.onClick
949
+ });
950
+ }
951
+ return items;
952
+ }
953
+ var safeHref = (h) => h && /^(https?:|mailto:|tel:|\/|#|\.)/i.test(h.trim()) ? h : "#";
954
+ var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: safeHref(href), ...rest, children });
955
+ function isNavItemActive(pathname, href) {
956
+ let targetPath = href;
957
+ let targetSearch = "";
958
+ if (/^https?:\/\//i.test(href)) {
959
+ if (typeof window === "undefined") return false;
960
+ try {
961
+ const u = new URL(href);
962
+ if (u.host !== window.location.host) return false;
963
+ targetPath = u.pathname;
964
+ targetSearch = u.search;
965
+ } catch {
966
+ return false;
967
+ }
968
+ } else {
969
+ const qi = href.indexOf("?");
970
+ if (qi >= 0) {
971
+ targetPath = href.slice(0, qi) || "/";
972
+ targetSearch = href.slice(qi);
973
+ }
974
+ }
975
+ if (targetSearch) {
976
+ if (typeof window === "undefined") return false;
977
+ if (pathname !== targetPath) return false;
978
+ const want = new URLSearchParams(targetSearch);
979
+ const have = new URLSearchParams(window.location.search);
980
+ for (const [k, v] of want) if (have.get(k) !== v) return false;
981
+ return true;
982
+ }
983
+ if (targetPath === "/") return pathname === "/";
984
+ return pathname === targetPath || pathname.startsWith(`${targetPath}/`);
985
+ }
986
+ function groupKeyOf(item, index) {
987
+ return item.href ?? `nav-group-${index}-${item.label}`;
988
+ }
989
+ function hasActiveChild(item, pathname, activeFn) {
990
+ return (item.children ?? []).some(
991
+ (child) => (child.href ? activeFn(pathname, child.href) : false) || // Depth-3: a grandchild owning the route also opens the top group.
992
+ (child.children ?? []).some(
993
+ (leaf) => leaf.href ? activeFn(pathname, leaf.href) : false
994
+ )
995
+ );
996
+ }
997
+ function envChipClasses(label) {
998
+ const l = label.toLowerCase();
999
+ if (l === "production" || l === "prod")
1000
+ return "bg-[#fee2e2] text-[#ef4444]";
1001
+ if (l === "staging" || l === "stage")
1002
+ return "bg-[#ffedd5] text-[#f97316]";
1003
+ if (l === "local" || l === "development" || l === "dev")
1004
+ return "bg-[#dcfce7] text-[#22c55e]";
1005
+ return "bg-[rgba(107,114,128,0.16)] text-[#6b7280]";
1006
+ }
1007
+ var ORG_PILL_DEFAULT = "var(--color-race-red)";
1008
+ function NavSidebar({
1009
+ pathname,
1010
+ brand,
1011
+ nav,
1012
+ org,
1013
+ mode,
1014
+ footer,
1015
+ linkComponent
1016
+ }) {
1017
+ const Link = linkComponent ?? DefaultLink;
1018
+ const activeFn = nav.isActive ?? isNavItemActive;
1019
+ const ModeIcon = mode?.icon;
1020
+ const orgBg = org?.brandColor || ORG_PILL_DEFAULT;
1021
+ const activeGroupKey = react.useMemo(() => {
1022
+ for (let i = 0; i < nav.items.length; i++) {
1023
+ const item = nav.items[i];
1024
+ if (item.children && item.children.length > 0 && hasActiveChild(item, pathname, activeFn)) {
1025
+ return groupKeyOf(item, i);
1026
+ }
1027
+ }
1028
+ return null;
1029
+ }, [nav.items, pathname, activeFn]);
1030
+ const [openGroup, setOpenGroup] = react.useState(activeGroupKey);
1031
+ react.useEffect(() => {
1032
+ setOpenGroup(activeGroupKey);
1033
+ }, [activeGroupKey]);
1034
+ const toggleGroup = (key) => setOpenGroup((prev) => prev === key ? null : key);
1035
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "fixed inset-y-0 left-0 z-30 hidden w-[240px] flex-col border-r border-[color:var(--line)] bg-[color:var(--nav-surface)] lg:flex", children: [
1036
+ /* @__PURE__ */ jsxRuntime.jsx(
1037
+ Link,
1038
+ {
1039
+ href: brand.homeHref ?? "/",
1040
+ className: "flex h-[64px] flex-shrink-0 items-center gap-2.5 px-6",
1041
+ children: brand.logoSrc ? /* @__PURE__ */ jsxRuntime.jsx(
1042
+ "img",
1043
+ {
1044
+ src: brand.logoSrc,
1045
+ alt: brand.logoAlt ?? brand.title,
1046
+ draggable: false,
1047
+ className: "h-10 w-auto object-contain"
1048
+ }
1049
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1050
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-[color:var(--color-race-red)] text-base font-black text-white shadow-sm", children: brand.badgeLabel }),
1051
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col leading-tight", children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base font-extrabold tracking-[-0.01em] text-[color:var(--color-race-red)]", children: brand.title }),
1053
+ brand.subtitle ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--muted)]", children: brand.subtitle }) : null
1054
+ ] })
1055
+ ] })
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col overflow-y-auto px-4 pb-2", children: [
1059
+ org ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3", children: [
1060
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between px-1", children: [
1061
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
1062
+ org.id ? /* @__PURE__ */ jsxRuntime.jsx(OrgIdCopyButton, { id: org.id }) : null
1063
+ ] }),
1064
+ org.organizations && org.organizations.length > 0 && org.onSwitch ? /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group relative", children: [
1065
+ /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)] [&::-webkit-details-marker]:hidden", children: [
1066
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1067
+ /* @__PURE__ */ jsxRuntime.jsx(
1068
+ "span",
1069
+ {
1070
+ style: { backgroundColor: orgBg },
1071
+ className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full text-[11px] font-bold text-white",
1072
+ children: org.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: org.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1073
+ }
1074
+ ),
1075
+ /* @__PURE__ */ jsxRuntime.jsx(
1076
+ "span",
1077
+ {
1078
+ title: org.name ?? void 0,
1079
+ className: "truncate text-sm font-semibold",
1080
+ children: org.name ?? "No organizations"
1081
+ }
1082
+ )
1083
+ ] }),
1084
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, {})
1085
+ ] }),
1086
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 right-0 z-40 mt-1 max-h-72 overflow-y-auto rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: org.organizations.map((o) => {
1087
+ const selected = o.id === org.id;
1088
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1089
+ "button",
1090
+ {
1091
+ type: "button",
1092
+ onClick: (e) => {
1093
+ e.currentTarget.closest("details")?.removeAttribute("open");
1094
+ if (!selected) org.onSwitch?.(o.id);
1095
+ },
1096
+ "aria-current": selected ? "true" : void 0,
1097
+ className: [
1098
+ "flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm",
1099
+ selected ? "font-semibold text-[color:var(--color-race-red)]" : "font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]"
1100
+ ].join(" "),
1101
+ children: [
1102
+ /* @__PURE__ */ jsxRuntime.jsx(
1103
+ "span",
1104
+ {
1105
+ style: { backgroundColor: o.brandColor || ORG_PILL_DEFAULT },
1106
+ className: "flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-[10px] font-bold text-white",
1107
+ children: o.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: o.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (o.name ?? "Org").slice(0, 2).toUpperCase()
1108
+ }
1109
+ ),
1110
+ /* @__PURE__ */ jsxRuntime.jsx("span", { title: o.name ?? o.id, className: "truncate", children: o.name ?? o.id.slice(0, 8) })
1111
+ ]
1112
+ },
1113
+ o.id
1114
+ );
1115
+ }) })
1116
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
1117
+ "button",
1118
+ {
1119
+ type: "button",
1120
+ onClick: org.onClick,
1121
+ className: "flex w-full items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)]",
1122
+ children: [
1123
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1124
+ /* @__PURE__ */ jsxRuntime.jsx(
1125
+ "span",
1126
+ {
1127
+ style: { backgroundColor: orgBg },
1128
+ className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full text-[11px] font-bold text-white",
1129
+ children: org.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: org.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase()
1130
+ }
1131
+ ),
1132
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-sm font-semibold", children: org.name ?? "No organizations" })
1133
+ ] }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, {})
1135
+ ]
1136
+ }
1137
+ )
1138
+ ] }) : null,
1139
+ mode ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: mode.options && mode.options.length > 1 && mode.onSelect ? /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group relative", children: [
1140
+ /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md bg-[color:var(--color-race-red)] px-2.5 py-2 text-white shadow-sm transition hover:opacity-95 [&::-webkit-details-marker]:hidden", children: [
1141
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2.5", children: [
1142
+ ModeIcon ? /* @__PURE__ */ jsxRuntime.jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
1143
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold tracking-[-0.01em]", children: mode.label })
1144
+ ] }),
1145
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, {})
1146
+ ] }),
1147
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 right-0 z-40 mt-1 overflow-hidden rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: mode.options.map((opt) => {
1148
+ const selected = opt.label === mode.label;
1149
+ const OptIcon = opt.icon;
1150
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1151
+ "button",
1152
+ {
1153
+ type: "button",
1154
+ onClick: (e) => {
1155
+ e.currentTarget.closest("details")?.removeAttribute("open");
1156
+ if (!selected) mode.onSelect?.(opt.id);
1157
+ },
1158
+ "aria-current": selected ? "true" : void 0,
1159
+ className: [
1160
+ "flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm",
1161
+ selected ? "font-semibold text-[color:var(--color-race-red)]" : "font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]"
1162
+ ].join(" "),
1163
+ children: [
1164
+ OptIcon ? /* @__PURE__ */ jsxRuntime.jsx(OptIcon, { size: 15, className: "shrink-0 text-current" }) : null,
1165
+ opt.label
1166
+ ]
1167
+ },
1168
+ opt.id
1169
+ );
1170
+ }) })
1171
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
1172
+ "button",
1173
+ {
1174
+ type: "button",
1175
+ className: "flex w-full items-center justify-between gap-2 rounded-md bg-[color:var(--color-race-red)] px-2.5 py-2 text-white shadow-sm",
1176
+ children: [
1177
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2.5", children: [
1178
+ ModeIcon ? /* @__PURE__ */ jsxRuntime.jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
1179
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold tracking-[-0.01em]", children: mode.label })
1180
+ ] }),
1181
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, {})
1182
+ ]
1183
+ }
1184
+ ) }) : null,
1185
+ /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "flex flex-col gap-1", children: [
1186
+ nav.groupLabel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: nav.groupLabel }) : null,
1187
+ nav.items.map((item, idx) => {
1188
+ if (item.children && item.children.length > 0) {
1189
+ const key = groupKeyOf(item, idx);
1190
+ const selfActive = item.href ? activeFn(pathname, item.href) : false;
1191
+ const parentActive = selfActive || hasActiveChild(item, pathname, activeFn);
1192
+ return /* @__PURE__ */ jsxRuntime.jsx(
1193
+ NavGroup,
1194
+ {
1195
+ item,
1196
+ active: parentActive,
1197
+ open: openGroup === key,
1198
+ onToggle: () => toggleGroup(key),
1199
+ pathname,
1200
+ activeFn,
1201
+ Link
1202
+ },
1203
+ key
1204
+ );
1205
+ }
1206
+ const active = item.href ? activeFn(pathname, item.href) : false;
1207
+ return /* @__PURE__ */ jsxRuntime.jsx(
1208
+ NavRow,
1209
+ {
1210
+ item,
1211
+ active,
1212
+ Link
1213
+ },
1214
+ item.href ?? `nav-${idx}-${item.label}`
1215
+ );
1216
+ })
1217
+ ] })
1218
+ ] }),
1219
+ footer ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 border-t border-[color:var(--line)] px-4 py-3", children: [
1220
+ footer.links && footer.links.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center justify-center gap-1.5 text-xs", children: footer.links.map((l, idx) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
1221
+ idx > 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }) : null,
1222
+ /* @__PURE__ */ jsxRuntime.jsx(
1223
+ "a",
1224
+ {
1225
+ href: safeHref(l.href),
1226
+ className: "font-medium text-[color:var(--foreground)] opacity-80 hover:opacity-100 hover:underline",
1227
+ children: l.label
1228
+ }
1229
+ )
1230
+ ] }, l.label)) }) : null,
1231
+ footer.envLabel ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1.5 flex flex-col items-center gap-0.5 font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--muted)]", children: [
1232
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
1233
+ /* @__PURE__ */ jsxRuntime.jsx(
1234
+ "span",
1235
+ {
1236
+ className: [
1237
+ "rounded-md px-1.5 py-0.5 text-[10px] font-semibold",
1238
+ envChipClasses(footer.envLabel)
1239
+ ].join(" "),
1240
+ children: footer.envLabel
1241
+ }
1242
+ ),
1243
+ footer.commit ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1244
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
1245
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commit.slice(-4) })
1246
+ ] }) : null,
1247
+ footer.version ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1248
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
1249
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1250
+ "v",
1251
+ footer.version
1252
+ ] })
1253
+ ] }) : null
1254
+ ] }),
1255
+ footer.commitDate || footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
1256
+ footer.commitDate ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(footer.commitDate).toLocaleDateString(void 0, {
1257
+ month: "short",
1258
+ day: "numeric"
1259
+ }) }) : null,
1260
+ footer.commitDate && footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }) : null,
1261
+ footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commitAuthor.split(/\s+/)[0] }) : null
1262
+ ] }) : null
1263
+ ] }) : null
1264
+ ] }) : null
1265
+ ] });
1266
+ }
1267
+ function ChevronDown({ className } = {}) {
1268
+ return /* @__PURE__ */ jsxRuntime.jsx(
1269
+ "svg",
1270
+ {
1271
+ width: 16,
1272
+ height: 16,
1273
+ viewBox: "0 0 24 24",
1274
+ fill: "none",
1275
+ stroke: "currentColor",
1276
+ strokeWidth: 2,
1277
+ strokeLinecap: "round",
1278
+ strokeLinejoin: "round",
1279
+ className: className ?? "shrink-0 opacity-80",
1280
+ "aria-hidden": "true",
1281
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 9 6 6 6-6" })
1282
+ }
1283
+ );
1284
+ }
1285
+ var NAV_HOVER_GRADIENT = "linear-gradient(90deg,#EA4751 0%,#E63946 100%)";
1286
+ function NavRow({
1287
+ item,
1288
+ active,
1289
+ Link,
1290
+ variant = "top"
1291
+ }) {
1292
+ const Icon = item.icon;
1293
+ const isSub = variant === "sub";
1294
+ const activeClasses = isSub ? "bg-[color:var(--panel-elevated)] text-[color:var(--color-race-red)] shadow-sm" : "bg-[color:var(--color-race-red)] text-white shadow-sm";
1295
+ const wrapperClasses = [
1296
+ "group relative block overflow-hidden rounded-md transition",
1297
+ active ? activeClasses : "text-[color:var(--muted)] hover:text-white"
1298
+ ].join(" ");
1299
+ const rowPadding = isSub ? "px-3 py-1.5 text-[13px]" : "px-3 py-2 text-sm";
1300
+ const iconSize = isSub ? 16 : 18;
1301
+ const iconClass = active && !isSub ? "text-white" : "text-current";
1302
+ const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1303
+ !active ? /* @__PURE__ */ jsxRuntime.jsx(
1304
+ "span",
1305
+ {
1306
+ "aria-hidden": "true",
1307
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
1308
+ style: { background: NAV_HOVER_GRADIENT }
1309
+ }
1310
+ ) : null,
1311
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `relative z-10 flex items-center gap-3 font-semibold ${rowPadding}`, children: [
1312
+ Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: iconSize, className: iconClass }) : null,
1313
+ item.label
1314
+ ] })
1315
+ ] });
1316
+ if (item.onClick && !item.href) {
1317
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: item.onClick, className: `w-full text-left ${wrapperClasses}`, children: inner });
1318
+ }
1319
+ return /* @__PURE__ */ jsxRuntime.jsx(
1320
+ Link,
1321
+ {
1322
+ href: item.href ?? "#",
1323
+ "aria-current": active ? "page" : void 0,
1324
+ className: wrapperClasses,
1325
+ children: inner
1326
+ }
1327
+ );
1328
+ }
1329
+ function NavGroup({
1330
+ item,
1331
+ active,
1332
+ open,
1333
+ onToggle,
1334
+ pathname,
1335
+ activeFn,
1336
+ Link
1337
+ }) {
1338
+ const Icon = item.icon;
1339
+ const headerClasses = [
1340
+ "group relative block w-full overflow-hidden text-left transition",
1341
+ open ? "rounded-t-md" : "rounded-md",
1342
+ active ? "bg-[color:var(--color-race-red)] text-white shadow-sm" : open ? "bg-[color:var(--panel)] text-[color:var(--foreground)]" : "text-[color:var(--muted)] hover:text-white"
1343
+ ].join(" ");
1344
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1345
+ /* @__PURE__ */ jsxRuntime.jsxs(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ onClick: onToggle,
1350
+ "aria-expanded": open,
1351
+ className: headerClasses,
1352
+ children: [
1353
+ !active && !open ? /* @__PURE__ */ jsxRuntime.jsx(
1354
+ "span",
1355
+ {
1356
+ "aria-hidden": "true",
1357
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
1358
+ style: { background: NAV_HOVER_GRADIENT }
1359
+ }
1360
+ ) : null,
1361
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative z-10 flex items-center gap-3 px-3 py-2 text-sm font-semibold", children: [
1362
+ Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 18, className: active ? "text-white" : "text-current" }) : null,
1363
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: item.label }),
1364
+ /* @__PURE__ */ jsxRuntime.jsx(
1365
+ ChevronDown,
1366
+ {
1367
+ className: [
1368
+ "shrink-0 opacity-80 transition-transform duration-200",
1369
+ open ? "rotate-180" : ""
1370
+ ].join(" ")
1371
+ }
1372
+ )
1373
+ ] })
1374
+ ]
1375
+ }
1376
+ ),
1377
+ open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-b-md bg-[color:var(--panel)] p-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", children: item.children.map((child, i) => {
1378
+ const childActive = child.href ? activeFn(pathname, child.href) : false;
1379
+ if (child.children && child.children.length > 0) {
1380
+ return /* @__PURE__ */ jsxRuntime.jsx(
1381
+ NavSubGroup,
1382
+ {
1383
+ item: child,
1384
+ pathname,
1385
+ activeFn,
1386
+ Link
1387
+ },
1388
+ child.href ?? `subgrp-${i}-${child.label}`
1389
+ );
1390
+ }
1391
+ return /* @__PURE__ */ jsxRuntime.jsx(
1392
+ NavRow,
1393
+ {
1394
+ item: child,
1395
+ active: childActive,
1396
+ Link,
1397
+ variant: "sub"
1398
+ },
1399
+ child.href ?? `sub-${i}-${child.label}`
1400
+ );
1401
+ }) }) }) : null
1402
+ ] });
1403
+ }
1404
+ function NavSubGroup({
1405
+ item,
1406
+ pathname,
1407
+ activeFn,
1408
+ Link
1409
+ }) {
1410
+ const ownsActive = (item.href ? activeFn(pathname, item.href) : false) || (item.children ?? []).some((c) => c.href ? activeFn(pathname, c.href) : false);
1411
+ const [open, setOpen] = react.useState(ownsActive);
1412
+ react.useEffect(() => {
1413
+ if (ownsActive) setOpen(true);
1414
+ }, [ownsActive]);
1415
+ const Icon = item.icon;
1416
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1417
+ /* @__PURE__ */ jsxRuntime.jsxs(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ onClick: () => setOpen((v) => !v),
1422
+ "aria-expanded": open,
1423
+ className: [
1424
+ "flex w-full items-center gap-2.5 rounded px-3 py-1.5 text-left text-[13px] font-medium transition",
1425
+ ownsActive ? "text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] hover:bg-[color:var(--panel-elevated)] hover:text-[color:var(--foreground)]"
1426
+ ].join(" "),
1427
+ children: [
1428
+ Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 15, className: "shrink-0 text-current" }) : null,
1429
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: item.label }),
1430
+ /* @__PURE__ */ jsxRuntime.jsx(
1431
+ ChevronDown,
1432
+ {
1433
+ className: [
1434
+ "shrink-0 opacity-70 transition-transform duration-200",
1435
+ open ? "rotate-180" : ""
1436
+ ].join(" ")
1437
+ }
1438
+ )
1439
+ ]
1440
+ }
1441
+ ),
1442
+ open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-4 flex flex-col gap-0.5 border-l border-[color:var(--line)] pl-2", children: item.children.map((leaf, i) => /* @__PURE__ */ jsxRuntime.jsx(
1443
+ NavRow,
1444
+ {
1445
+ item: leaf,
1446
+ active: leaf.href ? activeFn(pathname, leaf.href) : false,
1447
+ Link,
1448
+ variant: "sub"
1449
+ },
1450
+ leaf.href ?? `subsub-${i}-${leaf.label}`
1451
+ )) }) : null
1452
+ ] });
1453
+ }
1454
+ function OrgIdCopyButton({ id }) {
1455
+ const [copied, setCopied] = react.useState(false);
1456
+ return /* @__PURE__ */ jsxRuntime.jsx(
1457
+ "button",
1458
+ {
1459
+ type: "button",
1460
+ onClick: () => {
1461
+ try {
1462
+ void navigator.clipboard?.writeText(id);
1463
+ } catch {
1464
+ }
1465
+ setCopied(true);
1466
+ setTimeout(() => setCopied(false), 1500);
1467
+ },
1468
+ title: "Copy organization id",
1469
+ className: "rounded px-1 text-[11px] text-[color:var(--muted)] hover:bg-[color:var(--panel)]",
1470
+ children: copied ? "Copied" : `...${id.slice(-8)}`
1471
+ }
1472
+ );
1473
+ }
1474
+ function NavTopBar({
1475
+ title,
1476
+ subtitle,
1477
+ breadcrumbs,
1478
+ user,
1479
+ userMenu,
1480
+ onSignOut,
1481
+ actionsList,
1482
+ actions,
1483
+ themeToggle
1484
+ }) {
1485
+ const rawName = user?.name?.trim() || "";
1486
+ const hasRealName = rawName.length > 0 && !rawName.includes("@");
1487
+ const displayName = hasRealName ? formatFirstLast(rawName) : user?.email ?? "Signed in";
1488
+ const initials = (hasRealName ? rawName : user?.email ?? "P").slice(0, 2).toUpperCase();
1489
+ const secondary = hasRealName ? user?.email || [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member" : [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member";
1490
+ return /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "sticky top-0 z-20 flex h-20 items-center justify-between gap-4 border-b border-[color:var(--line)] bg-[color:var(--panel)] px-4 backdrop-blur-xl", children: [
1491
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
1492
+ /* @__PURE__ */ jsxRuntime.jsx(
1493
+ "h1",
1494
+ {
1495
+ className: "truncate text-lg font-bold tracking-[-0.01em] text-[color:var(--foreground)]",
1496
+ "data-display": "true",
1497
+ children: title
1498
+ }
1499
+ ),
1500
+ breadcrumbs && breadcrumbs.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Breadcrumb", className: "flex min-w-0 items-center gap-1.5 text-sm text-[color:var(--muted)]", children: breadcrumbs.map((b, i) => {
1501
+ const last = i === breadcrumbs.length - 1;
1502
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1503
+ i > 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-[color:var(--muted)] opacity-60", children: "\u203A" }) : null,
1504
+ b.href && !last ? /* @__PURE__ */ jsxRuntime.jsx(
1505
+ "a",
1506
+ {
1507
+ href: safeHref(b.href),
1508
+ className: "truncate hover:text-[color:var(--foreground)] hover:underline",
1509
+ children: b.label
1510
+ }
1511
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1512
+ "span",
1513
+ {
1514
+ "aria-current": last ? "page" : void 0,
1515
+ className: ["truncate", last ? "text-[color:var(--foreground)]" : ""].join(" "),
1516
+ children: b.label
1517
+ }
1518
+ )
1519
+ ] }, `${b.label}-${i}`);
1520
+ }) }) : subtitle ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm text-[color:var(--muted)]", children: subtitle }) : null
1521
+ ] }),
1522
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex shrink-0 items-center gap-[var(--topbar-icon-gap,8px)]", children: [
1523
+ actionsList && actionsList.length > 0 ? actionsList.map((action) => /* @__PURE__ */ jsxRuntime.jsx(NavTopBarActionButton, { action }, action.key)) : null,
1524
+ actions,
1525
+ themeToggle,
1526
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group/acct relative", children: [
1527
+ /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "group/tbtip relative flex cursor-pointer list-none items-center gap-2.5 rounded-full py-1 pl-1 pr-2 transition hover:bg-[color:var(--panel-elevated)] [&::-webkit-details-marker]:hidden", children: [
1528
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-[var(--topbar-icon-button-size,32px)] w-[var(--topbar-icon-button-size,32px)] items-center justify-center overflow-hidden rounded-full bg-[color:var(--color-race-red)] text-xs font-bold text-white", children: user?.avatarUrl || user?.memojiUrl ? /* @__PURE__ */ jsxRuntime.jsx(
1529
+ "img",
1530
+ {
1531
+ src: user.avatarUrl || user.memojiUrl,
1532
+ alt: "",
1533
+ className: "h-full w-full object-cover"
1534
+ }
1535
+ ) : initials }),
1536
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden flex-col leading-tight md:flex", children: [
1537
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: displayName }),
1538
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-[color:var(--muted)]", children: secondary })
1539
+ ] }),
1540
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, {}),
1541
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "group-open/acct:hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
1542
+ TopBarTooltip,
1543
+ {
1544
+ title: "My Account",
1545
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1546
+ hasRealName ? rawName : user?.email ?? "Signed in",
1547
+ user?.email ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1548
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
1549
+ user.email
1550
+ ] }) : null,
1551
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
1552
+ "Profile, organizations, appearance and sign out."
1553
+ ] })
1554
+ }
1555
+ ) })
1556
+ ] }),
1557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 z-30 mt-2 w-72 rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: [
1558
+ /* @__PURE__ */ jsxRuntime.jsx(TopBarPanelArrow, {}),
1559
+ userMenu?.myProfileHref ? /* @__PURE__ */ jsxRuntime.jsxs(
1560
+ "a",
1561
+ {
1562
+ href: safeHref(userMenu.myProfileHref),
1563
+ className: "flex w-full items-center gap-2.5 px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
1564
+ children: [
1565
+ /* @__PURE__ */ jsxRuntime.jsx(ProfileIcon, {}),
1566
+ "My Profile"
1567
+ ]
1568
+ }
1569
+ ) : null,
1570
+ userMenu?.organizationSettingsHref ? /* @__PURE__ */ jsxRuntime.jsxs(
1571
+ "a",
1572
+ {
1573
+ href: safeHref(userMenu.organizationSettingsHref),
1574
+ className: "flex w-full items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
1575
+ children: [
1576
+ /* @__PURE__ */ jsxRuntime.jsx(BriefcaseIcon, {}),
1577
+ "Organization Settings"
1578
+ ]
1579
+ }
1580
+ ) : null,
1581
+ userMenu?.roles && userMenu.roles.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
1582
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Roles & Organizations" }),
1583
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1.5", children: userMenu.roles.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex flex-wrap items-center gap-1.5 text-sm", children: [
1584
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-md bg-[color:var(--panel)] px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-[color:var(--foreground)]", children: r.role }),
1585
+ r.orgName ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[color:var(--muted)]", children: [
1586
+ "in ",
1587
+ r.orgName
1588
+ ] }) : null
1589
+ ] }, `${r.role}-${i}`)) })
1590
+ ] }) : null,
1591
+ userMenu?.accounts && userMenu.accounts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-[color:var(--line)] px-4 py-3", children: [
1592
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Accounts" }),
1593
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: userMenu.accounts.map((a) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-2.5", children: [
1594
+ /* @__PURE__ */ jsxRuntime.jsxs(
1595
+ "button",
1596
+ {
1597
+ type: "button",
1598
+ onClick: () => {
1599
+ if (!a.active) userMenu.onSwitchAccount?.(a.id);
1600
+ },
1601
+ className: "flex min-w-0 flex-1 items-center gap-2.5 rounded-lg px-1 py-1 text-left hover:bg-[color:var(--panel)]",
1602
+ children: [
1603
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[color:var(--color-race-red)] text-[11px] font-bold text-white", children: a.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: a.avatarUrl, alt: "", className: "h-full w-full object-cover" }) : (a.name ?? a.email ?? "?").slice(0, 2).toUpperCase() }),
1604
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col leading-tight", children: [
1605
+ /* @__PURE__ */ jsxRuntime.jsx(
1606
+ "span",
1607
+ {
1608
+ className: [
1609
+ "truncate text-sm font-semibold",
1610
+ a.active ? "text-[color:var(--color-race-red)]" : "text-[color:var(--foreground)]"
1611
+ ].join(" "),
1612
+ children: a.name ?? a.email ?? a.id.slice(0, 8)
1613
+ }
1614
+ ),
1615
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-[color:var(--muted)]", children: a.active ? "Active" : a.email ?? "" })
1616
+ ] })
1617
+ ]
1618
+ }
1619
+ ),
1620
+ !a.active && userMenu.onRemoveAccount ? /* @__PURE__ */ jsxRuntime.jsx(
1621
+ "button",
1622
+ {
1623
+ type: "button",
1624
+ "aria-label": `Remove ${a.name ?? a.email ?? "account"}`,
1625
+ onClick: () => userMenu.onRemoveAccount?.(a.id),
1626
+ className: "shrink-0 rounded p-1 text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
1627
+ children: /* @__PURE__ */ jsxRuntime.jsx(TrashIcon, {})
1628
+ }
1629
+ ) : null
1630
+ ] }, a.id)) }),
1631
+ userMenu.addAccountHref || userMenu.onAddAccount ? userMenu.addAccountHref ? /* @__PURE__ */ jsxRuntime.jsxs(
1632
+ "a",
1633
+ {
1634
+ href: safeHref(userMenu.addAccountHref),
1635
+ className: "mt-1.5 flex items-center gap-2.5 rounded-lg px-1 py-1.5 text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
1636
+ children: [
1637
+ /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
1638
+ "Add Account"
1639
+ ]
1640
+ }
1641
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1642
+ "button",
1643
+ {
1644
+ type: "button",
1645
+ onClick: userMenu.onAddAccount,
1646
+ className: "mt-1.5 flex w-full items-center gap-2.5 rounded-lg px-1 py-1.5 text-left text-sm font-semibold text-[color:var(--color-race-red)] hover:bg-[color:var(--panel)]",
1647
+ children: [
1648
+ /* @__PURE__ */ jsxRuntime.jsx(AddUserIcon, {}),
1649
+ "Add Account"
1650
+ ]
1651
+ }
1652
+ ) : null
1653
+ ] }) : null,
1654
+ userMenu?.theme ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-[color:var(--line)]", children: /* @__PURE__ */ jsxRuntime.jsx(
1655
+ NavThemeControl,
1656
+ {
1657
+ value: userMenu.theme.value,
1658
+ onChange: userMenu.theme.onChange
1659
+ }
1660
+ ) }) : null,
1661
+ onSignOut ? /* @__PURE__ */ jsxRuntime.jsxs(
1662
+ "button",
1663
+ {
1664
+ type: "button",
1665
+ onClick: onSignOut,
1666
+ className: "flex w-full items-center gap-2.5 border-t border-[color:var(--line)] px-4 py-2.5 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)] hover:text-[color:var(--color-race-red)]",
1667
+ children: [
1668
+ /* @__PURE__ */ jsxRuntime.jsx(SignOutIcon, {}),
1669
+ "Sign out"
1670
+ ]
1671
+ }
1672
+ ) : null
1673
+ ] })
1674
+ ] }),
1675
+ !hasRealName && user ? /* @__PURE__ */ jsxRuntime.jsx(NamePromptBubble, { profileHref: userMenu?.myProfileHref }) : null
1676
+ ] })
1677
+ ] });
1678
+ }
1679
+ function NamePromptBubble({ profileHref }) {
1680
+ const KEY = "politogy_name_prompt_dismissed";
1681
+ const [dismissed, setDismissed] = react.useState(true);
1682
+ react.useEffect(() => {
1683
+ try {
1684
+ setDismissed(sessionStorage.getItem(KEY) === "1");
1685
+ } catch {
1686
+ setDismissed(false);
1687
+ }
1688
+ }, []);
1689
+ if (dismissed) return null;
1690
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-4 top-full z-30 mt-2 w-64 rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] p-3 shadow-[var(--shadow-soft)]", children: [
1691
+ /* @__PURE__ */ jsxRuntime.jsx(
1692
+ "span",
1693
+ {
1694
+ "aria-hidden": true,
1695
+ 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)]"
1696
+ }
1697
+ ),
1698
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: "Add your name" }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs leading-snug text-[color:var(--muted)]", children: "Set your first and last name so teammates see you \u2014 not your email." }),
1700
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
1701
+ profileHref ? /* @__PURE__ */ jsxRuntime.jsx(
1702
+ "a",
1703
+ {
1704
+ href: safeHref(profileHref),
1705
+ className: "rounded-full bg-[color:var(--color-race-red)] px-3 py-1 text-xs font-semibold text-white hover:opacity-90",
1706
+ children: "Set name"
1707
+ }
1708
+ ) : null,
1709
+ /* @__PURE__ */ jsxRuntime.jsx(
1710
+ "button",
1711
+ {
1712
+ type: "button",
1713
+ onClick: () => {
1714
+ setDismissed(true);
1715
+ try {
1716
+ sessionStorage.setItem(KEY, "1");
1717
+ } catch {
1718
+ }
1719
+ },
1720
+ className: "rounded-full border border-[color:var(--line)] px-3 py-1 text-xs font-semibold text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
1721
+ children: "Dismiss"
1722
+ }
1723
+ )
1724
+ ] })
1725
+ ] });
1726
+ }
1727
+ function formatFirstLast(name) {
1728
+ const parts = name.split(/\s+/).filter(Boolean);
1729
+ if (parts.length === 1) return parts[0];
1730
+ return `${parts[0]} ${parts[parts.length - 1][0].toUpperCase()}.`;
1731
+ }
1732
+ function ProfileIcon() {
1733
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1734
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }),
1735
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7", r: "4" })
1736
+ ] });
1737
+ }
1738
+ function BriefcaseIcon() {
1739
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1740
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "7", width: "20", height: "14", rx: "2", ry: "2" }),
1741
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" })
1742
+ ] });
1743
+ }
1744
+ function TrashIcon() {
1745
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1746
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 6 5 6 21 6" }),
1747
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
1748
+ ] });
1749
+ }
1750
+ function AddUserIcon() {
1751
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1752
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }),
1753
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "7", r: "4" }),
1754
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "8", x2: "20", y2: "14" }),
1755
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "23", y1: "11", x2: "17", y2: "11" })
1756
+ ] });
1757
+ }
1758
+ function SignOutIcon() {
1759
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1760
+ "svg",
1761
+ {
1762
+ width: 15,
1763
+ height: 15,
1764
+ viewBox: "0 0 24 24",
1765
+ fill: "none",
1766
+ stroke: "currentColor",
1767
+ strokeWidth: 2,
1768
+ strokeLinecap: "round",
1769
+ strokeLinejoin: "round",
1770
+ "aria-hidden": "true",
1771
+ children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
1773
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 17 21 12 16 7" }),
1774
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "21", x2: "9", y1: "12", y2: "12" })
1775
+ ]
1776
+ }
1777
+ );
1778
+ }
1779
+ function NavTopBarActionButton({ action }) {
1780
+ const Icon = action.icon;
1781
+ const className = topBarIconButtonClass(action.tone ?? "default");
1782
+ const glyphName = useTopBarIconSlotGlyph(action.slot ?? "apps");
1783
+ const BrandGlyph = action.slot ? resolveBrandGlyph(glyphName) : null;
1784
+ const Glyph = BrandGlyph ?? Icon;
1785
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1786
+ /* @__PURE__ */ jsxRuntime.jsx(Glyph, { size: 20, className: "text-current" }),
1787
+ typeof action.badge === "number" && action.badge > 0 ? /* @__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: action.badge }) : null,
1788
+ /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: action.label, description: action.description })
1789
+ ] });
1790
+ const isExternal = action.href ? /^https?:\/\//.test(action.href) : false;
1791
+ if (action.href) {
1792
+ return /* @__PURE__ */ jsxRuntime.jsx(
1793
+ "a",
1794
+ {
1795
+ href: safeHref(action.href),
1796
+ "aria-label": action.label,
1797
+ className,
1798
+ ...isExternal ? { target: "_blank", rel: "noreferrer" } : {},
1799
+ children: content
1800
+ }
1801
+ );
1802
+ }
1803
+ return /* @__PURE__ */ jsxRuntime.jsx(
1804
+ "button",
1805
+ {
1806
+ type: "button",
1807
+ onClick: action.onClick,
1808
+ "aria-label": action.label,
1809
+ className,
1810
+ children: content
1811
+ }
1812
+ );
1813
+ }
1814
+ var BETA_BANNER_DEFAULTS = {
1815
+ text: "Our product is in Beta. Thank you for participating in the Pilot Program. ",
1816
+ linkLabel: "You can click here to see the Terms & Conditions for Beta users.",
1817
+ href: "https://app.politogyvrm.com/beta-terms-and-conditions"
1818
+ };
1819
+ function NavBetaBanner({ text, linkLabel, href }) {
1820
+ const body = text ?? BETA_BANNER_DEFAULTS.text;
1821
+ const label = linkLabel ?? BETA_BANNER_DEFAULTS.linkLabel;
1822
+ const target = href ?? BETA_BANNER_DEFAULTS.href;
1823
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1824
+ "div",
1825
+ {
1826
+ role: "note",
1827
+ className: "w-full border-y px-4 py-2 text-center text-sm",
1828
+ style: {
1829
+ background: "var(--beta-banner-bg)",
1830
+ borderColor: "var(--beta-banner-border)",
1831
+ color: "var(--beta-banner-text)"
1832
+ },
1833
+ children: [
1834
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: body }),
1835
+ /* @__PURE__ */ jsxRuntime.jsx(
1836
+ "a",
1837
+ {
1838
+ href: safeHref(target),
1839
+ target: "_blank",
1840
+ rel: "noreferrer",
1841
+ className: "font-bold underline",
1842
+ style: { color: "var(--beta-banner-text)" },
1843
+ children: label
1844
+ }
1845
+ )
1846
+ ]
1847
+ }
1848
+ );
1849
+ }
1850
+ function useCloseNavDetailsOnOutsideClick() {
1851
+ react.useEffect(() => {
1852
+ const close = (target) => {
1853
+ document.querySelectorAll("details[data-nav-details][open]").forEach((d) => {
1854
+ if (!target || !d.contains(target)) d.removeAttribute("open");
1855
+ });
1856
+ };
1857
+ const onPointerDown = (e) => close(e.target);
1858
+ const onKey = (e) => {
1859
+ if (e.key === "Escape") close(null);
1860
+ };
1861
+ document.addEventListener("mousedown", onPointerDown);
1862
+ document.addEventListener("keydown", onKey);
1863
+ return () => {
1864
+ document.removeEventListener("mousedown", onPointerDown);
1865
+ document.removeEventListener("keydown", onKey);
1866
+ };
1867
+ }, []);
1868
+ }
1869
+ function NavAppShell({ sidebar, topBar, betaBanner, children }) {
1870
+ useCloseNavDetailsOnOutsideClick();
1871
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]", children: [
1872
+ /* @__PURE__ */ jsxRuntime.jsx(NavSidebar, { ...sidebar }),
1873
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lg:pl-[240px]", children: [
1874
+ /* @__PURE__ */ jsxRuntime.jsx(NavTopBar, { ...topBar }),
1875
+ betaBanner ? /* @__PURE__ */ jsxRuntime.jsx(NavBetaBanner, { ...betaBanner }) : null,
1876
+ /* @__PURE__ */ jsxRuntime.jsx("main", { className: "px-6 py-7", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-7xl", children }) })
1877
+ ] })
1878
+ ] });
1879
+ }
1880
+
1881
+ // src/lib/breadcrumbs.ts
1882
+ function deriveBreadcrumbs(items, pathname, opts) {
1883
+ const search = opts?.search ?? (typeof window !== "undefined" ? window.location.search : "");
1884
+ const origin = opts?.origin ?? (typeof window !== "undefined" ? window.location.origin : "");
1885
+ const matches = (href) => {
1886
+ if (!href) return false;
1887
+ let path = href;
1888
+ let query = "";
1889
+ if (/^https?:\/\//i.test(href)) {
1890
+ try {
1891
+ const u = new URL(href);
1892
+ if (origin && u.origin !== origin) return false;
1893
+ path = u.pathname;
1894
+ query = u.search;
1895
+ } catch {
1896
+ return false;
1897
+ }
1898
+ } else {
1899
+ const qi = href.indexOf("?");
1900
+ if (qi >= 0) {
1901
+ path = href.slice(0, qi) || "/";
1902
+ query = href.slice(qi);
1903
+ }
1904
+ }
1905
+ if (path !== pathname) return false;
1906
+ if (!query) return true;
1907
+ const want = new URLSearchParams(query);
1908
+ const have = new URLSearchParams(search);
1909
+ for (const [k, v] of want) if (have.get(k) !== v) return false;
1910
+ return true;
1911
+ };
1912
+ const walk = (nodes, trail2) => {
1913
+ for (const node of nodes) {
1914
+ const here = [...trail2, node];
1915
+ if (node.children?.length) {
1916
+ const deeper = walk(node.children, here);
1917
+ if (deeper) return deeper;
1918
+ }
1919
+ if (matches(node.href)) return here;
1920
+ }
1921
+ return null;
1922
+ };
1923
+ const trail = walk(items, []);
1924
+ const dashboard = items.find((i) => i.label === "Dashboard");
1925
+ const crumbs = [{ label: "Dashboard", href: dashboard?.href }];
1926
+ if (!trail) return crumbs;
1927
+ for (const node of trail) {
1928
+ if (node.label === "Dashboard") continue;
1929
+ crumbs.push({ label: node.label, href: node.href });
1930
+ }
1931
+ const last = crumbs[crumbs.length - 1];
1932
+ if (last) delete last.href;
1933
+ return crumbs;
1934
+ }
1935
+
1936
+ // src/lib/identity.ts
1937
+ function looksLikeEmail(value) {
1938
+ return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
1939
+ }
1940
+ function resolveIdentity(input) {
1941
+ const apiName = looksLikeEmail(input.apiName) ? null : input.apiName ?? null;
1942
+ const fallbackName = looksLikeEmail(input.fallbackName) ? null : input.fallbackName ?? null;
1943
+ return {
1944
+ userName: apiName ?? fallbackName,
1945
+ email: input.email ?? null,
1946
+ avatarUrl: input.avatarUrl ?? null,
1947
+ loaded: input.loaded ?? false
1948
+ };
1949
+ }
1950
+ var POLITOGY_APPS = {
1951
+ vrm: { title: "Politogy VRM", origin: "https://app.politogyvrm.com", storagePrefix: "vrm" },
1952
+ contacts: { title: "Contacts", origin: "https://contacts.politogyvrm.com", storagePrefix: "contacts" },
1953
+ forms: { title: "Forms", origin: "https://forms.politogyvrm.com", storagePrefix: "forms" },
1954
+ surveys: { title: "Surveys", origin: "https://surveys.politogyvrm.com", storagePrefix: "surveys" },
1955
+ polls: { title: "Polls", origin: "https://polls.politogyvrm.com", storagePrefix: "polls" },
1956
+ petitions: { title: "Petitions", origin: "https://petitions.politogyvrm.com", storagePrefix: "petitions" }
1957
+ };
1958
+ function PolitogyAppFrame({
1959
+ app,
1960
+ pathname,
1961
+ search,
1962
+ linkComponent,
1963
+ session,
1964
+ onSignOut,
1965
+ buildMeta,
1966
+ data,
1967
+ children
1968
+ }) {
1969
+ const meta = POLITOGY_APPS[app];
1970
+ return /* @__PURE__ */ jsxRuntime.jsx(
1971
+ PolitogyAppChrome,
1972
+ {
1973
+ storagePrefix: meta.storagePrefix,
1974
+ fetchJson: data?.fetchJson,
1975
+ devNotes: data?.devNotes,
1976
+ org: data?.org,
1977
+ identity: {
1978
+ userId: session.userId,
1979
+ userName: session.userName ?? null,
1980
+ email: session.email,
1981
+ role: session.role
1982
+ },
1983
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1984
+ FrameBody,
1985
+ {
1986
+ app,
1987
+ pathname,
1988
+ search,
1989
+ linkComponent,
1990
+ session,
1991
+ onSignOut,
1992
+ buildMeta,
1993
+ data,
1994
+ children
1995
+ }
1996
+ )
1997
+ }
1998
+ );
1999
+ }
2000
+ function FrameBody({
2001
+ app,
2002
+ pathname,
2003
+ search,
2004
+ linkComponent,
2005
+ session,
2006
+ onSignOut,
2007
+ buildMeta,
2008
+ data,
2009
+ children
2010
+ }) {
2011
+ const meta = POLITOGY_APPS[app];
2012
+ const chrome = useChromeState();
2013
+ const identity = useChromeIdentity({ userName: session.userName, email: session.email });
2014
+ const userMenu = useChromeUserMenu({
2015
+ userId: session.userId,
2016
+ userName: identity.userName,
2017
+ email: identity.email
2018
+ });
2019
+ const footer = useChromeFooter(buildMeta ?? {});
2020
+ const mode = useChromeMode();
2021
+ const coreOrgs = chrome?.orgs ?? [];
2022
+ const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
2023
+ const navDisplay = chrome?.navDisplay ?? "both";
2024
+ const navItems = react.useMemo(
2025
+ () => standardPolitogyNav({
2026
+ coreBaseUrl: POLITOGY_APPS.vrm.origin,
2027
+ icons: standardNavIcons,
2028
+ // An app that OWNS a standard nav entry (Contacts) nests its own routes
2029
+ // under it. This is routing data, not chrome configuration.
2030
+ contactsItem: data?.contactsItem
2031
+ }),
2032
+ [data?.contactsItem]
2033
+ );
2034
+ const breadcrumbs = react.useMemo(
2035
+ () => deriveBreadcrumbs(navItems, pathname, { search, origin: meta.origin }),
2036
+ [navItems, pathname, search, meta.origin]
2037
+ );
2038
+ return /* @__PURE__ */ jsxRuntime.jsx(
2039
+ NavAppShell,
2040
+ {
2041
+ sidebar: {
2042
+ pathname,
2043
+ brand: {
2044
+ logoSrc: "/brand/logos/relationship-mode-logo.png",
2045
+ logoAlt: "Politogy",
2046
+ badgeLabel: "P",
2047
+ title: "Politogy"
2048
+ },
2049
+ nav: {
2050
+ items: navDisplay === "tabs" ? navItems.map(({ children: _sub, ...item }) => item) : navItems
2051
+ },
2052
+ org: activeCoreOrg ? {
2053
+ name: activeCoreOrg.name,
2054
+ id: activeCoreOrg.id,
2055
+ logoUrl: activeCoreOrg.logoUrl,
2056
+ brandColor: activeCoreOrg.brandColor,
2057
+ organizations: coreOrgs,
2058
+ onSwitch: chrome?.setOrgId
2059
+ } : { name: session.accountName, id: session.accountId },
2060
+ mode,
2061
+ footer,
2062
+ linkComponent
2063
+ },
2064
+ betaBanner: {},
2065
+ topBar: {
2066
+ title: meta.title,
2067
+ breadcrumbs,
2068
+ user: {
2069
+ name: identity.userName,
2070
+ email: identity.email,
2071
+ // The chrome API's avatar wins; the app may supply a fallback chain
2072
+ // (e.g. a stored memoji) for users with no uploaded photo.
2073
+ avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
2074
+ role: session.role
2075
+ },
2076
+ userMenu,
2077
+ onSignOut,
2078
+ actions: /* @__PURE__ */ jsxRuntime.jsx(ChromeTopBarActions, {})
2079
+ },
2080
+ children
2081
+ }
2082
+ );
2083
+ }
733
2084
  var standardNavIcons = {
734
2085
  home: fi.FiHome,
735
2086
  users: fi.FiUsers,
@@ -754,6 +2105,7 @@ var standardNavIcons = {
754
2105
  share: fi.FiShare2
755
2106
  };
756
2107
  var ORG_STORAGE_KEY = "vrm.activeOrg";
2108
+ var MODE_STORAGE_KEY = "vrm.activeMode";
757
2109
  var NAV_DISPLAY_KEY = "vrm.navDisplay";
758
2110
  var CORE_BASE_URL = "https://app.politogyvrm.com";
759
2111
  var ChromeStateContext = react.createContext(null);
@@ -938,6 +2290,16 @@ function useChromeFooter(meta) {
938
2290
  [isSuperAdmin, meta.envLabel, meta.version, meta.commit, meta.commitDate, meta.commitAuthor]
939
2291
  );
940
2292
  }
2293
+ function useChromeIdentity(fallback) {
2294
+ const { me, loaded } = useChromeMe();
2295
+ return resolveIdentity({
2296
+ apiName: me?.userName,
2297
+ fallbackName: fallback?.userName,
2298
+ email: fallback?.email,
2299
+ avatarUrl: me?.avatarUrl,
2300
+ loaded
2301
+ });
2302
+ }
941
2303
  function useChromeUserMenu({
942
2304
  userId,
943
2305
  userName,
@@ -981,11 +2343,19 @@ var MODE_DEFS = [
981
2343
  orgQuery: true
982
2344
  }
983
2345
  ];
984
- function useChromeMode(currentModeId = "relationship") {
2346
+ function useChromeMode(fallbackModeId = "relationship") {
985
2347
  const ctx = react.useContext(ChromeStateContext);
986
2348
  const nav = useNavChromeFetcher();
987
2349
  const orgId = ctx?.orgId ?? null;
988
2350
  const [entitled, setEntitled] = react.useState(null);
2351
+ const [selectedModeId, setSelectedModeId] = react.useState(null);
2352
+ react.useEffect(() => {
2353
+ try {
2354
+ const stored = localStorage.getItem(MODE_STORAGE_KEY);
2355
+ if (stored && MODE_DEFS.some((m) => m.id === stored)) setSelectedModeId(stored);
2356
+ } catch {
2357
+ }
2358
+ }, []);
989
2359
  react.useEffect(() => {
990
2360
  if (!nav || !orgId) return;
991
2361
  let cancelled = false;
@@ -999,8 +2369,9 @@ function useChromeMode(currentModeId = "relationship") {
999
2369
  cancelled = true;
1000
2370
  };
1001
2371
  }, [nav, orgId]);
1002
- const current = MODE_DEFS.find((m) => m.id === currentModeId) ?? MODE_DEFS[0];
1003
2372
  const options = MODE_DEFS.filter((m) => entitled?.[m.id]);
2373
+ const selected = selectedModeId ? options.find((m) => m.id === selectedModeId) ?? null : null;
2374
+ const current = selected ?? options[0] ?? MODE_DEFS.find((m) => m.id === fallbackModeId) ?? MODE_DEFS[0];
1004
2375
  const suffix = orgId ? `?org=${orgId.slice(-6)}` : "";
1005
2376
  return {
1006
2377
  label: current.label,
@@ -1009,6 +2380,11 @@ function useChromeMode(currentModeId = "relationship") {
1009
2380
  onSelect: options.length > 1 ? (id) => {
1010
2381
  const def = MODE_DEFS.find((m) => m.id === id);
1011
2382
  if (!def) return;
2383
+ setSelectedModeId(id);
2384
+ try {
2385
+ localStorage.setItem(MODE_STORAGE_KEY, id);
2386
+ } catch {
2387
+ }
1012
2388
  const sep = def.href.includes("?") ? "&" : "?";
1013
2389
  window.location.assign(
1014
2390
  def.orgQuery && suffix ? `${def.href}${sep}${suffix.slice(1)}` : def.href
@@ -1018,9 +2394,15 @@ function useChromeMode(currentModeId = "relationship") {
1018
2394
  }
1019
2395
 
1020
2396
  exports.ChromeTopBarActions = ChromeTopBarActions;
2397
+ exports.POLITOGY_APPS = POLITOGY_APPS;
1021
2398
  exports.PolitogyAppChrome = PolitogyAppChrome;
2399
+ exports.PolitogyAppFrame = PolitogyAppFrame;
2400
+ exports.deriveBreadcrumbs = deriveBreadcrumbs;
2401
+ exports.looksLikeEmail = looksLikeEmail;
2402
+ exports.resolveIdentity = resolveIdentity;
1022
2403
  exports.standardNavIcons = standardNavIcons;
1023
2404
  exports.useChromeFooter = useChromeFooter;
2405
+ exports.useChromeIdentity = useChromeIdentity;
1024
2406
  exports.useChromeMode = useChromeMode;
1025
2407
  exports.useChromeState = useChromeState;
1026
2408
  exports.useChromeUserMenu = useChromeUserMenu;