Clean modal
No divider by default, a coloured footer action when one earns it, a sectioned body instead of hand-divided children.
See also
What this solves
Every modal shipped two dividers a consumer then removed, footer buttons had no way to carry colour or shade without a className override, and a body with more than one section was hand-divided per call site. Ruling 6 (doc 80) asks for defaults tuned to the common case instead of academically pure generics.
Use it when
- A record view with more than one sectionModalBody + ModalSection, no nested Card
- A form that saves something realSchemaForm inside ModalBody, next coloured to match the action
- A destructive confirmConfirmModal preset="delete", now rhythmed by ModalBody
- A multi-step flowModalFooter spread, an extra ghost action in children, divider opted back on
Rendered
Fixture data; check both themes.patterns/clean-modal/example.tsx
Copy this into a fresh route and it renders as above.
"use client";import { Button, ConfirmModal, Modal, ModalBody, ModalFooter, ModalSection, ModalTitle, SchemaForm, Table, useModal,} from "@versable-git/ui";import { useState } from "react";const RECORD_ID = "clean-modal-record";const FORM_ID = "clean-modal-form";const CONFIRM_ID = "clean-modal-confirm";const WIZARD_ID = "clean-modal-wizard";const NAME_SCHEMA = { type: "object", properties: { name: { type: "string", title: "Name" }, email: { type: "string", title: "Notify", format: "email" }, }, required: ["name"],};/** * The four modal shapes the flipped defaults touch: a record view with a * sectioned body, a form inside `ModalBody`, a destructive confirm now * rhythmed by the same component, and a wizard footer opting `divider` on * to show it is still available. */export function CleanModal() { const { openModal, closeModal } = useModal(); const [formValue, setFormValue] = useState<Record<string, unknown>>(); return ( <div className="flex flex-wrap gap-3"> <Button size="sm" content="Record modal" Icon="Copy" onClick={() => openModal(RECORD_ID)} /> <Button size="sm" content="Form modal" Icon="InputForm" onClick={() => openModal(FORM_ID)} /> <Button size="sm" content="Destructive confirm" Icon="Trash" color="error" variant="outline" onClick={() => openModal(CONFIRM_ID)} /> <Button size="sm" content="Wizard footer" Icon="Right" onClick={() => openModal(WIZARD_ID)} /> <Modal elementId={RECORD_ID} size="md"> <ModalTitle title="Disc Brake Rotor" subtitle="SKU RT-330" identifier="prt_9f21a" Icon="PartType" /> <ModalBody> <ModalSection label="Details"> <dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm"> <dt className="text-base-content/65">Brand</dt> <dd>Brembo</dd> <dt className="text-base-content/65">Weight</dt> <dd>4.8 kg</dd> </dl> </ModalSection> <ModalSection label="Fitment" count="2 vehicles"> <Table density="compact" columns={[ { key: "make", header: "Make" }, { key: "model", header: "Model" }, ]} rows={[ { make: "Ford", model: "F-150" }, { make: "Ford", model: "Ranger" }, ]} rowKey={(r) => `${r.make}-${r.model}`} /> </ModalSection> </ModalBody> <ModalFooter next={{ content: "Close", onClick: () => closeModal() }} /> </Modal> <Modal elementId={FORM_ID} size="sm"> <ModalTitle title="Add a watcher" Icon="Team" /> <ModalBody> <SchemaForm schema={NAME_SCHEMA} value={formValue} onChange={setFormValue} /> </ModalBody> <ModalFooter back={{ content: "Cancel", onClick: () => closeModal() }} next={{ content: "Save", color: "success", onClick: () => closeModal() }} /> </Modal> <ConfirmModal elementId={CONFIRM_ID} preset="delete" title="Delete this part?" message="This removes it from every fitment list. This cannot be undone." /> <Modal elementId={WIZARD_ID} size="sm"> <ModalTitle title="Import mapping" subtitle="Step 2 of 3" Icon="Workflow" /> <ModalBody> <p className="text-base-content/70 text-sm">Match each source column to a kit field before continuing.</p> </ModalBody> <ModalFooter divider spread back={{ content: "Back" }} next={{ content: "Continue", color: "primary", onClick: () => closeModal() }}> <Button size="sm" variant="ghost" content="Save draft" onClick={() => closeModal()} /> </ModalFooter> </Modal> </div> );}Where it ships
packages/ui/src/modal/modal-elements.tsxModalTitle, ModalFooter, ModalBody, ModalSectionpackages/ui/src/modal/confirm-modal.tsxConfirmModal's message now renders through ModalBody
App-specific: Real callers still own their own schemas, row data and confirm copy; this page fixes static content in place of a live record so the chrome is what's on display.