# 로그인 패널

가운데 놓이는 로그인 패널이에요. 제공자 버튼을 세로로 쌓아요.

- Type: block
- Theme: base
- Tags: arc, block, auth, login, signin, panel, account, illustration
- English title: Auth panel
- URL: https://dsui.vercel.app/items/auth-panel

## Used items

- [일러스트](https://dsui.vercel.app/items/illustration): 히어로, 섹션 헤더, 카드 커버에 쓰는 도형 일러스트예요. 테마가 직접 그린 그림이 없으면 베이스 그림을 테마 색으로 그려요.

## Controls

| Key | Type | Label | Default |
| --- | --- | --- | --- |
| illustration | boolean | 일러스트 | true |
| title | text | 제목 | 로그인이 필요해요 |
| description | text | 설명 | 로그인하면 좋아요와 코멘트를 남길 수 있어요. |
| notice | text | 안내 문구 | 계속하면 이용약관과 개인정보 처리방침에 동의하는 것으로 봐요. |

## Source

### auth-panel.module.css

```css
.panel { display: flex; width: 100%; max-width: 26rem; margin-inline: auto; flex-direction: column; align-items: center; gap: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius-surface); padding: var(--space-10) var(--space-6); background: var(--surface); text-align: center; }
.icon { display: inline-flex; width: var(--space-12); height: var(--space-12); align-items: center; justify-content: center; border-radius: var(--radius-control); background: var(--highlight-subtle); color: var(--foreground); }
.title { margin: 0; color: var(--foreground); font-size: var(--text-xl); font-weight: 600; line-height: 1.4; text-wrap: balance; }
.description { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; word-break: keep-all; }
/* Stacked, not in a row: providers are not ranked, and a row makes the first one look like the right answer. */
.actions { display: flex; width: 100%; flex-direction: column; gap: var(--space-2); margin-block-start: var(--space-3); }
.actions > * { width: 100%; }
.notice { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.6; word-break: keep-all; }

/* The page-top illustration runs edge to edge across the panel top, under the rounded corners. */
.panel .art { width: calc(100% + var(--space-12)); margin: calc(var(--space-10) * -1) calc(var(--space-6) * -1) var(--space-2); }

```

### auth-panel.tsx

```tsx
import type { HTMLAttributes, ReactNode } from "react";
import { Illustration } from "../../illustration/illustration";
import styles from "./auth-panel.module.css";

export interface AuthPanelProps extends HTMLAttributes<HTMLElement> {
  title: string;
  /** Why signing in is worth it, in one line. */
  description?: ReactNode;
  /** The sign-in buttons, stacked. One per provider. */
  actions: ReactNode;
  /** Terms and privacy, under the buttons. */
  notice?: ReactNode;
  /** A mark above the title. */
  icon?: ReactNode;
  /** Draws the login page-top illustration (a lock and a key) at the top of the panel. It replaces `icon`. The theme's own art is used when it has one. */
  illustration?: boolean;
  /** The heading level. Default 1: this panel is usually the whole page. */
  headingLevel?: 1 | 2 | 3;
}

/**
 * The centred panel on a sign-in page, or wherever something asks the reader to sign in first.
 *
 * The description says what signing in gets them, not that they must. Someone who arrived here by following a link
 * has already been interrupted; the panel's job is to make the interruption worth it rather than to scold.
 *
 * The buttons stack rather than sitting in a row: providers are not ranked, and a row makes the first one look like
 * the right answer.
 */
export function AuthPanel({ className, title, description, actions, notice, icon, illustration = false, headingLevel = 1, ...props }: AuthPanelProps) {
  const Heading = `h${headingLevel}` as "h1" | "h2" | "h3";
  return (
    <section {...props} className={[styles.panel, className].filter(Boolean).join(" ")}>
      {illustration ? <Illustration name="page-login" animated className={styles.art} /> : icon ? <span className={styles.icon} aria-hidden="true">{icon}</span> : null}
      <Heading className={styles.title}>{title}</Heading>
      {description ? <p className={styles.description}>{description}</p> : null}
      <div className={styles.actions}>{actions}</div>
      {notice ? <p className={styles.notice}>{notice}</p> : null}
    </section>
  );
}

AuthPanel.displayName = "AuthPanel";

export default AuthPanel;

```
