feat: 评论按 ThreadParentID 嵌套树展示

修复 html/template 中 {{template}} 重置 $ 导致渲染失败,页面字段随 commentItemData 下传。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 04:28:44 +08:00
parent f0358e0f71
commit 7f048bd9b5
8 changed files with 310 additions and 148 deletions

View File

@@ -99,7 +99,7 @@
## F. 评论
- [x] 按帖拉取评论列表(楼层、引用目标)— SSR 帖详情;扁平列表 + `reply_to` 展示(嵌套树 UI 未做)
- [x] 按帖拉取评论列表(楼层、引用目标)— SSR 帖详情;`ThreadParentID` 嵌套树 + 全局 `#floor` 缩进
- [x] 发表评论(登录);支持 `reply_to`、私密评论 — `POST /post/:id/comments`
- [ ] 游客评论(公开接口可写,字段 guest_*
- [x] 编辑评论(时限);删除评论 — SSR 帖详情入口 + `/comments/:cid/edit|delete`;作者软删进回收站

View File

@@ -151,7 +151,7 @@
| 投票卡 | 选选项提交;显示百分比;作者可结束 — SSR `/post/:id/poll/vote|close` |
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 — SSR `/post/:id/bounty/award|refund` |
| 抽奖卡 | 显示参与人数;开奖;中奖名单 — SSR `/post/:id/lottery/draw` |
| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报编辑(时限)、删除、回复/@ 通知 — SSR;嵌套树 UI 未做 |
| 评论 | 楼层列表、`reply_to` 引用、嵌套树(`ThreadParentID` + 缩进,全局 `#floor`)、私密/赞/举报/编辑/删除、回复/@ 通知 — SSR |
| 修订 | 列表 + 单条快照 — SSR `/post/:id/revisions`(作者/管理员;无 diff |
| 图片 | Lightbox 查看 |

View File

@@ -2,8 +2,7 @@
> **读者**:产品方 / 实现 AI
> **分支**`rebuild/gitea-ssr``main` = SPA 对照)
> **验收清单** [02-features.md](02-features.md)
> **架构** [08-gitea-ssr-architecture.md](08-gitea-ssr-architecture.md)
> **验收清单** [02-features.md](02-features.md)
---
@@ -11,15 +10,15 @@
| 项 | 值 |
|----|-----|
| **上一刀** | 完整 Limits + 伪静态 Admin / 规范 URL |
| **下一刀** | **后置或体验**需点名OIDC、S3 热切换、编辑器、嵌套评论、Gitea…) |
| **上一刀** | 嵌套评论树(全局楼层 + 缩进) |
| **下一刀** | 需点名OIDC / S3 / 编辑器 / Gitea / 主题…) |
| **工作区** | 应干净 |
---
## 一句话状态
核心论坛、Admin、§H、品牌图、发评限流、**完整 Limits伪静态**已齐。余量多为后置产品化与体验打磨。
核心论坛、Admin、Limits/伪静态、**嵌套评论**已齐。余量多为后置与体验打磨。
---
@@ -27,22 +26,16 @@
| 域 | 状态 |
|----|------|
| Admin users / badges / media | 已迁 |
| §H 等级 / 徽章 / 防刷 | 已迁 |
| 品牌 Logo/Favicon/OG + 发评限流 | 已迁 |
| 内容 Limits + 伪静态 | 已迁 |
| 五种帖 + Admin P1 + §H | 已迁 |
| Limits / 伪静态 / 品牌图 | 已迁 |
| 评论嵌套树 UI | 已迁 |
---
## 未完成
### P3 体验
- 主题、侧栏折叠、虚拟滚动、TipTap、游客评论、嵌套树、修订 diff
### 后置
- OIDC Admin CRUD、S3 热切换 UI、Gitea `/projects`、WebP 产品化
- P3主题、TipTip、游客评论、修订 diff
- 后置OIDC、S3 热切换、Gitea `/projects`
---

View File

