@the-portland-company/shell
v0.43.1
Published
Shared chrome (header, sidebar, footer, theme, auth) for politogy apps. Drop into any Vite SPA to inherit the politogy look and signed-in user.
Downloads
11,007
Readme
@the-portland-company/shell
Shared chrome for politogy apps. One install, one wrapper, identical look.
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.
v0.3.x is a major rewrite. See the CHANGELOG 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.
Install
Pin peer dep versions explicitly — Vite scaffolds React 19 + Chakra v3 by default, neither of which the shell supports:
npm install @the-portland-company/shell \
react@^18 react-dom@^18 \
@chakra-ui/react@^2.8 @emotion/react@^11 @emotion/styled@^11 \
framer-motion@^11 \
@supabase/supabase-js@~2.95 \
react-router-dom@^6 react-icons@^5@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.
Minimum viable boot
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).
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { ShellProvider, AppLayout } from '@the-portland-company/shell'
import '@the-portland-company/shell/chrome.css'
import { createClient } from '@supabase/supabase-js'
import App from './App'
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
)
ReactDOM.createRoot(document.getElementById('root')!).render(
<ShellProvider currentApp="email" supabaseClient={supabase}>
<BrowserRouter>
<AppLayout>
<App />
</AppLayout>
</BrowserRouter>
</ShellProvider>,
)<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.
Real-world boot: feeding the chrome real data
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:
import {
ShellProvider,
ShellChrome,
AppLayout,
} from '@the-portland-company/shell'
function ChromeBridge({ children }) {
const { currentMode, switchMode, getModeConfig, getAvailableModes } = useMode()
const { currentOrg, organizations, switchOrganization } = useOrganization()
const { linkedAccounts, ... } = useLinkedAccounts()
// …compute the nav-items list for the active mode here…
return (
<ShellChrome
mode={{ currentMode, modes: getAvailableModes(), switchMode, getModeConfig }}
organization={{ currentOrg, organizations, switchOrganization }}
linkedAccounts={{ linkedAccounts, switchToAccount, onAddAccount, onRemoveAccount }}
navItems={navItems}
role={{ activeRoleConfig, isSuperAdmin }}
footer={{ links, commit, version, releaseName, environmentLabel }}
support={{ submitSupportRequest }}
brandLogoSrc="/brand/logos/email-mode-logo.png"
headerIconsSlot={<MyAppSpecificIcons />}
userAvatarUrl={profile.avatar_url ?? storedMemojiUrl ?? defaultAvatarUrl}
myProfileHref="/my-profile"
organizationSettingsHref={`/organization-settings?org=${currentOrg.id}`}
>
{children}
</ShellChrome>
)
}
// Then in main.tsx:
<ShellProvider currentApp="email" supabaseClient={supabase}>
<BrowserRouter>
<AuthProvider>
<OrganizationProvider>
<ModeProvider>
<ChromeBridge>
<AppLayout>
<Routes>…</Routes>
</AppLayout>
</ChromeBridge>
</ModeProvider>
</OrganizationProvider>
</AuthProvider>
</BrowserRouter>
</ShellProvider>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.
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).
What the shell exports
Components
| Export | Use |
|---|---|
| <ShellProvider> | Top-level provider. Wraps Chakra + auth + meta contexts. |
| <ShellChrome> | Mid-tree binding component. Feeds chrome contexts from app data. |
| <AppLayout> | Composes Header + Sidebar + main content + Footer. |
| <Header> | Header bar (logo, page title, breadcrumbs, icon stack, user menu). |
| <Sidebar> | Sidebar (branded logo, org pill, mode pill, nav list, footer). |
| <Footer> | Below main content. Usually not rendered separately. |
| <UserMenu> | Avatar dropdown (profile, theme, sign out, switch account). |
| <OrgPill> (alias <OrgSelector>) | Organization switcher pill rendered in the sidebar. |
| <ModePill> | Mode (Relationship / Campaign / Petition) switcher. |
| <NavItem> | Single sidebar nav row with active state + hover gradient. |
| <SupportRequestButton> | Header life-buoy icon that opens a support modal. |
| <BrandLogo> | <img> wrapper for the branded mode logo. |
| <BrandIcon name="..."> | Renders /brand/icons/<name>.svg from your public/. |
| <CommitCopyButton> | Footer commit/version display with copy-to-clipboard. |
| <Card>, <PageContainer>, <PageHeader>, <BreadcrumbBar>, <HeaderTabBar> | Page-level layout primitives. |
| <GlobalUiIdProvider> | Assigns stable UI IDs (for testing/analytics). |
Hooks
| Hook | Returns |
|---|---|
| useShellAuth() | { status, user, session, signOut } |
| useShellOrganization() | { currentOrg, organizations, switchOrganization, createOrgHref } \| null |
| useShellMode() | { currentMode, modes, switchMode, getModeConfig } \| null |
| useShellNavItems() | { items: ShellNavItem[] } \| null |
| useShellLinkedAccounts() | { linkedAccounts, switchToAccount, onAddAccount, onRemoveAccount } \| null |
| useShellSupport() | { submitSupportRequest } \| null |
| useShellRole() | { activeRoleConfig, isSuperAdmin } \| null |
| useShellFooter() | { links, version, commit, releaseName, … } \| null |
| useShellNavigationPreference() | { showSubMenu } \| null |
| useShellChrome() | Chrome configuration (devNotesMenu, myProfileHref, userAvatarUrl, pageTitle, breadcrumbs, headerActions, etc.) |
| useAppRegistry(), useCurrentApp() | App switcher metadata for cross-app speculation rules. |
Helpers
normalizeHexColor, lightenHexColor, hexToRgba, getOrganizationBrandColor, getOrganizationBadgeLetters, getUserDisplayName.
Theme
politogyTheme — Chakra theme with brand palette, mode-aware semantic tokens, and component overrides.
CSS imports
import '@the-portland-company/shell/chrome.css' // :root design tokens (--app-color-brand-*, --mode-*, --app-tab-*, --form-control-*)
import '@the-portland-company/shell/css' // view-transitions CSS for cross-app polishchrome.css is required for theme variables. css (view-transitions) is optional polish.
Mode-aware navigation (Campaign Mode)
PolitogyAppFrame and standardPolitogyNav render a different left-nav tree per
operating Mode — and an app passes nothing to select it. Which tree an app
renders is derived from the app's own mode in the POLITOGY_APPS registry:
- Unscoped apps (no
mode— forms / surveys / polls / contacts / vrm) render the Relationship tree, byte-for-byte as before, with an entitlement-driven Mode pill. - Campaign apps (
a2p,phonebank,canvass,signs, allmode: 'campaign') render the Campaign tree (Dashboard, Voter Contact, then the launched feature apps) and a static Campaign pill. - Petitions (
mode: 'petition') renders a minimal Petition tree.
Campaign features ship incrementally. CAMPAIGN_FEATURE_AVAILABILITY (exported
from @the-portland-company/shell/native) gates each feature: a disabled one is
omitted from the nav, and its flag flips to true in a patch release as the app
launches. Defaults: Polling Engine and Tribes on; A2P, Phone Banking, Field
Canvassing, Sign Mapping off.
Cross-app links in the Campaign/Petition trees carry the active org as
?org=<last6> — the same convention the Mode switcher and My Account menu use —
so the destination app opens on the same org. When the chrome API grants more
than one Mode (scope.modes), the pill becomes a switcher whose selection
navigates to that Mode's home carrying the active org.
Multi-SPA routing on app.politogy.com
Multiple politogy apps live behind one domain via a Cloudflare Worker edge router (in packages/edge-router of the monorepo).
When you deploy a new app to Cloudflare Pages, add its path prefix to the edge router's routes.ts:
{
pathPrefix: '/email',
origin: 'https://politogy-email.pages.dev',
}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.
Service worker (optional polish)
The package ships a precache service worker that caches the shell chrome on first visit so cross-app navigation feels instant.
// scripts/copy-shell-sw.mjs
import { copyFile } from 'node:fs/promises'
await copyFile(
'node_modules/@the-portland-company/shell/dist/shell-precache.worker.js',
'public/sw-shell.js',
)Wire it into your build ("postinstall": "node scripts/copy-shell-sw.mjs") and call registerShellPrecache() once on app boot.
Speculation Rules (optional polish)
Cross-app prerendering hints for the browser:
import { useAppRegistry, useCurrentApp, buildSpeculationRules } from '@the-portland-company/shell'
function SpeculationRules() {
const registry = useAppRegistry()
const current = useCurrentApp()
const json = buildSpeculationRules(current, registry)
if (!json) return null
return <script type="speculationrules" dangerouslySetInnerHTML={{ __html: json }} />
}Render once near the app root.
Reference + support
- Reference bridge:
politogy-vrm/react/app/src/providers/ShellChromeBridge.tsx— full chrome wiring for the main politogy app. - Architectural decisions, gotchas, session log:
politogy-vrm/docs/superpowers/. - Type definitions:
node_modules/@the-portland-company/shell/dist/index.d.ts— every prop and hook is fully typed. - CHANGELOG: CHANGELOG.md — every export added per version.
Questions or missing features: file an issue or ping the politogy team.
