@the-portland-company/shell 0.3.2 → 0.3.5

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 — 2026-05-14
4
+
5
+ ### Fixed
6
+
7
+ - `ShellModeValue.getModeConfig` is now optional. If omitted, shell auto-resolves the active mode config via `modes.find(m => m.id === currentMode)`. Defangs the common `this`-binding mistake where consumers wrote `getModeConfig() { return this.modes[0] }` and shell internals destructured the function (losing `this`) and crashed the chrome.
8
+ - All internal call sites (`<ModePill>`, `<UserMenu>`, `<NavItem>`) now use a defensive `resolveModeConfig(mode)` helper that try/catches the consumer-supplied function and falls back to derivation. A broken `getModeConfig` no longer blanks the page.
9
+
10
+ ## 0.3.4 — 2026-05-14
11
+
12
+ ### Fixed
13
+
14
+ - `package.json` `exports` for `./css` and `./chrome.css` now ship a `types` entry pointing at a tiny ambient module declaration (`dist/css-modules.d.ts`). Fixes TS2882 errors in fresh consumers importing the side-effect CSS subpaths.
15
+
16
+ ### Documentation
17
+
18
+ - README install command now pins peer-dep versions explicitly (`react@^18`, `@chakra-ui/react@^2.8`, etc.) so Vite-scaffolded apps on React 19 / Chakra v3 don't silently install incompatible versions.
19
+ - Clarified `@the-portland-company/devnotes` is optional.
20
+
21
+ ## 0.3.3 — 2026-05-14
22
+
23
+ ### Documentation
24
+
25
+ - Rewrote `README.md` to cover the v0.3.x chrome contract: every component / hook / helper / type, the `<ShellChrome>` binding pattern with a copy-pasteable bridge example, the `chrome.css` + `css` import paths, multi-SPA routing notes for the politogy edge router, and a pointer to `politogy-vrm/react/app/src/providers/ShellChromeBridge.tsx` as the reference implementation. No code changes.
26
+
3
27
  ## 0.3.2 — 2026-05-13
4
28
 
5
29
  ### Added
package/README.md CHANGED
@@ -2,47 +2,203 @@
2
2
 
3
3
  Shared chrome for politogy apps. One install, one wrapper, identical look.
4
4
 
5
+ Every politogy product (Email Blast, Contacts, Forms, future apps…) installs this package and gets the full branded experience — sidebar with VRM logo, organization switcher, mode selector, full nav list, header with notifications and support, branded footer with commit info — without rebuilding any of it. Path-based routing lets multiple separately-deployed Vite SPAs feel like a single continuous app on `app.politogy.com`.
6
+
7
+ > **v0.3.x is a major rewrite.** See the [CHANGELOG](./CHANGELOG.md) for the chrome component, hook, and prop additions over v0.2.x. The minimum viable boot below works for new apps; existing v0.2.x consumers can keep using `OrgSelector`, `useShellAuth`, etc. without changes.
8
+
9
+ ---
10
+
5
11
  ## Install
6
12
 
13
+ Pin peer dep versions explicitly — Vite scaffolds React 19 + Chakra v3 by default, neither of which the shell supports:
14
+
15
+ ```bash
16
+ npm install @the-portland-company/shell \
17
+ react@^18 react-dom@^18 \
18
+ @chakra-ui/react@^2.8 @emotion/react@^11 @emotion/styled@^11 \
19
+ framer-motion@^11 \
20
+ @supabase/supabase-js@~2.95 \
21
+ react-router-dom@^6 react-icons@^5
7
22
  ```
8
- npm install @the-portland-company/shell @chakra-ui/react @emotion/react @emotion/styled @supabase/supabase-js react react-dom
9
- ```
10
23
 
11
- ## Quick start
24
+ `@the-portland-company/devnotes` is **optional** — only needed if you want the dev-notes menu in the header. Skip it if your app doesn't use it.
25
+
26
+ ---
27
+
28
+ ## Minimum viable boot
29
+
30
+ The simplest possible startup renders the branded chrome with empty defaults — the sidebar appears, the header renders, but the org switcher / mode selector / nav list show "no data" states until you wire them up via `<ShellChrome>` (next section).
12
31
 
13
32
  ```tsx
