@campfire-interactive/help-panel
v0.3.1
Published
In-app product help panel for Campfire Suite apps — docked and expanded reading surfaces, search, screen blueprints, and an Ask chat. Chrome only; the host wires the content source.
Keywords
Readme
@campfire-interactive/help-panel
In-app product help for Campfire Suite apps: a drawer docked beside the page, an expanded reading surface with a contents tree and screen blueprints, and an optional Ask chat.
Chrome only. The package owns presentation and panel state; the host wires the
content source, the current route, its strings, and its chat endpoint. Same split
as the bug-report modal in shell-header.
Install
npm install @campfire-interactive/help-panelPeers: react, react-dom, @tanstack/react-query. No CSS import — the
stylesheet is injected by the bundle.
Wiring
import {
HelpPanelProvider, HelpDrawer, HelpOverlay, useHelpMode,
type HelpContentSource,
} from '@campfire-interactive/help-panel';
const contentSource: HelpContentSource = {
cacheKey: 'myapp',
getToc: () => apiGet('/v1/help/myapp'),
getPage: (slug) => apiGet(`/v1/help/myapp/${slug}`),
getForRoute: (path) => apiGet('/v1/help/myapp/for-route', { path }),
search: (q) => apiGet('/v1/help/myapp/search', { q }),
// Omit `chat` and there is no Ask tab. That is the feature switch.
chat: (vars) => apiPost('/v1/help/myapp/chat', vars),
};
<HelpPanelProvider
contentSource={contentSource}
currentPath={useLocation().pathname}
namespaceLabel={(ns) => NAMESPACE_LABELS[ns] ?? ns}
t={useTranslation('help').t}
urlSync={mySearchParamsUrlSync}
chatStarters={[{ key: 'chat.starter.quote', text: 'How do I create a quote?' }]}
>
<AppShell />
</HelpPanelProvider>Then render both surfaces inside the provider, and open from anywhere with
useHelpMode().openHelp().
Two host requirements the package cannot ship
1. min-width: 0 on your main content. The drawer is a flex sibling of your
page, not an overlay — that is what lets the page reflow beside it and stay
clickable while help is open. A flex item's default min-width: auto refuses to
shrink below its content, so without this the page shoves the drawer off the right
edge instead of making room:
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
<Sidebar />
<main style={{ minWidth: 0, flex: 1, overflowY: 'auto' }}>{children}</main>
<HelpDrawer />
<HelpOverlay />
</div>2. If you use react-router, supply your own urlSync. Do not use the exported
historyApiUrlSync() — react-router's BrowserRouter keeps its own history
object and does not observe an external replaceState, so the panel's write would
desync it and your next navigation would drop ?help=. Ten lines instead:
const [params, setParams] = useSearchParams();
const urlSync = useMemo(() => ({
read: () => {
const page = params.get('help');
if (!page) return null;
const anchor = window.location.hash.replace(/^#/, '');
return anchor ? { page, anchor } : { page };
},
write: (v) => setParams((p) => {
if (v) p.set('help', v.page); else p.delete('help');
return p;
}, { replace: true }), // replace, never push
subscribe: () => () => {}, // react-router re-renders on its own
}), [params, setParams]);replace, not push, is the point: help sits over the reader's own work, so a
history entry per help page would make Back mean "the previous help page" while
they are looking at their screen behind the panel. Omitting urlSync entirely is
fine — the panel simply is not linkable.
Resizing the drawer
The docked drawer opens at 336px and the reader drags its left edge to whatever
width suits the task; the width is remembered in that browser. Arrow keys on the
edge (a focusable separator) move it too, Shift for bigger steps, Home resets,
End goes to the widest allowed. Double-click resets. The drawer never covers the
page: it stops at the row's width minus 320px, and never goes below 280px or
above 1200px. Nothing to configure on the host side — the only requirement is the
min-width: 0 above, which is what lets the page reflow as the drawer grows.
The expand control is unchanged: it is a different act (the whole reading surface,
over the page), not a wide drawer.
Theming
Every colour goes through a --cfi-hp-* custom property with the shipped value as
its fallback. Override on .cfi-hp-root, or globally:
:root {
--cfi-hp-accent: #0f766e; /* the Ask surface */
--cfi-hp-frame: #1e293b; /* the expanded panel's border — match your shell */
}--cfi-hp-frame is worth setting: it defaults to the sidebar colour of the first
consuming app, so the expanded panel reads as that app's chrome closing around it.
i18n
t is optional. Every string carries an English default at its call site, so
omitting t renders correct English and the package ships no translation
machinery. Pass useTranslation('help').t (or any
(key, fallback, vars) => string) to translate.
Content contract
Page shapes mirror the platform guide contract registered in nextgen
CONTRACTS.md ("Help guide bundle"). Two properties matter for consumers:
- Anchors are authored, not derived. They arrive on the page payload as
sections, and the markdown does not contain them. That is what lets a heading be reworded without orphaning every citation and deep link pointing at it. - Slugs and anchors are stable identifiers. Citations address them, so moving one is a breaking change even though no field changes.
