Cloud|

@k2b/ui

DetailPanel

Quiet contextual inspectors with compact identity, flat sections, and one scroll owner.

DetailPanel.Action@k2b/ui

Full-width destinations keep navigation primary while a sibling overflow menu or one direct secondary action holds further commands. Disabled rows preserve unavailable resource context.

Related resourcesOpen the resource from the row; use the dots menu or the trash action for secondary commands.

Related resources

Release planningMail conversationMore actions for Release planning
Launch briefDocument · Updated today
Delete budget.xlsx

TSX

Copy
<DetailPanel.Section title="Related resources" icon="ti ti-link">
<DetailPanel.Action
href="/app/mail/inbox?conversation=Conv01"
leading={<i class="ti ti-mail" aria-hidden="true" />}
title="Release planning"
description="Mail conversation"
menuLabel="More actions for Release planning"
menuItems={[{ label: "Unlink", icon: "ti ti-unlink", action: unlink }]}
/>
<DetailPanel.Action
onClick={openBudget}
leading={<i class="ti ti-file-spreadsheet" aria-hidden="true" />}
title="budget.xlsx"
description="Spreadsheet · Uploaded today"
secondaryAction={{ icon: "ti ti-trash", label: "Delete budget.xlsx", variant: "danger", onClick: confirmDelete }}
/>
<DetailPanel.Action
type="button"
disabled
leading={<i class="ti ti-file-off" aria-hidden="true" />}
title="Archived brief"
description="Resource unavailable or no longer accessible"
/>
</DetailPanel.Section>
DetailPanel@k2b/uiDescriptionList@k2b/uiDiscussion@k2b/ui

Two production-shaped inspectors demonstrate the final contract: padded identity headers, one stable scroll gutter absorbed by the trailing host inset, one flat frame for every summary, group, lone section and comment thread, and the same heading, icon and count columns in all of them.

Customer requestRequest state and customer context form clear, related surfaces.

Bug report

No preview · 5 minutes ago

Close request details

Workflow

Status
Assignee
Priority
Labels
Thread tier
Growth

Links

content-mobbin

Company actions
Company tier
Growth
Slack channels
Not connected
Domain
content-mobbin.com

alexsmith

Contact actions
Email
alexsmith@content-mobbin.com
Groups
Not assigned
alexsmithalexsmith@content-mobbin.comMore actions for alexsmith

Recent threads

Subscription plan details

Retry subscription sync
Collaborative noteDocument navigation and collaboration remain distinct but consistent.

Welcome!

Collaborative note

Close note details

Contents

  1. H1
  2. H2
  3. H2
  4. H2
  5. H2
  6. H2
  7. H2

Comments

1
  1. MK
    Mara Klein
    Reply to Mara Klein

    The introduction is ready for review.

Post comment

Online

VKValentin Kolb

Versions

TSX

Copy
<DetailPanel.Group label="Customer context">
<DetailPanel.Section title="Company" icon="ti ti-building" tone="accent">
<DescriptionList layout="rows" size="sm" items={companyItems} />
</DetailPanel.Section>
<DetailPanel.Section title="Contact" icon="ti ti-user" tone="warning">
<DetailPanel.Action
href="/contacts/ada"
target="_blank"
rel="noopener noreferrer"
title="Ada Lovelace"
menuLabel="More actions for Ada Lovelace"
menuItems={[{ label: "Related requests", href: "/requests?contact=ada" }]}
/>
</DetailPanel.Section>
<DetailPanel.Section title="Recent threads" icon="ti ti-history" tone="success">…</DetailPanel.Section>
</DetailPanel.Group>
<DetailPanel.Section title="Subscription plan details" icon="ti ti-alert-triangle" tone="danger" actions={retry} />
<Discussion label="Comments" icon="ti ti-messages" count={1}>
<Discussion.List>…</Discussion.List>
<Discussion.Composer
label="Add comment"
placeholder="Add a comment"
submitLabel="Post comment"
onSubmit={postComment}
/>
</Discussion>

DetailPanel gives contextual inspectors one quiet content structure without owning their surrounding drawer, workspace region, dialog, or domain state.

DetailPanel.Body automatically fades its top and bottom edges while content remains beyond that edge. Header and footer controls stay outside the fade. Set scrollFade={false} to disable it. The hint follows resize and live content changes, reserves no space, and is disabled in forced-color mode.

Import

tsx
import { DescriptionList, DetailPanel, NumberInput } from "@k2b/ui";

Use DetailPanel

