컴포넌트
List
콘텐츠를 목록으로 표시합니다.
설치
pnpm dlx ply-ui add list badge icon공통 설치 설정을 먼저 적용합니다.
사용법
항목마다 Item과 Content를 구성합니다. 추가 설명은 Description에 넣고, 클릭할 행에만 Action을 사용합니다.
구성
| 구성 요소 | 역할 |
|---|---|
Root | 목록 전체입니다. variant로 테두리와 구분선을 지정합니다. |
Item | 하나의 목록 행입니다. |
Content | 행의 본문과 leading, trailing 영역을 배치합니다. |
Title | 행의 주된 정보입니다. |
Description | 행의 보조 정보입니다. 필요할 때만 넣습니다. |
Action | 클릭할 수 있는 행에 사용합니다. 본문을 감싸는 버튼입니다. |
Standard
Root variant="standard"는 기본 목록입니다.
Boxed
Root variant="boxed"는 테두리로 목록을 감쌉니다.
Inset
Root variant="inset"는 구분선 시작점을 안쪽으로 들여 표시합니다. 앞쪽 아이콘이 있는 행에 사용할 수 있습니다.
작업이 있는 행
Action은 버튼입니다. onClick에 동작을 연결하고 내부에는 다른 버튼이나 링크를 넣지 않습니다.
속성
이 프로젝트에서 추가하거나 제한한 속성입니다.
Root
속성
타입
Content
속성
타입
구현 코드
components/ui/list.tsx
"use client";
import type { ComponentProps, ReactNode } from "react";
import { Button } from "@base-ui/react/button";
import { cx, withClassName } from "../../lib/cx";
import "./list.css";
export type ListRootProps = ComponentProps<"ul"> & {
/** 목록 테두리와 구분선입니다. @defaultValue "standard" */
variant?: "standard" | "boxed" | "inset";
};
function ListRoot({
variant = "standard",
className,
...props
}: ListRootProps) {
return (
<ul
{...props}
data-variant={variant}
className={cx("rbx-list", className)}
/>
);
}
function ListItem({ className, ...props }: ComponentProps<"li">) {
return <li {...props} className={cx("rbx-list-item", className)} />;
}
/** 상호작용이 있는 행만 Button을 씁니다. 버튼 안에 다른 버튼을 넣지 마세요. */
function ListAction({ className, ...props }: ComponentProps<typeof Button>) {
return (
<Button
{...props}
className={withClassName("rbx-list-action", className)}
/>
);
}
export type ListContentProps = ComponentProps<"div"> & {
/** 내용 앞에 표시할 요소입니다. */
leading?: ReactNode;
/** 내용 뒤에 표시할 요소입니다. */
trailing?: ReactNode;
};
function ListContent({
leading,
trailing,
children,
className,
...props
}: ListContentProps) {
return (
<div {...props} className={cx("rbx-list-content", className)}>
{leading && <span className="rbx-list-leading">{leading}</span>}
<div className="rbx-list-body">{children}</div>
{trailing && <span className="rbx-list-trailing">{trailing}</span>}
</div>
);
}
function ListTitle({ className, ...props }: ComponentProps<"div">) {
return <div {...props} className={cx("rbx-list-title", className)} />;
}
function ListDescription({ className, ...props }: ComponentProps<"div">) {
return <div {...props} className={cx("rbx-list-description", className)} />;
}
export const List = {
Root: ListRoot,
Item: ListItem,
Action: ListAction,
Content: ListContent,
Title: ListTitle,
Description: ListDescription,
};
components/ui/list.css
/* Security list: outer 12px horizontal inset, inner 16px vertical inset, 12px gaps. */
.rbx-list {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
.rbx-list-item {
border-bottom: 1px solid var(--rbx-color-stroke-default);
}
.rbx-list-action {
display: block;
width: 100%;
background: transparent;
color: inherit;
border: 0;
padding: 0;
text-align: left;
cursor: pointer;
}
.rbx-list-action:hover {
background: var(--rbx-color-state-hover);
}
.rbx-list-action:active {
background: var(--rbx-color-state-press);
}
.rbx-list-content {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 12px;
}
.rbx-list-body {
flex: 1;
min-width: 0;
}
.rbx-list-leading,
.rbx-list-trailing {
display: inline-flex;
flex-shrink: 0;
}
.rbx-list-title {
color: var(--rbx-color-content-emphasis);
font: var(--rbx-typography-title-medium-font);
}
.rbx-list-description {
margin-top: 4px;
color: var(--rbx-color-content-default);
font: var(--rbx-typography-body-medium-font);
}
.rbx-list[data-variant="boxed"] {
border: 1px solid var(--rbx-color-stroke-default);
border-radius: 16px;
overflow: hidden;
}
.rbx-list[data-variant="boxed"] .rbx-list-content {
padding-inline: 20px;
}
.rbx-list[data-variant="boxed"] .rbx-list-item:last-child {
border-bottom: 0;
}
.rbx-list[data-variant="inset"] .rbx-list-item {
border-bottom: 0;
position: relative;
}
.rbx-list[data-variant="inset"] .rbx-list-item:not(:last-child)::after {
content: "";
display: block;
margin-inline: 56px 12px;
border-bottom: 1px solid var(--rbx-color-stroke-default);
}