블록
로그인 패널
가운데 놓이는 로그인 패널이에요. 제공자 버튼을 세로로 쌓아요.
- #arc
- #block
- #auth
- #login
- #signin
- #panel
- #account
- #illustration
1280px
설치
이 명령 한 줄이면 필요한 파일과 패키지가 함께 설치돼요.
터미널터미널터미널bash
npx shadcn@latest add https://dsui.vercel.app/r/auth-panel.json파일
components/arc/blocks/auth-panel/auth-panel.module.csscomponents/arc/blocks/auth-panel/auth-panel.tsx
함께 오는 항목
auth-panel.module.cssauth-panel.module.cssauth-panel.module.csscss
.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.tsxauth-panel.tsxauth-panel.tsxtsx
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;
사용된 항목
- 일러스트컴포넌트
히어로, 섹션 헤더, 카드 커버에 쓰는 도형 일러스트예요. 테마가 직접 그린 그림이 없으면 베이스 그림을 테마 색으로 그려요.