# 댓글 줄

한 개의 코멘트예요. 줄 동작과 신고 폼이 그 줄 안에서 열려요.

- Type: component
- Theme: base
- Tags: arc, component, comment, community, thread, report, moderation
- English title: Comment row
- URL: https://dsui.vercel.app/items/comment-row

## Controls

| Key | Type | Label | Default |
| --- | --- | --- | --- |
| hidden | boolean | 숨김 | false |
| time | text | 시각 | 2026년 10월 15일 18:04 |

## Source

### comment-row.module.css

```css
.row { display: flex; gap: var(--space-3); padding: var(--space-4) var(--space-5); }
/* Dimmed rather than removed: the thread still reads, and the reason is said in words beside the name. */
.row[data-hidden] { opacity: .55; }
.avatar { display: inline-flex; width: var(--space-8); height: var(--space-8); flex: none; align-items: center; justify-content: center; overflow: hidden; border-radius: var(--radius-pill); background: var(--highlight); color: var(--highlight-foreground); font-size: var(--text-xs); font-weight: var(--label-weight); }
.main { display: flex; min-width: 0; flex: 1 1 auto; flex-direction: column; gap: var(--space-2); }
.byline { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.author { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; }
.time { color: var(--text-secondary); font-size: var(--text-xs); }
.hidden { border-radius: var(--radius-pill); padding: 0 var(--space-2); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); }
.body { color: var(--foreground); font-size: var(--text-sm); line-height: 1.7; word-break: keep-all; overflow-wrap: anywhere; }
.body > :first-child { margin-block-start: 0; }
.body > :last-child { margin-block-end: 0; }
/* Quiet at rest: a column of buttons down a thread turns reading into scanning. */
.actions { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-inline-start: calc(var(--space-3) * -1); }
/* The form opens in the row, so the comment it is about stays on screen while it is described. */
.form { border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--space-3); }

```

### comment-row.tsx

```tsx
"use client";

import type { ReactNode } from "react";
import styles from "./comment-row.module.css";

export interface CommentRowAuthor {
  name: string;
  /** An Avatar, or anything round. Initials are drawn without one. */
  avatar?: ReactNode;
}

export interface CommentRowProps {
  author: CommentRowAuthor;
  /** The machine-readable time, for the `<time>` element. */
  dateTime?: string;
  /** The time as the reader sees it. */
  time?: string;
  children: ReactNode;
  /** Buttons for this row: delete, report. Usually ghost Buttons. */
  actions?: ReactNode;
  /** A form opened from one of the actions, shown inside this row. */
  form?: ReactNode;
  /** Dims the row and says why, for a comment an admin has hidden. */
  hidden?: boolean;
  hiddenLabel?: string;
  lang?: string;
  className?: string;
}

/** Two letters at most, so the circle stays readable at 32px. */
function initialsOf(name: string): string {
  return name.trim().split(/\s+/).slice(0, 2).map(part => part[0]?.toUpperCase() ?? "").join("") || "?";
}

/**
 * One comment: who, when, what they said, and what can be done about it.
 *
 * A row's form — reporting, usually — opens **inside the row** rather than in a dialog. A dialog takes the comment
 * off the screen at the moment the reader has to describe what is wrong with it.
 *
 * The actions stay quiet (ghost) at rest. They are not what anyone came to the page for, and a column of buttons
 * down the side of a thread turns reading into scanning.
 */
export function CommentRow({
  author,
  dateTime,
  time,
  children,
  actions,
  form,
  hidden = false,
  hiddenLabel = "숨긴 코멘트예요",
  lang,
  className,
}: CommentRowProps) {
  return (
    <article className={[styles.row, className].filter(Boolean).join(" ")} data-hidden={hidden ? "" : undefined}>
      <span className={styles.avatar} aria-hidden="true">{author.avatar ?? initialsOf(author.name)}</span>
      <div className={styles.main}>
        <div className={styles.byline}>
          <span className={styles.author}>{author.name}</span>
          {time ? <time dateTime={dateTime} className={styles.time}>{time}</time> : null}
          {hidden ? <span className={styles.hidden}>{hiddenLabel}</span> : null}
        </div>
        <div className={styles.body} lang={lang}>{children}</div>
        {actions ? <div className={styles.actions}>{actions}</div> : null}
        {form ? <div className={styles.form}>{form}</div> : null}
      </div>
    </article>
  );
}

CommentRow.displayName = "CommentRow";

export default CommentRow;

```