Place it inside AppWorkspace.Detail for persistent selection context, or in another host when the same inspector content is reused. The host owns opening, closing, sizing, and persistence. An AppWorkspace.Detail column shares the workspace surface; a waiting Detail opens as a raised drawer. Its sections and groups have no boxes of their own. The application owns data, permissions, mutations, and which sections are present. See Detail and panel.

DetailPanel.Header keeps identity and actions in one compact region. Pass icon for the standard accent-tinted identity tile or leading for an avatar or another custom identity; they are mutually exclusive. Pass actions for compact utilities such as more and close, and primaryActions for the small set of prominent commands below the identity row. On touch devices, primary actions keep 0.625rem from the identity row and between wrapped lines, so their tap areas stay clear of a meta action or the line above. The title wraps to at most two lines, with the full string in its native hint. The subtitle stays on one line and ends with an ellipsis; meta sits at its trailing edge instead of competing with the title. The subtitle keeps at least 6rem, so a meta too wide for that moves below it, still on the trailing edge, and wraps inside the header instead of hiding the subtitle; plain text in meta uses the subtitle's size and color, while badges and buttons keep their own. DetailPanel.Body is the single scrolling element and accepts a scrollPreserveKey. Its stable scrollbar gutter prevents content from shifting when expanding content first makes the panel overflow. Inside AppWorkspace.Detail, that gutter occupies the host's existing trailing inset instead of adding a second gap. On a device whose primary pointer is coarse, which usually draws overlay scrollbars that reserve no gutter, the panel keeps the host inset instead, so its content has the same margin on both sides. The pointer only stands in for the scrollbar style: a touch device with classic scrollbars shows the gutter next to the inset, and a desktop with overlay scrollbars keeps the panel at the trailing edge. Do not add a second full-height scroller inside it. The panel keeps a 0.5rem horizontal gutter; the AppWorkspace.Detail column adds its own 0.5rem inset.

Use DetailPanel.Summary once, directly below the header, when the selected item has a primary set of facts or controls. In other hosts, the summary keeps its own surface. Inside AppWorkspace.Detail, it shares the panel surface and aligns with the header title. Do not repeat the summary for every group.

The panel uses --k2b-detail-panel-accent for restrained identity and action accents, with the portable UI accent as its fallback. Text actions in section and summary headings fall back to the theme's action color instead, so they keep their contrast in dark mode. A host may map that hook to its own theme token; DetailPanel does not know how the host derives it.

DetailPanel.Section draws no frame of its own. It groups content through spacing and a sentence-case title, not a card, divider, or decorative background; the group or the body frames it. Pass icon for a fixed section icon slot and tone to distinguish portable accent, neutral, success, warning, or danger roles through text color only. Pass description for short supporting context, count for a numeric count chip, and meta for short state text such as “2 of 4 done”. Both sit immediately after the title. A zero count reads “None yet”, localized through the inherited UI locale. A stream such as activity has no count. Give a normal section at most one trailing control through actions. Only the description is muted: a field in actions keeps its own description and error styles. A normal section may omit its body to represent a compact, actionable empty group. Set collapsible for secondary content. Closed sections show a compact action row; open sections show a heading with a collapse button on the right. Both states share one geometry with the other section headings: the icon, title, meta, and chevron keep their columns and the row its height when the section opens or closes. Hover colors the title and chevron, and its fill bleeds 0.5rem into the panel gutter. Keyboard focus adds the shared focus ring, drawn inside the row where the panel would clip it. Their content stays mounted when closed, preserving unsaved input and child state. Collapsible sections reserve the header action for collapsing.

Use defaultOpen for an initially expanded section, or control it with open and onOpenChange. Controlled state is authoritative: the callback requests a change but does not open or close the section itself. This lets an editor open the relevant section after validation fails. disabled disables both toggle buttons; it does not disable the child controls. Use a fieldset for that. The content owns its layout: wrap form controls in a column with a gap.

Use DetailPanel.Group when one or more sections form one stable context, such as a company and its contacts or a document and its derived metadata. Merge adjacent sections when they belong to that same context. Outside AppWorkspace.Detail, the group owns the same quiet surface as the summary. Inside it, the group is transparent. Spacing and section headings separate its sections without borders or lines. Pass label when the shared context benefits from an accessible group name.

