186 lines
7.1 KiB
React
186 lines
7.1 KiB
React
import { useState } from "react";
|
|
|
|
import { PageHeader } from "../components/page-header.jsx";
|
|
import { Alert } from "../components/ui/alert.jsx";
|
|
import { Badge } from "../components/ui/badge.jsx";
|
|
import { Button } from "../components/ui/button.jsx";
|
|
import { Card } from "../components/ui/card.jsx";
|
|
import { Dialog } from "../components/ui/dialog.jsx";
|
|
import { TextField } from "../components/ui/text-field.jsx";
|
|
|
|
const COLOR_TOKENS = Object.freeze([
|
|
["Surface", "--color-surface"],
|
|
["Muted surface", "--color-surface-muted"],
|
|
["Content", "--color-content"],
|
|
["Muted content", "--color-content-muted"],
|
|
["Action", "--color-action"],
|
|
["Danger", "--color-danger"],
|
|
["Focus", "--color-focus"],
|
|
]);
|
|
|
|
export default function UiGalleryPage() {
|
|
const [projectName, setProjectName] = useState("");
|
|
const [fieldTouched, setFieldTouched] = useState(false);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [notice, setNotice] = useState(
|
|
"구성요소를 조작하면 결과가 여기에 표시됩니다.",
|
|
);
|
|
const [alertVisible, setAlertVisible] = useState(true);
|
|
const fieldError =
|
|
fieldTouched && projectName.trim().length === 0
|
|
? "프로젝트 이름을 입력해 주세요."
|
|
: undefined;
|
|
|
|
/** @param {React.FormEvent<HTMLFormElement>} event */
|
|
function submitExample(event) {
|
|
event.preventDefault();
|
|
setFieldTouched(true);
|
|
if (projectName.trim().length === 0) {
|
|
setNotice("입력값을 확인해 주세요.");
|
|
return;
|
|
}
|
|
setNotice(`“${projectName.trim()}” 입력을 확인했습니다.`);
|
|
}
|
|
|
|
return (
|
|
<section className="ui-page">
|
|
<PageHeader
|
|
eyebrow="예제"
|
|
title="UI 구성요소"
|
|
description="제품 도메인과 독립적인 공통 컨트롤, 피드백, 표면과 디자인 토큰을 직접 조작할 수 있습니다."
|
|
/>
|
|
|
|
<section className="gallery-section" aria-labelledby="controls-title">
|
|
<header className="gallery-section__header">
|
|
<h2 id="controls-title">버튼과 입력</h2>
|
|
<p>키보드, 비활성 상태, 오류 설명을 포함한 기본 상호작용입니다.</p>
|
|
</header>
|
|
<div className="component-grid component-grid--two">
|
|
<Card title="버튼" description="의미와 위험도에 따라 변형을 선택합니다.">
|
|
<div className="button-row">
|
|
<Button onClick={() => setNotice("기본 작업을 실행했습니다.")}>
|
|
기본 작업
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setNotice("보조 작업을 실행했습니다.")}
|
|
>
|
|
보조 작업
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
onClick={() => setNotice("위험 작업 예제를 선택했습니다.")}
|
|
>
|
|
위험 작업
|
|
</Button>
|
|
<Button disabled>사용 불가</Button>
|
|
</div>
|
|
</Card>
|
|
<Card title="입력창" description="레이블과 도움말, 오류가 입력에 연결됩니다.">
|
|
<form className="example-form" noValidate onSubmit={submitExample}>
|
|
<TextField
|
|
label="프로젝트 이름"
|
|
description="새 도메인을 연결할 때 사용할 중립적인 예제입니다."
|
|
error={fieldError}
|
|
value={projectName}
|
|
required
|
|
onChange={(event) => setProjectName(event.currentTarget.value)}
|
|
/>
|
|
<Button type="submit">입력 확인</Button>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="gallery-section" aria-labelledby="feedback-title">
|
|
<header className="gallery-section__header">
|
|
<h2 id="feedback-title">피드백과 모달</h2>
|
|
<p>상태 전달은 색에만 의존하지 않으며, 모든 제어에는 이름이 있습니다.</p>
|
|
</header>
|
|
<div className="component-grid component-grid--two">
|
|
<Card title="알림과 배지" description="짧은 상태와 문맥형 피드백입니다.">
|
|
<div className="component-stack">
|
|
{alertVisible ? (
|
|
<Alert
|
|
title="설정이 저장되었습니다."
|
|
variant="success"
|
|
onDismiss={() => setAlertVisible(false)}
|
|
>
|
|
<p>운영 환경에는 실제 저장 포트를 연결하세요.</p>
|
|
</Alert>
|
|
) : (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setAlertVisible(true)}
|
|
>
|
|
알림 다시 표시
|
|
</Button>
|
|
)}
|
|
<div className="badge-row" aria-label="배지 변형">
|
|
<Badge>중립</Badge>
|
|
<Badge variant="info">정보</Badge>
|
|
<Badge variant="success">준비됨</Badge>
|
|
<Badge variant="warning">확인 필요</Badge>
|
|
<Badge variant="danger">실패</Badge>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<Card title="모달" description="배경과 키보드 Esc로 닫고 포커스를 복원합니다.">
|
|
<Button onClick={() => setDialogOpen(true)}>모달 열기</Button>
|
|
<Dialog
|
|
open={dialogOpen}
|
|
onClose={() => setDialogOpen(false)}
|
|
title="연동 확인"
|
|
description="도메인 작업을 실행하기 전 확인 화면의 기본 구조입니다."
|
|
actions={
|
|
<>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setDialogOpen(false)}
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setNotice("모달의 확인 작업을 실행했습니다.");
|
|
setDialogOpen(false);
|
|
}}
|
|
>
|
|
확인
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<p>민감한 값이나 구현 세부정보는 확인 문구에 포함하지 않습니다.</p>
|
|
</Dialog>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="gallery-section" aria-labelledby="tokens-title">
|
|
<header className="gallery-section__header">
|
|
<h2 id="tokens-title">디자인 토큰</h2>
|
|
<p>구성요소가 사용하는 의미 기반 색상과 형태 토큰입니다.</p>
|
|
</header>
|
|
<div className="token-grid">
|
|
{COLOR_TOKENS.map(([label, token]) => (
|
|
<article className="token-swatch" key={token}>
|
|
<span
|
|
className="token-swatch__color"
|
|
style={{ backgroundColor: `var(${token})` }}
|
|
aria-hidden="true"
|
|
/>
|
|
<strong>{label}</strong>
|
|
<code>{token}</code>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<output className="gallery-notice" aria-live="polite">
|
|
{notice}
|
|
</output>
|
|
</section>
|
|
);
|
|
}
|