@campminder/campminder-shell-react
v0.3.2
Published
> Published on public npm. Install and render: [`docs/consuming-the-shell.md`](../../docs/consuming-the-shell.md). > Working in here: [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
Maintainers
Keywords
Readme
@campminder/campminder-shell-react
Published on public npm. Install and render:
docs/consuming-the-shell.md. Working in here:CONTRIBUTING.md.
The React face of the Campminder shell — the "maximally React-friendly wrapper" ADR D3 prescribes.
It wraps the shell, not the chrome, and that distinction is load-bearing: this package
depends on @campminder/campminder-shell, not on @campminder/components. That is why
both the package and its directory are portal-scoped — a CampInTouch portal gets its own
@campminder/campintouch-shell-react beside this one, not a shared generic wrapper.
Port from: core-js/packages/navigation (campminder-navigation v0.0.1)
This one is unusual: it already exists and already ships. Three core-js apps
(reporting, admin, communications) consume it today via workspace:*. It is
unpublishable by a single word — "private": true — which is the only reason it
is not a dependency already.
That makes #25850 a publish, not a build.
Port inventory
Coming across: AppLayout, TopBar, SideNav, SuperUserBanner, and the
useLoadScript / useCreateElement hooks.
Strip on the way in
data.tsdoes not come across. 37,556 bytes on three lines — a hardcoded fixture with real test-tenant PII baked in. This is defect D1. The tree is passed in as configuration.- DOM sniffing in
AppLayout— it readshasSideNavoff the DOM to computeleftOffsetPx. The shell knows which chrome it composed; nothing needs to ask the document. - The QA default URL — see the Phase 8 exit criteria in the technical plan.
Why the name changed
The source package is unscoped and private (campminder-navigation). Publishing
needs a scope, and -react says what it is: the React face of chrome that is
canonically Lit.
Floor 9 — the public surface
Assessed 2026-09-08 as fresh composition that borrows the hook knowledge, not a copy
of AppLayout — four of that component's five files exist only to solve CDN script
loading, which stops being a problem once the chrome is a real dependency. What ships
here is <CampminderShell> and useWebComponentProperty, sized to make five real
consumer defects (found adopting this shell in reports, AB#25851) impossible to
reproduce:
'use client'
import {useRef} from 'react'
import {CampminderShell, useWebComponentProperty} from '@campminder/campminder-shell-react'
import type {CampminderAppShell} from '@campminder/campminder-shell'
function Page() {
const navRef = useRef<HTMLElement & {tree: unknown}>(null)
useWebComponentProperty(navRef, 'tree', CAMPMINDER_TREE)
return (
<CampminderShell portal="campminder" brandColor="#5B2D8E">
<cm-side-nav ref={navRef} slot="side-nav" />
<main>…page content…</main>
</CampminderShell>
)
}- Browser-only loading of both packages (finding 1).
<CampminderShell>never statically imports@campminder/campminder-shellfor its runtime value — only its types, which TypeScript erases. The real import is a dynamicimport()inside an effect, so the module is safe to import on the server; it rendersnulluntil that import resolves. Registering the shell's custom element also registers the rest of the chrome, because the shell itself now imports@campminder/componentsas a side effect (Floor 10) — oneimport()here is enough for both packages. treeas a property, never a JSX attribute (findings 2 and 3).cm-side-nav'streeis{attribute: false}in@campminder/components; a JSX prop on it is a silent no-op.useWebComponentProperty(ref, 'tree', value)is the one sanctioned way this package offers to assign it, viauseLayoutEffectso it lands before paint. It is generic on purpose — the same footgun applies tocm-top-bar'sdata.- The definite-height requirement (finding 4) is documented, not silently patched —
see "Definite height" below — but
<CampminderShell>also measures itself after mount andconsole.warns once if it renders at 0px height, so the failure is loud rather than a page that quietly renders with no chrome visible. - The collapse toggle (finding 5) is wired, not inert.
cm-side-navdispatchescm-side-nav-toggle(bubbles, composed) and deliberately does not mutate its owncollapsed— the shell is the declared owner.<CampminderShell>listens for it on its own host element and applies the result to the shell'scollapsedproperty, withcollapsed/defaultCollapsed/onCollapsedChangefor a consumer that wants to control or observe it. (Floor 10 also teaches<campminder-app-shell>itself to own this, with per-portal persistence — this wrapper's own listener means a React consumer is never left with an inert toggle even against an older shell version.)
Definite height
<campminder-app-shell>'s :host { height: 100% } only resolves against a containing
block that itself has a definite height. Most Next.js App Router pages do not have
one — html and body are unconstrained by default — so the chrome silently collapses
to its content height. Give an ancestor a definite height:
html,
body {
height: 100%;
}or set one on <CampminderShell> itself (e.g. height: 100dvh on a wrapping element).
There is no fix for this from inside the shell's shadow root — it depends on an ancestor
chain the shell does not control — which is why the failure is a console warning rather
than something this package can silently correct.
shouldRenderSideNav / shouldRenderTopBar / shouldRenderSuperUserBanner
Named to match campminder-navigation's AppLayout, whose booleans this wrapper is
where the shell's inverted-attribute polarity gets absorbed: <campminder-app-shell>
uses no-side-nav / no-top-bar because an HTML boolean attribute cannot default to
true. A React consumer of this package never sees the negative attributes — side nav
and top bar default true, the banner defaults false.
Next.js App Router — "use client"
<CampminderShell> and useWebComponentProperty are marked 'use client' in source,
but this package's own Vite/Rollup build strips module-level directives from the
compiled dist/index.js — a known limitation of building a library with Rollup rather
than Next's own toolchain. A consumer must re-declare the boundary in their own file
that imports this package:
// app/campminder-shell.client.tsx
'use client'
export {CampminderShell, useWebComponentProperty} from '@campminder/campminder-shell-react'and import from that file rather than from @campminder/campminder-shell-react
directly in a Server Component tree. Tracked as a Floor 10 release-checklist item; not
fixed in this package because it costs every consumer more to work around than to
declare it once, and the workaround is the standard shape recommended for any
third-party React package that ships client-only components.
Not in this package's scope
Following the README's boundary above (this wraps the shell, not the chrome): it does
not depend on, import, or re-export anything from @campminder/components. A consumer
renders <cm-top-bar> / <cm-super-user-banner> the same way as <cm-side-nav> in the
example above — plain custom elements, slotted as children, with useWebComponentProperty
for any non-string property.