A section placed directly in DetailPanel.Body is a group of one. It follows the group’s surface and inset in its host, so its icon, title, count, and content start in the same columns as every grouped section. A Discussion inside the panel does the same. Do not wrap a standalone section in a group only to frame it, and do not add an application frame, border, or padding around a section. Only a notice, a progress bar, a short row of commands, or a feed whose items are each one frame stands in the body without a section frame. Loose text belongs in the header subtitle or a section, and a block with its own heading becomes a section. Give every section of a panel an icon so the titles share one column, and use size="xs" for text buttons in its actions, so their label text ends at the same trailing edge as row metadata.

Sections accept arbitrary content. Use DescriptionList layout="rows" for compact properties. When people edit those properties in place, make them property rows: a control with appearance="plain" as the row's value shows the value as text, and the whole row opens its picker. Keep the summary's pencil action for the full edit form. Use normal shared inputs for a full form inspector, and the appropriate shared list, table, notice, preview, or editor for specialized content. Use Discussion directly in the body when notes or comments need their own labelled composer and author timeline. It takes the frame and heading of a section, so it needs no group or wrapper. Rows that are not DetailPanel.Action, such as a static address or an empty message, start at the content edge without their own horizontal padding. A left-aligned Placeholder in a section does the same on its own. Do not add domain variants such as record, mail, or workflow to DetailPanel.

Use DetailPanel.Action for a full-width destination or command such as a related record, attachment, or in-panel jump. Pass href for native link semantics; omit it for a native button. leading, title, optional description, and trailing keep row geometry and interaction states consistent. Action rows align their optional leading icon with the section icon column and keep their titles at the section title's text edge, even without leading. All other content of a section with an icon inside a DetailPanel starts at that same text edge: comment authors, static text, progress, and row titles. A section used outside a panel, such as a form section in an editor dialog, keeps its full width. Do not add a second indent around action rows. Hover fill extends 0.5rem into the panel gutter without moving the row. current marks the current destination in the panel accent and sets aria-current (location for links, true for buttons). wrap allows the title to use two lines; otherwise it ends with an ellipsis. When a destination has secondary actions, pass declarative menuItems together with the required accessible menuLabel. The primary link and the Dots trigger render as sibling controls, so the whole main row remains a native destination without nesting a button inside the link. With a mouse or trackpad, including a tablet with a mouse, the Dots trigger stays hidden until the row is hovered, keyboard-focused, or its menu is open. Touch-only screens always show it. Do not use it for static key-value data, comments, history, or form fields. Stacked action rows sit flush, so on touch devices each row is at least 44 px (2.75rem) tall, including its secondary and Dots buttons, and its tap area keeps the row height instead of reaching into the neighbouring row. This follows the device, not the input in use: a laptop with a touch screen gets the taller rows with a mouse too, and devices without a touch screen keep the compact rows.

When one secondary command is frequent enough to deserve a direct control, such as deleting an attachment, pass secondaryAction with an icon, an accessible label, and onClick. Name the label after the row, such as Delete budget.xlsx, so repeated buttons stay distinguishable. It renders as a sibling icon button at the end of the row, before an optional Dots trigger, and appears on row hover or keyboard focus with any pointer that can hover. Touch-only screens always show it. Set variant: "danger" for a destructive command; hover and focus then use the danger text color. The button only reports the click. The application confirms destructive work, performs it, and refreshes the list. When that removes the focused row, the application moves focus to the row that takes its place. Keep further commands in menuItems.

API reference

ts
type DetailPanelProps = {
  children: JSX.Element; class?: string;
};

type DetailPanelHeaderBaseProps = {
  title: JSX.Element; subtitle?: JSX.Element; meta?: JSX.Element; actions?: JSX.Element;
  primaryActions?: JSX.Element; class?: string;
};

type DetailPanelHeaderProps = DetailPanelHeaderBaseProps &
  (
    | {
        leading?: JSX.Element;
        icon?: never;
      }
    | {
        leading?: never;
        icon?: string;
      }
  );

type DetailPanelBodyProps = {
  children: JSX.Element; scrollPreserveKey?: string; scrollFade?: boolean; class?: string;
};

type DetailPanelSummaryProps = {
  title: JSX.Element; children: JSX.Element; actions?: JSX.Element; class?: string;
};

type DetailPanelGroupProps = {
  children: JSX.Element; label?: string; class?: string;
};

type DetailPanelTone = "accent" | "neutral" | "success" | "warning" | "danger";

type DetailPanelSectionBaseProps = {
  title: JSX.Element; icon?: string; tone?: DetailPanelTone; description?: JSX.Element; count?: number; meta?: JSX.Element;
  class?: string;
};

