EventCard

EventCard groups event rows inside a white card. Rows show a label, value, and optional details. The parent supplies content and formatting.

import EventCard, { EventCardItem } from "@haloid/azucar/ui/event-card";
import Heading from "@haloid/azucar/ui/heading";
import Text from "@haloid/azucar/ui/text";

Rows

Omit children for a static row. Add children for expandable details. This example shows both forms.

<EventCard
className="mx-auto max-w-120"
summary={<Heading level={2} muted="lasted for 12m">Activity</Heading>}
footer={{
  leading: <><SFArrowForward size={16} aria-hidden="true" /> Direct</>,
  trailing: <time dateTime="2026-01-29T14:13:00Z">02:13 PM</time>,
}}
>
<EventCardItem icon={<SFEye size={16} />} label="Step A" value="2s" />
<EventCardItem icon={<SFCheckmarkCircle size={16} />} label="Step B" value="+71" tone="good" defaultOpen>
  <div className="flex flex-col gap-3">
    <div className="flex items-center justify-between gap-3">
      <Text inline>Item A</Text>
      <Text inline>71</Text>
    </div>
    <Text variant="secondary">Step B completed on Jan 29, 2026 at 02:13 PM.</Text>
  </div>
</EventCardItem>
</EventCard>

summary sits above the rows. footer.leading holds context, and footer.trailing holds metadata or a timestamp.

The card includes the Card shell. Use it directly inside a TimelineItem, or elsewhere on a page.

Sizing

The parent sets width. Labels and values wrap. Rows grow with their content, and footer sections wrap on narrow screens.

EventCardItem also works alone. This example uses a narrow container and a long label.

<div className="mx-auto w-full max-w-72">
<EventCardItem
  icon={<SFCheckmarkCircle size={16} />}
  label="A longer event label that wraps across several lines"
  value="+1,234"
  tone="good"
>
  <Text variant="secondary">Details follow the label's alignment and wrap within the row.</Text>
</EventCardItem>
</div>

Expansion

Expandable rows start closed. Set defaultOpen to open a row on mount. Rows expand independently.

Control a row with open and onOpenChange. The close button demonstrates focus returning from hidden details to the trigger.

function ControlledEventExample() {
const [open, setOpen] = useState(false);
const [changes, setChanges] = useState(0);
return (
  <div className="mx-auto flex max-w-120 flex-col gap-4">
    <EventCard summary="Controlled details">
      <EventCardItem
        label="Step C"
        value={open ? "Open" : "Closed"}
        open={open}
        onOpenChange={(next) => {
          setOpen(next);
          setChanges((count) => count + 1);
        }}
      >
        <div className="flex flex-col items-start gap-3">
          <Text variant="secondary">The parent owns this row's state.</Text>
          <Button variant="ghost" onClick={() => setOpen(false)}>Close details</Button>
        </div>
      </EventCardItem>
    </EventCard>
    <Text variant="secondary">Trigger changes: {changes}</Text>
  </div>
);
}
render(<ControlledEventExample />);

Controlled rows require a handler. Do not combine open and defaultOpen, or change control modes while a row stays mounted.

onOpenChange reports trigger actions. Initial rendering and parent updates do not call it.

Details mount on opening and unmount after closing. Local detail state resets on reopening. Keep persistent state in the parent.

Stable React keys preserve row expansion when entries move. To coordinate rows, control their open values in the parent.

Accessibility

The summary of an expandable row is a button. Enter and Space toggle it. Its visible label names the button.

Collapsed details leave the tab order and accessibility tree. Collapsing focused details returns focus to the trigger.

Keep icon and value content passive. Put links, menus, and controls inside details or footer slots.

Icons beside labels are decorative. Supply accessible text for metadata icons. Use <time> for timestamps.

Panel and chevron transitions respect reduced-motion preferences.

Typography

The summary uses the same 16px medium title style as CardHeader. Compose Heading for a title with a muted qualifier.

Rows use Text body styling at 14px. Details and footer metadata use Text secondary styling.

The summary slot adds no heading level itself. The example supplies Heading level={2} explicitly.

Tones

tone changes the value's color. Labels and card surfaces stay neutral. The default tone is neutral.

This example shows all five tones. Words and signs preserve meaning without color.

<EventCard className="mx-auto max-w-120" summary="Event values">
<EventCardItem label="Step A" value="12s" />
<EventCardItem label="Step B" value="+71" tone="good" />
<EventCardItem label="Step C" value="Needs review" tone="warn" />
<EventCardItem label="Step D" value="-36" tone="bad" />
<EventCardItem label="Step E" value="Updated" tone="info" />
</EventCard>

API

interface EventCardProps {
children: React.ReactNode;
summary?: React.ReactNode;
footer?: { leading?: React.ReactNode; trailing?: React.ReactNode };
className?: string;
}
// Every row: { label: string; icon?; value?; tone?: BadgeTone; className? }
// Static: omit children and expansion props.
// Uncontrolled: { children; defaultOpen?: boolean; onOpenChange? }
// Controlled: { children; open: boolean; onOpenChange: (open: boolean) => void }

Types are exported from @haloid/azucar/ui/event-card/types. className merges onto each component's outer surface.

Client components can use EventCard.Item. Import EventCardItem by name from Server Components.

Use in north

North supplies event labels, details, icons, and formatted values. It selects tones from meaning and handles empty groups before rendering.