Pluto Design System
Components

ActionTile

세로 stack 타일형 클릭 카드. Header / Content / Footer 3 영역으로 구성. Grid에서 가로로 여러 개 배치할 때 사용.

ActionCard 와 같은 시각 토큰(radius / variant / focus ring)을 공유하지만, 슬롯이 세로로 stack 되는 타일형. Grid 3-column 같은 화면 구성에서 카드 여러 개를 가로로 배치할 때 사용.

3 개의 영역으로 구성:

  • Header — 옵션. free-form 박스. 메타 row / NEW pill / kebab 등
  • Content — 의미 슬롯 묶음 (Leading + Title + Description). 내부 typography 와 슬롯 간 간격은 컴포넌트가 책임
  • Footer — 옵션. free-form 박스. 하단 메타 row / 액션 등

영역 간 간격은 headerGap / footerGap prop (tight / normal / wide), 영역별 정렬은 headerAlign / contentAlign / footerAlign prop 으로 따로따로 제어.

사용

import {
  ActionTile,
  ActionTileHeader,
  ActionTileContent,
  ActionTileLeading,
  ActionTileTitle,
  ActionTileDescription,
  ActionTileFooter,
} from "@fluxloop-ai/pds-ui/components/action-tile";
import { Icon } from "@fluxloop-ai/pds-ui/components/icon";
import { Badge } from "@fluxloop-ai/pds-ui/components/badge";
import { Compass } from "@fluxloop-ai/pds-icons/icons";

<ActionTile headerGap="tight" footerGap="wide" headerAlign="between" onClick={...}>
  <ActionTileHeader>
    <span>Tutorial</span>
    <Badge size="xs" color="accent" accentColor="blue">NEW</Badge>
  </ActionTileHeader>

  <ActionTileContent>
    <ActionTileLeading><Icon icon={Compass} size="lg" /></ActionTileLeading>
    <ActionTileTitle>Where to start</ActionTileTitle>
    <ActionTileDescription>Pick the first thing to look at</ActionTileDescription>
  </ActionTileContent>

  <ActionTileFooter>
    <Badge size="xs">5 min</Badge>
  </ActionTileFooter>
</ActionTile>

구조

슬롯필수역할
ActionTile필수클릭 표면. border / radius / padding / hover · focus state. 기본 <button type="button">, asChild 로 polymorphic
ActionTileHeader옵션상단 free-form row. 안에 무엇을 넣고 어떻게 분포할지는 호출부 자유. 정렬은 headerAlign
ActionTileContent필수의미 슬롯 묶음. Leading + Title + Description 을 내부에 넣는다. 가로 정렬은 contentAlign
ActionTileLeading옵션Content 안 상단 visual. Icon / 타일 wrapper / cover img 자유
ActionTileTitle필수Content 안. startIcon prop 으로 text 좌측 16px 인라인 마커
ActionTileDescription옵션Content 안. muted 텍스트
ActionTileFooter옵션하단 free-form row. 정렬은 footerAlign

기본

Content 만 사용한 가장 단순한 형태. Leading + Title + Description.

상단에 Header 영역을 추가. 보통 카테고리/메타 라벨 + 우측 NEW/알림 마커. headerAlign="between" 으로 양 끝 분포.

하단 free-form 영역. Badge 묶음 / 메타 / secondary action 등 자유.

Content 정렬

contentAlign="center" 로 Content 내부 가운데 정렬. Leading 도 함께 가운데로.

Full spec

3 영역 모두 사용. gap 토큰과 영역별 align 조합 예시.

Variant

ActionCard 와 동일한 3 종 — outlined (기본), filled, ghost.

variantresthover
outlinedborder 1px --pds-line-normal-normal + bg 투명bg --pds-fill-alternative
filledborder 없음 + bg --pds-fill-alternativebg --pds-fill-normal
ghostborder 없음 + bg 투명bg --pds-fill-alternative

카드 위에 별도 액션(예: bookmark / kebab) 올리기

ActionTile 자체가 <button> 이라, 그 안에 또 다른 <button>(IconButton 등)을 넣으면 nested button — invalid HTML 이고 click 이벤트가 예측 불가능해진다. 카드 본체 클릭과 별개로 동작하는 액션 버튼이 필요하면, ActionTile 외부에 absolute 오버레이로 띄우는 패턴을 사용한다.

import { ActionTile, ActionTileContent, ActionTileTitle, ActionTileDescription, ActionTileFooter } from "@fluxloop-ai/pds-ui/components/action-tile";
import { IconButton } from "@fluxloop-ai/pds-ui/components/icon-button";
import { Icon } from "@fluxloop-ai/pds-ui/components/icon";
import { Bookmark } from "@fluxloop-ai/pds-icons/icons";

