Design system

Decision modal

The small modal that asks for a note before a recommendation is put on hold or rejected.

import { DecisionModal } from "@/pages/recommendations/index";

DecisionModal is a centred modal for one decision on one recommendation. It says what will happen, asks for a note, and commits with one click. Hold and Reject need a note; the confirm button stays disabled until the note has text. There is no toast afterwards: the recommendation changing state is the confirmation.

Usage

Under an open recommendation, Hold and Reject open the modal. Render it inside AnimatePresence from motion/react, so the backdrop fades out when it closes.

const [modal, setModal] = useState(null);

<Button variant="secondary" size="md" icon={PauseCircle} label="Hold" onClick={() => setModal("on-hold")} />
<Button variant="ghost" size="md" icon={Prohibit} label="Reject" onClick={() => setModal("rejected")} />

<AnimatePresence>
  {(modal === "on-hold" || modal === "rejected") && (
    <DecisionModal
      kind={modal}
      item={item}
      platform={platform}
      onCancel={() => setModal(null)}
      onConfirm={(note) => { setModal(null); onDecide(modal, note); }}
    />
  )}
</AnimatePresence>

The three kinds

The wording for each kind is in the MODAL object above the component.

kindTitleWhat it says will happenNote labelConfirmNote
"on-hold"Put this on holdNothing is applied. The recommendation stays in your queue as On hold until you decide.What are you waiting on? (required)Put on holdRequired
"rejected"Reject this recommendationNothing is applied. Your reason is saved with this recommendation, and future runs work within it.Why are you rejecting this? (required)RejectRequired
"accepted"Accept this recommendationDepends on item.type. See below.Add a note (optional)AcceptOptional

For "accepted", the text names the platform: "Petavue applies the change to LinkedIn Ads and confirms the saved settings before reporting it as done." A test starts a capped test instead, and a handoff creates tasks in HubSpot.

<DecisionModal kind="accepted" item={item} platform="LinkedIn Ads" onCancel={close} onConfirm={accept} />

Nothing opens the "accepted" kind today. Apply on the Recommendations page opens ApplyModal, which lists each change and lets people untick or edit them first. The Accept wording is kept in MODAL but is not shown in the product.

Anatomy

PartStyle
BackdropCovers the window (fixed inset-0, z-[80]) in rgba(15, 22, 36, 0.28). Fades in over 150ms. A click on it cancels.
PanelWhite, 480px wide (at most 92% of the window), 12px radius, Tailwind shadow-2xl. Rises 8px into place over 180ms.
Body20px padding, 12px between parts. A 16px semibold title, then the consequence in 12px --text-secondary.
NoteA 12px semibold label in --text-secondary, over a three-row text area: 12px text, --color-grey-200 border, 6px radius, --color-primary-500 border on focus.
Footer--color-grey-50, with a --color-grey-100 rule above. Cancel (ghost) and the confirm button (primary), both md, on the right.

The backdrop colour and the shadow are not tokens. The scrim in the Dialog examples is rgba(31, 33, 46, 0.45).

Props

PropTypeDefaultNotes
kind"on-hold" | "rejected" | "accepted"Picks the wording and whether the note is required. Any other value throws.
itemobjectThe recommendation. Only item.type is read, for the "accepted" wording.
platformstringThe channel name, such as "LinkedIn Ads", for the "accepted" wording. Falls back to "the platform".
onCancel() => voidCalled by Cancel, a click on the backdrop and the Escape key.
onConfirm(note: string | null) => voidCalled by the confirm button with the trimmed note, or null when it is empty. The modal does not close itself.

The note is kept inside the modal and starts empty each time it opens.

Accessibility

  • The panel has role="dialog" and its title as aria-label. It has no aria-modal.
  • The note field takes focus when the modal opens, and its label wraps it, so the question is read with the field.
  • Escape cancels. The listener is on window, so it works wherever focus is.
  • Focus is not kept inside the modal: Tab moves on to the page behind it. When the modal closes, focus is not returned to Hold or Reject.
  • "(required)" is only in the label text. The field has no required or aria-required, and nothing says why the confirm button is disabled.

When to use it

Use DecisionModal only for decisions on a recommendation. For other confirmations and short forms, use Dialog, wrapped with a backdrop and focus handling as its page shows.

Do and don't

  • Do ask for a note when the decision changes what Petavue does next. Hold and Reject do; the note is what future runs work within.
  • Do write the consequence as plain fact: what is applied, what is saved, what happens next.
  • Don't add a toast after confirming. Update the recommendation's status instead.
  • Don't use it for Apply. Apply needs the list of changes, which is ApplyModal.

Where it is used

FileUse
src/pages/recommendations/index.jsxIn Detail, for Hold and Reject on an open recommendation, and for Reject on one that is on hold after "Decide now".

The function is defined in that file and exported from it for these docs.

On this page