@spark-web/design-system
Version:
--- title: Design System ---
217 lines (163 loc) • 10.2 kB
Markdown
# Vendor admin — interaction and pattern rules
These rules apply to all components and patterns built for vendor admin
surfaces. They sit above individual component rules. When a conflict exists
between these rules and a component-level CLAUDE.md, these rules take precedence
for vendor-admin surfaces.
Vendor-admin is the surface used by external vendors, installers, and partners
signing in to manage their own work (leads, applications, settings). It is a
distinct surface from internal-admin and has different chrome — do not apply
internal-admin rules here, and do not "correct" vendor-admin choices to match
internal-admin where they intentionally diverge (see Page title rule below).
---
## Layout shell and content region
Vendor-admin screens render inside a fixed application shell:
- A **fixed, sticky Header** pinned to the top, full width
(`position: fixed; top: 0`). It is `72px` tall on mobile and `90px` on tablet
and up.
- A **fixed left NavBar sidebar** on tablet and up
(`position: fixed; left: 0; width: 270px`, full height). On mobile the sidebar
collapses behind a hamburger (`MenuIcon`) toggle in the Header.
- A **main content region** that is offset to clear the shell and owns a
full-height scroll area:
- top offset `72px` on mobile, `90px` on tablet and up (clears the Header),
- left offset `0` on mobile, `270px` on tablet and up (clears the sidebar),
- height `calc(100svh - 90px)` so the region — not the document — scrolls.
The shell frame (Header + sidebar + offset content region) is the surface rule:
every vendor-admin page renders into this region and must not reproduce the
Header or sidebar itself. The **shell implementation is consumer-owned** — the
Header, NavBar, and the `_app`-level wrapper are local components, not published
`@spark-web/*` components. The concrete substitutions (component names, props)
live in the consumer overlay (a sibling file):
`node_modules/@spark-web/design-system/patterns/vendor-admin/vendor-portal.md`
The region and offsets above are the rule; the implementation is the overlay's.
A page authored for vendor-admin therefore starts at the page-content level (the
page title row, filters, content) and assumes the shell already exists around
it.
---
## Page title rule
Vendor-admin uses `Heading level="2"` for the page title — **not**
internal-admin's `PageHeader` / `H1`.
This is the clearest divergence from internal-admin and is intentional: the two
surfaces have different chrome. On vendor-admin the fixed Header already carries
the product identity and primary navigation, so the in-page title is a section
heading within the shell, expressed as `<Heading level="2">`. Do not replace it
with `PageHeader` or promote it to `level="1"`. Within a page, sub-section
titles step down to `<Heading level="3">`.
**Dashboard exception.** The dashboard / overview screen type is a documented
exception to this single-page-title rule: an overview page has no single
dominant subject, so it leads with **section-level `Heading level="2"`**
headings (e.g. "Summary", "Support & resources", "What's New") and **no separate
page title**. Sub-headings within a dashboard section step down to `level="3"`.
This exception applies only to the dashboard/overview shell — see
`node_modules/@spark-web/design-system/patterns/vendor-admin/dashboard.md`. All
other screen types keep the single `Heading level="2"` page title above.
Page-level actions (e.g. an "Export" `Button`) sit on the same row as the title,
right-aligned, in a flex row that stacks to a column on mobile. A count or limit
indicator (`Badge`) may sit next to the title on the left.
---
## Spacing and density
Vendor-admin pages use a generous rhythm. The observed page-level pattern across
screens:
- List pages wrap their content in a `Stack` with `gap="large"` and
`margin="xxlarge"` (often `marginBottom="none"` so the scroll region reaches
the viewport edge), `height="full"`.
- Form / settings pages center content in a `Container size="medium"` and use a
`Stack` with `gap="xxlarge"` between sections, with `marginY="xxlarge"`.
- Filter rows within a list page use `gap="medium"`.
Use these spacing tokens — never raw pixel gaps or margins. When in doubt for a
new vendor-admin page, default to `Stack gap="large"` for the page body and
`gap="xxlarge"` to separate major form sections.
---
## Detail interaction rule — side panel vs full page
Vendor-admin opens a record's detail in a **slide-in side panel**, not by
navigating to a separate detail page.
- The side panel is a right-anchored slide-in within the content region
(`position: absolute; right: 0`): **~`90vw` on mobile, up to `500px`
(`maxWidth`) from tablet** (vendor-portal's current panel also offsets below
the header — see the overlay). It overlays the list rather than reflowing it.
- Opening a row's detail sets the selected record (typically via a URL query
param so the panel is deep-linkable) and renders the panel alongside the still
visible list.
**When to use a side panel vs a full page:**
- **Side panel** — the default for inspecting / lightly acting on a single
record while staying in the list context (viewing a lead, an application
summary, quick status changes). Use it whenever the user benefits from
remaining anchored to the list and the detail is bounded.
- **Full page** — reserve for heavyweight, standalone flows that do not belong
beside a list: multi-step forms, a dedicated settings screen, or a legacy /
embedded screen. Settings is a full page (`Container`-centered), not a panel.
The side panel itself is **consumer-supplied** and is a **COMPONENT GAP**: there
is no `@spark-web` drawer or side-panel component (verification note: see the
overlay). Until a Spark drawer exists, mark its use with
`// COMPONENT GAP: SidePanel/Drawer needed — not yet in Spark` and use the
consumer's `SidePanel`; see the overlay (a sibling file):
`node_modules/@spark-web/design-system/patterns/vendor-admin/vendor-portal.md`
---
## List loading rule — infinite scroll vs pagination
Choose the list-loading strategy by the shape of the dataset:
- **Infinite scroll** — use for large or streamed datasets where the total is
unbounded or expensive to count, and the natural interaction is "keep
scrolling" (e.g. leads). Implement by fetching the next page when the bottom
of the scroll region is reached and appending rows.
- **Pagination** — use for bounded result sets where the user benefits from
jumping to a known page and seeing the total (e.g. applications, with an "X of
Y results" summary and page controls).
Default: if the PRD describes an open-ended, continuously growing list →
infinite scroll; if it describes a finite, countable list the user pages through
→ pagination.
- **Load More** (button-triggered incremental) — a THIRD list-loading mode: a
"Load More" `Button` fetches and appends the next page on click (the Button's
`loading` prop bound to the in-flight fetch), rather than auto-fetching on
scroll (infinite scroll) or exposing page controls (pagination). Use it for a
short, browse-occasionally feed where the user opts in to more — e.g. the
dashboard announcements / "What's New" feed (see
`node_modules/@spark-web/design-system/patterns/vendor-admin/dashboard.md`).
The **pagination control is consumer-supplied** and is a **COMPONENT GAP**:
there is no `@spark-web` pagination component. Mark its use with
`// COMPONENT GAP: TablePagination needed — not yet in Spark` and use the
consumer's pagination component; see the overlay.
---
## Bulk actions rule
When a list supports selecting multiple rows, selecting one or more rows reveals
a **bottom-anchored bulk-action toast** pinned to the content region
(`position: absolute; bottom: 0; left: 0`). It exposes the actions that apply to
the current selection (e.g. assign, close) and a way to clear the selection.
Rules:
- The bulk-action toast appears **only when** the list is multi-select **and**
at least one row is selected; it is hidden when the selection is empty.
- A bulk-action toast and a record side panel are mutually exclusive in
practice: show the detail panel only when the selection is a single record,
and the bulk toast only when one or more rows are selected for a bulk
operation.
- Disable the toast's actions while a bulk mutation is in flight.
---
## Badge and pill rules — tone mapping
Vendor-admin reuses the internal-admin badge tone mapping verbatim. Do not
re-derive it here. Read and apply the authoritative mapping at:
`node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md` (see
"Badge and pill rules" / "Badge tone mapping").
If a vendor-admin screen has status vocabulary not covered by the internal-admin
mapping (e.g. lead intents, vendor lead statuses, accreditation states), map
each new value with that same logic and record the new vocabulary in the
relevant pattern file rather than redefining tones here.
---
## Role and feature-flag gating
Vendor-admin screens and individual nav items / actions may be gated by role
(e.g. vendor-admin-only screens) or by feature flags. This gating is **consumer
logic, not a design-system rule** — it lives in the consuming app (the shell's
nav config, route guards, and flag checks), not in `@spark-web` components or in
these surface rules.
Where it sits: gating wraps the page or nav entry at the shell / routing layer
and conditionally renders flag-guarded sections. A pattern or component doc
should never encode a specific role name or flag key. When a PRD specifies that
a screen is admin-only or behind a flag, treat that as the consumer's
responsibility and note it; build the screen's component tree per the pattern
regardless.
---
## Rules that update as the system grows
When a new pattern or component is added to the vendor-admin surface, update
this file with any new interaction rules that apply globally. Do not duplicate
rules that already exist here in component-level CLAUDE.md files. If a new
pattern file is added under `patterns/vendor-admin/`, also update the
feature-type table in `node_modules/@spark-web/design-system/patterns/CLAUDE.md`
in the same commit.