Reticle@etched/reticle 0.0.0

Layer card

A card. Without a header row it is one surface. With title or actions it has two layers: the header row on the outer layer and the content on the inner one. Native <div> attributes and ref pass through. Never put a LayerCard inside another one (Kumo design rule): put a heading above the card, or rows inside it.

Kumo source

Next steps

Book the onsite loop for Ana Ruiz with Sam Lee.

import { Button } from "@etched/reticle/button";
import { Text } from "@etched/reticle/text";
import { LayerCard } from "@etched/reticle/layer-card";

export function Preview() {
  return (
    <LayerCard title="Next steps" actions={<Button size="sm">Edit</Button>} style={{ inlineSize: "16rem" }}>
      <Text>Book the onsite loop for Ana Ruiz with Sam Lee.</Text>
    </LayerCard>
  );
}

Installation

import { LayerCard, LayerCardPrimary, LayerCardSecondary } from "@etched/reticle/layer-card";

Examples

Details card

title and actions add a header row on the outer layer. The details sit on the inner layer.

Details

Role
ASIC design engineer
Stage
Interviewing
Owner
Sam Lee
Source
Referral from Kai Ito
import { type ReactNode } from "react";
import { Button } from "@etched/reticle/button";
import { Pill } from "@etched/reticle/pill";
import { Text } from "@etched/reticle/text";
import { LayerCard } from "@etched/reticle/layer-card";

/** One detail: the term in a fixed column, like Orbit's candidate details. */
function Detail({ term, children }: { term: string; children: ReactNode }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "6rem minmax(0, 1fr)", gap: "var(--et-space-4)" }}>
      <Text as="dt" variant="secondary">
        {term}
      </Text>
      <Text as="dd">{children}</Text>
    </div>
  );
}

export function DetailsCard() {
  return (
    <LayerCard title="Details" actions={<Button size="sm">Edit</Button>} style={{ maxInlineSize: "26rem" }}>
      <dl style={{ display: "grid", gap: "var(--et-space-2)", margin: 0 }}>
        <Detail term="Role">ASIC design engineer</Detail>
        <Detail term="Stage">
          <Pill tone="cyan">Interviewing</Pill>
        </Detail>
        <Detail term="Owner">Sam Lee</Detail>
        <Detail term="Source">Referral from Kai Ito</Detail>
      </dl>
    </LayerCard>
  );
}

Coverage cards are buttons

render={<button type="button" />} with onClick makes the whole card one button; Enter and Space press it. Here each card opens its list.

Pick a card to see its list.

import { useState } from "react";
import { Text } from "@etched/reticle/text";
import { LayerCard } from "@etched/reticle/layer-card";

const coverage = [
  { label: "Profiles", value: "12,480", detail: "From LinkedIn and Ashby" },
  { label: "Warm paths", value: "3,214", detail: "Through 412 employees" },
  { label: "Needs review", value: "86", detail: "Referrals with no owner" },
];

export function CoverageCards() {
  const [list, setList] = useState<string>();

  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)", maxInlineSize: "40rem" }}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: "var(--et-space-4)" }}>
        {coverage.map(({ label, value, detail }) => (
          <LayerCard key={label} render={<button type="button" />} onClick={() => setList(label)}>
            <Text as="span" variant="secondary">
              {label}
            </Text>
            <Text as="span" variant="heading1">
              {value}
            </Text>
            <Text as="span" size="sm" variant="secondary">
              {detail}
            </Text>
          </LayerCard>
        ))}
      </div>
      <Text aria-live="polite">{list ? `Showing ${list.toLowerCase()}` : "Pick a card to see its list."}</Text>
    </div>
  );
}

Setting rows

The setting's name in the header row, its switch at the end, and the help text below.

Swarm LinkedIn sending

When off, Swarm saves messages as drafts instead of sending them on LinkedIn.

Referral reminders

Reminds owners about referrals with no reply after three days.

