saturation/uisaturation/ui
DocsComponentsBlocksPagesEmails
GitHub

Sections

IntroductionComponentsInstallationMCPThemingDesignComposing a Detail Panel

Components

Components

General

AvatarBadgeButtonKbdProgressSeparatorSkeletonSpin ResolveSpinnerSplit ButtonSync ButtonTypography

Forms & Inputs

Address LookupCalendar PickerCheckboxColor PickerComboboxDate PickerEmoji PickerFavicon SearchFieldInputInput GroupInput OTPRadio GroupSelectSliderSwitchTextareaToggleToggle Group

Data Display

AccordionAlertCardChartComparison SliderCredit CardData TableEmptyItemMarkdownSaturation Credit CardStatus BadgeTableTree

Detail Panels

Detail FieldDetail HeroDetail Toolbar

Navigation

BreadcrumbCommandMenubarNavigation MenuPaginationTabs

Overlays

CollapsibleContext MenuDialogDropdown MenuHover CardSheet

Layout

Button GroupFont ProviderWizard Split Layout

Feedback

Sonner

Animation & Effects

Animated GroupAnimated ListAnimated NumberBeamBlur FadeBorder TrailGlow EffectLiquid MetalLoading StateParallaxPixelProgressive BlurRippleSpotlightText EffectText Shimmer

Productivity

Agent ChatAI Chat InputCoding AgentFiltersFull CalendarKanbanNovel Editor
Docs/Composing a Detail Panel

Composing a Detail Panel

Assemble an entity detail sidebar from the Detail* block kit — hero, sections, def-driven fields, and footer actions.

The Detail* family is a kit: every entity detail sidebar (a transaction, a contact, a purchase order…) is assembled from the same five concepts — hero size, section, field def, action button, and (once, app-wide) editor injection. Parts docs cover each block; this page shows the assembly.

The five concepts

ConceptBlockThe one decision you make
Hero sizeDetail Herosize="S" | "M" | "L" — a matched set (container + type scale + extras change together)
SectionAccordion (via the DetailSection preset)icon + label — count / onAdd are base AccordionTrigger options
Field defDetail Fielda FieldDef per field — kind, state, and inline affordances
ActionButton (via the DetailActionButton preset)primary / confirm / danger → Button variants inverse / confirm / destructive-outline
EditorsDetailFieldEditorsinjected ONCE in an app wrapper, never per panel

A complete panel

import {
  Accordion,
  DetailSection,
  DetailActionButton,
  DetailHero,
  SHADER_STYLE,
} from "@saturation-ui/react"
// App wrapper: the kit DetailField with your entity editors baked in
// (date / ref / tags / unit) — see "Editor injection" below.
import { DetailField, type FieldDef } from "@/components/ui/detail-field"
import { BuildingIcon, TagIcon, CalendarIcon, CheckIcon, InfoIcon } from "lucide-react"

const FIELDS: { id: string; def: FieldDef }[] = [
  { id: "merchant", def: { label: "Merchant", icon: <BuildingIcon />, kind: "ref",
      navTo: "contact", entities: contacts } },                        // entity editor, app-injected
  { id: "category", def: { label: "Category", icon: <TagIcon />, kind: "select",
      options: CATEGORY_OPTS,
      source: { kind: "ai", tooltip: "Suggested from the receipt scan" } } }, // AI marker
  { id: "settled",  def: { label: "Settled", icon: <CalendarIcon />, kind: "date",
      state: "readonly", lock: { reason: "From bank feed" } } },       // locked, read-only
]

export function BankingTransactionPanel({ rec, set, onOpen }: Props) {
  return (
    <>
      <DetailHero
        size="M"
        variant="shader"
        background={<GrainShader style={SHADER_STYLE} />}  {/* slot — shader deps stay app-side */}
        title={rec.merchant}
        value={money(rec.amount)}
        replayKey={rec.id}
      />
      <Accordion type="multiple" defaultValue={["details"]}>
        <DetailSection value="details" icon={<InfoIcon />} label="Details">
          {FIELDS.map((f) => (
            <DetailField key={f.id} def={f.def} value={rec[f.id]} onChange={set(f.id)} onOpen={onOpen} />
          ))}
        </DetailSection>
      </Accordion>
      <div className="px-4 pt-3">
        <DetailActionButton primary icon={<CheckIcon size={15} />}>
          Actualize
        </DetailActionButton>
      </div>
    </>
  )
}

No provider, no context, no per-panel wiring. The panel owns the record and passes value + onChange — every field is controlled.

Field defs drive everything

A field's kind + state decide its full treatment, so the rules apply in every panel automatically:

  • ref → navigable link (name opens the entity via onOpen(def.navTo)), with the pick-list editor injected by the app
  • editable (default) → the right editor: text, email/phone/url link with the matching soft keyboard, select dropdown, date calendar
  • readonly → bright value, copy-on-hover, no box
  • computed → bright value + optional phase tint, plus a derived source marker

Copy-on-hover derives automatically (email/phone always; non-editable scalars). lock renders the hover lock with an optional click-to-unlock; source renders a provenance marker (ai / derived / integration) with a tooltip and optional popover.

Editor injection — once, app-wide

The kit has no data layer. The four data-coupled kinds (date, ref, tags, unit) render through editors you inject — typically baked into one app wrapper so panels never see them:

// components/ui/detail-field.tsx (your app)
import { DetailField as KitDetailField, type DetailFieldProps } from "@saturation-ui/react"
import { appEditors } from "./detail-field-editors" // date/ref/tags/unit wired to your data

export function DetailField(props: DetailFieldProps) {
  if (props.def) return <KitDetailField {...props} editors={props.editors ?? appEditors} />
  return <KitDetailField {...props} />
}

Without an injected editor the kit renders the formatted read-only value and warns in dev — a panel never crashes on a missing editor.

Hero sizes are matched sets

size changes the container, avatar box, type scale, and extras together — you never mix a big value into a small hero:

  • S — inline identity row (widget embeds, chat cards)
  • M — the 184px panel hero (the default)
  • L — 240px, and the only size that renders actions

Arrangement (bottom-left inline vs centered profile) is entity-constant: a contact hero is always centered, a transaction hero always inline. Set the centered flag once in your entity's hero wrapper — it is not a per-instance axis.

Chrome that completes a panel

  • Detail Toolbar — sticky back/title/actions/close; the title crossfades into a compact identity on scroll (useCompactOnScroll)
  • Panel chrome — DetailListCard (entity lists), DetailTimeline / DetailStepper (lifecycle readouts), DetailMeter (limits), DetailDropZone (attachments) — see them composed in the Detail Panel block
  • Section empties are a DetailSection option (empty={{ icon, label, hint }})
PreviousDesignNextIntroduction

On This Page

  • The five concepts
  • A complete panel
  • Field defs drive everything
  • Editor injection — once, app-wide
  • Hero sizes are matched sets
  • Chrome that completes a panel