@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 +24 -0
- package/README.md +198 -27
- package/dist/css-modules.d.ts +6 -0
- package/dist/index.cjs +18 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
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
|
-
|
|
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
|
-
|
|
15
|
-
import '
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
{ id: 'crm', label: 'CRM', path: '/' },
|
|
21
|
-
{ id: 'messaging', label: 'Messaging', path: '/messaging' },
|
|
22
|
-
]
|
|
60
|
+
---
|
|
23
61
|
|
|
24
|
-
|
|
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
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
### Theme
|
|
44
166
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2138
|
-
const modeConfig =
|
|
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;
|