feat(web): show PR state badge on item cards (TASK-1230) (#436)

Render a state-colored pill badge in the top-right corner of ItemCard
whenever an item has a linked GitHub PR. The badge overlaps the card
border ("sticker on card" feel), shows the PR number, and opens the PR
URL in a new tab on click without navigating to the item.

PR data is read from `item.code_context?.pull_request` which the server
already derives from `fields.github_pr` via ExtractItemCodeContext —
no new types, helpers, or API calls. State colors:

- OPEN    -> var(--accent-green)
- MERGED  -> var(--accent-purple, #8b5cf6)
- CLOSED  -> var(--accent-red, #ef4444)
- DRAFT   -> var(--text-muted)
- default -> var(--text-muted)

Implementation matches the existing star-btn pattern: a <button> (not
nested <a>) with preventDefault + stopPropagation, then window.open
with noopener,noreferrer. Visible in both default and compact card
variants; tooltip on hover surfaces the PR title + state.

Implements [[IDEA-1214]]. Stale-state refresh (badge may show OPEN
after PR is merged) is intentionally out of scope and tracked
separately under IDEA-1214 (sibling task).

Verification:
- golangci-lint run --timeout=5m ./...  -> 0 issues
- go test ./...                          -> all pass
- cd web && npm run build                -> clean
- svelte-check                           -> 0 errors

Note: `make check` also runs govulncheck which flags 4 pre-existing
Go stdlib vulnerabilities (GO-2026-4982 / 4980 / 4971 / 4918) on the
1.26.2 baseline. Tracked under TASK-1232 (Bump Go toolchain to 1.26.3).
Unrelated to this PR.
This commit is contained in:
xarmian
2026-05-07 22:50:17 -04:00
committed by GitHub
parent d915cc3cf8
commit e932fd4fcd
@@ -33,8 +33,28 @@
!!onStatusClick && !!statusOptions && statusOptions.length > 1 && !!fields.status
);
let pullRequest = $derived(item.code_context?.pull_request);
let pulsing = $state(false);
function prStateColor(state: string): string {
switch (state?.toUpperCase()) {
case 'OPEN': return 'var(--accent-green)';
case 'MERGED': return 'var(--accent-purple, #8b5cf6)';
case 'CLOSED': return 'var(--accent-red, #ef4444)';
case 'DRAFT': return 'var(--text-muted)';
default: return 'var(--text-muted)';
}
}
function openPullRequest(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (pullRequest?.url) {
window.open(pullRequest.url, '_blank', 'noopener,noreferrer');
}
}
function cycleStatus(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
@@ -81,6 +101,17 @@
</script>
<a href={itemUrl} class="item-card" class:compact class:focused>
{#if pullRequest}
<button
type="button"
class="pr-badge"
style:background={prStateColor(pullRequest.state)}
onclick={openPullRequest}
title="#{pullRequest.number} {pullRequest.title} ({(pullRequest.state ?? '').toLowerCase()})"
>
#{pullRequest.number}
</button>
{/if}
<div class="card-top-row">
<button
class="star-btn"
@@ -154,6 +185,7 @@
<style>
.item-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -166,6 +198,34 @@
transition: background 0.1s;
}
.pr-badge {
position: absolute;
top: -6px;
right: -6px;
z-index: 1;
border: 1px solid var(--bg-primary);
border-radius: 10px;
padding: 1px 7px;
font-family: var(--font-mono);
font-size: 0.7em;
font-weight: 600;
line-height: 1.4;
color: #fff;
cursor: pointer;
white-space: nowrap;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
transition: transform 0.1s, filter 0.1s;
}
.pr-badge:hover {
filter: brightness(1.1);
transform: scale(1.05);
}
.pr-badge:active {
transform: scale(0.95);
}
.item-card:hover,
.item-card.focused {
background: var(--bg-hover);