@the-portland-company/shell 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/native.cjs CHANGED
@@ -88,6 +88,56 @@ function normalize(s) {
88
88
  return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
89
89
  }
90
90
 
91
+ // src/nav-preference.ts
92
+ var NAV_DISPLAY_STORAGE_KEY = "politogy_nav_display";
93
+ var NAV_DISPLAY_COOKIE = "politogy_nav_display";
94
+ `(function(){try{
95
+ var KEY=${JSON.stringify(NAV_DISPLAY_STORAGE_KEY)};
96
+ var VALID=["menu","tabs","both"];
97
+ function fromCookie(){try{var m=document.cookie.match(/(?:^|;\\s*)${NAV_DISPLAY_COOKIE}=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;}}
98
+ var cookie=fromCookie();
99
+ if(cookie&&VALID.indexOf(cookie)!==-1){var ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}
100
+ if(cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}}
101
+ }catch(e){}})();`;
102
+ var RAIL_STORAGE_KEY = "politogy_rail";
103
+ var RAIL_COOKIE = "politogy_rail";
104
+ var RAIL_DEFAULT = "collapsed";
105
+ function isRailPreference(v) {
106
+ return v === "expanded" || v === "collapsed";
107
+ }
108
+ function readRailPreference() {
109
+ if (typeof document !== "undefined") {
110
+ try {
111
+ const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${RAIL_COOKIE}=([^;]+)`));
112
+ const v = m ? decodeURIComponent(m[1]) : null;
113
+ if (isRailPreference(v)) return v;
114
+ } catch {
115
+ }
116
+ }
117
+ if (typeof localStorage !== "undefined") {
118
+ try {
119
+ const v = localStorage.getItem(RAIL_STORAGE_KEY);
120
+ if (isRailPreference(v)) return v;
121
+ } catch {
122
+ }
123
+ }
124
+ return RAIL_DEFAULT;
125
+ }
126
+ function writeRailPreference(pref) {
127
+ if (typeof document === "undefined" || typeof window === "undefined") return;
128
+ const host = window.location.hostname;
129
+ const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
130
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
131
+ try {
132
+ document.cookie = `${RAIL_COOKIE}=${pref}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
133
+ } catch {
134
+ }
135
+ try {
136
+ localStorage.setItem(RAIL_STORAGE_KEY, pref);
137
+ } catch {
138
+ }
139
+ }
140
+
91
141
  // src/theme-preference.ts
92
142
  var THEME_STORAGE_KEY = "politogy_theme";
93
143
  var THEME_COOKIE = "politogy_theme";
@@ -1882,7 +1932,6 @@ var PHONEBANK_ORIGIN = "https://phonebank.politogyvrm.com/";
1882
1932
  var CANVASS_ORIGIN = "https://canvass.politogyvrm.com/";
1883
1933
  var SIGNS_ORIGIN = "https://signs.politogyvrm.com/";
1884
1934
  var POLLS_ORIGIN = "https://polls.politogyvrm.com/";
1885
- var CONTACTS_TRIBES = "https://contacts.politogyvrm.com/tribes";
1886
1935
  var PETITION_MODE_ORIGIN = "https://petition-mode.politogyvrm.com/";
1887
1936
  function applyHiddenItems(items, hidden) {
1888
1937
  if (!hidden || hidden.length === 0) return items;
@@ -1952,10 +2001,6 @@ var APP_POLLS_CHILDREN = [
1952
2001
  { label: "Command Center", icon: "grid", href: "https://polls.politogyvrm.com/" },
1953
2002
  { label: "New Poll", icon: "edit", href: "https://polls.politogyvrm.com/polls/new" }
1954
2003
  ];
1955
- var APP_CTAS_CHILDREN = [
1956
- { label: "CTAs", icon: "list", href: "https://ctas.politogyvrm.com/" },
1957
- { label: "Canvas", icon: "edit", href: "https://ctas.politogyvrm.com/?tab=canvas" }
1958
- ];
1959
2004
  var APP_A2P_CHILDREN = [
1960
2005
  { label: "Dashboard", icon: "grid", path: "dashboard" },
1961
2006
  { label: "Compose", icon: "edit", path: "compose" },
@@ -2003,16 +2048,27 @@ function standardPolitogyNav(opts) {
2003
2048
  return { label: c.label, icon: icon(c.icon), href: withOrg(withMode) };
2004
2049
  });
2005
2050
  };
2051
+ const accountRows = () => [
2052
+ { kind: "section", label: "Account" },
2053
+ // My Profile is global — NOT org-suffixed (main-app ShellChromeBridge).
2054
+ { label: "My profile", icon: icon("circleUser"), href: withOrg(abs("/my-profile")) },
2055
+ {
2056
+ label: "Organization settings",
2057
+ icon: icon("settings"),
2058
+ href: withOrg(abs("/organization-settings")),
2059
+ children: branches(ORG_SETTINGS_CHILDREN)
2060
+ }
2061
+ ];
2006
2062
  const buildRelationship = () => {
2007
2063
  const contacts = opts.contactsItem ? {
2008
2064
  label: "Contacts",
2009
- icon: icon("users"),
2065
+ icon: icon("usersRound"),
2010
2066
  href: opts.contactsItem.href,
2011
2067
  children: opts.contactsItem.children,
2012
2068
  badge: opts.contactsItem.count ?? void 0
2013
2069
  } : {
2014
2070
  label: "Contacts",
2015
- icon: icon("users"),
2071
+ icon: icon("usersRound"),
2016
2072
  href: withOrg(`${CONTACTS_APP}/`),
2017
2073
  children: APP_CONTACTS_CHILDREN.map((c) => ({
2018
2074
  label: c.label,
@@ -2022,7 +2078,7 @@ function standardPolitogyNav(opts) {
2022
2078
  };
2023
2079
  return [
2024
2080
  { kind: "section", label: "Relationship" },
2025
- { label: "Dashboard", icon: icon("home"), href: withOrg(abs("/relationship/dashboard")) },
2081
+ { label: "Dashboard", icon: icon("layoutDashboard"), href: withOrg(abs("/relationship/dashboard")) },
2026
2082
  contacts,
2027
2083
  {
2028
2084
  label: "Email broadcast",
@@ -2045,40 +2101,20 @@ function standardPolitogyNav(opts) {
2045
2101
  },
2046
2102
  { kind: "section", label: "Collect" },
2047
2103
  // Carries no top-level href in VRM core (parent row only toggles).
2048
- { label: "Calls to action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
2104
+ { label: "Calls to action", icon: icon("mousePointerClick"), children: leaves(CTA_CHILDREN) },
2049
2105
  {
2050
2106
  label: "Forms",
2051
- icon: icon("activity"),
2107
+ icon: icon("fileText"),
2052
2108
  href: withOrg("https://forms.politogyvrm.com/"),
2053
2109
  children: APP_FORMS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2054
2110
  },
2055
2111
  {
2056
2112
  label: "Surveys",
2057
- icon: icon("fileText"),
2113
+ icon: icon("clipboardList"),
2058
2114
  href: withOrg("https://surveys.politogyvrm.com/surveys"),
2059
2115
  children: APP_SURVEYS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2060
2116
  },
2061
- {
2062
- label: "Polls",
2063
- icon: icon("barChart"),
2064
- href: withOrg("https://polls.politogyvrm.com/"),
2065
- children: APP_POLLS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2066
- },
2067
- {
2068
- label: "CTAs",
2069
- icon: icon("layout"),
2070
- href: withOrg("https://ctas.politogyvrm.com/"),
2071
- children: APP_CTAS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) }))
2072
- },
2073
- { kind: "section", label: "Account" },
2074
- // My Profile is global — NOT org-suffixed (main-app ShellChromeBridge).
2075
- { label: "My profile", icon: icon("user"), href: withOrg(abs("/my-profile")) },
2076
- {
2077
- label: "Organization settings",
2078
- icon: icon("settings"),
2079
- href: withOrg(abs("/organization-settings")),
2080
- children: branches(ORG_SETTINGS_CHILDREN)
2081
- }
2117
+ ...accountRows()
2082
2118
  ];
2083
2119
  };
2084
2120
  const buildCampaign = () => {
@@ -2087,40 +2123,57 @@ function standardPolitogyNav(opts) {
2087
2123
  ...opts.campaignAvailability
2088
2124
  };
2089
2125
  const rows = [
2090
- { label: "Dashboard", icon: icon("home"), href: withOrg(abs("/campaign/dashboard")) },
2091
- { label: "Voter Contact", icon: icon("target"), href: withOrg(abs("/campaign/campaigns")) },
2126
+ // Left-Navigation §9.2: Race data -> Research -> GOTV -> Account.
2127
+ // Battleground is the Campaign landing page; there is no Campaign
2128
+ // Dashboard row, and Voter Contact and Tribes have no row.
2129
+ { kind: "section", label: "Race data" },
2130
+ { label: "Battleground", icon: icon("crosshair"), href: withOrg(abs("/campaign/battleground")) },
2131
+ { label: "District data", icon: icon("landmark"), href: withOrg(abs("/campaign/districts")) },
2132
+ { kind: "section", label: "Research" },
2092
2133
  {
2093
- label: "A2P",
2094
- icon: icon("messageSquare"),
2095
- href: withOrg(A2P_ORIGIN),
2096
- children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN),
2097
- flag: "a2p"
2134
+ label: "Polling",
2135
+ icon: icon("chartNoAxes"),
2136
+ href: withOrg(POLLS_ORIGIN),
2137
+ children: APP_POLLS_CHILDREN.map((c) => ({ label: c.label, icon: icon(c.icon), href: withOrg(c.href) })),
2138
+ flag: "polling"
2098
2139
  },
2140
+ { kind: "section", label: "GOTV" },
2099
2141
  {
2100
- label: "Phone Banking",
2142
+ label: "Field canvassing",
2143
+ icon: icon("doorOpen"),
2144
+ href: withOrg(CANVASS_ORIGIN),
2145
+ children: appLeaves(CANVASS_ORIGIN, APP_CANVASS_CHILDREN),
2146
+ flag: "canvass"
2147
+ },
2148
+ {
2149
+ label: "Phone banking",
2101
2150
  icon: icon("phone"),
2102
2151
  href: withOrg(PHONEBANK_ORIGIN),
2103
2152
  children: appLeaves(PHONEBANK_ORIGIN, APP_PHONEBANK_CHILDREN),
2104
2153
  flag: "phonebank"
2105
2154
  },
2106
2155
  {
2107
- label: "Field Canvassing",
2108
- icon: icon("map"),
2109
- href: withOrg(CANVASS_ORIGIN),
2110
- children: appLeaves(CANVASS_ORIGIN, APP_CANVASS_CHILDREN),
2111
- flag: "canvass"
2156
+ label: "A2P texting",
2157
+ icon: icon("messageSquare"),
2158
+ href: withOrg(A2P_ORIGIN),
2159
+ children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN),
2160
+ flag: "a2p"
2112
2161
  },
2113
2162
  {
2114
- label: "Sign Mapping",
2115
- icon: icon("mapPin"),
2163
+ label: "Sign mapping",
2164
+ icon: icon("signpost"),
2116
2165
  href: withOrg(SIGNS_ORIGIN),
2117
2166
  children: appLeaves(SIGNS_ORIGIN, APP_SIGNS_CHILDREN),
2118
2167
  flag: "signs"
2119
2168
  },
2120
- { label: "Tribes", icon: icon("users"), href: withOrg(CONTACTS_TRIBES), flag: "tribes" },
2121
- { label: "Polling Engine", icon: icon("barChart"), href: withOrg(POLLS_ORIGIN), flag: "polling" }
2169
+ ...accountRows()
2122
2170
  ];
2123
- return rows.filter((r) => !r.flag || avail[r.flag]).map(({ flag: _flag, ...item }) => item);
2171
+ const kept = rows.filter((r) => !r.flag || avail[r.flag]).map(({ flag: _flag, ...item }) => item);
2172
+ return kept.filter((item, idx) => {
2173
+ if (item.kind !== "section") return true;
2174
+ const next = kept[idx + 1];
2175
+ return !!next && next.kind !== "section";
2176
+ });
2124
2177
  };
2125
2178
  const buildPetition = () => [
2126
2179
  opts.petitionsItem ? {
@@ -2227,7 +2280,9 @@ function NavSidebar({
2227
2280
  footer,
2228
2281
  linkComponent,
2229
2282
  open,
2230
- onClose
2283
+ onClose,
2284
+ onOpen,
2285
+ onNavigate
2231
2286
  }) {
2232
2287
  const Link = linkComponent ?? DefaultLink;
2233
2288
  const collapsible = !open;
@@ -2324,7 +2379,7 @@ function NavSidebar({
2324
2379
  style: { fontFamily: 'var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif)' },
2325
2380
  onKeyDown: trapTab,
2326
2381
  onClick: (e) => {
2327
- if (open && e.target.closest("a[href]")) onClose?.();
2382
+ if (open && e.target.closest("a[href]")) (onNavigate ?? onClose)?.();
2328
2383
  },
2329
2384
  children: [
2330
2385
  open ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2333,18 +2388,30 @@ function NavSidebar({
2333
2388
  type: "button",
2334
2389
  ref: closeButtonRef,
2335
2390
  onClick: onClose,
2336
- "aria-label": "Close menu",
2391
+ "aria-label": "Collapse navigation",
2337
2392
  className: "absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--muted)] transition hover:bg-[color:var(--panel-elevated)] hover:text-[color:var(--foreground)] md:hidden",
2338
2393
  children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
2339
2394
  }
2340
2395
  ) : null,
2341
2396
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-shrink-0 flex-col gap-[14px] border-b border-[color:var(--sidebar-line,var(--line))] px-[18px] py-4 ${rc("max-md:items-center max-md:px-[10px]")}`, children: [
2397
+ onOpen ? /* @__PURE__ */ jsxRuntime.jsx(
2398
+ "button",
2399
+ {
2400
+ type: "button",
2401
+ onClick: onOpen,
2402
+ "aria-label": "Expand navigation",
2403
+ "aria-expanded": open ?? false,
2404
+ "data-rail-toggle": "expand",
2405
+ className: `hidden h-8 w-8 items-center justify-center rounded-lg ${rc("max-md:flex")}`,
2406
+ children: brand.logoSquareSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: brand.logoSquareSrc, alt: "", draggable: false, className: "h-8 w-8 object-contain" }) : brand.logoPending ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "block h-8 w-8" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-xl bg-[color:var(--color-accent)] text-sm font-black text-[#FFFFFF]", children: brand.badgeLabel })
2407
+ }
2408
+ ) : null,
2342
2409
  /* @__PURE__ */ jsxRuntime.jsxs(
2343
2410
  Link,
2344
2411
  {
2345
2412
  href: brand.homeHref ?? "/",
2346
2413
  "aria-label": brand.logoAlt ?? brand.title,
2347
- className: `flex items-center gap-2.5 ${rc("max-md:justify-center")}`,
2414
+ className: `flex items-center gap-2.5 ${rc("max-md:justify-center")} ${onOpen ? rc("max-md:hidden") : ""}`,
2348
2415
  children: [
2349
2416
  brand.logoSquareSrc ? /* @__PURE__ */ jsxRuntime.jsx(
2350
2417
  "img",
@@ -3118,7 +3185,7 @@ function NavTopBar({
3118
3185
  const initials = (hasRealName ? rawName : user?.email ?? "P").slice(0, 2).toUpperCase();
3119
3186
  const secondary = hasRealName ? user?.email || [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member" : [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member";
3120
3187
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sticky top-0 z-20", children: [
3121
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex h-[var(--topbar-height,60px)] items-center justify-between gap-[14px] border-b border-[color:var(--line)] bg-[color:var(--panel)] px-[var(--topbar-padding-x,20px)]", children: [
3188
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex min-w-0 h-[var(--topbar-height,60px)] items-center justify-between gap-[8px] md:gap-[14px] border-b border-[color:var(--line)] bg-[color:var(--panel)] px-[var(--topbar-padding-x,20px)]", children: [
3122
3189
  onOpenNav ? /* @__PURE__ */ jsxRuntime.jsx(
3123
3190
  "button",
3124
3191
  {
@@ -3131,7 +3198,7 @@ function NavTopBar({
3131
3198
  children: /* @__PURE__ */ jsxRuntime.jsx(HamburgerIcon, {})
3132
3199
  }
3133
3200
  ) : null,
3134
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "relative flex h-9 w-[320px] max-w-full shrink-0 items-center", children: [
3201
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "relative flex h-9 w-[320px] min-w-0 max-w-full shrink items-center md:shrink-0", children: [
3135
3202
  /* @__PURE__ */ jsxRuntime.jsx(SearchIcon, { className: "pointer-events-none absolute left-[10px] h-[15px] w-[15px] text-[color:var(--muted)]" }),
3136
3203
  /* @__PURE__ */ jsxRuntime.jsx(
3137
3204
  "input",
@@ -3151,13 +3218,13 @@ function NavTopBar({
3151
3218
  "kbd",
3152
3219
  {
3153
3220
  "aria-hidden": true,
3154
- className: "pointer-events-none absolute right-[10px] rounded-[3px] border border-[color:var(--line)] px-[5px] py-px font-[family-name:var(--font-plex-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--muted)]",
3221
+ className: "pointer-events-none absolute right-[10px] hidden md:block rounded-[3px] border border-[color:var(--line)] px-[5px] py-px font-[family-name:var(--font-plex-mono,ui-monospace,monospace)] text-[12px] leading-[1.2] text-[color:var(--muted)]",
3155
3222
  children: "/"
3156
3223
  }
3157
3224
  )
3158
3225
  ] }),
3159
3226
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative ml-auto flex shrink-0 items-center gap-[var(--topbar-icon-gap,8px)]", children: [
3160
- /* @__PURE__ */ jsxRuntime.jsxs(
3227
+ buildNotes ? /* @__PURE__ */ jsxRuntime.jsxs(
3161
3228
  "button",
3162
3229
  {
3163
3230
  type: "button",
@@ -3168,7 +3235,7 @@ function NavTopBar({
3168
3235
  buildNotes?.label ?? "Build notes"
3169
3236
  ]
3170
3237
  }
3171
- ),
3238
+ ) : null,
3172
3239
  actionsList && actionsList.length > 0 ? actionsList.map((action) => /* @__PURE__ */ jsxRuntime.jsx(NavTopBarActionButton, { action }, action.key)) : null,
3173
3240
  actions,
3174
3241
  themeToggle,
@@ -3743,9 +3810,22 @@ function NavAppShell({
3743
3810
  );
3744
3811
  }
3745
3812
  const [navOpen, setNavOpen] = react.useState(false);
3813
+ const expandRail = () => {
3814
+ writeRailPreference("expanded");
3815
+ setNavOpen(true);
3816
+ };
3817
+ const collapseRail = () => {
3818
+ writeRailPreference("collapsed");
3819
+ setNavOpen(false);
3820
+ };
3746
3821
  react.useEffect(() => {
3747
3822
  setNavOpen(false);
3748
3823
  }, [sidebar.pathname]);
3824
+ react.useEffect(() => {
3825
+ if (readRailPreference() !== "expanded") return;
3826
+ const narrow = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 767.98px)").matches : false;
3827
+ if (narrow) setNavOpen(true);
3828
+ }, []);
3749
3829
  react.useEffect(() => {
3750
3830
  if (!navOpen) return;
3751
3831
  const previous = document.body.style.overflow;
@@ -3766,15 +3846,23 @@ function NavAppShell({
3766
3846
  "data-politogy-mode": modeId,
3767
3847
  className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]",
3768
3848
  children: [
3769
- /* @__PURE__ */ jsxRuntime.jsx(NavSidebar, { ...sidebar, footer: sidebarFooter, open: navOpen, onClose: () => setNavOpen(false) }),
3849
+ /* @__PURE__ */ jsxRuntime.jsx(
3850
+ NavSidebar,
3851
+ {
3852
+ ...sidebar,
3853
+ footer: sidebarFooter,
3854
+ open: navOpen,
3855
+ onClose: collapseRail,
3856
+ onOpen: expandRail,
3857
+ onNavigate: () => setNavOpen(false)
3858
+ }
3859
+ ),
3770
3860
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pl-[var(--sidebar-collapsed-width,64px)] md:pl-[var(--sidebar-width,236px)]", children: [
3771
3861
  /* @__PURE__ */ jsxRuntime.jsx(
3772
3862
  NavTopBar,
3773
3863
  {
3774
3864
  ...topBar,
3775
- accountMenu: topBar.accountMenu ?? (accountMenuPlacement === "sidebar" ? "sidebar" : "topbar"),
3776
- onOpenNav: () => setNavOpen(true),
3777
- navOpen
3865
+ accountMenu: topBar.accountMenu ?? (accountMenuPlacement === "sidebar" ? "sidebar" : "topbar")
3778
3866
  }
3779
3867
  ),
3780
3868
  betaBanner ? /* @__PURE__ */ jsxRuntime.jsx(NavBetaBanner, { ...betaBanner }) : null,