UNPKG

@spark-web/design-system

Version:

--- title: Design System ---

344 lines (276 loc) 12.2 kB
# Internal admin — portal-hub consumer overlay Consumer overlay for the **portal-hub** repo (see Consumer overlays in `node_modules/@spark-web/design-system/patterns/CLAUDE.md`) — it overrides the internal-admin pattern files and component-level CLAUDE.md files ONLY where it explicitly says so; every other rule applies to portal-hub unchanged. --- ## Filter fields (overrides list-page.md Section 4) `@spark-web/multi-select` is not available to app code in portal-hub (it is a dependency of the internal ui-components package only). Filter and search fields come from `@brighte/ui-components` instead. This override applies ONLY to component substitutions; all other Section 4 rules — search input always first, filter ordering broadest to most specific, hidden labels, omit the section when no filtering exists — apply unchanged. The substitutions: - Multi-select filter dropdowns use `MultiSelectField` from `@brighte/ui-components` - Single-select filter dropdowns use `SelectField` from `@brighte/ui-components` - The search input uses `TextInputField` from `@brighte/ui-components` with the same start-adornment `SearchIcon` Always pass `fieldProps={{ labelVisibility: 'hidden' as const }}` on both `MultiSelectField` and `SelectField` — define it as a constant outside the component to avoid re-renders: ```tsx const FieldProps = { labelVisibility: 'hidden' as const }; <Columns gap="large" collapseBelow="desktop"> <TextInputField control={control} name="search" label="Search" placeholder="Search by..." FieldProps={FieldProps} > <InputAdornment placement="start"> <SearchIcon size="xxsmall" tone="muted" /> </InputAdornment> </TextInputField> <MultiSelectField control={control} name="fieldName" label="Label" options={options} placeholder="Filter by..." fieldProps={FieldProps} /> </Columns>; ``` Note the casing: the prop is `FieldProps` (capital F) on `TextInputField` and `fieldProps` (lowercase f) on `MultiSelectField`/`SelectField` — match the snippet exactly. Do NOTs (portal-hub): - NEVER import `@spark-web/multi-select` directly in portal-hub app code; use `MultiSelectField` from `@brighte/ui-components` - NEVER omit the hidden-label `fieldProps` constant on SelectField filter dropdowns — define it once outside the component and pass it everywhere --- ## Text inputs (overrides the 'Controlled usage with form state' sections in text-input/text-area CLAUDE.md) In portal-hub, prefer the `Field`-wrapped bindings from `@brighte/ui-components` over hand-wiring `@spark-web/text-input` / `@spark-web/text-area` into forms: - Prefer `TextInputField` from `@brighte/ui-components` when used with `react-hook-form`. Pass `placeholder` directly, and use `FieldProps` to pass `description` (renders as muted hint text below the label): ```tsx <TextInputField control={control} name="subject" label="Subject" placeholder="Type here..." FieldProps={{ description: 'Enter a brief summary of the issue' }} /> ``` - Prefer `TextAreaField` from `@brighte/ui-components` when used with `react-hook-form`. Pass `placeholder` directly: ```tsx <TextAreaField control={control} name="note" label="Note" placeholder="Type your note here..." /> ``` --- ## Section cards (overrides detail-page.md Section 7) portal-hub uses a custom wrapper at `@components/PortalTable/SectionCard` — not `@spark-web/section-card`. The API differs from the Spark component: ```tsx import { SectionCard } from '@components/PortalTable/SectionCard'; <SectionCard label="Section Title">{/* section content */}</SectionCard>; ``` | Prop | Type | Notes | | ---------- | ----------- | ---------------------------------------------------------------------------- | | `label` | `string` | Optional — card header text; usually provided — header only renders when set | | `tag` | `TagProps` | Optional — tag rendered in the card header | | `action` | `ReactNode` | Optional — right-side header control | | `controls` | `ReactNode` | Optional — additional header controls | Return `null` for sections conditionally hidden — never render an empty card. --- ## Modal sizing (ACCREDITATION_MODAL_CSS) The shared admin modal size constant described in `node_modules/@spark-web/modal-dialog/CLAUDE.md` already exists in portal-hub — do not define a new one. The standard size constant used across all admin confirmation modals is defined in `apps/admin-portal/src/utils/constants.tsx`: ```ts export const ACCREDITATION_MODAL_CSS = { width: '100vw', maxWidth: '550px', } as const; ``` Always pass this constant via `css={ACCREDITATION_MODAL_CSS}` — never set width inline or use a raw pixel value. --- ## Row-as-link reference implementation The row-as-link navigation pattern documented in `node_modules/@spark-web/data-table/CLAUDE.md` has a reference implementation in portal-hub at `apps/admin-portal/src/components/RowLink` (uses TanStack Router). --- ## TablePagination portal-hub supplies its own `TablePagination` component. It is the `TablePagination` referenced by both the list-page and detail-page patterns. Props used by the patterns: | Prop | Notes | | ------------- | ------------------------------------------------- | | `total` | Total record count — from a dedicated count query | | `pageSize` | 20 on list pages; 5 on detail-page section tables | | `dataShowing` | Number of rows on the current page | | `onChange` | Page change handler | | `current` | Current page number | This is a confirmed COMPONENT GAP — no `@spark-web` pagination component exists yet. Until one ships, every consumer supplies its own pagination component. --- ## Multi-select with per-item toggle rows When a multi-select picks N items and each selected item also needs its own on/off flag, pair the selector with a list of **toggle rows generated from the current selection** — one bordered row per item, label on the left and an `@spark-web/switch` `Switch` on the right. The toggle list binds to a react-hook-form field whose value is a `string[]` of the IDs currently switched on. Use it when a per-item boolean accompanies a multi-select choice (e.g. flagging some of the selected vendors as Authority Contacts). For a single form-wide boolean use `ToggleField`; for choosing items with no per-item flag, the multi-select alone is enough. Two parts: - **Selector** — `ComboboxField isMulti` or `MultiSelectField` from `@brighte/ui-components` (never `@spark-web/multi-select` directly — see Filter fields above). Its value is the set of selected items. - **Toggle rows** — derive `{ id, name }[]` from the current selection and render one row each, bound to a separate `string[]` field of the "on" IDs. Render nothing when the selection is empty. Each row is a bordered card wrapping a space-between flex row, with the Switch's accessible name supplied via `aria-label` (so the name isn't rendered twice; target a row with `getByRole('switch', { name })`). ### Row content Each row's children follow a consistent layout: a label stack on the left and an optional `Switch` on the right. The label stack contains the item name and an optional description line (`Text size="small" tone="muted"`) providing context such as the item's current status or effective date: ```tsx <ToggleRow variant="active"> <Stack gap="small"> <Text>{item.name}</Text> <Text size="small" tone="muted"> Current authority contact </Text> </Stack> <Switch aria-label={item.name} checked onCheckedChange={() => toggle(item.id)} > {''} </Switch> </ToggleRow> ``` The description is optional — when no secondary line is needed, drop the `Stack` wrapper and render a single `<Text>{item.name}</Text>` directly as the left-side child of `ToggleRow`. ### Row variants A toggle row has three visual variants. Use a reusable `ToggleRow` component (see Reference implementation below) to encapsulate these — never hand-write the border/background per call site. **Default** — standard field border, surface background. Used when the switch is off (item not currently active): ```tsx <Box border="field" borderRadius="medium" padding="medium" background="surface"> <Box display="flex" alignItems="center" justifyContent="spaceBetween" gap="medium" > {children} </Box> </Box> ``` **Active** — primary border with a green tint background. Used when the switch is on (item currently active): ```tsx <Box border="primary" borderRadius="medium" padding="medium" background="primaryLow" > <Box display="flex" alignItems="center" justifyContent="spaceBetween" gap="medium" > {children} </Box> </Box> ``` **Ended** — field border, reduced opacity, no switch. Used for historical items that can no longer be changed (e.g. an authority contact whose term has ended). Show these as read-only rows alongside active toggles when the modal displays history for context: ```tsx <Box border="field" borderRadius="medium" padding="medium" background="surface" opacity={0.6} > <Box display="flex" alignItems="center" justifyContent="spaceBetween" gap="medium" > {children} </Box> </Box> ``` **Important:** always provide a `background` value (even `"surface"` for non-active variants) so the Box's internal `BackgroundProvider` stays mounted across variant changes — toggling between `background={undefined}` and a value unmounts the provider and recreates the subtree, which breaks Switch state in tests and can cause flicker. ### Keeping the toggle field consistent - **Prune on change** — when an item is deselected (or an upstream condition turns the section off), drop its id from the toggle field. Do this in the consumer (a small `useEffect`/watch) and keep the row list presentational. - **Shape the payload in one place** — map `(selectedIds, onIds)` `[{ id, <flag>: onIds.includes(id) }]` in a single helper so the field's wire shape lives in one spot. ### Reference implementation > **Note:** paths below are in the **portal-hub** consumer repo > (`brighte-labs/portal-hub`), not this repository. `ToggleRow` — a reusable row container at `apps/admin-portal/src/components/ToggleRow/ToggleRow.tsx` that accepts a `variant` prop (`'default' | 'active' | 'ended'`) and renders the correct border/background/opacity combination using only Spark Box tokens. Children are placed inside the inner flex Box. Usage in Authority Contacts flows (portal-hub): - **Edit AC modal** (vendor Overview) — `apps/admin-portal/src/pages/Vendors/VendorDetail/Overview/EditAuthorityContactsModal.tsx` — active contacts get `variant="active"` with a Switch, ended contacts get `variant="ended"` with no Switch. - **Create/Edit vendor-user** — `apps/admin-portal/src/components/AuthorityContactToggleList` — the row list (props: `name`, `control`, `vendors: { id; name }[]`, optional `label`/`description`/`rules`). - `apps/admin-portal/src/utils/authorityContact` `buildVendorAcPayload` — payload shaping. - `apps/admin-portal/src/hooks/useAuthorityContactSync` — prunes the field on role/selection change. ### Do NOTs (portal-hub) - NEVER pass raw CSS to Spark `Box` layout props — they take token KEYS: `justifyContent="spaceBetween"` (NOT `"space-between"`), `alignItems`=`start|center|end|stretch`. An unrecognised value is silently dropped and the row falls back to `flex-start` (the switch clumps next to the label instead of aligning right). - NEVER hand-write border/background per toggle row — use the `ToggleRow` component with the appropriate `variant`. - NEVER put visibility/eligibility logic inside the row list — the consumer gates it and owns pruning the field.