<script lang="ts">
import { issues } from "../../stores/issues.svelte.js";
import { marked } from "marked";
import DOMPurify from "dompurify";
import AppIcon from "../layout/AppIcon.svelte";
let commentBody = $state("");
let submitting = $state(false);
function renderMarkdown(md: string | null): string {
if (!md) return "<p class='empty-body'>No description provided.</p>";
return DOMPurify.sanitize(marked.parse(md) as string);
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
async function handleSubmitComment() {
const issue = issues.selectedIssue;
if (!issue || !commentBody.trim()) return;
submitting = true;
try {
const [owner, repo] = issue.repo.split("/");
await issues.addComment(owner!, repo!, issue.number, commentBody.trim());
commentBody = "";
} finally {
submitting = false;
}
}
async function handleToggleState() {
const issue = issues.selectedIssue;
if (!issue) return;
const [owner, repo] = issue.repo.split("/");
const newState = issue.state === "open" ? "closed" : "open";
await issues.updateIssue(owner!, repo!, issue.number, {
state: newState,
});
await issues.selectIssue(issue);
}
</script>
{#if issues.selectedIssue}
{@const issue = issues.selectedIssue}
<div class="detail">
<div class="detail-header">
<button class="detail-back" onclick={() => issues.deselectIssue()} aria-label="Back to issue list">
<AppIcon name="chevron-right" size={12} class="detail-back-icon" />
</button>
<div class="detail-title-row">
<h2 class="detail-title">{issue.title}</h2>
<span class="detail-number">#{issue.number}</span>
</div>
</div>
<div class="detail-meta">
<span class="detail-state" class:open={issue.state === "open"} class:closed={issue.state === "closed"}>
{issue.state}
</span>
<span class="detail-repo">{issue.repo}</span>
<span class="detail-author">by {issue.user.login}</span>
<span class="detail-date">{formatDate(issue.created_at)}</span>
<button class="detail-toggle-btn" onclick={handleToggleState}>
{issue.state === "open" ? "Close issue" : "Reopen issue"}
</button>
<a class="detail-link" href={issue.html_url} target="_blank" rel="noopener">
View on GitHub
</a>
</div>
{#if issue.labels.length > 0}
<div class="detail-labels">
{#each issue.labels as label}
<span
class="detail-label"
style:background="#{label.color}20"
style:color="#{label.color}"
style:border-color="#{label.color}40"
>
{label.name}
</span>
{/each}
</div>
{/if}
<div class="detail-body">
{@html renderMarkdown(issue.body)}
</div>
<div class="detail-comments">
<h3 class="comments-title">
Comments ({issue.comments})
</h3>
{#if issues.commentsLoading}
<div class="comments-loading">Loading comments...</div>
{:else if issues.commentsError}
<div class="comments-error">{issues.commentsError}</div>
{:else if issues.comments.length === 0}
<div class="comments-empty">No comments yet</div>
{:else}
{#each issues.comments as comment (comment.id)}
<div class="comment">
<div class="comment-header">
{#if comment.user.avatar_url}
<img
class="comment-avatar"
src={comment.user.avatar_url}
alt={comment.user.login}
width="20"
height="20"
/>
{/if}
<span class="comment-author">{comment.user.login}</span>
<span class="comment-date">{formatDate(comment.created_at)}</span>
</div>
<div class="comment-body">
{@html renderMarkdown(comment.body)}
</div>
</div>
{/each}
{/if}
<div class="comment-form">
<textarea
class="comment-input"
placeholder="Write a comment..."
bind:value={commentBody}
rows="3"
></textarea>
<button
class="comment-submit"
disabled={!commentBody.trim() || submitting}
onclick={handleSubmitComment}
>
{submitting ? "Posting..." : "Comment"}
</button>
</div>
</div>
</div>
{/if}
<style>
.detail {
padding: var(--wb-spacing-20);
}
.detail-header {
display: flex;
align-items: flex-start;
gap: var(--wb-spacing-8);
margin-bottom: var(--wb-spacing-12);
}
.detail-back {
width: var(--wb-control-size-28);
height: var(--wb-control-size-28);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--wsb-radius-sm);
color: var(--vscode-disabledForeground);
cursor: pointer;
flex-shrink: 0;
}
.detail-back:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-editor-foreground);
}
.detail-title-row {
display: flex;
align-items: baseline;
gap: var(--wb-spacing-8);
flex: 1;
min-width: 0;
}
.detail-title {
font-size: var(--wb-font-size-18);
font-weight: 600;
color: var(--vscode-editor-foreground);
margin: 0;
line-height: 1.3;
}
.detail-number {
font-size: var(--wb-font-size-18);
font-weight: 400;
color: var(--vscode-disabledForeground);
flex-shrink: 0;
}
.detail-meta {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
margin-bottom: var(--wb-spacing-12);
font-size: var(--wb-font-size-12);
color: var(--vscode-disabledForeground);
flex-wrap: wrap;
}
.detail-state {
font-size: var(--wb-font-size-11);
font-weight: 600;
padding: var(--wb-spacing-2) var(--wb-spacing-8);
border-radius: var(--wb-corner-radius-10);
text-transform: capitalize;
}
.detail-state.open {
color: var(--vscode-testing-iconPassed);
background: color-mix(in srgb, var(--vscode-testing-iconPassed) 10%, transparent);
}
.detail-state.closed {
color: var(--vscode-textPreformat-foreground);
background: color-mix(in srgb, var(--vscode-textPreformat-foreground) 10%, transparent);
}
.detail-repo {
font-family: var(--wsb-font-mono);
font-size: var(--wb-font-size-11);
}
.detail-toggle-btn {
font-size: var(--wb-font-size-11);
font-weight: 500;
color: var(--vscode-focusBorder);
cursor: pointer;
margin-left: auto;
}
.detail-toggle-btn:hover {
text-decoration: underline;
}
.detail-link {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
text-decoration: none;
}
.detail-link:hover {
color: var(--vscode-focusBorder);
text-decoration: underline;
}
.detail-labels {
display: flex;
flex-wrap: wrap;
gap: var(--wb-spacing-4);
margin-bottom: var(--wb-spacing-16);
}
.detail-label {
font-size: var(--wb-font-size-11);
font-weight: 500;
padding: var(--wb-spacing-2) var(--wb-spacing-8);
border-radius: var(--wb-corner-radius-10);
border: 1px solid;
}
.detail-body {
padding: var(--wb-spacing-16);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wsb-radius-md);
font-size: var(--wb-font-size-13);
line-height: 1.6;
color: var(--vscode-editor-foreground);
margin-bottom: var(--wb-spacing-20);
}
.detail-body :global(p) {
margin: 0 0 var(--wb-spacing-12);
}
.detail-body :global(p:last-child) {
margin-bottom: 0;
}
.detail-body :global(code) {
font-family: var(--wsb-font-mono);
font-size: var(--wb-font-size-12);
padding: var(--wb-spacing-1) var(--wb-spacing-4);
background: var(--wb-code-bg);
color: var(--wb-code-text);
border-radius: var(--wb-corner-radius-3);
}
.detail-body :global(pre) {
background: var(--wb-code-bg);
color: var(--wb-code-text);
padding: var(--wb-spacing-12);
border-radius: var(--wsb-radius-sm);
overflow-x: auto;
font-size: var(--wb-font-size-12);
}
.detail-body :global(.empty-body) {
color: var(--vscode-disabledForeground);
font-style: italic;
}
.detail-comments {
border-top: 1px solid var(--vscode-widget-border);
padding-top: var(--wb-spacing-16);
}
.comments-title {
font-size: var(--wb-font-size-13);
font-weight: 600;
color: var(--vscode-foreground);
margin: 0 0 var(--wb-spacing-12);
}
.comments-loading,
.comments-empty {
font-size: var(--wb-font-size-12);
color: var(--vscode-disabledForeground);
padding: var(--wb-spacing-8) 0;
}
.comments-error {
font-size: var(--wb-font-size-12);
color: var(--vscode-errorForeground);
padding: var(--wb-spacing-8) 0;
}
.comment {
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wsb-radius-md);
margin-bottom: var(--wb-spacing-8);
}
.comment-header {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-8) var(--wb-spacing-12);
background: var(--vscode-input-background);
border-bottom: 1px solid var(--vscode-widget-border);
border-radius: var(--wsb-radius-md) var(--wsb-radius-md) 0 0;
font-size: var(--wb-font-size-12);
}
.comment-avatar {
width: var(--wb-icon-size-20);
height: var(--wb-icon-size-20);
border-radius: 50%;
}
.comment-author {
font-weight: 600;
color: var(--vscode-editor-foreground);
}
.comment-date {
color: var(--vscode-disabledForeground);
margin-left: auto;
}
.comment-body {
padding: var(--wb-spacing-12);
font-size: var(--wb-font-size-13);
line-height: 1.5;
color: var(--vscode-editor-foreground);
}
.comment-body :global(p) {
margin: 0 0 var(--wb-spacing-8);
}
.comment-body :global(p:last-child) {
margin: 0;
}
.comment-form {
margin-top: var(--wb-spacing-12);
display: flex;
flex-direction: column;
gap: var(--wb-spacing-8);
}
.comment-input {
width: 100%;
padding: var(--wb-spacing-10) var(--wb-spacing-12);
font-size: var(--wb-font-size-13);
font-family: var(--wsb-font-sans);
color: var(--vscode-editor-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-widget-border);
border-radius: var(--wsb-radius-md);
resize: vertical;
min-height: var(--wb-control-size-60);
}
.comment-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.comment-submit {
align-self: flex-end;
height: var(--wb-control-size-30);
padding: 0 var(--wb-spacing-14);
font-size: var(--wb-font-size-12);
font-weight: 600;
color: white;
background: var(--vscode-focusBorder);
border-radius: var(--wsb-radius-sm);
cursor: pointer;
}
.comment-submit:disabled {
opacity: 0.5;
cursor: default;
}
.comment-submit:hover:not(:disabled) {
opacity: 0.9;
}
</style>