リストカードListCardBeta

一覧の1項目を、先頭アクセサリ、本文、状態、メタ情報、選択状態付きで表示します。

プレビュー

状態とバリエーション

選択可能

onSelect を渡すとカード全体がボタンになります。

表示専用

onSelect を省略すると button ではなく表示用 div になります。

JC
中央線快速

人身事故の影響

運転見合わせ7:42 更新
10:42 → 11:14

32分・乗換0回・34.1km

最速最安
¥580

重さで面を変える

severity が変えるのは境界線と淡い背景だけで、行の意味そのものは status の文字が持ちます。色が見えなくても「停止中」と読めるので、重さは色に載せません。neutral は面を変えません。

配送ラインA

停止から18分

停止中
配送ラインB

処理待ちが積み上がっています

遅れ
配送ラインC

10:00 から保守の予定

予定あり
配送ラインD

所要時間は平常どおり

正常
配送ラインE

本日の稼働はありません

停止(予定)

プロパティ

表は横にスクロールできます
プロパティ初期値説明
titleReactNode-主要行です。
leadingReactNode-先頭アクセサリです。アイコン、アバター、路線チップなどを置きます。
descriptionReactNode-タイトル下の副次行です。
tagsReactNode-タイトル下に折り返すチップ列です。
statusReactNode-右側の状態 Badge です。色だけに依存しない文言を含めます。
metaReactNode-右側の補足値です。
trailingReactNode-末尾アクセサリです。onSelect 時は既定で chevron です。
severity"critical" | "warning" | "info" | "success" | "neutral"-重大度に応じた境界線と淡い背景です。状態の意味は status の文言で伝えます。
selectedboolean-選択中の見た目です。
onSelect() => void-渡すとカード全体が44px以上のボタンになります。

使い方

import * as React from "react";
import { Badge, LineChip, ListCard, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@gunjo/ui";

export function RouteResults() {
  const rootRef = React.useRef<HTMLDivElement | null>(null);
  const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
  const [selectedId, setSelectedId] = React.useState<"incident" | "route-1" | null>(null);
  const [open, setOpen] = React.useState(false);
  const [isLargeViewport, setIsLargeViewport] = React.useState(false);

  React.useEffect(() => {
    setPortalContainer(rootRef.current?.closest<HTMLElement>("[data-doc-component-preview-surface]") ?? rootRef.current);
  }, []);

  React.useEffect(() => {
    const media = window.matchMedia("(min-width: 1024px)");
    const update = () => setIsLargeViewport(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  const detail = selectedId === "incident"
    ? { title: "中央線快速", status: "運転見合わせ", summary: "人身事故の影響で上下線の運転を見合わせています。", variant: "destructive" as const }
    : { title: "10:42 → 11:14", status: "推奨経路", summary: "乗換なしで到着する最速・最安の経路です。", variant: "secondary" as const };

  const select = (id: "incident" | "route-1") => {
    setSelectedId(id);
    setOpen(true);
  };

  return (
    <div ref={rootRef} className="relative grid w-full max-w-2xl gap-4 overflow-hidden rounded-lg border bg-card p-4">
      <div className="grid content-start gap-3">
        <ListCard
          leading={<LineChip label="JC" color="#f15a24" />}
          title="中央線快速"
          description="人身事故の影響"
          status={<Badge variant="destructive">運転見合わせ</Badge>}
          meta="7:42 更新"
          severity="critical"
          selected={selectedId === "incident"}
          onSelect={() => select("incident")}
        />
        <ListCard
          title="10:42 → 11:14"
          description="32分・乗換0回・34.1km"
          tags={<><Badge variant="secondary">最速</Badge><Badge variant="secondary">最安</Badge></>}
          meta="¥580"
          selected={selectedId === "route-1"}
          onSelect={() => select("route-1")}
        />
      </div>
      <Sheet open={open && selectedId != null} onOpenChange={setOpen} modal={false}>
        <SheetContent
          side={isLargeViewport ? "right" : "bottom"}
          portalContainer={portalContainer}
          overlayClassName="rounded-md"
          closeLabel="閉じる"
          className={isLargeViewport ? "grid w-[300px] max-w-[calc(100%-2rem)] p-4" : "max-h-[72%] overflow-y-auto p-4"}
        >
          <SheetHeader>
            <SheetTitle>詳細</SheetTitle>
            <SheetDescription>{detail.title}</SheetDescription>
          </SheetHeader>
          <div className="grid content-start gap-3 rounded-lg border bg-card p-3">
            <p className="text-sm font-semibold text-foreground">{detail.title}</p>
            <Badge variant={detail.variant} className="w-fit">{detail.status}</Badge>
            <p className="text-sm leading-6 text-muted-foreground">{detail.summary}</p>
          </div>
        </SheetContent>
      </Sheet>
    </div>
  );
}

設計の判断

  • 押せるようにするかどうかを、既定では決めない。onSelect を渡したときだけ 44px 以上の button になり、渡さなければただの div です。資料は「カード全体をリンクにするか、部分だけにするか」を2番目の判断に挙げています。GUNJO はこれを既定で決めず、呼ぶ側の宣言にしました。
  • 状態は色だけに乗せない。severity が変えるのは全周の境界線と淡い背景だけで、状態そのものは status に渡す Badge の文字が持ちます。色を見分けられない人にも同じ情報が届くようにするためです。
  • 右側は「状態」と「補足」の2段に固定した。status(Badge)と meta(価格・時刻・件数)は右上と右下に縦に積み、meta は折り返しません。行を何十も並べたときに、値の位置が行ごとにずれないようにするためです。
    一般のカードの設計は UIXHERO の「カード」にあります。 UIXHERO: カード(Card)

使用コンポーネント