@spark-web/design-system
Version:
--- title: Design System ---
682 lines (585 loc) • 29.9 kB
Markdown
# Vendor admin — dashboard / overview page pattern
## Before using this pattern
Read `node_modules/@spark-web/design-system/patterns/vendor-admin/CLAUDE.md`
fully before implementing this pattern. The layout-shell rule, the page-title
rule (and its **documented dashboard exception** — see "Page titling" below),
the spacing/density rule, the role/feature-flag gating rule, and the badge tone
mapping defined there all apply to this pattern and take precedence over
component-level rules.
Working in vendor-portal? Apply the substitutions in
`node_modules/@spark-web/design-system/patterns/vendor-admin/vendor-portal.md`.
## What this pattern is
The vendor landing / overview / home screen — the first screen a vendor /
installer / partner sees after signing in. It summarises the account at a
glance: a **metrics summary** (stat cards that click through to the underlying
list), a **resources section** (downloadable assets, links, promo cards), and an
optional, often flag-gated **announcements / news feed** ("What's New"). On a
vendor's first visit it may also raise a **first-run consent modal** that must
be acknowledged before the screen is usable.
It renders inside the vendor-admin shell (fixed Header + left NavBar) like every
other vendor-admin page, but its **page shell differs** from both the list page
and the form page: it is a responsive **multi-panel layout** — a primary content
panel plus an optional secondary feed panel — not the full-height single scroll
region of the list page and not the `Container`-centered column of the form
page.
## When to use this pattern
Use this pattern when the PRD describes any of the following:
- A "dashboard", "home", "overview", "landing", or "welcome" screen for a vendor
- A screen that summarises counts / totals across the account (applications
under review, approved, paid) as headline stats the user can click into
- A screen that bundles resources, downloads, promo cards, and/or a product
announcements / news feed
- A screen with a first-run consent / onboarding acknowledgement gate
If the screen is primarily a searchable, filterable table of records, use the
list-page pattern; if it is a settings / edit form, use the form-page pattern:
- `node_modules/@spark-web/design-system/patterns/vendor-admin/list-page.md`
- `node_modules/@spark-web/design-system/patterns/vendor-admin/form-page.md`
---
## Component docs to read
Read these before implementing — they own the component-level rules. Only read
the ones the screen actually uses:
- `node_modules/@spark-web/row/CLAUDE.md` — Row API; the primary/secondary panel
layout (and its `flexDirection` collapse on mobile)
- `node_modules/@spark-web/columns/CLAUDE.md` — Columns API; `collapseBelow`,
`gap`, `template` — the metrics and resources grids
- `node_modules/@spark-web/box/CLAUDE.md` — Box layout utilities (panels, card
placeholders, banner)
- `node_modules/@spark-web/stack/CLAUDE.md` — vertical stacking, gap
- `node_modules/@spark-web/heading/CLAUDE.md` — section headings (`level="2"`),
card sub-headings (`level="3"`)
- `node_modules/@spark-web/text/CLAUDE.md` — Text API (stat numbers, labels,
card copy)
- `node_modules/@spark-web/icon/CLAUDE.md` — icon sizing / tones
(`ArrowRightIcon`, `ArrowNarrowRightIcon`, `PhoneIcon`)
- `node_modules/@spark-web/button/CLAUDE.md` — `Button` / `ButtonLink` (resource
card actions, "Load More", consent Continue), `loading` prop
- `node_modules/@spark-web/text-link/README.md` — `TextLink` (in-prose links,
e.g. the consent modal's requirements link)
- `node_modules/@spark-web/modal-dialog/CLAUDE.md` — `ContentDialog` (the
first-run consent modal and any document dialog)
- `node_modules/@spark-web/alert/CLAUDE.md` — sanitized announcement banner /
page-level feedback
---
## Page structure
```
Page (renders into the vendor-admin shell content region — no Header/NavBar here)
Row (panel frame) — flexDirection row; collapses to column on mobile (responsive prop)
Primary panel (Box) — flexGrow={1}; padding responsive; the metrics + resources content
Banner (conditional) — sanitized announcement Alert/Text — NEVER dangerouslySetInnerHTML
Metrics section
Heading level="2" — section heading (dashboard exception: no single page title)
Columns — MetricCard placeholders (COMPONENT GAP) — collapseBelow, gap
Resources section
Heading level="2" — section heading
Columns — resource / promo cards (primitives — Box/Stack/Button/icon)
Help / contact banner (optional) — Box with PhoneIcon + Text (+ TextLink/Link)
Secondary panel (conditional) — announcements feed; rendered only when the feed flag is on
AnnouncementsFeed — "What's New" timeline + "Load More" (COMPONENT GAP placeholder)
ContentDialog (conditional) — first-run consent modal; showCloseButton={false}, Continue persists flag
```
The primary panel and the secondary feed panel sit side by side on tablet and up
and **stack vertically on mobile**. The feed panel is optional and frequently
behind a feature flag (consumer logic — see the surface gating rule).
---
## Page shell — multi-panel layout
The dashboard shell is a responsive two-panel frame, **not** the list page's
`Stack height="full"` scroll region and **not** the form page's
`Container size="medium"`. Use `Row` from `-web/row` for the panel frame:
a primary content panel (`flexGrow={1}`) and an optional secondary feed panel.
`Row` renders a non-wrapping flex row and **locks `flexDirection`** (it is a
removed Box prop on `Row`). To flip the panels to a vertical stack on mobile,
Row alone cannot do it — so the canonical shell uses a `Box display="flex"` with
a **responsive `flexDirection`** prop, which is the supported Spark way to
express the collapse without raw CSS:
```tsx
import { Box } from '@spark-web/box';
<Box display="flex" flexDirection={{ mobile: 'column', tablet: 'row' }}>
<Box
flexGrow={1}
padding={{ mobile: 'medium', tablet: 'xlarge' }}
width={{ mobile: 'full' }}
>
{/* primary panel — metrics + resources */}
</Box>
{feedEnabled && (
<Box
padding={{ mobile: 'medium', tablet: 'xlarge' }}
width={{ mobile: 'full' }}
>
{/* secondary panel — announcements feed */}
</Box>
)}
</Box>;
```
Rules:
- The shell is a `Box display="flex"` with
`flexDirection={{ mobile: 'column', tablet: 'row' }}` — primary panel
side-by-side with the optional feed on tablet+, stacked on mobile. This is the
Spark-native responsive collapse; **do not** reach for a raw
` (max-width: …)` `flexDirection` flip (the vendor-portal source does
this with `css`/`className`; the canonical pattern does not — see the
Documented exceptions table).
- `Row` from `-web/row` is the correct primitive **only when the layout
never needs to flip axis** (e.g. an action bar). Because the dashboard panels
must stack on mobile and `Row` locks `flexDirection`, use the responsive `Box`
above for the panel frame. (You may still use `Row` for inner left/right rows
such as a card's value-plus-arrow line.)
- Each panel sets its own responsive `padding`
(`{ mobile: 'medium', tablet: 'xlarge' }`) and `width={{ mobile: 'full' }}` —
spacing comes from tokens, never raw pixels.
- The page does **not** reproduce the shell Header / NavBar — the shell wraps
the page (surface rule); the page starts at the panel frame.
- A fixed secondary-panel width (e.g. the feed taking ~40% on tablet+) is a
product-design decision: prefer `Columns template={[…]}` or a panel `width`
token. If a precise non-token width is genuinely required it goes in the
Documented exceptions table — do not inline a raw width silently.
---
## Page titling — documented dashboard exception
**The dashboard is a documented exception to the surface page-title rule** (see
the surface rules' "Page title rule"): it leads with **section-level
`Heading level="2"` headings** ("Summary", "Support & resources", "What's New")
standing in for the page title, with **no separate page title**.
Rationale: (1) the fixed shell Header already carries the product identity
("Brighte Partner Portal"), so a redundant in-page "Dashboard" title adds
nothing; (2) a dashboard is inherently multi-panel with several co-equal
sections, none of which is "the page"; (3) it matches the observed
implementation. The surface rules note this exception and point here.
Rules:
- Headings **within** a section (e.g. a card title, the document-dialog title)
step down to `Heading level="3"` (or `level="4"` for a dialog sub-title where
the dialog itself owns the `level="3"`).
- Do **not** add a single `Heading level="2">Dashboard</Heading>` above the
sections and then push the section headings to `level="3"` — that is the
rejected alternative. Lead with section-level `level="2"` headings directly.
- `Heading`'s `level` union is `'1' | '2' | '3' | '4'`; its `tone` may be
`neutral` (the observed section-heading tone), `primary`, `primaryActive`, or
`muted`.
---
## Section 1 — Metrics summary
A row of metric / stat cards in a `Columns` grid: a headline number, a label, a
subtitle (e.g. a financed total), and a click-through that opens the underlying
list filtered to that status.
There is **no `-web` stat / metric / KPI card component** (verified — no
package under `packages/` provides one). The metric card is therefore a
**COMPONENT GAP**. **COMPONENT GAP protocol** (this applies to every gap in this
file): build the placeholder from primitives, mark the first use with a
`// COMPONENT GAP: <Name> needed — not yet in Spark` comment, and flag it for
**product design review before production — the placeholder is a stop-gap; do
not ship as-is**.
```tsx
import { Box } from '@spark-web/box';
import { Heading } from '@spark-web/heading';
import { Columns } from '@spark-web/columns';
import { Stack } from '@spark-web/stack';
import { Text } from '@spark-web/text';
import { ArrowRightIcon } from '@spark-web/icon';
<Box marginTop="small">
<Heading level="2" tone="neutral">
Summary
</Heading>
<Columns
marginTop="xlarge"
gap={{ mobile: 'small', tablet: 'large' }}
collapseBelow="wide"
>
{metrics.map(metric => (
/* COMPONENT GAP: MetricCard needed — not yet in Spark — protocol: see
the Section 1 intro; prop contract: see the rules below. */
<Box
key={metric.id}
background="surface"
border="standard"
borderRadius="large"
padding="large"
cursor="pointer"
onClick={() => onMetricClick(metric)}
>
<Stack gap="medium">
<Text size="xxlarge" weight="bold" tone="neutral">
{metric.value}
</Text>
<Text weight="semibold" tone="primary">
{metric.label}
</Text>
<Box display="flex" justifyContent="spaceBetween" alignItems="center">
{metric.subtitle && (
<Text size="xsmall" tone="muted">
{metric.subtitle}
</Text>
)}
<ArrowRightIcon size="xsmall" tone="primary" />
</Box>
</Stack>
</Box>
))}
</Columns>
</Box>;
```
Rules:
- **MetricCard is a COMPONENT GAP** — mark the first use with
`// COMPONENT GAP: MetricCard needed — not yet in Spark` (protocol: see the
Section 1 intro). Prop contract: `value` (string — the headline number, e.g. a
`toLocaleString()`'d count), `label` (string), `subtitle` (string, optional),
and exactly one of `href` / `onClick` for the click-through.
- Lay cards out with `Columns` (`collapseBelow="wide"`, responsive `gap`) — one
card per column, equal width. Do not hand-roll a flex grid.
- The headline number is a `Text` with a large size/weight — **never** a raw
`style={{ fontSize: '34px' }}` (the vendor-portal source does this; use a Text
size token such as `xxlarge` — 35px, the closest token to the raw 34px —
instead). Status counts are plain numbers here, not `Badge`s — the badge tone
mapping applies to status _labels_ in tables, not to dashboard headline
figures.
- The click-through navigates to the underlying list filtered to that metric
(e.g. `/applications?status=approved`); wire it via `onClick`/`href`. Role /
flag visibility of individual metrics (e.g. hiding "Paid" for non-vendor
roles) is consumer gating logic, not a design-system rule.
- Working in vendor-portal? The overlay substitutes the consumer's
`DashboardMetrics` / `MetricCard` — see the overlay.
---
## Section 2 — Resources
A `Columns` grid of resource / promo cards — a media image, a heading, body
copy, and an action (`Button` / `ButtonLink` / link), and/or a link list that
opens in a document dialog. Resource cards are **lighter than a component gap**:
they compose cleanly from primitives (`Box`/`Stack`/`Text`/`Button`/`Icon`/
`Image`), so build them from primitives directly — no `// COMPONENT GAP` flag is
required. (They are a reusable _consumer_ card, not a missing Spark primitive;
if a repo wants to factor the repeated card chrome into a local component, that
is a consumer convenience, noted in the overlay — not a Spark gap.)
```tsx
import { Box } from '-web/box';
import { Heading } from '-web/heading';
import { Columns } from '-web/columns';
import { Button, ButtonLink } from '-web/button';
import { Stack } from '-web/stack';
import { Text } from '-web/text';
import { ArrowNarrowRightIcon } from '-web/icon';
<Box marginTop="xxlarge">
<Heading level="2" tone="neutral">
Support & resources
</Heading>
<Columns
marginTop="xlarge"
gap={{ mobile: 'medium', tablet: 'medium', desktop: 'large' }}
collapseBelow="wide"
>
{resources.map(resource => (
<Box
key={resource.id}
background="surface"
border="standard"
borderRadius="large"
padding="large"
display="flex"
flexDirection="column"
height="full"
>
<Stack gap="large">
<Text size="large" weight="semibold" tone="neutral">
{resource.heading}
</Text>
<Box>
<ButtonLink href={resource.href} target="_blank">
{resource.actionLabel}
<ArrowNarrowRightIcon size="xxsmall" tone="primary" />
</ButtonLink>
</Box>
</Stack>
</Box>
))}
</Columns>
</Box>;
```
Rules:
- Build resource / promo cards from primitives (`Box` card chrome —
`background="surface" border="standard" borderRadius="large" padding="large"`
— plus `Stack`/`Text`/`Button`). No COMPONENT GAP flag — these are not a
missing Spark primitive.
- Card actions are `Button` (`onClick`) or `ButtonLink` (`href`, opens in a new
tab with `target="_blank"`) from `-web/button` — never a raw `<a>`
styled as a button.
- A long list of downloadable documents that opens in a modal is a
`ContentDialog` from `-web/modal-dialog` (see Section 4 — same component
family as the consent modal): a `size="small"` dialog whose body is a `Stack`
of `TextLink`s. Working in vendor-portal? The overlay maps its
`DashboardAds`/`AdCard` and `DocumentsDialog`.
---
## Section 3 — Announcements / news feed (optional, flag-gated)
An optional secondary-panel "What's New" feed: a vertical timeline of article
items (date, image, heading, intro, "Learn more" link), with incremental **"Load
More"** loading. The feed is usually behind a feature flag (consumer gating) and
renders only when enabled.
There is **no `-web` feed / timeline / article-list component** — the feed
container is a **COMPONENT GAP** (protocol: see Section 1); the placeholder is
`Box as="article"` items in a `Stack`, plus a "Load More" `Button`.
```tsx
import { Box } from '-web/box';
import { Heading } from '-web/heading';
import { Stack } from '-web/stack';
import { Text } from '-web/text';
import { Button } from '-web/button';
import { TextLink } from '-web/text-link';
{
/* COMPONENT GAP: AnnouncementsFeed needed — not yet in Spark — protocol: see
Section 1; prop contract: see the rules below. */
}
{
feedEnabled && (
<Stack gap="xlarge" width={{ mobile: 'full' }}>
<Heading level="2" tone="neutral">
What's New
</Heading>
<Stack gap="xlarge">
{items.map(item => (
<Box as="article" key={item.id}>
<Stack gap="small">
<Text size="small" tone="muted">
{item.date}
</Text>
<Text weight="bold" tone="neutral">
{item.heading}
</Text>
<Text size="small" tone="neutral">
{item.intro}
</Text>
<TextLink href={item.href} target="_blank">
Learn more
</TextLink>
</Stack>
</Box>
))}
</Stack>
{hasNextPage && (
<Button tone="neutral" loading={isLoading} onClick={onLoadMore}>
Load More
</Button>
)}
</Stack>
);
}
```
Rules:
- **AnnouncementsFeed is a COMPONENT GAP** — mark the first use with
`// COMPONENT GAP: AnnouncementsFeed needed — not yet in Spark` (protocol: see
Section 1). Prop contract: `items`
(`Array<{ id, date, heading, intro, href, imageUrl? }>`), `hasNextPage`
(boolean), `isLoading` (boolean), `onLoadMore` (`() => void`).
- Each feed entry is a `Box as="article"` for correct semantics; the date is a
muted `Text`, the heading a bold `Text`, and "Learn more" a `TextLink` (in
body copy) or `ButtonLink` (as a button).
- **"Load More" is a THIRD list-loading mode.** The list-page pattern recognises
infinite scroll and pagination; this feed uses **button-triggered incremental
loading** — a "Load More" `Button` that fetches and appends the next page on
click, with the Button's `loading` prop bound to the in-flight fetch. It is
distinct from auto-fetch-on-scroll (infinite scroll) and from page controls
(pagination). Use "Load More" for a short, browse-occasionally feed where the
user opts in to more rather than paging or endlessly scrolling. The surface
rules list it as the third recognised mode.
- The feed panel renders only when its feature flag is on (consumer gating) —
when off, omit the secondary panel entirely (the primary panel then fills the
shell). Flag gating is consumer logic, not a design-system rule.
- The vertical connector line / image styling in the observed implementation is
consumer chrome; keep the canonical placeholder to the semantic structure
above. Working in vendor-portal? The overlay maps its
`DashboardProductAnnouncements`.
---
## Section 4 — First-run consent / onboarding modal
On first visit a vendor may need to acknowledge a consent / requirements
statement before using the dashboard. This is a **real** `-web` component
— `ContentDialog` from `-web/modal-dialog` — **not** a component gap.
```tsx
import { Button } from '@spark-web/button';
import { ContentDialog } from '@spark-web/modal-dialog';
import { Text } from '@spark-web/text';
import { TextLink } from '@spark-web/text-link';
<ContentDialog
isOpen={showConsent}
title="Vendor requirements"
showCloseButton={false}
footer={
<Button tone="primary" onClick={handleConsent}>
Continue
</Button>
}
onToggle={onConsentToggle}
>
<Text>
By clicking Continue, you confirm your understanding and acceptance of the{' '}
<TextLink href={requirementsUrl} target="_blank">
vendor requirements
</TextLink>{' '}
document.
</Text>
</ContentDialog>;
```
Rules:
- Build it on `ContentDialog` (`-web/modal-dialog`) — the controlled form
(`isOpen` + `onToggle`). Read the modal-dialog doc for the full API.
- **`showCloseButton={false}`** — the gate must be acknowledged; do not give the
user an X to dismiss it.
- The single footer action is a `Button tone="primary"` labelled "Continue" (or
similar). Its handler **persists the acknowledgement flag** (e.g. POSTs
`initialized: true` to the profile) so the modal does not reappear on the next
visit, then closes the modal.
- **Gate placement:** drive `isOpen` from the persisted profile/initialised flag
— open the modal in an effect when the profile has loaded **and** the flag is
not yet set (`profile && !profile.initialized`), not unconditionally on mount.
Render the `ContentDialog` once, at the page root (a sibling of the panel
frame), with its open state as a prop — not conditionally mounted — so the
close transition runs. Keep the gate at the page level, not buried inside a
panel.
- In-prose links inside the modal body are `TextLink` from
`-web/text-link` (with `target="_blank"` for external docs) — never a
raw `<a>`.
- A document-list dialog (Section 2) uses the **same** `ContentDialog` component
with `size="small"`; it differs only in that it may be dismissable.
---
## Security — NEVER use `dangerouslySetInnerHTML`
The observed implementation renders an announcement banner via
`<Text … dangerouslySetInnerHTML={{ __html: title + content }} />`. **Never do
this.** Injecting server- or CMS-supplied HTML straight into the DOM is an XSS
vector (the source even flags it `// this one may cause security issue (XSS)`).
Render banner content as **sanitized text**, not HTML:
```tsx
import { Alert } from '-web/alert';
import { Text } from '-web/text';
{
banner && (
<Alert tone="info">
<Text>{banner.message}</Text>
</Alert>
);
}
```
Rules:
- A dashboard banner is an `Alert` from `-web/alert` (or a `Box` +
`Text`), with its message rendered as **plain text children** — `Alert`'s
`tone` union is `caution | critical | info | positive` (use `info` for a
neutral announcement, `caution` for a time-sensitive notice).
- If the banner genuinely needs rich content (a link), compose it from `Text` +
`TextLink` as React children — never pass an HTML string.
- **NEVER** pass `dangerouslySetInnerHTML` to `Text`, `Box`, or any element on a
vendor-admin page. If the source HTML cannot be expressed as React children,
sanitize it server-side and render the sanitized text — do not inject raw
HTML.
---
## Structural skeleton
Use this skeleton as the starting point for new builds and uplifts. Do not use
existing page implementations as a structural reference.
```tsx
import { Box } from '-web/box';
<>
<Box display="flex" flexDirection={{ mobile: 'column', tablet: 'row' }}>
{/* Primary panel — metrics + resources */}
<Box
flexGrow={1}
padding={{ mobile: 'medium', tablet: 'xlarge' }}
width={{ mobile: 'full' }}
>
{/* Sanitized banner Alert (conditional) — NEVER dangerouslySetInnerHTML
— see the Security section above */}
{/* Section 1 — metrics: Heading level="2" + Columns of MetricCard
placeholders (COMPONENT GAP) — see above */}
{/* Section 2 — resources: Heading level="2" + Columns of primitive
resource / promo cards — see above */}
</Box>
{/* Secondary panel — Section 3: AnnouncementsFeed placeholder
(flag-gated, optional; COMPONENT GAP) — see above */}
</Box>
{/* Section 4 — first-run consent ContentDialog (real component, NOT a gap;
showCloseButton={false}, Continue persists the flag) — see above */}
</>;
```
---
## Documented exceptions summary
No raw CSS is required by this pattern's canonical snippets; if you add any,
list it here with a justification and apply it via the `css` prop — never via
`className` (`Stack` omits `className`; do not use `className` on `Box` either —
the styling escape hatch on both is the `css` prop).
| Value | Property | Reason |
| ----- | -------- | ------------- |
| — | — | none required |
---
## Do NOTs
- NEVER add a single `Heading level="2"` page title above the sections — the
dashboard is a documented exception: it leads with section-level `level="2"`
headings and has no separate page title (sub-headings within a section step to
`level="3"`).
- NEVER use the list page's `Stack height="full"` scroll region or the form
page's `Container size="medium"` as the dashboard shell — the dashboard shell
is a multi-panel `Box display="flex"` with a responsive `flexDirection`.
- NEVER use a raw ` (max-width: …)` `flexDirection` flip (via `css` or
`className`) for the panel collapse — use the responsive
`flexDirection={{ mobile: 'column', tablet: 'row' }}` prop.
- NEVER set `className` on `Stack` or `Box` — `Stack` omits `className`, and
`className` must not be used on `Box` either; the styling escape hatch on both
is the `css` prop, and only for a Documented exception.
- NEVER reproduce the shell Header or NavBar inside the page — the shell wraps
the page (surface rule); the page starts at the panel frame.
- NEVER render a metric headline number with a raw `style={{ fontSize: … }}` —
use a `Text` size token (e.g. `size="xxlarge"`).
- NEVER use a `Badge` for a dashboard headline figure — badges map _status
labels_ in tables; dashboard stats are plain `Text` numbers.
- NEVER import a metric card, stat card, or announcements feed from `-web`
— they are COMPONENT GAPs. Build each as a primitives placeholder
(`Box`/`Stack`/`Text`/`Icon`/`Button`), mark it with a `// COMPONENT GAP:`
comment, and flag it for product design review before production — do not ship
the placeholder as-is. (In vendor-portal, the overlay's local components
remain the canonical substitution.)
- NEVER flag the consent modal as a component gap or hand-roll a custom overlay
— it is a real `ContentDialog` from `-web/modal-dialog`.
- NEVER give the first-run consent modal a close button — use
`showCloseButton={false}`; the Continue action persists the flag and closes
it.
- NEVER open the consent modal unconditionally on mount — gate it on the
persisted profile/initialised flag (`profile && !profile.initialized`).
- NEVER use `dangerouslySetInnerHTML` on `Text`, `Box`, or any element to render
a banner / announcement — render sanitized content via `Text` / `Alert` (XSS
risk).
- NEVER treat "Load More" as pagination or infinite scroll — it is a distinct
third loading mode (button-triggered incremental append); bind the Button's
`loading` prop to the in-flight fetch.
---
## Validation checklist
Run this checklist before marking any vendor-admin dashboard task complete. Fix
every violation first. The uplift protocol in
`node_modules/@spark-web/design-system/CLAUDE.md` also runs this checklist
against existing pages and reports PASS/FAIL per item.
1. The page leads with **section-level `Heading level="2"`** headings (e.g.
"Summary", "Support & resources", "What's New") and has **no single page
title** — the documented dashboard exception; sub-headings within a section
are `level="3"` (or `level="4"` inside a dialog). No `PageHeader`, no
`level="1"`.
2. The shell is a multi-panel `Box display="flex"` with
`flexDirection={{ mobile: 'column', tablet: 'row' }}` — a primary panel
(`flexGrow={1}`) plus an optional secondary feed panel — **not**
`Stack height="full"` (list page) and **not** `Container size="medium"` (form
page); the page does not reproduce the shell Header / NavBar.
3. The responsive panel collapse uses the `flexDirection` responsive prop —
there is no raw `` `flexDirection` flip and no `className` on any
`Stack` / `Box`; spacing is all tokens.
4. Metrics render in a `Columns` grid (`collapseBelow`, responsive `gap`); each
metric card is a primitives placeholder (`Box`/`Stack`/`Text`/`Icon`) flagged
`// COMPONENT GAP: MetricCard needed — not yet in Spark`, with the headline
number a `Text` size token (not a raw `fontSize`) and a click-through
(`onClick`/`href`) to the filtered list; no `Badge` for the headline figure.
5. Resource / promo cards are composed from primitives (`Box` card chrome +
`Stack`/`Text`/`Button`/`ButtonLink`) with actions as `Button`/`ButtonLink`
(not raw `<a>`); no `// COMPONENT GAP` flag is required for them.
6. If an announcements / news feed is present: it is in the secondary panel,
rendered only when its feature flag is on, built as a primitives placeholder
(`Box as="article"` items + "Load More" `Button`) flagged
`// COMPONENT GAP: AnnouncementsFeed needed — not yet in Spark`, and uses
**"Load More"** (button-triggered incremental, `Button loading` bound to the
fetch) — not infinite scroll, not pagination.
7. The first-run consent modal (if present) is a real `ContentDialog` from
`-web/modal-dialog` with `showCloseButton={false}`, a single
`Button tone="primary"` Continue footer whose handler persists the
profile/initialised flag, gated open on `profile && !profile.initialized`,
rendered once at page root with open state as a prop — not flagged as a
component gap, not a custom overlay.
8. No element uses `dangerouslySetInnerHTML`; banner / announcement content is
sanitized text rendered via `Text` / `Alert` (XSS rule).
9. No raw CSS values beyond the Documented exceptions table (which is empty by
default); every component is `-web/*` or an explicit consumer-overlay
substitute, and anything missing (MetricCard, AnnouncementsFeed) is flagged
with a `// COMPONENT GAP:` comment and called out for product design review.