type DetailPanelSectionProps = DetailPanelSectionBaseProps &
  (
    | {
        children?: JSX.Element;
        actions?: JSX.Element;
        collapsible?: false;
        defaultOpen?: never;
        open?: never;
        onOpenChange?: never;
        disabled?: never;
      }
    | {
        children: JSX.Element;
        actions?: never;
        collapsible: true;
        defaultOpen?: boolean;
        open?: boolean;
        onOpenChange?: (open: boolean) => void;
        disabled?: boolean;
      }
  );

type DetailPanelActionSecondary = {
  icon: string; label: string; onClick: () => void; variant?: "danger";
};

type DetailPanelActionBaseProps = {
  title: JSX.Element; description?: JSX.Element; leading?: JSX.Element; trailing?: JSX.Element;
  secondaryAction?: DetailPanelActionSecondary; current?: boolean; wrap?: boolean; class?: string;
};

type DetailPanelActionMenuProps =
  | {
      menuItems?: never;
      menuLabel?: never;
    }
  | {
      menuItems: readonly DropdownItem[];
      menuLabel: string;
    };

type DetailPanelActionLinkProps = DetailPanelActionBaseProps &
  DetailPanelActionMenuProps &
  Omit<ButtonLinkProps, "children" | "class" | "size" | "title" | "variant"> & {
    href: string;
  };

type DetailPanelActionButtonProps = DetailPanelActionBaseProps &
  DetailPanelActionMenuProps &
  Omit<ButtonProps, "children" | "class" | "size" | "title" | "variant"> & {
    href?: never;
  };

type DetailPanelActionProps = DetailPanelActionLinkProps | DetailPanelActionButtonProps;

The *BaseProps shapes explain shared fields of the public unions. Header chooses icon/title or custom leading content through its union; Action chooses a menu, link, or native button. DropdownItem uses the menu contract; native button/link attributes retain their usual types. Boolean features are off unless enabled; Body owns the scroll area by default.

Accessibility

The header title is an h2; normal section titles are labelled h3 headings. Decorative header and section icons are hidden from assistive technology, so their adjacent text remains the label and color is never the only signal. Collapsible sections use native buttons with expanded state and a relationship to their content. Focus follows the toggle when its counterpart becomes visible. Every icon-only action and every control embedded in a description value still needs its own accessible name. DetailPanel.Action keeps its visible title as the accessible name and renders a real link or button. Its secondary action is a separate button in the tab order, named by label.

Runtime

The composition and its initial open state are server-renderable. Toggling a collapsible section requires browser code. Interactive children keep their own state and remain mounted when the section closes.

Example

tsx
<AppWorkspace.Detail id="item" open={selectedId() !== null} onClose={closeDetails} width="md">
  <DetailPanel>
    <DetailPanel.Header
      icon="ti ti-building-warehouse"
      title="Studio shelf"
      subtitle="Locations · version 2"
      primaryActions={<Toolbar label="Location actions"><Button size="xs">Edit</Button></Toolbar>}
      actions={<IconButton label="Close details" onClick={closeDetails}><i class="ti ti-x" aria-hidden="true" /></IconButton>}
    />
    <DetailPanel.Body scrollPreserveKey="location-detail">
      <DetailPanel.Summary title="Overview">
        <DescriptionList
          layout="rows"
          size="sm"
          actionVisibility="progressive"
          items={[
            { term: "Room", description: "Studio" },
            {
              term: "Quantity",
              description: (
                <NumberInput aria-label="Quantity" appearance="plain" min={0} value={quantity} onValueCommit={saveQuantity} />
              ),
            },
          ]}
        />
      </DetailPanel.Summary>
      <DetailPanel.Section
        title="Notes"
        icon="ti ti-notes"
        count={0}
        description="Keep decisions with this location"
        actions={<Button variant="ghost" size="xs">Add note</Button>}
      />
      <DetailPanel.Group label="Inventory context">
        <DetailPanel.Section title="Related records" icon="ti ti-link" tone="accent">
          <DetailPanel.Action
            href="/app/grids/locations/records/st-02"
            leading={<i class="ti ti-building-warehouse" aria-hidden="true" />}
            title="Studio"
            description="Room · ST-02"
            trailing={<i class="ti ti-chevron-right" aria-hidden="true" />}
          />
        </DetailPanel.Section>
        <DetailPanel.Section title="Attachments" icon="ti ti-paperclip" tone="neutral" count={2} />
      </DetailPanel.Group>
      <DetailPanel.Section title="History" icon="ti ti-history" collapsible>
        …
      </DetailPanel.Section>
    </DetailPanel.Body>
  </DetailPanel>
</AppWorkspace.Detail>

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.