Component
Comment row
One comment, with its row actions and a report form that opens inside the row.
- #arc
- #component
- #comment
- #community
- #thread
- #report
- #moderation
1280px
Install
One command installs the files and packages this item needs.
TerminalTerminalTerminalbash
npx shadcn@latest add https://dsui.vercel.app/r/comment-row.jsonFiles
components/arc/comment-row/comment-row.module.csscomponents/arc/comment-row/comment-row.tsx
Registry items it pulls in
Source
View as Markdowncomment-row.module.csscomment-row.module.csscomment-row.module.csscss
.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.tsxcomment-row.tsxcomment-row.tsxtsx
"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;