Copy these before writing any list, rail, inbox, or overview page in a lantern app. Never hand-roll rows, glyphs, rings, or rails.

Each recipe is one when-to-use line plus one HEEx block built only from lantern components (lantern classes and --lantern-* tokens are fine; no raw Tailwind utilities, no arbitrary text-[Npx] values). The HEEx is the same source the test suite renders (test/support/recipes/).

Origins are the flicker pages these shapes were extracted from:

Swap the fixture assigns (@ticket, @paths, …) for the host's LiveView assigns. String paths in the recipes compile without verified routes; prefer ~p in the app.

Linear-style list row

When to use: A dense issue row — priority glyph, id, status glyph, title, tags, progress ring, date — sitting under a tinted group band.

Origin: flicker #1404 tickets list.

<.group_band name="In progress" count={1}>
  <:glyph><.status_glyph status={:in_progress} /></:glyph>
  <:action navigate="/tickets/new?status=in_progress" label="New ticket in In progress">
    <.icon name="plus" />
  </:action>
</.group_band>
<.list_row
  identifier={@ticket.identifier}
  title={@ticket.title}
  parent={@ticket.parent}
  navigate={@ticket.href}
>
  <:leading>
    <.priority_glyph priority={@ticket.priority} />
    <.status_glyph status={@ticket.status} />
  </:leading>
  <:meta>
    <.badge size="sm">{@ticket.tag}</.badge>
    <.progress shape="ring" completed={@ticket.completed} scope={@ticket.scope} size="sm" />
  </:meta>
  <:trailing>{@ticket.date}</:trailing>
</.list_row>

Grouped list with toolbar

When to use: A ticket index grouped into collapsible bands, with All/Active/Backlog on the left and Filter/Display icon buttons on the right.

Origin: flicker #1404 tickets list.

<.card flush>
  <:header>
    <.tabs_list
      id="tickets-scope"
      variant="segmented"
      size="sm"
      active_tab={@scope}
      aria-label="View"
    >
      <:tab name="all" patch={@paths.all}>All</:tab>
      <:tab name="active" patch={@paths.active}>Active</:tab>
      <:tab name="backlog" patch={@paths.backlog}>Backlog</:tab>
    </.tabs_list>
  </:header>
  <:actions>
    <.button size="icon" variant="ghost" label="Filter" kbd="F">
      <.icon name="funnel" />
    </.button>
    <.button size="icon" variant="ghost" label="Display" kbd="D">
      <.icon name="adjustments-horizontal" />
    </.button>
  </:actions>
  <.scroll_area label="Tickets" data-lantern-list-nav>
    <.group_band
      name="In progress"
      count={2}
      group="tickets:in_progress"
      data-lantern-persist="tickets:in_progress"
    >
      <:glyph><.status_glyph status={:in_progress} /></:glyph>
      <:action navigate={@paths.new_in_progress} label="New ticket in In progress">
        <.icon name="plus" />
      </:action>
    </.group_band>
    <.list_row
      :for={ticket <- @tickets}
      group="tickets:in_progress"
      identifier={ticket.identifier}
      title={ticket.title}
      parent={ticket.parent}
      navigate={ticket.href}
      selected={ticket.selected}
      data-lantern-list-item
    >
      <:leading><.status_glyph status={ticket.status} /></:leading>
      <:meta>
        <.badge size="sm">{ticket.tag}</.badge>
      </:meta>
      <:trailing>{ticket.date}</:trailing>
    </.list_row>
    <.group_band
      name="Done"
      count={40}
      group="tickets:done"
      collapsed
      data-lantern-persist="tickets:done"
    >
      <:glyph><.status_glyph status={:done} /></:glyph>
    </.group_band>
  </.scroll_area>
</.card>

Record page with inspector rail

When to use: A record page: breadcrumb actions hold the panel toggle, title plus a long body (LiveCode in flicker; the description card is the host slot), and a collapsible side panel of property rows.

Origin: flicker #1404 ticket show.

<.breadcrumb_bar id="ticket-crumb">
  <.breadcrumb home="/" items={@crumbs} />
  <:actions label="Toggle panel">
    <.side_panel_toggle
      id="ticket-panel-toggle"
      panel_id="ticket-panel"
      panel_key="ticket"
      open={@panel_open}
      kbd="]"
    />
  </:actions>
</.breadcrumb_bar>
<.page_header title={@ticket.title} description={@ticket.identifier} />
<.card title="Description">
  {@ticket.body}
