UNPKG

@spark-web/design-system

Version:

--- title: Design System ---

310 lines (239 loc) 15.9 kB
# Vendor admin — vendor-portal consumer overlay This is the consumer overlay for the **vendor-portal** repo, per the Consumer overlays convention in `node_modules/@spark-web/design-system/patterns/CLAUDE.md`. Apply these substitutions only when working in the vendor-portal repo. This file overrides pattern files AND component-level docs ONLY where it explicitly says so (each heading names what it overrides); every other surface and pattern rule applies to vendor-portal unchanged. --- ## Pagination (overrides vendor-admin CLAUDE.md "List loading rule" — pagination control) vendor-portal supplies its own `TablePagination` at `src/components/TablePagination.tsx` — the consumer substitution for the surface rules' "List loading rule" pagination control (`// COMPONENT GAP: TablePagination needed — not yet in Spark`). It wraps `rc-pagination` and renders a `Text` summary `Show {dataShowing} of {total} Results` beside the page controls. Props used by the patterns: | Prop | Type | Notes | | ------------- | -------------------------- | ------------------------------------------------ | | `total` | `number` | Total record count — from a count query | | `current` | `number` | Current page number | | `pageSize` | `number` | Defaults to `DATA_PER_PAGE` (`@/util/constants`) | | `dataShowing` | `number` | Number of rows on the current page | | `onChange` | `(page, pageSize) => void` | Page change handler | `TablePagination` returns `null` when `total` is falsy — never render an empty pagination bar. --- ## Side panel (overrides vendor-admin CLAUDE.md "Detail interaction rule" — side panel implementation) vendor-portal supplies its own `SidePanel` at `src/components/SidePanel.tsx` — the consumer substitution for the surface rules' "Detail interaction rule" drawer (`// COMPONENT GAP: SidePanel/Drawer needed — not yet in Spark`; verified: the only overlay primitive published is `@spark-web/modal-dialog`, a centered modal, not a right-anchored drawer). It is an absolute-positioned right slide-in built on `@spark-web/stack` / `@spark-web/box`, `90vw` wide on mobile up to `maxWidth: 500px` from tablet, offset below the header (`top: 64`), with an optional `heading` + close (`XIcon`) header row and a built-in `loading` skeleton state. Props: | Prop | Type | Notes | | ----------- | ------------ | --------------------------------------------------------------------------------- | | `children` | `ReactNode` | Panel body | | `heading` | `string` | Optional — header title (renders header only when `heading` AND `onHide` are set) | | `onHide` | `() => void` | Optional — close handler; renders the close button | | `loading` | `boolean` | Optional — shows the skeleton placeholder state | | `fullWidth` | `boolean` | Optional — removes horizontal body padding | | `...props` | `StackProps` | Spread onto the outer `Stack` | --- ## Multi-select filter (overrides list-page.md Section 4 filter usage — substitution note) The canonical vendor-admin choice for a multi-select filter dropdown is `@spark-web/multi-select` (verified current export: it exports `MultiSelect`, not `MultiSelectField`). vendor-portal currently ships a local `MultiselectCheckbox` at `src/components/MultiselectCheckbox.tsx`. **Prefer `MultiSelect` from `@spark-web/multi-select`** for new work — the two have effectively the same public API, so the substitution is direct: | Prop | `MultiselectCheckbox` (local) | `@spark-web/multi-select` `MultiSelect` | | ---------------- | ------------------------------------------------- | --------------------------------------- | | `options` | `Options` (`Array<{ label, options: Option[] }>`) | same grouped shape | | `onChange` | `(selected: SelectedOptions) => void` | same — grouped by group label | | `placeholder` | `string` (defaults `"Filter By..."`) | same | | `defaultOptions` | `DefaultOption[]` (`{ label, value }`) | `Option[]` — same shape | Both group selections into `SelectedOptions` keyed by group label (e.g. `{ Months: ["12", "24"] }` in `ProductSettings`). Because the shapes match, swap the import and component name with no call-site changes. The local `MultiselectCheckbox` is retained only where already wired; do not reach for it in new code. --- ## Form fields (overrides @spark-web/field CLAUDE.md "controlled usage" — react-hook-form wrapper) vendor-portal wraps `@spark-web/field` in a local `FormField` exported as `Field` (`src/components/FormField.tsx`). It maps a react-hook-form `errors[name]` entry to the field's error presentation: ```tsx import { Field } from '@/components/FormField'; <Field label="Email" name="email" errors={errors}> <TextInput {...register('email')} /> </Field>; ``` Wrapper signature: `FieldProps & { name: string; errors?: FieldErrors }`. When `errors[name]` is present it surfaces `errors[name].message`; otherwise it falls back to a passed `message`. Either way the wrapper sets `tone="critical"` — its `(!!errors && errors[name]) || { message }` expression is always truthy. Note: a static help `message` with no validation error still renders with critical tone — a known quirk in the consumer wrapper. Wire forms with `react-hook-form` and Zod schemas via `@hookform/resolvers` (`zodResolver`) — pass the resolver's `errors` straight into `Field`. Prefer this `Field` wrapper over hand-wiring `@spark-web/field` with manual error props on vendor-portal form pages. --- ## Section / setting wrapper (overrides detail/form section grouping) vendor-portal uses a local `SettingsPanel` (`src/components/settings/SettingsPanel.tsx`) for settings sections: a 2-column `Columns` layout with a left `heading` (`Heading level="4"`) + `description` (`Text`) and a right control slot (`children`). ```tsx import { SettingsPanel } from '@/components/settings/SettingsPanel'; <SettingsPanel heading="Terms" description="Enable the terms available..."> <MultiSelect options={termsOptionList} onChange={handleTermUpdates} /> </SettingsPanel>; ``` | Prop | Type | Notes | | ------------- | ----------------------- | ---------------------------- | | `heading` | `string` | Required — left-column title | | `description` | `string \| JSX.Element` | Required — left-column body | | `children` | `ReactNode` | Right-column control slot | **Relationship to `@spark-web/section-card`:** these are NOT interchangeable. `SectionCard` is a bordered card with `header` / `footer` / `children` slots (compose with `@spark-web/section-header`) — use it for record-detail section **cards**. `SettingsPanel` is a label-beside-control settings row with no card chrome. Canonically, prefer `@spark-web/section-card` for detail-page section cards; keep `SettingsPanel` for the settings-page label/description/control layout it was built for. Do not substitute one for the other. --- ## Feedback (overrides @spark-web/alert usage — local FlashMessage) vendor-portal's `FlashMessage` (`src/components/settings/FlashMessage.tsx`) is a thin wrapper over `@spark-web/alert`: ```tsx <Alert tone={success ? 'positive' : 'critical'} onClose={onClose} closeLabel="Dismiss" > {message} </Alert> ``` It maps `{ success, message, onClose }` to an `Alert` with `tone="positive"` (success) or `tone="critical"` (failure). The canonical feedback component is `@spark-web/alert` (`node_modules/@spark-web/alert/CLAUDE.md`); `FlashMessage` is just the local success/error binding. Prefer `Alert` directly for new inline-feedback needs; reuse `FlashMessage` only where the success/error toggle is convenient. --- ## Read-only display (overrides detail read-only display — local InfoBox to portal-table) vendor-portal's profile page uses a local `InfoBox` (`src/pages/profile.tsx`) that renders an icon + `label` + `value` row for read-only display. This is the label/value display the canonical pattern fulfils with `@spark-web/portal-table` (`PortalTable`), whose rows are `DescriptionListItem`s (`{ label, value }`, where `value` may be a `ReactNode`). Prefer `@spark-web/portal-table` for read-only label/value record displays; the local `InfoBox` is retained only for its icon-decorated profile layout. --- ## Data conventions (vendor-portal data fetching) vendor-portal is a Next.js Pages-Router app with two data layers: - **Server-side GraphQL** via Apollo Client inside `getServerSideProps`. Create the client with `createClient(session.access_token)` from `@/lib/apollo-client` and run queries (`@/queries/*`) there; pass results to the page as props. - **Client-side** via `@tanstack/react-query`. Use `useInfiniteQuery` for infinite-scroll lists (e.g. leads) and `useQuery` for bounded reads. Mutations live in custom hooks under `src/hooks/**` that wrap `useMutation` (typically `fetch`-ing a Next.js API route) and expose `mutate` / `mutateAsync`. These conventions are consumer logic, not a design-system rule — but follow them when wiring vendor-portal screens rather than introducing a new data layer. --- ## Legacy / embedded screens (overlay note — NOT a Layer-2 pattern) Some vendor-portal routes do not build a Spark component tree at all: - **Legacy iframe fallback**`PortalIframe` (`src/components/portal-iframe.tsx`) embeds the legacy portal in an `<iframe>`. Used (often gated by a LaunchDarkly feature flag) on `applications/[id]`, `leads/[id]`, `users`, `users/add`, and as a fallback on `applications/index` (e.g. behind `vendorPortalOwnApplicationPage`). - **Dynamic micro-frontend embed**`referrals/add` (`src/pages/referrals/add.tsx`) injects a remote `vendor_form` script and mounts it into a container `div` (version pinned by a flag). These screens have **no Layer-2 pattern**. Do not try to reconstruct them from a pattern file. When working on them, follow the consumer's existing iframe / micro-frontend convention (mount container, feature-flag gate, session token plumbing) — they are intentionally outside the pattern system. --- ## Dashboard pieces (overrides dashboard.md Sections 1-3 placeholders — consumer substitutions) vendor-portal supplies its own dashboard components in `src/components/dashboard/**` and `src/components/**`. These are the consumer substitutions for the placeholders the dashboard pattern flags as COMPONENT GAPs. Use them in vendor-portal in place of the primitives placeholders; in new code outside vendor-portal, the pattern's placeholders remain the stop-gap until a Spark component exists. ### Metrics — `DashboardMetrics` + `MetricCard` (overrides dashboard.md Section 1) `DashboardMetrics` (`src/components/dashboard/DashboardMetrics.tsx`) renders a `@spark-web/columns` `Columns` (`collapseBelow="wide"`) of local `MetricCard`s (`src/components/MetricCard.tsx`). This is the consumer substitution for the **MetricCard COMPONENT GAP** (dashboard.md Section 1). `MetricCard` props: | Prop | Type | Notes | | ---------- | ------------ | -------------------------------------------------- | | `header` | `string` | The headline number (the pattern's `value`) | | `title` | `string` | The card label (the pattern's `label`) | | `subTitle` | `string` | Optional secondary line (the pattern's `subtitle`) | | `onClick` | `() => void` | Click-through (the pattern's `onClick`/`href`) | `DashboardMetrics` props: `data` (`TMetricSummary[]`) and `onMetricClick(metric)`. Note the local `MetricCard` renders the headline number with a raw `style={{ fontSize: '34px' }}` — that inline `fontSize` is a known deviation from the pattern (which mandates a `Text` size token); do not copy it into new cards. ### Resources — `DashboardAds` / `AdCard` + `DocumentsDialog` (overrides dashboard.md Section 2) `DashboardAds` (`src/components/dashboard/DashBoardAds.tsx`) renders a `Columns` of local `AdCard`s (`src/components/AdCard.tsx`) plus a `DocumentsDialog`. These are the consumer's resource/promo cards — the pattern treats resource cards as buildable from primitives (no gap flag), so these are a convenience factoring, not a substitution for a missing Spark primitive. `AdCard` props: `mediaSrc`, `mediaAlt`, `cardContent` (`ReactNode`), `cardActions` (`ReactNode`). It composes `@spark-web/box` + `@spark-web/text` + a `next/image` media region. `DocumentsDialog` (`src/components/dashboard/DocumentsDialog.tsx`) is the document-list modal — built **canonically** on `@spark-web/modal-dialog` `ContentDialog` (`size="small"`, `showCloseButton={false}`) with a body of `next/link` items. Props: `links` (`Array<{ title; link }>`), `isOpen`, `onClose`. (Canonically the dashboard pattern uses `@spark-web/text-link` `TextLink` for the link rows; the local dialog uses raw `next/link` — prefer `TextLink` in new code.) ### Announcements feed — `DashboardProductAnnouncements` (overrides dashboard.md Section 3) `DashboardProductAnnouncements` (`src/components/dashboard/DashboardProductAnnouncements.tsx`) is the consumer substitution for the **AnnouncementsFeed COMPONENT GAP** (dashboard.md Section 3). It renders the "What's New" timeline (article items built from `@spark-web/box as="article"` / `@spark-web/stack` / `@spark-web/text`) and the **"Load More"** `@spark-web/button` `Button` (button-triggered incremental loading — the third list-loading mode), with a `@tanstack/react-query` `useQuery` driving the incremental fetch on the `/dashboard/articles` page. Props: `data` (the announcements array) and `apiUrl`. The feed is gated by the `newsFeedVendorPortal` LaunchDarkly flag in `src/pages/dashboard/index.tsx` (consumer gating). The connector-line / `next/image` chrome is consumer styling applied via `@emotion/css` `css(...)` — outside vendor-portal, keep to the pattern's semantic placeholder. ### Consent modal — `ContentDialog` (no substitution) The first-run consent modal in `src/pages/dashboard/index.tsx` uses `@spark-web/modal-dialog` `ContentDialog` **canonically** (`showCloseButton={false}`, a `Button tone="primary"` Continue footer that POSTs `initialized: true` to `/api/user-profile`, gated on `userProfile && !userProfile.initialized`). There is **no consumer substitution** — build it exactly as the pattern's Section 4 describes. ### Dashboard route gating The whole dashboard is gated by the `vendorPortalOwnDashboard` LaunchDarkly flag; when off, `src/pages/dashboard/index.tsx` falls back to a `PortalIframe` (`users/dashboard`) — a legacy embed with **no Layer-2 pattern** (see "Legacy / embedded screens" above). This flag gating is consumer logic, not a design-system rule. --- ## No `@brighte/ui-components` Unlike the portal-hub overlay, vendor-portal does **NOT** use `@brighte/ui-components` — it is not a dependency (verified: absent from `package.json`). Do not import from `@brighte/ui-components` in vendor-portal. Filter and form fields come from `@spark-web/*` (and the local `Field` / `MultiselectCheckbox` wrappers documented above), not from that package.