function DiagnoseCard() {
  const [bookmarked, setBookmarked] = React.useState(false);
  return (
    <div style={{ position: "relative" }}>
      <ActionTile
        onClick={() => { /* 카드 본체 액션 */ }}
        // IconButton 자리만큼 우측 padding 확보 — 콘텐츠가 button 위까지 침범하지 않게
        className="pr-[52px]"
      >
        <ActionTileContent>
          <ActionTileTitle>Diagnose</ActionTileTitle>
          <ActionTileDescription>Surface improvements from usage logs</ActionTileDescription>
        </ActionTileContent>
        <ActionTileFooter>...</ActionTileFooter>
      </ActionTile>

      <IconButton
        aria-label={bookmarked ? "Remove bookmark" : "Bookmark"}
        variant="subtle"
        size="sm"
        style={{ position: "absolute", top: 12, right: 12 }}
        onClick={(e) => {
          e.stopPropagation(); // 카드 본체 onClick 으로 전파되지 않도록
          setBookmarked((b) => !b);
        }}
      >
        <Icon icon={Bookmark} weight={bookmarked ? "fill" : "regular"} />
      </IconButton>
    </div>
  );
}

체크리스트:

  • wrapper에 position: relative — IconButton 의 absolute 기준점
  • ActionTile 에 우측 padding 보강 — IconButton 자리만큼 (className="pr-[52px]" 등). 안 하면 긴 title / description 이 IconButton 아래로 깔림
  • e.stopPropagation() on IconButton onClick — 안 하면 카드 본체 onClick 도 같이 발화
  • aria-label on IconButton — 시각 텍스트가 없으니 스크린 리더용 라벨 필수

Footer 안 단일 액션(예: Footer 우측 정렬된 IconButton) 도 같은 이유로 nested button 이 된다 — 같은 패턴(Footer 자리에 placeholder + 카드 외부 absolute IconButton)으로 회피.

asChild

기본 <button type="button">. 링크나 다른 element 로 polymorphic 사용은 asChild.

<ActionTile asChild>
  <a href="/skills/diagnose">
    <ActionTileContent>
      <ActionTileLeading><Icon icon={Stethoscope} size="lg" /></ActionTileLeading>
      <ActionTileTitle>Diagnose this skill</ActionTileTitle>
      <ActionTileDescription>Surface improvements from logs</ActionTileDescription>
    </ActionTileContent>
  </a>
</ActionTile>

Props

ActionTile

Prop타입기본설명
variant"outlined" | "filled" | "ghost""outlined"시각 variant
padding"compact" | "normal" | "spacious""normal"카드 외곽 padding. compact = 18/20/20, normal = 26/28/24, spacious = 36/38/28 (top/bottom/horizontal)
headerGap"tight" | "normal" | "wide""normal"Header ↔ Content 사이 간격. Header 가 없으면 무시. tight = 4px, normal = 8px, wide = 12px
footerGap"tight" | "normal" | "wide""normal"Content ↔ Footer 사이 간격. Footer 가 없으면 무시. tight = 4px, normal = 8px, wide = 12px
headerAlign"start" | "center" | "end" | "between""start"Header 영역 내부 정렬 (row main-axis). between 은 양 끝 분포
contentAlign"start" | "center" | "end" | "between""start"Content 영역 내부 정렬. start/center/end 는 가로 정렬 + text-align, between 은 column 분포 (Content 가 카드 세로 공간을 채울 때 의미)
footerAlign"start" | "center" | "end" | "between""start"Footer 영역 내부 정렬 (row main-axis)
asChildbooleanfalsetrue 면 자식 element 가 클릭 표면이 됨
disabledbooleannative button disabled. opacity 60% + cursor not-allowed
onClick(e) => void
기타React.ButtonHTMLAttributes<HTMLButtonElement>

ActionTileTitle

Prop타입기본설명
startIconPhosphorIcon | ReactNodetitle text 좌측 inline 요소. 16px 정사각. 컴포넌트 함수면 <Icon size="sm"> 으로 자동 wrap, ReactNode 면 그대로 렌더

React.HTMLAttributes<HTMLDivElement> 그대로. Header / Footer 는 default 로 flex flex-row items-center gap-[6px] w-full — 정렬은 ActionTile 의 align prop 으로 제어. 안의 내용 자유.

Tokens

항목
radius16px
padding 토큰compact: 18/20/20 · normal: 26/28/24 · spacious: 36/38/28 (top/bottom/horizontal)
Leading ↔ Title 간격8px
Title ↔ Description 간격2px
Header / Footer 내부 element 간 default gap6px
headerGap / footerGap 토큰tight: 4px / normal: 8px / wide: 12px
w-full (부모 grid/flex 가 결정)

ActionCard 와의 선택 가이드

케이스사용
리스트로 세로로 쌓는 row 카드 — permission row, 단일 trigger 우측 (Check/Switch)ActionCard
Grid 로 가로 배치, 폭 좁고 세로로 긴 타일, Header / Footer 자유 영역 필요ActionTile