14
- import { ShellProvider, AppLayout, registerShellPrecache } from '@the-portland-company/shell'
15
- import '@the-portland-company/shell/css'
33
+ // src/main.tsx
34
+ import React from 'react'
35
+ import ReactDOM from 'react-dom/client'
36
+ import { BrowserRouter } from 'react-router-dom'
37
+ import { ShellProvider, AppLayout } from '@the-portland-company/shell'
38
+ import '@the-portland-company/shell/chrome.css'
39
+ import { createClient } from '@supabase/supabase-js'
40
+ import App from './App'
41
+
42
+ const supabase = createClient(
43
+ import.meta.env.VITE_SUPABASE_URL,
44
+ import.meta.env.VITE_SUPABASE_ANON_KEY,
45
+ )
46
+
47
+ ReactDOM.createRoot(document.getElementById('root')!).render(
48
+ <ShellProvider currentApp="email" supabaseClient={supabase}>
49
+ <BrowserRouter>
50
+ <AppLayout>
51
+ <App />
52
+ </AppLayout>
53
+ </BrowserRouter>
54
+ </ShellProvider>,
55
+ )
56
+ ```
16
57
 
17
- registerShellPrecache().catch(() => {})
58
+ `<AppLayout>` composes the shell `<Header>`, `<Sidebar>`, your routed content, and `<Footer>`. `<ShellProvider>` owns the Supabase auth lifecycle and exposes `useShellAuth()` for status / user / sign-out.
18
59
 
19
- const APPS = [
20
- { id: 'crm', label: 'CRM', path: '/' },
21
- { id: 'messaging', label: 'Messaging', path: '/messaging' },
22
- ]
60
+ ---
23
61
 
24
- export default function App() {
62
+ ## Real-world boot: feeding the chrome real data
63
+
64
+ Most apps have an `<AuthProvider>`, `<OrganizationProvider>`, `<ModeProvider>`, etc. that live *below* `<ShellProvider>` in the tree. To push their data up into the shell's chrome contexts, drop a `<ShellChrome>` binding component as deep in the tree as the data is available:
65
+
66
+ ```tsx
67
+ import {
68
+ ShellProvider,
69
+ ShellChrome,
70
+ AppLayout,
71
+ } from '@the-portland-company/shell'
72
+
73
+ function ChromeBridge({ children }) {
74
+ const { currentMode, switchMode, getModeConfig, getAvailableModes } = useMode()
75
+ const { currentOrg, organizations, switchOrganization } = useOrganization()
76
+ const { linkedAccounts, ... } = useLinkedAccounts()
77
+ // …compute the nav-items list for the active mode here…
25
78
  return (
26
- <ShellProvider currentApp="crm" supabaseClient={supabase} appRegistry={APPS}>
27
- <AppLayout>{/* your routes */}</AppLayout>
28
- </ShellProvider>
79
+ <ShellChrome
80
+ mode={{ currentMode, modes: getAvailableModes(), switchMode, getModeConfig }}
81
+ organization={{ currentOrg, organizations, switchOrganization }}
82
+ linkedAccounts={{ linkedAccounts, switchToAccount, onAddAccount, onRemoveAccount }}
83
+ navItems={navItems}
84
+ role={{ activeRoleConfig, isSuperAdmin }}
85
+ footer={{ links, commit, version, releaseName, environmentLabel }}
86
+ support={{ submitSupportRequest }}
87
+ brandLogoSrc="/brand/logos/email-mode-logo.png"
88
+ headerIconsSlot={<MyAppSpecificIcons />}
89
+ userAvatarUrl={profile.avatar_url ?? storedMemojiUrl ?? defaultAvatarUrl}
90
+ myProfileHref="/my-profile"
91
+ organizationSettingsHref={`/organization-settings?org=${currentOrg.id}`}
92
+ >
93
+ {children}
94
+ </ShellChrome>
29
95
  )
30
96
  }