</.card>
<.side_panel id="ticket-panel" open={@panel_open} aria-label="Ticket properties">
  <.inspector aria-label="Ticket">
    <.inspector_section title="Properties">
      <.description_list layout="dense">
        <:item label="Status">{@ticket.status}</:item>
        <:item label="Priority">{@ticket.priority}</:item>
        <:item label="Tags">
          <.badge size="sm">{@ticket.tag}</.badge>
        </:item>
      </.description_list>
    </.inspector_section>
  </.inspector>
</.side_panel>

Three-column inbox

When to use: Inbox · reading pane · properties. List on the left, selected record in the middle, inspector on the right.

Origin: flicker #1407 suggestions inbox.

<.card_grid>
  <.card flush title={"Inbox · #{@inbox_count}"}>
    <:actions>
      <.button size="icon" variant="ghost" label="Filter" kbd="F">
        <.icon name="funnel" />
      </.button>
    </:actions>
    <.scroll_area label="Inbox" data-lantern-list-nav>
      <.list_row
        :for={item <- @inbox_items}
        identifier={item.identifier}
        title={item.title}
        navigate={item.href}
        selected={item.selected}
        data-lantern-list-item
      >
        <:leading><.status_glyph status={item.status} /></:leading>
        <:meta>
          <.badge size="sm">{item.kind}</.badge>
        </:meta>
      </.list_row>
    </.scroll_area>
  </.card>
  <.card title={@selected.title} description={@selected.meta}>
    {@selected.body}
  </.card>
  <.card flush title="Properties">
    <.inspector aria-label="Suggestion">
      <.inspector_section title="Properties">
        <.description_list layout="dense">
          <:item label="Status">{@selected.status}</:item>
          <:item label="Source">{@selected.source}</:item>
        </.description_list>
      </.inspector_section>
    </.inspector>
  </.card>
</.card_grid>

Project overview

When to use: Hub header, a progress ring with a stats strip, then grouped children. Do not invent a custom overview grid.

Origin: flicker #1408 project hub.

<.page_header title={@project.name} description={@project.summary}>
  <:actions>
    <.progress
      shape="ring"
      completed={@project.completed}
      scope={@project.scope}
      label="Progress"
    >
      {@project.completed} / {@project.scope}
    </.progress>
  </:actions>
</.page_header>
<.stat_grid>
  <:stat label="Apps" value={@stats.apps} />
  <:stat label="Databases" value={@stats.databases} />
  <:stat label="Environments" value={@stats.environments} />
</.stat_grid>
<.card flush title="Tickets">
  <.group_band name="In progress" count={1}>
    <:glyph><.status_glyph status={:in_progress} /></:glyph>
  </.group_band>
  <.list_row
    :for={child <- @children}
    identifier={child.identifier}
    title={child.title}
    navigate={child.href}
  >
    <:leading><.status_glyph status={child.status} /></:leading>
    <:trailing>{child.date}</:trailing>
  </.list_row>
</.card>

Icon toolbar with kbd hints

When to use: Icon-only actions with an accessible label and a keyboard hint in the tooltip. Used on the inbox thread chrome.

Origin: flicker #1407 suggestions inbox.

<.card>
  <:actions>
    <.button size="icon" label="Promote to ticket" kbd="P" variant="solid">
      <.icon name="arrow-up-tray" />
    </.button>
    <.button size="icon" label="Snooze" kbd="H" variant="ghost">
      <.icon name="clock" />
    </.button>
    <.button size="icon" label="Mark resolved" kbd="E" variant="ghost">
      <.icon name="check" />
    </.button>
    <.button size="icon" label="More" variant="ghost">
      <.icon name="ellipsis-horizontal" />
    </.button>
  </:actions>
  Inbox thread
</.card>

Capacity strip

When to use: N slots · busy · waiting · cost as lantern stats and badges. Do not hand-roll a metric row.

Origin: flicker #1408 project hub (factory capacity on the hub).

<.stat_grid>
  <:stat label="Slots" value={@capacity.slots} />
  <:stat label="Busy" value={@capacity.busy} />
  <:stat label="Waiting" value={@capacity.waiting} />
  <:stat label="Cost" value={@capacity.cost} />
</.stat_grid>
<.badge color="warning" size="sm">{@capacity.waiting} waiting</.badge>
<.badge color="success" size="sm">{@capacity.busy} busy</.badge>