Page Composition
The In Page Header gives a content page its breadcrumb, title, primary actions, and (optionally) section-global search. The Page Toolbar sits beneath it with the page-scoped filter row. Page templates define the canonical shell for report, AI insights, and agentic layout pages.
In Page Header Component
Canonical page header at the top of every list / browse / content management page. Left column: breadcrumb stacked above a title (or just the title on top-level pages). Right cluster: optional section-global search, at most one primary action button, optional ellipsis overflow menu. Sticky-pinned to the top of its scroll container with a 1px bottom rule. Page-scoped controls (search, filter, sort, view-toggle) DO NOT belong here — they live in the Page Toolbar below. Section-global search is a different control and may live in the right cluster via the globalSearch config. NEVER hardcode In Page Header HTML — always use the .bsn-in-page-header-mount mount point.
With breadcrumb + single primary action
With breadcrumb + primary action + overflow menu
With back button — for return-to-task from a search/filter result
No breadcrumb — top-level page (height-matched to nav-sidebar-header)
With section-global search New — chrome wayfinding, leftmost in the right cluster
Pass globalSearch: { id, placeholder, ariaLabel?, kbd? } to render a 260px search input as the leftmost item in the right cluster. This is the canonical home for section-global search (e.g. an entire Learning section, not just this page) — it stays in a stable position across every page within the section. Suppressed in sections where it doesn't apply by simply not passing the config. The ⌘K hint inside the input is a promise — pages MUST bind Cmd/Ctrl+K to focus the input. See accessibility.md → Keyboard Accelerators.
With section-global search + primary action New — search composes with existing right-cluster items
<div class="bsn-in-page-header-mount" data-bsn-in-page-header='{"breadcrumb":[…],"title":"…","actions":[…],"overflowActions":[…],"back":{…},"globalSearch":{…},"sticky":true}'></div>
globalSearch (leftmost) · primary action · overflow menu (rightmost). The cluster composes additively — pages can carry any subset.
overflowActions (ellipsis menu). Top-level pages MUST omit breadcrumb — the no-breadcrumb variant's height matches the navigation top section. Page-scoped controls (search/filter/sort/view-toggle) belong in the Page Toolbar below, NEVER in this header. Section-global search is a different control and is permitted in the right cluster via globalSearch — it is wayfinding chrome, not a page filter.
Page Toolbar Component
The page-level filter bar rendered via buildPageToolbar(). Sits directly beneath the In Page Header (see Navigation System). Layout: search + Filters cluster on the LEFT as the scope-narrowing controls; sort + viewToggle + optional Export live on the RIGHT. Export is opt-in — most list / browse surfaces don't need it; configure it only when exporting the current view makes sense for the user. Title / chip / primary action buttons are not part of this component — they belong in the In Page Header above. NEVER hardcode page-toolbar HTML — always use the .bsn-page-toolbar-mount mount point.
Full toolbar — search · sort · view toggle · Filters (drawer) · Export · active chips
Minimal toolbar — search · sort · Filters (popover) · compact chips maxInline: 1
Search + Filters (popover) only · chips maxInline: 3
Pill Tabs — second-row layout · Filters (popover) · active chips
When the first row carries search or filters, pill-tabs drop to a full-width second row beneath it, divided by a 1px rule. Use when pill-tabs are an additional always-visible single-select dimension on top of free-text search or multi-dimensional filters (marketplaces, content libraries).
Pill Tabs — inline-left layout
When the toolbar has no search and no filters, pill-tabs render INSIDE the toolbar's left cluster as the page's primary control. Single row, no divider. Use when pill-tabs ARE the primary control on the page (browse-by-category surfaces, type pickers). The layout is auto-selected — same pillTabs config, different layout based on context. Force a specific layout with pillTabs.layout: "inline" | "row".
<div class="bsn-page-toolbar-mount" data-bsn-page-toolbar='{"search":{"placeholder":"…"},"sort":{…},"viewToggle":[…],"filters":{"label":"Filters","filtersKey":"my-page"},"export":{"formats":[…]},"pillTabs":{"items":[…]}}'></div>
buildFilters({ key: filtersKey, mode: "drawer" }) — the canonical Filters component (see below). Register the dimension config once per page; Page Toolbar resolves to it via filtersKey. The filter-count badge derives automatically from buildFilters's applied state — pages do not pass count. The trigger stays visible while the drawer is open; it is the canonical handle for both opening AND closing. .has-active is set by Page Toolbar wiring whenever R.activeCount(filtersKey) > 0; aria-expanded is toggled by the drawer lifecycle. Both states compose additively — a button can simultaneously read as "drawer open" AND "filters applied". Pages do not manage either class by hand.
window.openExportMenu(triggerEl, key, formats[]) — pages override this to render the menu. Configure formats with {key, label, meta, icon} (typical: CSV, PDF) plus extraActions for utilities like Copy shareable link. Export must always be configured alongside Filters; never render Filters without Export on a list / browse page, and vice-versa.
search + Filters cluster on the LEFT (so the "narrow the scope" controls read together); sort, view-toggle, and (when present) Export on the RIGHT. Export is optional — only configure it on surfaces where exporting the current list/view makes sense (e.g. a content library you'd hand off as CSV). Do not stack multiple page-toolbars on the same page. Always place the toolbar directly below the In Page Header, above any content (KPI strip, cards, table, etc.).
activeFilters: { maxInline, filtersKey } to render chips inline immediately after the Filters button. Set filters.mode: "popover" to open an anchored popover instead of a full-width drawer. Per-chip × resets that dimension to its default value immediately, leaving all other active filters intact. Overflow beyond maxInline collapses to a dashed +N more chip that re-opens the filter panel scrolled to the first hidden section.
maxInline: "auto" — the toolbar measures available space in the first row via ResizeObserver and recomputes how many chips fit live, every time the row width changes (window resize, panel expand, sidebar toggle, etc.). Chips that no longer fit are folded into +N more automatically; chips reappear as space opens back up. Use "auto" as the default on full-width list / browse pages where toolbar width is variable.
maxInline: 1 — compact mode: only one chip is ever rendered inline. A second active filter immediately becomes +1 more; a third becomes +2 more, and so on. The chip count in the overflow badge always reflects the full active-filter tally. Use maxInline: 1 on dense or narrow toolbars — widget-level views, sidebar-embedded toolbars, or any surface where chip row space is at a premium. Paired with filters.mode: "popover" this is the standard widget-level filter-summary pattern.
"auto" for full-width page toolbars where width varies; 1 for narrow / widget toolbars; 2–3 for medium-width panels; 4+ only when the toolbar has guaranteed wide space (e.g. a dedicated filter-bar layout with no search or sort controls sharing the row). Never hard-code a number so large the toolbar cannot reliably fit it without crowding other controls — prefer "auto" when in doubt.
<div class="bsn-active-filters-mount" data-bsn-active-filters='{"source":"yourKey","maxInline":4,"showClearAll":true}'></div>. Subscribes to source in the filter registry; renders nothing when no dimension is active. Each chip's × resets that dimension to its default immediately, leaving other active filters intact; +N more opens the filter panel scrolled to the first hidden section. Set showClearAll: true to append a "Clear all" control at the trailing edge. Configure maxInline by placement: 2 inside a scope bar (space is shared with date and export controls), 4–6 below a full-width Page Toolbar. Read-only — never owns state, only reflects what buildFilters committed. NEVER hardcode chip HTML.
pillTabs config. The toolbar picks the layout automatically: inline-left when neither search nor filters is configured (pills become the primary first-row control, single row, no divider — for browse-by-category surfaces like By Application / By Publisher / By Topic / By Type), or second-row when the first row carries search or filters (pills drop below the first row, divided by a 1px rule — for marketplaces and libraries with All / Free / Paid or All / Active / Draft / Archived). Force a specific layout with pillTabs.layout: "inline" or pillTabs.layout: "row". pillTabs takes the same shape as buildPillTabs() ({items[], ariaLabel?, key?, class?, count?}) including the optional count badge per item. The pill-tabs are rendered automatically by the toolbar; no separate .bsn-pill-tabs-mount is needed. Reserve this variant for the single most-important coarse filter; multi-dimensional refinement (text-search, ranges, multi-selects) still belongs in the Filters drawer.
Filters Component
The single canonical filter mechanism in the design system, rendered via buildFilters(). Surfaces in two modes — drawer (right-side panel with scrim, for page-level scope) and popover (anchored panel, for widget-level scope). Same body in both modes: a stack of collapsible sections with field types including select, multi-search (typeahead + checklist), pill-group, toggle, range. Apply commits draft state to applied state and re-queries; sections collapse by default when at default value and expand when set. NEVER hardcode filter UI — always use the .bsn-filters-mount mount point.
Visual reference — open drawer & popover
Static side-by-side of the drawer (page-level scope) and the popover (widget-level scope) in their open state, with one section expanded showing each field type. Both renderings share the same body atoms — only the chrome (width, scrim, anchor) differs.
Drawer · page-level scope · 28rem
Popover · widget-level scope · 19rem
Drawer mode · page-level scope
select, multi-search, pill-group, toggle, range). Sections start collapsed when at default; expand when set. Apply commits and closes.
Popover mode · widget-level scope
<div class="bsn-filters-mount" data-bsn-filters='{"key":"…","mode":"drawer","sections":[…]}'></div>
window.bsnOpenFilters("yourKey"), or pass { focus: "sectionKey" } to land the user on a specific dimension. Page Toolbar's Filters button and Report Scope Bar's filter icon both call this internally.
select, multi-search, pill-group, toggle, range, date-range (reserved). Each section's collapsed header shows a summary value — Alpine + 600 weight when set, muted when default.
Report Scope Bar Component
A slim, sticky row that scopes a report's payload on §14.2 / §14.3 pages, rendered via buildReportScopeBar(). Composes the Date menu button, date caption, buildActiveFilters, the buildFilters drawer trigger, and an Export menu. Lives in the scroll content (NOT inside the In Page Header zone) and sticky-pins beneath the header at top: 4.8125rem. NEVER hardcode scope-bar HTML.
Full bar — date menu, active-filter chips, Filter drawer trigger, Export menu
Try it: open the date menu, open the filter drawer and apply a couple of dimensions, then watch chips appear and the Filter badge count up. Open the export menu to see CSV / PDF / Copy link.
<div class="bsn-report-scope-bar-mount" data-bsn-report-scope-bar='{"key":"…","filtersKey":"…","date":{…},"export":{…}}'></div>
filtersKey with the registered buildFilters config.
position: sticky; top: 4.8125rem by default (matches In Page Header height) — pass "sticky": false for embedded previews.
14.1.1 — Standard Page Template Layout
The canonical layout for a non-agentic, non-reporting content page — list / browse / content management. Primary nav fixed left, scrollable Salt content area with an In Page Header (breadcrumb + title + right-anchored actions), a Page Toolbar (search + Filters drawer-trigger + sort + view toggle), and a content body that renders the same items as both a grid of content cards and a list/table view. The view toggle in the Page Toolbar switches between them in place. No BrainStorm AI bar, no KPI strips, no charts — this is the simpler counterpart to §14.2 / §14.3 for pages that don't need them.
<div class="bsn-standard-page-mount" data-bsn-standard-page='{"prefix":"…","navConfig":{…},"inPageHeaderConfig":{…},"pageToolbarConfig":{…},"items":[…]}'></div>
grid/list view toggle into the Page Toolbar automatically (you can override via pageToolbarConfig.viewToggle). Clicking either button flips the content container's data-view attribute, which CSS uses to show the grid or the list. No page reload, no re-fetch — same data, different layout.
buildNav(), buildInPageHeader(), and buildPageToolbar(). Swap any config block to retarget the template for a new page (Users, Teams, Flows, etc.) without duplicating chrome.
14.2 — Report Page Template Layout
The canonical layout for a data visualization report page — e.g. Flow Adoption (in the demo below), which sits as a sibling of Summary and AI Insights under the Reporting primary nav item. Primary nav fixed left, scrollable Salt content area with structured report zones. The content area follows a top-to-bottom analytical narrative: context → KPIs → trends → breakdowns → detail. The page header uses the In Page Header component — just the title; no inline header actions — and every scope / export / filter control lives in the Report Scope Bar directly below the header. The §12.3 BrainStorm AI input bar is intentionally not mounted on report pages (reports are static, not conversation surfaces). For an agentic, conversation-driven analytical surface, use §14.3 AI Insights Page Template instead.
flex:1; display:flex; flex-direction:column; overflow:hidden · Scroll area = flex:1; overflow-y:auto, full vertical space available (no Storm bar to clear). The In Page Header and Report Scope Bar both live inside the scroll container so they sticky-pin (header at top:0, scope bar at top:4.8125rem beneath it); the analytical zones (KPI → Trends → Breakdowns → Detail) sit in a separate inner padded wrapper so the two sticky bars can span edge-to-edge. Report pages do not mount the §12.3 BrainStorm AI input bar, and they do not carry an inline Ask BrainStorm AI header button — the agentic entry point on this surface is via the global Storm input bar on conversational templates (§14.3 AI Insights, §14.4 Agentic Page Layout). Keeping the report header free of CTAs preserves its identity-only role and avoids competing actions with the Report Scope Bar below it.
buildInPageHeader()'s no-breadcrumb height-matched variant. The nav (Reporting expanded, Flow Adoption highlighted as the active sibling) carries the location.
videos-ai-insights-report.html prototype in the AI Insights project.
14.3 — AI Insights Page Template Agentic
The AI Insights page is a conversation-driven report surface. A user prompt triggers BrainStorm AI to generate a structured response: an agent summary, an AI-generated report section, and recommended action items — followed by a Chat Follow-Up Footer with suggested next steps. Unlike the standard Report Page Template, the content is generated on demand and the primary CTA is starting a new chat.
activeKey: "reporting-ai-insights", expanding Reporting and showing AI Insights as active.
flex:1; display:flex; flex-direction:column; overflow:hidden · Scroll area = flex:1; overflow-y:auto · Prompt row = white bg, border-bottom, right-aligned bubble · Agent summary = left-border Alpine 3px, agent-bg fill · Report = standard chart-card, padding:0 · Action items = agent-action-card pattern · Follow-Up Footer = Salt bg, border-top, pill buttons · Storm bar = §12.3 component via .bsn-storm-mount (buildStorm builder), pinned bottom — never hardcode
14.4 — Agentic Page Layout Template Agentic
The canonical layout for any page that surfaces BrainStorm AI. Sidebar = Alpine dark, 270px, full height · Content column = flex:1; display:flex; flex-direction:column; overflow:hidden · Scroll area = flex:1; overflow-y:auto · In Page Header (§11) sits at the top of the scroll area via .bsn-in-page-header-mount and pins with position:sticky · BrainStorm AI bar (§12.3) = flex-shrink:0 pinned to bottom of the flex column via .bsn-storm-mount — no position:fixed needed. All three shells (header, breadcrumb, storm bar) are mounted via builders — never hardcoded.
.bsn-storm-mount + buildStorm(). Focus the textarea (CSS :focus-within drives State 2), type to trigger State 3 (send button appears, attach button drops .resting), clear text to revert. The In Page Header at the top of the scroll area is the canonical §11 component, mounted via .bsn-in-page-header-mount + buildInPageHeader(); it pins to the top of the scroll container via position:sticky, and because breadcrumb is omitted the no-breadcrumb height-matched variant renders (top-level page). Layout rules: Sidebar = Alpine dark, full height, fixed width 270px · Content column = flex column, fills remaining width · Scroll area = flex:1; overflow-y:auto, scrolls independently · In Page Header = sticky top, full-width (page body padding lives in an inner wrapper, not the scroll container) · BrainStorm AI bar = flex-shrink:0 at flex column bottom — sticks without position:fixed. Always use .bsn-storm-mount, .bsn-in-page-header-mount, and .bsn-breadcrumb-mount — never hardcode these shells.
Global Search Typeahead Component
Grouped-suggestions popover that pairs with the In Page Header globalSearch input. Owns matching, grouping, focused-row state, match highlighting, and the "See all results" submit handoff. Mount via .bsn-global-search-typeahead-mount + buildGlobalSearchTypeahead(), or attach imperatively via window.bsnGlobalSearchTypeahead(cfg) when you need real function references for onSubmit / onSelect. Try typing "excel", "pivot", "salesforce", or "slack" below — note arrow keys to navigate, Enter to select the focused row, Esc to close. The description snippet under a result surfaces only when the match was found in the description (not the title) — so it's clear why that row is here. The See all footer fires onSubmit(query) on click or Enter-without-focus.
onSelect / onSubmit — the action will appear here.
title, description, and meta. Title-hits rank above description-only-hits. When the match was only in the description, the row surfaces a 2-line description snippet with the matched substring highlighted in Alpine. Groups render in the order specified by groupOrder; unknown kind values render alphabetically AFTER listed ones. ↑ / ↓ wrap at ends; Enter on a focused row calls its onSelect, Enter with no focused row falls through to onSubmit. Esc closes the popover and blurs the input.
index item carries {kind, title, description?, meta, icon, onSelect}. Surfacing search hits that lead nowhere defeats the affordance — wire onSelect on every item.
.bsn-global-search-typeahead-mount + data-bsn-global-search-typeahead) works for static demos. For real product code that needs customMatch, onSubmit, or per-row onSelect as actual function references, call window.bsnGlobalSearchTypeahead(cfg) after the In Page Header has mounted.
Cmd/Ctrl+K to focus the anchor input. The typeahead does not own that binding — it lives at the page level. See accessibility.md → Keyboard Accelerators.
window.addEventListener('DOMContentLoaded', function () {
window.bsnGlobalSearchTypeahead({
anchor: '#global-search-typeahead-demo-input',
groupOrder: ['Apps', 'Content'],
seeAllLabel: 'See all {n} results for "{q}"',
emptyLabel: 'No matches for "{q}"',
index: [
{ kind:'Apps', title:'Microsoft Excel',
meta:'Microsoft 365 · 18 courses', icon:'fa-cube',
onSelect: q => demoResult('Selected app: Microsoft Excel') },
{ kind:'Apps', title:'Salesforce',
meta:'CRM · 12 courses', icon:'fa-cube',
onSelect: q => demoResult('Selected app: Salesforce') },
{ kind:'Apps', title:'Slack',
meta:'Collaboration · 9 courses', icon:'fa-cube',
onSelect: q => demoResult('Selected app: Slack') },
{ kind:'Content', title:'Excel — Pivot tables',
description:'Summarize, group, and explore data interactively',
meta:'Flow · 12 min', icon:'fa-diagram-project',
onSelect: q => demoResult('Opened content: Excel — Pivot tables') },
{ kind:'Content', title:'VLOOKUP & XLOOKUP fundamentals',
description:'Look up values across sheets with confidence',
meta:'Guide · 8 min', icon:'fa-book',
onSelect: q => demoResult('Opened content: VLOOKUP fundamentals') },
{ kind:'Content', title:'Salesforce — Opportunity stages',
description:'How pipeline progresses through your funnel',
meta:'Video · 9 min', icon:'fa-video',
onSelect: q => demoResult('Opened content: Opportunity stages') }
],
onSubmit: function (q) { demoResult('Submitted: "' + q + '" → results page'); }
});
});