import { Switch } from "@etched/reticle/switch";
import { Text } from "@etched/reticle/text";
import { LayerCard } from "@etched/reticle/layer-card";

const settings = [
  { name: "Swarm LinkedIn sending", help: "When off, Swarm saves messages as drafts instead of sending them on LinkedIn.", on: true },
  { name: "Referral reminders", help: "Reminds owners about referrals with no reply after three days.", on: false },
];

export function SettingRows() {
  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)", maxInlineSize: "36rem" }}>
      {settings.map(({ name, help, on }) => (
        <LayerCard key={name} title={name} actions={<Switch aria-label={name} defaultChecked={on} />}>
          <Text variant="secondary">{help}</Text>
        </LayerCard>
      ))}
    </div>
  );
}

Padding sizes

size="sm" (12 by 16 px) for side panels and dense lists; base (16 by 20 px) for the rest. Without a header row, a card is one surface.

Small: 12 by 16 px.

Base: 16 by 20 px.

Reminders

Small, with a header row.

Reminders

Base, with a header row.

import { Text } from "@etched/reticle/text";
import { LayerCard } from "@etched/reticle/layer-card";

export function PaddingSizes() {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(14rem, 1fr))", gap: "var(--et-space-4)", maxInlineSize: "36rem" }}>
      <LayerCard size="sm">
        <Text>Small: 12 by 16 px.</Text>
      </LayerCard>
      <LayerCard>
        <Text>Base: 16 by 20 px.</Text>
      </LayerCard>
      <LayerCard size="sm" title="Reminders">
        <Text>Small, with a header row.</Text>
      </LayerCard>
      <LayerCard title="Reminders">
        <Text>Base, with a header row.</Text>
      </LayerCard>
    </div>
  );
}

Layers by hand

LayerCardSecondary and LayerCardPrimary are the two layers that title and actions build. Use them for a header row that is not a title.

Last run

Run 1284 passed at 71 °C.

import { Text } from "@etched/reticle/text";
import { LayerCard, LayerCardPrimary, LayerCardSecondary } from "@etched/reticle/layer-card";

export function LayersByHand() {
  return (
    <LayerCard style={{ maxInlineSize: "26rem" }}>
      <LayerCardSecondary>Last run</LayerCardSecondary>
      <LayerCardPrimary>
        <Text>Run 1284 passed at 71 °C.</Text>
      </LayerCardPrimary>
    </LayerCard>
  );
}

Table card

size="none" lets a table fill the card; the header row keeps its padding.

Open roles

RoleCandidates
ASIC design engineer14
Firmware engineer9
import { Button } from "@etched/reticle/button";
import { Table } from "@etched/reticle/table";
import { LayerCard } from "@etched/reticle/layer-card";

export function TableCard() {
  return (
    <LayerCard size="none" title="Open roles" actions={<Button size="sm">Add role</Button>}>
      <Table aria-label="Open roles" density="dense">
        <thead>
          <tr>
            <th>Role</th>
            <th style={{ inlineSize: "6rem" }}>Candidates</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>ASIC design engineer</td>
            <td>14</td>
          </tr>
          <tr>
            <td>Firmware engineer</td>
            <td>9</td>
          </tr>
        </tbody>
      </Table>
    </LayerCard>
  );
}

API reference

PropTypeDefaultDescription
size"sm" | "base" | "none""base"Padding of the content, and of the header row's sides.
titleReactNode–Adds a header row with this title above the content.
titleAs"h2" | "h3" | "h4" | "h5" | "h6""h2"The title's element, for its place in the outline.
actionsReactNode–Buttons or a switch at the end of the header row, for example <Button size="sm">Edit</Button>.
renderReactElement<{ className?: string }>–Makes the whole card one link or one button, with a hover fill and the focus ring: render={<a href="/roles/hardware" />}, a router link, or render={<button type="button" />} with onClick. Such a card holds no buttons, links or fields (and no actions): a link or a button cannot hold another control.