Compound Components

The toolkit's lowest-level UI primitives. The state machine ships with the library; the layout is yours.

Overview

Compound components are namespace objects (Consent, DSR, Breach, etc.) whose properties are small, focused sub-components that share state through React context. You assemble them like building blocks rather than configuring a single monolithic component via props.

They sit between the two ends of the toolkit's API spectrum:

  • Presets — drop in a single component, get a working flow.
  • Compound components — keep the toolkit's state machine, control every DOM node.
  • Headless hooks — pure state, no markup at all.

Use a compound component when you want the toolkit to own the consent / DSR / DPIA lifecycle (persistence, validation, "Accept All" semantics, NDPA invariants) but you want to write the layout yourself — different copy, different ordering, different wrapping elements, your own design system.

Custom sub-components with useConsentCompound

To write your own buttons or read-only views, call useConsentCompound inside any descendant of Consent.Provider:

import { useConsentCompound } from '@tantainnovative/ndpr-toolkit/consent';

function AnalyticsToggle() {
  const { hasConsent, updateConsent } = useConsentCompound();
  const enabled = hasConsent('analytics');

  return (
    <label className="flex items-center gap-2">
      <input
        type="checkbox"
        checked={enabled}
        onChange={e => updateConsent({ analytics: e.target.checked })}
      />
      Enable analytics
    </label>
  );
}

The hook returns the full ConsentContextValue: options, settings, hasConsent, updateConsent, acceptAll, rejectAll, resetConsent, shouldShowBanner, isValid, validationErrors, and isLoading.

Calling it outside a Consent.Provider throws — keep custom components inside the same subtree.

Compound namespaces for the other modules

The other modules ship a Provider plus a slot for each existing high-level component. They are thinner than the Consent compound — useful when you want a Provider/context wrapper for state sharing and a stable naming convention, but not a per-button breakdown.

NamespaceImportSub-components
Consent/consentProvider, OptionList, AcceptButton, RejectButton, SaveButton, Banner, Settings, Storage
DSR/dsrProvider, Form, Dashboard, Tracker
DPIA/dpiaProvider, Questionnaire, Report, StepIndicator
Breach/breachProvider, ReportForm, RiskAssessment, NotificationManager, ReportGenerator
Policy/policyProvider, Generator, Preview, Exporter
LawfulBasis/lawful-basisProvider, Tracker
CrossBorder/cross-borderProvider, Manager
ROPA/ropaProvider, Manager

Only the Consent compound exposes a granular button/option breakdown today. For the other modules, use the compound when you want a Provider with state context, and reach for headless hooks (e.g. useDSR, useBreach) when you need to swap the whole UI.

DSR example

import { DSR } from '@tantainnovative/ndpr-toolkit/dsr';
import type { RequestType } from '@tantainnovative/ndpr-toolkit/dsr';

const requestTypes: RequestType[] = [
  {
    id: 'access',
    name: 'Access my data',
    description: 'Request a copy of your personal data.',
    ndpaSection: 'Section 34(1)(a)–(b)',
    estimatedCompletionTime: 30,
    requiresAdditionalInfo: false,
  },
];

export function DSRPage() {
  return (
    <DSR.Provider
      requestTypes={requestTypes}
      onSubmit={(req) => console.log('submitted', req.id)}
    >
      <DSR.Form />
      <DSR.Dashboard />
    </DSR.Provider>
  );
}

Breach example

import { Breach } from '@tantainnovative/ndpr-toolkit/breach';
import type { BreachCategory } from '@tantainnovative/ndpr-toolkit/breach';

const categories: BreachCategory[] = [
  { id: 'unauthorized_access', name: 'Unauthorized access', description: '…', defaultSeverity: 'high' },
];

export function BreachResponse() {
  return (
    <Breach.Provider categories={categories}>
      <Breach.ReportForm />
      <Breach.RiskAssessment />
      <Breach.NotificationManager />
      <Breach.ReportGenerator />
    </Breach.Provider>
  );
}

Unstyled mode

Every Consent sub-component (OptionList, AcceptButton, RejectButton, SaveButton) accepts an unstyledprop that strips the toolkit's default BEM classes. Pair it with className or classNames to plug in your own design system.

<Consent.Provider options={options}>
  <Consent.OptionList unstyled classNames={{ root: 'space-y-2' }} />
  <Consent.AcceptButton unstyled className="btn-primary">Accept all</Consent.AcceptButton>
  <Consent.RejectButton unstyled className="btn-ghost">Reject all</Consent.RejectButton>
</Consent.Provider>