@@ -412,10 +412,28 @@ body.j13-body {
}
.j13-comments { margin-top: 2rem; }
.j13-comment-list { list-style: none; margin: 0; padding: 0; }
.j13-comment-list--nested {
margin: 0.5rem 0 0;
padding: 0 0 0 0.85rem;
border-left: 2px solid var(--j13-border);
}
.j13-comment {
padding: 0.85rem 0;
border-bottom: 1px solid var(--j13-border);
}
.j13-comment-list--nested > .j13-comment {
border-bottom-color: transparent;
padding-left: 0.65rem;
}
.j13-comment--depth-0 { }
.j13-comment--depth-1,
.j13-comment--depth-2,
.j13-comment--depth-3,
.j13-comment--depth-4,
.j13-comment--depth-5,
.j13-comment--depth-6 {
background: transparent;
}
.j13-comment__meta {
display: flex;
flex-wrap: wrap;
@@ -425,16 +443,26 @@ body.j13-body {
margin-bottom: 0.35rem;
align-items: center;
}
.j13-comment__floor { font-weight: 600; color: var(--j13-fg, inherit); }
.j13-comment__reply-to a { color: var(--j13-accent); text-decoration: none; }
.j13-comment__actions {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 0.4rem;
flex-wrap: wrap;
}
.j13-comment-reply-hint { margin: 0 0 0.5rem; }
.j13-comment-form textarea { width: 100%; }
.j13-comment-form .j13-form__row { margin-top: 0.5rem; }
@media (max-width: 600px) {
.j13-comment-list--nested {
padding-left: 0.55rem;
}
.j13-comment-list--nested > .j13-comment {
padding-left: 0.4rem;
}
}
.j13-muted { color: var(--j13-muted); }
.j13-install fieldset {
border: 1px solid var(--j13-border);

View File

@@ -17,37 +17,37 @@ import (
// PostPageData 帖详情
type PostPageData struct {
PageChrome
PostID uint
PostHref string
PostPath string
PostTitle string
AuthorName string
AuthorID uint
AuthorHref string
BoardID uint
BoardHref string
BoardName string
Pinned bool // 展示用:全局或版内置顶
GlobalPinned bool
BoardPinned bool
Featured bool
EditLocked bool
PostTypeLabel string
CreatedLabel string
ViewCount int
LikeCount int
Liked bool
Favorited bool
BodyHTML string
CommentCount int
Comments []CommentView
CommentsLocked bool
CanEdit bool
CanViewRevisions bool
CanReportPost bool
IsAdmin bool
Status string
StatusLabel string
PostID uint
PostHref string
PostPath string
PostTitle string
AuthorName string
AuthorID uint
AuthorHref string
BoardID uint
BoardHref string
BoardName string
Pinned bool // 展示用:全局或版内置顶
GlobalPinned bool
BoardPinned bool
Featured bool
EditLocked bool
PostTypeLabel string
CreatedLabel string
ViewCount int
LikeCount int
Liked bool
Favorited bool
BodyHTML string
CommentCount int
Comments []commentItemData
CommentsLocked bool
CanEdit bool
CanViewRevisions bool
CanReportPost bool
IsAdmin bool
Status string
StatusLabel string
ShowModerationBanner bool
IsQuestion bool
QuestionResolved bool
@@ -67,8 +67,8 @@ type postBountyView struct {
}
type postLotteryWinnerView struct {
UserID uint
Name string
UserID uint
Name string
FloorHint string
}
@@ -90,16 +90,16 @@ type postPollOptionView struct {
}
type postPollView struct {
Multi bool
MaxChoices int
Closed bool
EndsLabel string
TotalVotes int
ShowResults bool
HasVoted bool
CanVote bool
CanClose bool
Options []postPollOptionView
Multi bool
MaxChoices int
Closed bool
EndsLabel string
TotalVotes int
ShowResults bool
HasVoted bool
CanVote bool
CanClose bool
Options []postPollOptionView
}
// CommentView 评论
@@ -114,6 +114,9 @@ type CommentView struct {
ReplyToID uint
ReplyToFloor int
ReplyToAuthor string
ThreadParentID uint
Depth int
Children []CommentView
LikeCount int
Liked bool
IsPrivate bool
@@ -126,6 +129,102 @@ type CommentView struct {
IsBountyAward bool
}
const commentNestMaxDepth = 6
// commentItemData 嵌套评论渲染上下文({{template}} 会重置 $,需自带页面字段)
type commentItemData struct {
CommentView
PostID uint
CSRF string
LoggedIn bool
CommentsLocked bool
PermalinkSuffix string
}
// Wrap 子评论继承同一帖上下文
func (d commentItemData) Wrap(c CommentView) commentItemData {
return commentItemData{
CommentView: c,
PostID: d.PostID,
CSRF: d.CSRF,
LoggedIn: d.LoggedIn,
CommentsLocked: d.CommentsLocked,
PermalinkSuffix: d.PermalinkSuffix,
}
}
func (d Deps) commentItemsForPage(ctx *webctx.Context, postID uint, locked bool, tree []CommentView) []commentItemData {
out := make([]commentItemData, 0, len(tree))
base := commentItemData{
PostID: postID,
CSRF: ctx.EnsureCSRF(),
LoggedIn: ctx.IsSigned(),
CommentsLocked: locked,
PermalinkSuffix: d.permalink().Suffix(),
}
for _, c := range tree {
item := base
item.CommentView = c
out = append(out, item)
}
return out
}
// buildCommentTree 按 ThreadParentID 组装嵌套树(同层保持楼层顺序)
func buildCommentTree(flat []CommentView) []CommentView {
if len(flat) == 0 {
return nil
}
nodes := make([]CommentView, len(flat))
copy(nodes, flat)
byID := make(map[uint]int, len(nodes))
for i := range nodes {
byID[nodes[i].ID] = i
nodes[i].Children = nil
}
childIdx := make(map[uint][]int, len(nodes))
roots := make([]int, 0, len(nodes))
for i := range nodes {
pid := nodes[i].ThreadParentID
if pid == 0 {
roots = append(roots, i)
continue
}
if _, ok := byID[pid]; !ok {
roots = append(roots, i)
continue
}
childIdx[pid] = append(childIdx[pid], i)
}
var attach func(i, depth int) CommentView
attach = func(i, depth int) CommentView {
n := nodes[i]
if depth > commentNestMaxDepth {
depth = commentNestMaxDepth
}
n.Depth = depth
kids := childIdx[n.ID]
if len(kids) == 0 {
n.Children = nil
return n
}
n.Children = make([]CommentView, 0, len(kids))
nextDepth := depth + 1
if nextDepth > commentNestMaxDepth {
nextDepth = commentNestMaxDepth
}
for _, ci := range kids {
n.Children = append(n.Children, attach(ci, nextDepth))
}
return n
}
out := make([]CommentView, 0, len(roots))
for _, i := range roots {
out = append(out, attach(i, 0))
}
return out
}
// PostView GET /post/:id
func (d Deps) PostView(c *gin.Context) {
ctx := d.ctx(c)
@@ -156,6 +255,7 @@ func (d Deps) PostView(c *gin.Context) {
(ctx.IsAdmin() || post.UserID == ctx.UserID())
comments, _ := d.Comment.ListByPost(uint(id), ctx.UserID(), ctx.IsAdmin(), post.UserID, nil)
commentTotal := len(comments)
cv := make([]CommentView, 0, len(comments))
for _, cm := range comments {
an := strings.TrimSpace(cm.User.Nickname)
@@ -165,7 +265,7 @@ func (d Deps) PostView(c *gin.Context) {
view := CommentView{
ID: cm.ID, Floor: cm.Floor, AuthorName: an, AuthorID: cm.UserID,
CreatedLabel: formatTime(cm.CreatedAt),
Content: cm.Content, ContentHidden: cm.ContentHidden,
Content: cm.Content, ContentHidden: cm.ContentHidden,
LikeCount: cm.LikeCount, Liked: cm.Liked, IsPrivate: cm.IsPrivate,
CanReport: ctx.IsSigned() && (cm.UserID == 0 || cm.UserID != ctx.UserID()),
CanEdit: !cm.ContentHidden && d.Comment.CanUserEditComment(&cm, ctx.UserID(), ctx.IsAdmin()),
@@ -175,6 +275,9 @@ func (d Deps) PostView(c *gin.Context) {
cm.Status == models.ContentStatusPublished && cm.UserID != post.UserID && cm.UserID > 0,
IsBountyAward: post.BountyCommentID > 0 && cm.ID == post.BountyCommentID,
}
if cm.ThreadParentID != nil {
view.ThreadParentID = *cm.ThreadParentID
}
if cm.Status == models.ContentStatusPending || cm.Status == models.ContentStatusRejected {
if ctx.IsAdmin() || (cm.UserID > 0 && cm.UserID == ctx.UserID()) {
view.StatusLabel = contentStatusLabel(cm.Status)
@@ -196,6 +299,7 @@ func (d Deps) PostView(c *gin.Context) {
}
cv = append(cv, view)
}
cv = buildCommentTree(cv)
author := strings.TrimSpace(post.User.Nickname)
if author == "" {
@@ -267,24 +371,25 @@ func (d Deps) PostView(c *gin.Context) {
ctx.HTML(http.StatusOK, "post", PostPageData{
PageChrome: chrome, PostID: post.ID,
PostHref: d.postHref(post.ID),
PostPath: url.QueryEscape(d.postHref(post.ID)),
PostHref: d.postHref(post.ID),
PostPath: url.QueryEscape(d.postHref(post.ID)),
PostTitle: post.Title, AuthorName: author, AuthorID: post.UserID,
AuthorHref: d.userHref(post.UserID),
BoardID: post.BoardID, BoardHref: d.boardHref(post.BoardID), BoardName: boardName,
BoardID: post.BoardID, BoardHref: d.boardHref(post.BoardID), BoardName: boardName,
Pinned: post.Pinned || post.BoardPinned, GlobalPinned: post.Pinned, BoardPinned: post.BoardPinned,
Featured: post.Featured, EditLocked: post.EditLocked,
PostTypeLabel: postTypeLabel(post.PostType), CreatedLabel: formatTime(post.CreatedAt),
ViewCount: post.ViewCount, LikeCount: post.LikeCount,
Liked: d.Post.IsLiked(ctx.UserID(), post.ID), Favorited: d.Post.IsFavorited(ctx.UserID(), post.ID),
BodyHTML: body, CommentCount: len(cv), Comments: cv,
CommentsLocked: post.CommentsLocked,
CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()),
CanViewRevisions: canViewPostRevisions(post, ctx.UserID(), ctx.IsAdmin()),
CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(),
IsAdmin: ctx.IsAdmin(),
Status: post.Status,
StatusLabel: statusLabel,
BodyHTML: body, CommentCount: commentTotal,
Comments: d.commentItemsForPage(ctx, post.ID, post.CommentsLocked, cv),
CommentsLocked: post.CommentsLocked,
CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()),
CanViewRevisions: canViewPostRevisions(post, ctx.UserID(), ctx.IsAdmin()),
CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(),
IsAdmin: ctx.IsAdmin(),
Status: post.Status,
StatusLabel: statusLabel,
ShowModerationBanner: showBanner,
IsQuestion: post.PostType == models.PostTypeQuestion,
QuestionResolved: post.QuestionResolved,
@@ -316,9 +421,9 @@ func mapPollView(pv *services.PollView, loggedIn, canClose bool) *postPollView {
Multi: pv.Multi, MaxChoices: pv.MaxChoices, Closed: pv.Closed,
EndsLabel: ends, TotalVotes: pv.TotalVotes,
ShowResults: showResults, HasVoted: hasVoted,
CanVote: loggedIn && !pv.Closed && !hasVoted,
CanVote: loggedIn && !pv.Closed && !hasVoted,
CanClose: canClose && !pv.Closed,
Options: opts,
Options: opts,
}
}
@@ -685,10 +790,10 @@ func (d Deps) PostLotteryDrawPost(c *gin.Context) {
type commentEditData struct {
PageChrome
PostID uint
PostID uint
CommentID uint
Floor int
Content string
Floor int
Content string
}
// CommentEditGet 编辑评论页
@@ -827,9 +932,9 @@ func commentHTMLToPlain(s string) string {
}
type postRevisionRow struct {
ID uint
EditorName string
Title string
ID uint
EditorName string
Title string
CreatedLabel string
}

View File

@@ -204,73 +204,7 @@
{{else}}
<ul class="j13-comment-list">
{{range .Comments}}
<li class="j13-comment" id="floor-{{.Floor}}" data-comment-id="{{.ID}}" data-floor="{{.Floor}}" data-author="{{.AuthorName}}">
<div class="j13-comment__meta">
<span class="j13-comment__floor">#{{.Floor}}</span>
{{if .AuthorID}}<a href="/user/{{.AuthorID}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}}
<span>{{.CreatedLabel}}</span>
{{if .StatusLabel}}<span class="j13-tag j13-tag--{{.Status}}">{{.StatusLabel}}</span>{{end}}
{{if .IsPrivate}}<span class="j13-tag">私密</span>{{end}}
{{if .IsBountyAward}}<span class="j13-tag j13-tag--resolved">已采纳</span>{{end}}
{{if .ReplyToFloor}}
<span class="j13-comment__reply-to">回复 <a href="#floor-{{.ReplyToFloor}}">#{{.ReplyToFloor}}</a>{{if .ReplyToAuthor}} {{.ReplyToAuthor}}{{end}}</span>
{{end}}
</div>
{{if .ContentHidden}}
<p class="j13-muted">(私密评论不可见)</p>
{{else}}
<div class="j13-comment__body">{{safeHTML .Content}}</div>
{{end}}
{{if $.LoggedIn}}
<div class="j13-comment__actions">
{{if .CanAwardBounty}}
<form method="post" action="/post/{{$.PostID}}/bounty/award" class="j13-inline-form">
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
<input type="hidden" name="comment_id" value="{{.ID}}"/>
<button type="submit">采纳并发放悬赏</button>
</form>
{{end}}
<form method="post" action="/post/{{$.PostID}}/comments/{{.ID}}/like" class="j13-inline-form">
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
<button type="submit" class="j13-linkbtn">{{if .Liked}}取消赞{{else}}赞{{end}} ({{.LikeCount}})</button>
</form>
{{if not $.CommentsLocked}}
<button type="button" class="j13-linkbtn j13-comment__reply-btn" data-reply-to="{{.ID}}" data-reply-floor="{{.Floor}}" data-reply-author="{{.AuthorName}}">回复</button>
{{end}}
{{if .CanEdit}}
<a class="j13-linkbtn" href="/post/{{$.PostID}}/comments/{{.ID}}/edit">编辑</a>
{{end}}
{{if .CanDelete}}
<form method="post" action="/post/{{$.PostID}}/comments/{{.ID}}/delete" class="j13-inline-form" onsubmit="return confirm('确定删除该评论?回复也会一并移入回收站。');">
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
<button type="submit" class="j13-linkbtn">删除</button>
</form>
{{end}}
{{if .CanReport}}
<details class="j13-report">
<summary class="j13-linkbtn">举报</summary>
<form method="post" action="/post/{{$.PostID}}/comments/{{.ID}}/report" class="j13-report__form">
<input type="hidden" name="_csrf" value="{{$.CSRF}}"/>
<label>原因
<select name="reason" required>
<option value="">请选择</option>
<option value="spam">垃圾广告</option>
<option value="abuse">人身攻击 / 辱骂</option>
<option value="illegal">违法违规</option>
<option value="irrelevant">内容无关 / 灌水</option>
<option value="other">其他</option>
</select>
</label>
<label>补充说明(可选)
<input name="detail" maxlength="500" placeholder="可选"/>
</label>
<button type="submit" class="j13-btn j13-btn--sm">提交举报</button>
</form>
</details>
{{end}}
</div>
{{end}}
</li>
{{template "post/comment_item" .}}
{{end}}
</ul>
{{end}}

View File

@@ -0,0 +1,74 @@
{{define "post/comment_item"}}
<li class="j13-comment j13-comment--depth-{{.Depth}}" id="floor-{{.Floor}}" data-comment-id="{{.ID}}" data-floor="{{.Floor}}" data-author="{{.AuthorName}}" data-depth="{{.Depth}}">
<div class="j13-comment__meta">
<span class="j13-comment__floor">#{{.Floor}}</span>
{{if .AuthorID}}<a href="/user/{{.AuthorID}}{{.PermalinkSuffix}}">{{.AuthorName}}</a>{{else}}<span>{{.AuthorName}}</span>{{end}}
<span>{{.CreatedLabel}}</span>
{{if .StatusLabel}}<span class="j13-tag j13-tag--{{.Status}}">{{.StatusLabel}}</span>{{end}}
{{if .IsPrivate}}<span class="j13-tag">私密</span>{{end}}
{{if .IsBountyAward}}<span class="j13-tag j13-tag--resolved">已采纳</span>{{end}}
{{if .ReplyToFloor}}
<span class="j13-comment__reply-to">回复 <a href="#floor-{{.ReplyToFloor}}">#{{.ReplyToFloor}}</a>{{if .ReplyToAuthor}} {{.ReplyToAuthor}}{{end}}</span>
{{end}}
</div>
{{if .ContentHidden}}
<p class="j13-muted">(私密评论不可见)</p>
{{else}}
<div class="j13-comment__body">{{safeHTML .Content}}</div>
{{end}}
{{if .LoggedIn}}
<div class="j13-comment__actions">
{{if .CanAwardBounty}}
<form method="post" action="/post/{{.PostID}}/bounty/award" class="j13-inline-form">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<input type="hidden" name="comment_id" value="{{.ID}}"/>
<button type="submit">采纳并发放悬赏</button>
</form>
{{end}}
<form method="post" action="/post/{{.PostID}}/comments/{{.ID}}/like" class="j13-inline-form">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<button type="submit" class="j13-linkbtn">{{if .Liked}}取消赞{{else}}赞{{end}} ({{.LikeCount}})</button>
</form>
{{if not .CommentsLocked}}
<button type="button" class="j13-linkbtn j13-comment__reply-btn" data-reply-to="{{.ID}}" data-reply-floor="{{.Floor}}" data-reply-author="{{.AuthorName}}">回复</button>
{{end}}
{{if .CanEdit}}
<a class="j13-linkbtn" href="/post/{{.PostID}}/comments/{{.ID}}/edit">编辑</a>
{{end}}
{{if .CanDelete}}
<form method="post" action="/post/{{.PostID}}/comments/{{.ID}}/delete" class="j13-inline-form" onsubmit="return confirm('确定删除该评论?回复也会一并移入回收站。');">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<button type="submit" class="j13-linkbtn">删除</button>
</form>
{{end}}
{{if .CanReport}}
<details class="j13-report">
<summary class="j13-linkbtn">举报</summary>
<form method="post" action="/post/{{.PostID}}/comments/{{.ID}}/report" class="j13-report__form">
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
<label>原因
<select name="reason" required>
<option value="">请选择</option>
<option value="spam">垃圾广告</option>
<option value="abuse">人身攻击 / 辱骂</option>
<option value="illegal">违法违规</option>
<option value="irrelevant">内容无关 / 灌水</option>
<option value="other">其他</option>
</select>
</label>
<label>补充说明(可选)
<input name="detail" maxlength="500" placeholder="可选"/>
</label>
<button type="submit" class="j13-btn j13-btn--sm">提交举报</button>
</form>
</details>
{{end}}
</div>
{{end}}
{{if .Children}}
<ul class="j13-comment-list j13-comment-list--nested">
{{range .Children}}{{template "post/comment_item" ($.Wrap .)}}{{end}}
</ul>
{{end}}
</li>
{{end}}

View File

@@ -412,10 +412,28 @@ body.j13-body {
}
.j13-comments { margin-top: 2rem; }
.j13-comment-list { list-style: none; margin: 0; padding: 0; }
.j13-comment-list--nested {
margin: 0.5rem 0 0;
padding: 0 0 0 0.85rem;
border-left: 2px solid var(--j13-border);
}
.j13-comment {
padding: 0.85rem 0;
border-bottom: 1px solid var(--j13-border);
}
.j13-comment-list--nested > .j13-comment {
border-bottom-color: transparent;
padding-left: 0.65rem;
}
.j13-comment--depth-0 { }
.j13-comment--depth-1,
.j13-comment--depth-2,
.j13-comment--depth-3,
.j13-comment--depth-4,
.j13-comment--depth-5,
.j13-comment--depth-6 {
background: transparent;
}
.j13-comment__meta {
display: flex;
flex-wrap: wrap;
@@ -425,16 +443,26 @@ body.j13-body {
margin-bottom: 0.35rem;
align-items: center;
}
.j13-comment__floor { font-weight: 600; color: var(--j13-fg, inherit); }
.j13-comment__reply-to a { color: var(--j13-accent); text-decoration: none; }
.j13-comment__actions {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 0.4rem;
flex-wrap: wrap;
}
.j13-comment-reply-hint { margin: 0 0 0.5rem; }
.j13-comment-form textarea { width: 100%; }
.j13-comment-form .j13-form__row { margin-top: 0.5rem; }
@media (max-width: 600px) {
.j13-comment-list--nested {
padding-left: 0.55rem;
}
.j13-comment-list--nested > .j13-comment {
padding-left: 0.4rem;
}
}
.j13-muted { color: var(--j13-muted); }
.j13-install fieldset {
border: 1px solid var(--j13-border);