컴포넌트
읽기용 본문
블로그, 약관, 문서처럼 긴 글의 조판이에요. Markdown이 내놓은 그대로를 받아요.
- #arc
- #component
- #prose
- #typography
- #markdown
- #article
- #reading
1280px
설치
이 명령 한 줄이면 필요한 파일과 패키지가 함께 설치돼요.
터미널터미널터미널bash
npx shadcn@latest add https://dsui.vercel.app/r/prose.json파일
components/arc/prose/prose.module.csscomponents/arc/prose/prose.tsx
함께 오는 항목
prose.module.cssprose.module.cssprose.module.csscss
/* Element selectors hang off .prose, so Markdown output needs no classes of its own. Arc tokens only, so light and dark follow the theme. */
.prose { color: var(--foreground); font-size: var(--text-base); letter-spacing: var(--tracking-body); line-height: 1.8; word-break: keep-all; overflow-wrap: anywhere; }
/* A touch larger once there is room, which is where the long reads happen. */
@media (min-width: 768px) { .prose { font-size: 1.0625rem; } }
.default { max-width: 46rem; }
.wide { max-width: 60rem; }
.full { max-width: none; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose p, .prose ul, .prose ol, .prose blockquote, .prose pre, .prose figure, .prose table { margin: 0 0 var(--space-5); }
/* Headings keep enough room above to read as the start of something, and scroll-margin so an anchor link does not hide them under a sticky header. */
.prose h2, .prose h3, .prose h4 { color: var(--foreground); font-family: var(--font-display); font-weight: 500; letter-spacing: var(--tracking-display); text-wrap: balance; scroll-margin-top: var(--space-24); }
.prose h2 { margin: var(--space-12) 0 var(--space-4); font-size: var(--text-2xl); line-height: 1.3; }
.prose h3 { margin: var(--space-8) 0 var(--space-3); font-size: var(--text-xl); line-height: 1.35; }
.prose h4 { margin: var(--space-6) 0 var(--space-2); font-size: var(--text-lg); line-height: 1.4; }
.prose ul, .prose ol { padding-left: 1.4em; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li { margin: var(--space-2) 0; padding-left: .2em; }
.prose li::marker { color: var(--text-muted); }
/* The underline is the link, so it stays on at rest and only firms up under the pointer. */
.prose a { border-radius: 2px; color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in oklab, var(--accent) 40%, transparent); text-decoration-thickness: 1px; text-underline-offset: 4px; transition: text-decoration-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
@media (hover: hover) and (pointer: fine) { .prose a:hover { text-decoration-color: currentColor; } }
.prose a:active { color: var(--accent-strong); }
.prose strong { font-weight: 600; }
.prose hr { margin: var(--space-10) 0; border: 0; border-top: 1px solid var(--border); }
.prose code { border: 1px solid var(--border-subtle); border-radius: 6px; padding: .1em .35em; background: var(--surface-muted); font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; font-size: .88em; }
.prose pre { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-panel); padding: var(--space-5); background: var(--surface-sunken); font-size: var(--text-sm); line-height: 1.7; }
.prose pre code { border: 0; padding: 0; background: none; font-size: inherit; }
.prose blockquote { border-left: 3px solid var(--accent); border-radius: 0 var(--radius-control) var(--radius-control) 0; padding: var(--space-3) var(--space-5); background: var(--surface-muted); color: var(--text-secondary); }
.prose blockquote > :last-child { margin-bottom: 0; }
/* A wide table scrolls inside its own box instead of pushing the page sideways. */
.prose table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; font-size: var(--text-sm); }
.prose th, .prose td { border-bottom: 1px solid var(--border); padding: var(--space-2) var(--space-3); text-align: left; }
.prose th { color: var(--text-secondary); font-weight: 500; }
.prose figure img { max-width: 100%; height: auto; border-radius: var(--radius-panel); }
.prose figcaption { margin-top: var(--space-2); color: var(--text-secondary); font-size: var(--text-sm); }
@media (prefers-reduced-motion: reduce) { .prose a { transition: none; } }
prose.tsxprose.tsxprose.tsxtsx
import type { ElementType, HTMLAttributes, ReactNode } from "react";
import styles from "./prose.module.css";
export interface ProseProps extends HTMLAttributes<HTMLElement> {
/** The element to render. Default `div`; use `article` for a post and `section` for a part of one. */
as?: ElementType;
/** Reading width. `default` is about 46rem; `wide` lets tables and code breathe; `full` leaves the width to you. */
width?: "default" | "wide" | "full";
children: ReactNode;
}
/**
* Reading typography for long text: a post, a legal page, a document. The styles hang off element selectors inside
* this one class, so Markdown output needs no classes of its own and nothing has to be rewritten when the content
* changes. Keep the measure near 46rem — past about 80 characters a line, the eye loses its place on the way back.
*/
export function Prose({ as, width = "default", className, children, ...props }: ProseProps) {
const Tag = (as ?? "div") as ElementType;
return (
<Tag {...props} className={[styles.prose, styles[width], className].filter(Boolean).join(" ")}>
{children}
</Tag>
);
}
Prose.displayName = "Prose";
export default Prose;