actuel-inactuel/site/plugins/loop/frontend/src/lib/Comment.svelte
isUnknown ab7fd8b2ea
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
add kirby-loop plugin with French translations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 21:41:50 +01:00

227 lines
5.9 KiB
Svelte

<script lang="ts">
import { resolveComment, unresolveComment } from "../store/api.svelte";
import { t } from "../store/translations.svelte";
import type { Comment } from "../types";
import { panel } from "../store/ui.svelte";
import Button from "./Button.svelte";
import CommentForm from "./CommentForm.svelte";
import Reply from "./Reply.svelte";
import { formatDate } from "../composables/formatDate";
import { formatDateISO } from "../composables/formatDateISO";
import { decodeHTMLEntities } from "../composables/decodeHTMLEntities";
const {
comment,
scrollIntoView,
handleSubmit,
cancel,
}: {
comment: Comment;
scrollIntoView: (id: number) => void;
handleSubmit: (e: SubmitEvent) => void;
cancel: () => void;
} = $props();
let openReplyForm = $state(false);
let detailsOpen = $state(
comment.replies?.length > 0 && !panel.showResolvedOnly,
);
</script>
<details
id="comment-{comment.id}"
class="comment comment--{comment.status}"
class:comment--current={panel.currentCommentId === comment.id}
bind:open={detailsOpen}
>
<summary
class="comment__header"
aria-label="{t(
'ui.comment.summary.aria.label',
'Comment by',
)} {comment.author}: {decodeHTMLEntities(comment.comment)}"
>
<Button
style="button--marker button--marker-{comment.status} {panel.currentCommentId ===
comment.id
? 'button--marker-highlighted'
: ''}"
onclick={() => scrollIntoView(comment.id)}
onmouseenter={() => (panel.pulseMarkerId = comment.id)}
onmouseout={() => (panel.pulseMarkerId = 0)}
ariaLabel={`${t("ui.comment.maker.aria.label", "Jump to marker")} ${comment.id}`}
>
{comment.id}
</Button>
<div class="comment__content">
<header>
<strong>{comment.author}</strong>
<time
datetime={formatDateISO(comment.timestamp)}
title={formatDate(comment.timestamp, false)}
>
{formatDate(comment.timestamp)}
</time>
</header>
<div class="comment__text">{decodeHTMLEntities(comment.comment)}</div>
</div>
{#if !detailsOpen}
<Button
style="button--solid button--small comment__replies-count"
ariaLabel={`${t("ui.comment.replies.aria.label", "Show replies")} ${comment.id}`}
onclick={() => {
detailsOpen = !detailsOpen;
}}
>
{comment.replies?.length > 0 ? `+${comment.replies.length}` : "+"}
</Button>
{/if}
</summary>
{#if comment.replies?.length > 0}
<ul class="comment__replies">
{#each comment.replies as reply (reply.id)}
<li>
<Reply {reply} />
</li>
{/each}
</ul>
{/if}
<footer>
{#if openReplyForm}
<CommentForm
handleSubmit={(e) => {
openReplyForm = false;
handleSubmit(e);
}}
cancel={() => {
openReplyForm = false;
cancel();
}}
parentId={comment.id}
/>
{:else}
<div class="buttons">
{#if comment.status === "OPEN"}
<Button style="button--solid" onclick={() => (openReplyForm = true)}>
{t("ui.reply.submit", "Reply")}
</Button>
<Button onclick={() => resolveComment(comment)}>
{t("ui.comment.mark.solved", "Resolve")}
</Button>
{:else}
<Button onclick={() => unresolveComment(comment)}>
{t("ui.comment.mark.unsolved", "Reopen")}
</Button>
{/if}
</div>
{/if}
</footer>
</details>
<style>
.comment {
--loop-marker-background: var(--comment-marker-background);
--loop-marker-color: var(--comment-marker-color);
--marker-size: var(--comment-avatar-size);
position: relative;
> * {
z-index: 1;
position: relative;
}
&::after {
content: "";
position: absolute;
left: var(--comment-line-offset);
top: 1.5rem;
width: var(--comment-line-width);
height: calc(100% - 4rem);
background-color: var(--comment-line-background);
z-index: 0;
}
}
.comment:not([open]) {
&::after {
height: calc(100% - 2.75rem);
}
}
.comment__header {
display: flex;
align-items: center;
font-size: var(--comment-header-font-size);
padding: var(--comment-header-padding);
align-items: flex-start;
gap: var(--comment-header-gap);
cursor: pointer;
border-radius: var(--comment-header-border-radius);
&:focus-visible {
outline: 2px solid var(--comment-header-outline-color);
outline-offset: var(--comment-header-outline-offset);
}
:global(.comment__replies-count) {
position: absolute;
bottom: 0;
left: var(--space-s);
min-width: var(--comment-avatar-size);
}
header {
display: flex;
gap: var(--comment-author-gap);
align-items: center;
justify-content: flex-start;
margin-bottom: var(--comment-author-margin-bottom);
time {
font-size: var(--comment-timestamp-font-size);
color: var(--comment-timestamp-color);
}
}
.comment__content {
padding: var(--comment-content-padding);
background-color: var(--comment-content-background);
border-radius: var(--comment-content-border-radius);
flex: 1;
@media (prefers-color-scheme: dark) {
background-color: var(--comment-content-background-dark);
}
}
.comment__text {
white-space: pre-line;
}
}
.comment__replies {
list-style: none;
margin: 0;
padding: var(--comment-replies-padding);
display: flex;
flex-direction: column;
gap: var(--comment-replies-gap);
}
footer {
display: flex;
flex-direction: column;
gap: var(--comment-footer-gap);
padding: var(--comment-footer-padding);
.buttons {
display: flex;
gap: var(--comment-buttons-gap);
align-items: flex-end;
}
}
.is-hidden {
display: none;
}
</style>