97
+
98
+ // Then in main.tsx:
99
+ <ShellProvider currentApp="email" supabaseClient={supabase}>
100
+ <BrowserRouter>
101
+ <AuthProvider>
102
+ <OrganizationProvider>
103
+ <ModeProvider>
104
+ <ChromeBridge>
105
+ <AppLayout>
106
+ <Routes>…</Routes>
107
+ </AppLayout>
108
+ </ChromeBridge>
109
+ </ModeProvider>
110
+ </OrganizationProvider>
111
+ </AuthProvider>
112
+ </BrowserRouter>
113
+ </ShellProvider>
31
114
  ```
32
115
 
33
- ## Slot props on `<AppLayout>`
116
+ **Reference implementation:** `politogy-vrm`'s `react/app/src/providers/ShellChromeBridge.tsx` (~600 lines, every chrome wiring pattern). Copy as a starting point for new apps.
117
+
118
+ All `<ShellChrome>` props are optional — only supply what your app has. The chrome gracefully omits the bits it doesn't get data for (e.g., no `support` prop → no support icon rendered).
119
+
120
+ ---
121
+
122
+ ## What the shell exports
123
+
124
+ ### Components
125
+
126
+ | Export | Use |
127
+ |---|---|
128
+ | `<ShellProvider>` | Top-level provider. Wraps Chakra + auth + meta contexts. |
129
+ | `<ShellChrome>` | Mid-tree binding component. Feeds chrome contexts from app data. |
130
+ | `<AppLayout>` | Composes Header + Sidebar + main content + Footer. |
131
+ | `<Header>` | Header bar (logo, page title, breadcrumbs, icon stack, user menu). |
132
+ | `<Sidebar>` | Sidebar (branded logo, org pill, mode pill, nav list, footer). |
133
+ | `<Footer>` | Below main content. Usually not rendered separately. |
134
+ | `<UserMenu>` | Avatar dropdown (profile, theme, sign out, switch account). |
135
+ | `<OrgPill>` (alias `<OrgSelector>`) | Organization switcher pill rendered in the sidebar. |
136
+ | `<ModePill>` | Mode (Relationship / Campaign / Petition) switcher. |
137
+ | `<NavItem>` | Single sidebar nav row with active state + hover gradient. |
138
+ | `<SupportRequestButton>` | Header life-buoy icon that opens a support modal. |
139
+ | `<BrandLogo>` | `<img>` wrapper for the branded mode logo. |
140
+ | `<BrandIcon name="...">` | Renders `/brand/icons/<name>.svg` from your `public/`. |
141
+ | `<CommitCopyButton>` | Footer commit/version display with copy-to-clipboard. |
142
+ | `<Card>`, `<PageContainer>`, `<PageHeader>`, `<BreadcrumbBar>`, `<HeaderTabBar>` | Page-level layout primitives. |
143
+ | `<GlobalUiIdProvider>` | Assigns stable UI IDs (for testing/analytics). |
144
+
145
+ ### Hooks
146
+
147
+ | Hook | Returns |
148
+ |---|---|
149
+ | `useShellAuth()` | `{ status, user, session, signOut }` |
150
+ | `useShellOrganization()` | `{ currentOrg, organizations, switchOrganization, createOrgHref } \| null` |
151
+ | `useShellMode()` | `{ currentMode, modes, switchMode, getModeConfig } \| null` |
152
+ | `useShellNavItems()` | `{ items: ShellNavItem[] } \| null` |
153
+ | `useShellLinkedAccounts()` | `{ linkedAccounts, switchToAccount, onAddAccount, onRemoveAccount } \| null` |
154
+ | `useShellSupport()` | `{ submitSupportRequest } \| null` |
155
+ | `useShellRole()` | `{ activeRoleConfig, isSuperAdmin } \| null` |
156
+ | `useShellFooter()` | `{ links, version, commit, releaseName, … } \| null` |
157
+ | `useShellNavigationPreference()` | `{ showSubMenu } \| null` |
158
+ | `useShellChrome()` | Chrome configuration (`devNotesMenu`, `myProfileHref`, `userAvatarUrl`, `pageTitle`, `breadcrumbs`, `headerActions`, etc.) |
159
+ | `useAppRegistry()`, `useCurrentApp()` | App switcher metadata for cross-app speculation rules. |
160
+
161
+ ### Helpers
34
162
 
35
- | Prop | Position | Use for |
36
- |---|---|---|
37
- | `headerExtrasSlot` | Right of header, before user menu | App-specific header widgets |
38
- | `modalsSlot` | Above main content, position absolute | Modal dialogs |
39
- | `notificationsSlot` | Inside main, top-right | Toasts, badges |
40
- | `sidebarFooterSlot` | Bottom of sidebar | Org switcher, status |
41
- | `overlaySlot` | Top of everything | Always-on-top indicators |
163
+ `normalizeHexColor`, `lightenHexColor`, `hexToRgba`, `getOrganizationBrandColor`, `getOrganizationBadgeLetters`, `getUserDisplayName`.
42
164
 
43
- ## Service worker
165
+ ### Theme
44
166
 
45
- The package ships a precache SW. Copy it into your `public/` dir during postinstall:
167
+ `politogyTheme` — Chakra theme with brand palette, mode-aware semantic tokens, and component overrides.
168
+
169
+ ---
170
+
171
+ ## CSS imports
172
+
173
+ ```tsx
174
+ import '@the-portland-company/shell/chrome.css' // :root design tokens (--app-color-brand-*, --mode-*, --app-tab-*, --form-control-*)
175
+ import '@the-portland-company/shell/css' // view-transitions CSS for cross-app polish
176
+ ```
177
+
178
+ `chrome.css` is required for theme variables. `css` (view-transitions) is optional polish.
179
+
180
+ ---
181
+
182
+ ## Multi-SPA routing on `app.politogy.com`
183
+
184
+ Multiple politogy apps live behind one domain via a Cloudflare Worker edge router (in `packages/edge-router` of the monorepo).
185
+
186
+ When you deploy a new app to Cloudflare Pages, add its path prefix to the edge router's `routes.ts`:
187
+
188
+ ```ts
189
+ {
190
+ pathPrefix: '/email',
191
+ origin: 'https://politogy-email.pages.dev',
192
+ }
193
+ ```
194
+
195
+ After deploy, users hitting `app.politogy.com/email/*` get transparently proxied to your app. Other paths keep going to whichever app owns them. The shared shell + same Supabase session means it feels like one app.
196
+
197
+ ---
198
+
199
+ ## Service worker (optional polish)
200
+
201
+ The package ships a precache service worker that caches the shell chrome on first visit so cross-app navigation feels instant.
46
202
 
47
203
  ```js
48
204
  // scripts/copy-shell-sw.mjs
@@ -53,11 +209,13 @@ await copyFile(
53
209
  )
54
210
  ```
55
211
 
56
- Then call `registerShellPrecache()` at the top of your app.
212
+ Wire it into your build (`"postinstall": "node scripts/copy-shell-sw.mjs"`) and call `registerShellPrecache()` once on app boot.
213
+
214
+ ---
57
215
 
58
- ## Speculation Rules
216
+ ## Speculation Rules (optional polish)
59
217
 
60
- Emit cross-app prerendering hints:
218
+ Cross-app prerendering hints for the browser:
61
219
 
62
220
  ```tsx
63
221
  import { useAppRegistry, useCurrentApp, buildSpeculationRules } from '@the-portland-company/shell'
@@ -70,3 +228,16 @@ function SpeculationRules() {
70
228
  return <script type="speculationrules" dangerouslySetInnerHTML={{ __html: json }} />
71
229
  }
72
230
  ```
231
+
232
+ Render once near the app root.
233
+
234
+ ---
235
+
236
+ ## Reference + support
237
+
238
+ - **Reference bridge:** `politogy-vrm/react/app/src/providers/ShellChromeBridge.tsx` — full chrome wiring for the main politogy app.
239
+ - **Architectural decisions, gotchas, session log:** `politogy-vrm/docs/superpowers/`.
240
+ - **Type definitions:** `node_modules/@the-portland-company/shell/dist/index.d.ts` — every prop and hook is fully typed.
241
+ - **CHANGELOG:** [CHANGELOG.md](./CHANGELOG.md) — every export added per version.
242
+
243
+ Questions or missing features: file an issue or ping the politogy team.
@@ -0,0 +1,6 @@
1
+ // Side-effect type declarations for the CSS subpath exports so TS consumers
2
+ // can `import '@the-portland-company/shell/css'` and
3
+ // `import '@the-portland-company/shell/chrome.css'` without ambient-shim
4
+ // boilerplate.
5
+ declare module '@the-portland-company/shell/css'
6
+ declare module '@the-portland-company/shell/chrome.css'
package/dist/index.cjs CHANGED
@@ -1275,6 +1275,20 @@ function getUserDisplayName(user) {
1275
1275
  if (!user) return "User";
1276
1276
  return user.profile?.full_name || user.profile?.name || user.user_metadata?.full_name || user.user_metadata?.name || (typeof user.email === "string" ? user.email.split("@")[0] : "") || user.email || "User";
1277
1277
  }
1278
+
1279
+ // src/lib/resolveModeConfig.ts
1280
+ function resolveModeConfig(mode) {
1281
+ if (!mode) return null;
1282
+ const modes = mode.modes ?? [];
1283
+ if (typeof mode.getModeConfig === "function") {
1284
+ try {
1285
+ const result = mode.getModeConfig();
1286
+ if (result) return result;
1287
+ } catch {
1288
+ }
1289
+ }
1290
+ return modes.find((m) => m.id === mode.currentMode) ?? modes[0] ?? null;
1291
+ }
1278
1292
  function avatarUrlFromUser(user) {
1279
1293
  if (!user) return void 0;
1280
1294
  const meta = user.raw.user_metadata;
@@ -1299,7 +1313,7 @@ function UserMenu() {
1299
1313
  if (status !== "authenticated" || !user) return null;
1300
1314
  const displayName = getUserDisplayName(user.raw);
1301
1315
  const avatarUrl = userAvatarUrl ?? avatarUrlFromUser(user);
1302
- const modeConfig = mode?.getModeConfig() ?? null;
1316
+ const modeConfig = resolveModeConfig(mode);
1303
1317
  const currentOrg = org?.currentOrg ?? null;
1304
1318
  const linkedAccounts = linked?.linkedAccounts ?? [];
1305
1319
  const activeRoleConfig = role?.activeRoleConfig ?? null;
@@ -1587,7 +1601,7 @@ function NavItem({
1587
1601
  const navigate = reactRouterDom.useNavigate();
1588
1602
  const mode = useShellMode();
1589
1603
  const currentMode = mode?.currentMode ?? null;
1590
- const modeConfig = mode?.getModeConfig() ?? null;
1604
+ const modeConfig = resolveModeConfig(mode);
1591
1605
  const navPref = useShellNavigationPreference();
1592
1606
  const showSubMenu = navPref?.showSubMenu ?? true;
1593
1607
  const navHoverGradient = currentMode === MODE_CAMPAIGN ? "linear-gradient(90deg, #F2C94C 0%, #E63946 100%)" : currentMode === MODE_PETITION ? "linear-gradient(90deg, #F2C94C 0%, #007BFF 100%)" : "linear-gradient(90deg, #007BFF 0%, #E63946 100%)";
@@ -2134,8 +2148,8 @@ var OrgPill_default = OrgPill;
2134
2148
  function ModePill() {
2135
2149
  const mode = useShellMode();
2136
2150
  if (!mode) return null;
2137
- const { currentMode, modes, switchMode, getModeConfig } = mode;
2138
- const modeConfig = getModeConfig();
2151
+ const { currentMode, modes, switchMode } = mode;
2152
+ const modeConfig = resolveModeConfig(mode);
2139
2153
  if (!modeConfig) return null;
2140
2154
  const hasMultipleModes = modes.length > 1;
2141
2155
  const isDisabled = !hasMultipleModes;