add kirby-loop plugin with French translations
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ea5f0c462
commit
ab7fd8b2ea
74 changed files with 16423 additions and 2 deletions
227
site/plugins/loop/frontend/src/lib/Comment.svelte
Normal file
227
site/plugins/loop/frontend/src/lib/Comment.svelte
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue