feat(integrations): add Azure work item state management and UI tweaks

Add support for listing and setting Azure DevOps work item states from the
integration layer. New helpers build and validate JSON-patch payloads and
expose two Tauri commands so the frontend can enumerate available states
and apply state changes. The Issue Center now loads Azure states and can
change an issue's state; dialog dismiss controls and related CSS were
consolidated and board UI feedback was simplified for clarity.

- Add Tauri commands to list and set Azure work item states.
- Load and present Azure states in Issue Center and allow state changes.
- Unify dialog dismiss attributes and refine hover/focus styling.
This commit is contained in:
2026-09-11 08:43:06 +02:00
parent c42f8eb352
commit d0dd79354a
12 changed files with 151 additions and 66 deletions
+7 -13
View File
@@ -9031,30 +9031,24 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
color: #d7dae0;
}
/* Keep every dialog close action neutral until it is intentionally targeted. */
:root .dialog-close:hover:not(:disabled),
:root .dialog-close:focus-visible:not(:disabled),
:root .dialog-icon-button:hover:not(:disabled),
:root .dialog-icon-button:focus-visible:not(:disabled),
:root .cred-close:hover:not(:disabled),
:root .cred-close:focus-visible:not(:disabled) {
/* All dialog dismiss buttons share hover and keyboard-focus feedback. */
:root :is(.dialog-close, .dialog-icon-button, .cred-close, .help-close, [data-dialog-close]):is(:hover, :focus-visible):not(:disabled):not([aria-disabled="true"]) {
color: #ffffff;
border-color: #f0646d;
background: #d93641;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
:root[data-theme="light"] .dialog-close:hover:not(:disabled),
:root[data-theme="light"] .dialog-close:focus-visible:not(:disabled),
:root[data-theme="light"] .dialog-icon-button:hover:not(:disabled),
:root[data-theme="light"] .dialog-icon-button:focus-visible:not(:disabled),
:root[data-theme="light"] .cred-close:hover:not(:disabled),
:root[data-theme="light"] .cred-close:focus-visible:not(:disabled) {
:root[data-theme="light"] :is(.dialog-close, .dialog-icon-button, .cred-close, .help-close, [data-dialog-close]):is(:hover, :focus-visible):not(:disabled):not([aria-disabled="true"]) {
color: #ffffff;
border-color: #a9212b;
background: #c92f3a;
}
:root :is(.dialog-close, .dialog-icon-button, .cred-close, .help-close, [data-dialog-close]):is(:hover, :focus-visible):not(:disabled):not([aria-disabled="true"]) svg {
color: inherit;
}
/* Keep the repository overview as a compact icon tab. */
.repo-tab.management {
flex: 0 0 38px;
+1 -1
View File
@@ -97,7 +97,7 @@
<dialog bind:this={dialog} aria-labelledby="create-review-title" oncancel={(event) => { event.preventDefault(); if (!busy) onClose(); }} onclick={(event) => { if (event.target === dialog && !busy) { const rect = dialog.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) onClose(); } }}>
<form onsubmit={submit}>
<header><div class="heading-icon"><GitPullRequest size={19} /></div><div><h2 id="create-review-title">{heading}</h2><p>{source.label}</p></div><button class="close" type="button" aria-label={de ? "Schließen" : "Close"} disabled={busy} onclick={onClose}><X size={18}/></button></header>
<header><div class="heading-icon"><GitPullRequest size={19} /></div><div><h2 id="create-review-title">{heading}</h2><p>{source.label}</p></div><button data-dialog-close class="close" type="button" aria-label={de ? "Schließen" : "Close"} disabled={busy} onclick={onClose}><X size={18}/></button></header>
<div class="body">
{#if error}<div class="error" role="alert">{error}{#if !repositories.length && !loading}<button type="button" onclick={loadRepositories}>{de ? "Erneut laden" : "Retry"}</button>{/if}</div>{/if}
<div class="repository-field"><div class="field-heading"><span>Repository</span><small>{loading ? "…" : `${repositories.length} ${de ? "verfügbar" : "available"}`}</small></div>
+2 -18
View File
@@ -29,7 +29,6 @@
let moving = false;
let dragged: { cardId: string; columnId: string } | null = null;
let dropColumn = "";
let moveMessage = "";
$: canMove = board?.columns.some(column => !!column.moveTarget) ?? false;
let error = "";
let query = "";
@@ -169,7 +168,6 @@
dragged = null;
dropColumn = "";
error = "";
moveMessage = de ? "Verschiebung wird gespeichert …" : "Saving move …";
try {
const credential = await timeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), 15_000);
if (!credential?.password) throw new Error(de ? "Kein Token gespeichert." : "No token stored.");
@@ -178,17 +176,14 @@
boardCache.delete(`${preferenceKey}:${url}`);
if (destroyed || generation !== requestGeneration) return;
boardCache.set(`${preferenceKey}:${url}`, board!);
moveMessage = de ? "Verschoben. Board wird aktualisiert …" : "Moved. Refreshing board …";
moving = false;
if (selected?.id === cardId) selectedColumnId = to;
await loadBoard(url);
if (!destroyed) moveMessage = error ? (de ? "Verschoben; der angezeigte Stand konnte noch nicht aktualisiert werden." : "Moved; the displayed board could not be refreshed yet.") : (de ? "Karte verschoben." : "Card moved.");
} catch (cause) {
if (!destroyed && generation === requestGeneration) {
board = previousBoard;
if (selected?.id === cardId) selectedColumnId = previousSelectedColumn;
error = `${de ? "Verschieben nicht bestätigt. Bitte aktualisieren und Schreibrechte prüfen." : "Move not confirmed. Refresh and check write access."} ${String(cause)}`;
moveMessage = "";
}
} finally { if (!destroyed) moving = false; }
}
@@ -205,15 +200,6 @@
async function openOriginal(url: string) {
try { await openInBrowser(url); } catch (cause) { error = String(cause); }
}
function notice(text: string) {
if (!de) return text;
if (text.startsWith("Team scope")) return "Team-Zuordnung, Spalten, geteilte Spalten und Swimlane-Namen werden übernommen. Temporäre Browser-Filter und das Alterslimit für abgeschlossene Karten werden nicht angewendet.";
if (text.startsWith("Saved board scope")) return "Die gespeicherte Board-Konfiguration wird angewendet. Temporäre Suchfilter aus dem Browser werden nicht übernommen.";
if (text.startsWith("Archived cards")) return "Archivierte Karten werden ausgeblendet. Die Spalten entsprechen den Auswahloptionen des Board-Feldes.";
if (text.startsWith("The saved GitHub view filter")) return `Der gespeicherte GitHub-Ansichtsfilter wird nicht angewendet. Alle nicht archivierten Projektkarten erscheinen in ihren ursprünglichen Spalten. ${text.split(": ")[1]?.split(". All non-archived")[0] ?? ""}`;
return text;
}
</script>
<svelte:window onkeydown={event => { if (event.key === "Escape") selected = null; }} />
@@ -228,7 +214,7 @@
{#if discoveryWarnings.length}<details class="board-discovery-notice" open={!savedBoards.length}><summary>{de ? "Hinweise zur Board-Suche" : "Board discovery notices"} ({discoveryWarnings.length})</summary>{#each discoveryWarnings as warning}<p>{warning}</p>{/each}</details>{/if}
{#if board}
<div class="board-header">
<div class="board-title"><h2>{boardLabel({ title: board.title, webUrl: board.webUrl, scope: "" })}</h2><small>{source.label} <span>·</span> {canMove ? (de ? "Karten zwischen Spalten verschieben" : "Move cards between columns") : (de ? "Lesende Ansicht" : "Read-only view")}{#if loading} <span>·</span> {de ? "Aktualisierung läuft …" : "Updating …"}{/if}</small></div>
<div class="board-title"><h2>{boardLabel({ title: board.title, webUrl: board.webUrl, scope: "" })}</h2></div>
<div class="board-tools">
<SelectMenu class="repository-select" value={repositoryFilter} options={repositoryOptions} ariaLabel={de ? "Repository filtern" : "Filter repository"} onChange={value => { repositoryFilter = value; selected = null; }} />
<label class="workspace-search"><Search size={15} /><input bind:value={query} aria-label={de ? "Board durchsuchen" : "Search board"} placeholder={de ? "Karten suchen …" : "Search cards …"} /></label>
@@ -237,10 +223,8 @@
</div>
</div>
{/if}
{#if moveMessage}<p class="board-notice" role="status">{moveMessage}</p>{/if}
{#if error}<div class="workspace-notice error" role="alert">{error}{#if board}<p>{de ? "Der zuletzt geladene Stand bleibt sichtbar." : "The last loaded board remains visible."}</p>{/if}</div>{/if}
{#if board}
{#if board.notice}<p class="board-notice">{notice(board.notice)}</p>{/if}
<div class="board-body">
<div class="board-columns" aria-label={de ? "Board-Spalten" : "Board columns"}>
{#each columns as column (column.id)}
@@ -272,7 +256,7 @@
<div><Columns3 size={17} /><strong>{source.label} · {de ? "Karte" : "Card"}</strong></div>
<div class="issue-detail-header-actions">
{#if selected.webUrl}<button class="workspace-button close-button" onclick={() => openOriginal(selected!.webUrl)} aria-label={de ? "Im Anbieter öffnen" : "Open in provider"}><ExternalLink size={16} /></button>{/if}
<button class="workspace-button close-button" onclick={() => { selected = null; }} aria-label={de ? "Kartendetails schließen" : "Close card details"}><X size={17} /></button>
<button data-dialog-close class="workspace-button close-button" onclick={() => { selected = null; }} aria-label={de ? "Kartendetails schließen" : "Close card details"}><X size={17} /></button>
</div>
</header>
<div class="issue-detail-body">
+54 -9
View File
@@ -15,7 +15,7 @@
import IntegrationBoardView from "./IntegrationBoardView.svelte";
import SelectMenu from "./SelectMenu.svelte";
import { readWorkspacePreferences, writeWorkspacePreferences } from "../workspacePreferences";
import { listIntegrationIssues, closeIntegrationIssue, openInBrowser } from "../git";
import { listIntegrationIssues, closeIntegrationIssue, listAzureIssueStates, setAzureIssueState, openInBrowser } from "../git";
import { configuredIntegrationSources, integrationCredentialKey } from "../integrations";
import type { GitIntegrationSettings, IntegrationIssue, StoredCredential } from "../types";
@@ -40,6 +40,12 @@
let closingId = "";
let closingKey = "";
let actionError = "";
let azureStates: string[] = [];
let statesLoading = false;
let statesError = "";
let statesGeneration = 0;
let lastStateKey = "";
let targetState = "";
let closedStates = new Map<string, string>();
let error = "";
let mounted = false;
@@ -74,6 +80,17 @@
$: stateOptions = [{ value: "", label: de ? "Alle Status" : "All states" }, ...[...new Set(issues.map(issue => issue.state))].sort().map(value => ({ value, label: value }))];
$: selected = issues.find(issue => issue.id === selectedId);
$: description = selected ? plainDescription(selected) : "";
$: stateKey = source?.provider === "azure-devops" && selected ? JSON.stringify([sourceKey, selected.id]) : "";
$: if (stateKey !== lastStateKey) {
lastStateKey = stateKey;
statesGeneration++;
azureStates = [];
statesError = "";
statesLoading = false;
actionError = "";
targetState = "";
if (stateKey) void loadAzureStates();
}
onMount(() => {
const saved = readWorkspacePreferences(preferenceKey);
@@ -93,7 +110,7 @@
window.removeEventListener("pointerdown", closeDetailsOutside, true);
};
});
onDestroy(() => { generation++; });
onDestroy(() => { generation++; statesGeneration++; });
function restoreFilters(key: string) {
const saved = readWorkspacePreferences(`${preferenceKey}:${key}`);
@@ -157,7 +174,26 @@
return document.body.textContent?.trim() ?? "";
}
async function closeIssue() {
async function loadAzureStates() {
if (!selected || source?.provider !== "azure-devops") return;
const current = source;
const issue = selected;
const requestGeneration = ++statesGeneration;
statesLoading = true;
statesError = "";
try {
const credential = await withTimeout(loadCredential(integrationCredentialKey(current.provider, current.accountId)), 15_000);
if (!credential?.password) throw new Error(de ? "Kein Token gespeichert." : "No token stored.");
const states = await withTimeout(listAzureIssueStates(current.baseUrl, credential.username, credential.password, issue.repositoryName, issue.number), 70_000);
if (requestGeneration !== statesGeneration) return;
azureStates = states;
targetState = issue.state;
} catch (cause) {
if (requestGeneration === statesGeneration) statesError = `${de ? "Status konnten nicht geladen werden." : "Could not load states."} ${String(cause)}`;
} finally { if (requestGeneration === statesGeneration) statesLoading = false; }
}
async function changeIssueState(nextState?: string) {
if (!selected || !source || closingId || loading) return;
const issue = selected;
const current = source;
@@ -168,13 +204,15 @@
try {
const credential = await withTimeout(loadCredential(integrationCredentialKey(current.provider, current.accountId)), 15_000);
if (!credential?.password) throw new Error(de ? "Kein Token gespeichert." : "No token stored.");
const state = await closeIntegrationIssue(current.provider, current.baseUrl, credential.username, credential.password, issue.repositoryName, issue.number);
const state = nextState && current.provider === "azure-devops"
? await setAzureIssueState(current.baseUrl, credential.username, credential.password, issue.repositoryName, issue.number, nextState)
: await closeIntegrationIssue(current.provider, current.baseUrl, credential.username, credential.password, issue.repositoryName, issue.number);
const saved = cache.get(key);
if (saved) cache.set(key, { ...saved, issues: saved.issues.map(item => item.id === issue.id ? { ...item, state } : item) });
closedStates = new Map(closedStates).set(`${key}:${issue.id}`, state);
if (!nextState) closedStates = new Map(closedStates).set(`${key}:${issue.id}`, state);
if (sourceKey === key) issues = issues.map(item => item.id === issue.id ? { ...item, state } : item);
} catch (cause) {
if (sourceKey === key && selectedId === issue.id) actionError = `${de ? "Issue konnte nicht geschlossen werden. Bitte Schreibrechte prüfen." : "Could not close issue. Check write permissions."} ${String(cause)}`;
if (sourceKey === key && selectedId === issue.id) actionError = `${de ? "Status konnte nicht geändert werden. Bitte Schreibrechte und Übergangsregeln prüfen." : "Could not change state. Check write permissions and transition rules."} ${String(cause)}`;
} finally { closingId = ""; }
}
@@ -244,7 +282,7 @@
<div><CircleDot size={17} /><strong>{source?.label} · Issue</strong></div>
<div class="issue-detail-header-actions">
{#if selected.webUrl}<button class="workspace-button close-button" onclick={() => openIssue(selected!.webUrl)} aria-label={de ? "Im Anbieter öffnen" : "Open in provider"}><ExternalLink size={16} /></button>{/if}
<button class="workspace-button close-button" onclick={closeDetails} aria-label={de ? "Details schließen" : "Close details"}><X size={17} /></button>
<button data-dialog-close class="workspace-button close-button" onclick={closeDetails} aria-label={de ? "Details schließen" : "Close details"}><X size={17} /></button>
</div>
</header>
<div class="issue-detail-body">
@@ -256,8 +294,15 @@
{#if source}{#key `${sourceKey}:${selected.id}`}<IssueComments {source} issue={selected} {language} {loadCredential} />{/key}{/if}
</div>
<div class="issue-detail-sidebar">
{#if selected.state !== "closed" && closedStates.get(`${sourceKey}:${selected.id}`) !== selected.state}
<button class="workspace-button issue-close-action" disabled={!!closingId || loading} onclick={closeIssue}><XCircle size={15} />{closingId === selected.id ? (de ? "Wird geschlossen …" : "Closing …") : (de ? "Issue schließen" : "Close issue")}</button>
{#if source?.provider === "azure-devops"}
<section>
<h3>{de ? "Status ändern" : "Change state"}</h3>
<SelectMenu value={targetState} options={azureStates.map(value => ({ value, label: value }))} disabled={statesLoading || !!closingId || loading || !azureStates.length} placeholder={statesLoading ? (de ? "Status werden geladen …" : "Loading states …") : selected.state} ariaLabel={de ? "Neuer Status" : "New state"} onChange={value => { targetState = value; }} />
<button class="workspace-button issue-state-action" disabled={statesLoading || !!closingId || loading || !targetState || targetState === selected.state || !azureStates.includes(targetState)} onclick={() => changeIssueState(targetState)}>{closingId === selected.id ? (de ? "Wird gespeichert …" : "Saving …") : (de ? "Status übernehmen" : "Apply state")}</button>
{#if statesError}<p class="comment-error" role="alert">{statesError}</p><button class="workspace-button" onclick={loadAzureStates}>{de ? "Erneut versuchen" : "Retry"}</button>{/if}
</section>
{:else if selected.state !== "closed" && closedStates.get(`${sourceKey}:${selected.id}`) !== selected.state}
<button class="workspace-button issue-close-action" disabled={!!closingId || loading} onclick={() => changeIssueState()}><XCircle size={15} />{closingId === selected.id ? (de ? "Wird geschlossen …" : "Closing …") : (de ? "Issue schließen" : "Close issue")}</button>
{/if}
{#if actionError}<p class="comment-error" role="alert">{actionError}</p>{/if}
{#if selected.webUrl}<button class="workspace-button inspector-open" onclick={() => openIssue(selected!.webUrl)}><ExternalLink size={14} />{de ? "Im Browser öffnen" : "Open in browser"}</button>{/if}
+1 -1
View File
@@ -302,7 +302,7 @@
<div class="dialog-header-actions">
<button class="external-diff" type="button" onclick={onExternalDiff} disabled={isBusy || isLoading} title={t(`In ${diffName} öffnen`, `Open in ${diffName}`)}><span>{t("Extern öffnen", "Open externally")}</span><ExternalLink size={14} /><span>·</span><span>{diffName}</span></button>
<button class="icon-action" type="button" onclick={onRefresh} disabled={isBusy || isLoading} aria-label={t("Aktualisieren", "Refresh")} title={t("Aktualisieren", "Refresh")}><RefreshCw size={16} /></button>
<button class="icon-action" type="button" onclick={onClose} disabled={isBusy} aria-label={t("Schließen", "Close")}><X size={16} /></button>
<button data-dialog-close class="icon-action" type="button" onclick={onClose} disabled={isBusy} aria-label={t("Schließen", "Close")}><X size={16} /></button>
</div>
</header>
@@ -247,7 +247,7 @@
<dialog class="workspace-dialog" bind:this={workspaceDialog} aria-labelledby="workspace-title">
<form onsubmit={createWorkspace}>
<header><h2 id="workspace-title">{de ? "Workspace anlegen" : "Create workspace"}</h2><button type="button" onclick={() => workspaceDialog.close()} aria-label={de ? "Dialog schließen" : "Close dialog"}><X size={14} /></button></header>
<header><h2 id="workspace-title">{de ? "Workspace anlegen" : "Create workspace"}</h2><button data-dialog-close type="button" onclick={() => workspaceDialog.close()} aria-label={de ? "Dialog schließen" : "Close dialog"}><X size={14} /></button></header>
<label for="workspace-name">Name</label>
<input id="workspace-name" bind:this={workspaceInput} bind:value={workspaceName} maxlength="64" autocomplete="off" aria-invalid={Boolean(workspaceError)} />
{#if workspaceError}<p class="dialog-error" role="alert">{workspaceError}</p>{/if}
+1 -1
View File
@@ -285,7 +285,7 @@
<h2><GitMerge size={24} />{t("Konflikte lösen", "Resolve conflicts")}</h2>
<div class="header-actions">
{#if conflictTarget}<button type="button" onclick={() => onExternalMerge(conflictTarget)} disabled={isBusy} title={mergeName}><ExternalLink size={18} />{t("Externes Merge-Tool", "External merge tool")}</button>{/if}
<button class="close-button" type="button" onclick={onClose} disabled={isBusy} aria-label={t("Schließen", "Close")}><X size={22} /></button>
<button data-dialog-close class="close-button" type="button" onclick={onClose} disabled={isBusy} aria-label={t("Schließen", "Close")}><X size={22} /></button>
</div>
</header>
{#if conflictedFiles.length === 0}
+1 -1
View File
@@ -494,7 +494,7 @@
<aside bind:this={detailPanel} class="detail-panel" transition:fly={{ x: 140, duration: 210, easing: cubicOut }}>
<header class="detail-header">
<div class="detail-provider"><GitPullRequest size={17} /><strong>{providerLabel(selected.provider)} {requestTypeLabel(selected.provider)}</strong></div>
<div class="detail-header-actions"><button class="icon-button" type="button" aria-label={de ? "Im Anbieter öffnen" : "Open in provider"} onclick={() => void openRequest()}><ExternalLink size={16} /></button><button class="icon-button" type="button" aria-label={de ? "Detailansicht schließen" : "Close details"} onclick={() => { detailOpen = false; }}><X size={17} /></button></div>
<div class="detail-header-actions"><button class="icon-button" type="button" aria-label={de ? "Im Anbieter öffnen" : "Open in provider"} onclick={() => void openRequest()}><ExternalLink size={16} /></button><button data-dialog-close class="icon-button" type="button" aria-label={de ? "Detailansicht schließen" : "Close details"} onclick={() => { detailOpen = false; }}><X size={17} /></button></div>
</header>
<div class="detail-content">
<main class="detail-main">
+8
View File
@@ -710,3 +710,11 @@ export function createIntegrationReviewRequest(provider: GitIntegrationProvider,
export function listIntegrationRepositoryBranches(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, repository: GitIntegrationRepository): Promise<{branches: string[]; defaultBranch: string}> {
return invoke("list_integration_repository_branches", {provider, baseUrl, username, token, repository});
}
export function listAzureIssueStates(baseUrl: string, username: string, token: string, repository: string, number: number): Promise<string[]> {
return invoke("list_azure_issue_states", { baseUrl, username, token, repository, number });
}
export function setAzureIssueState(baseUrl: string, username: string, token: string, repository: string, number: number, state: string): Promise<string> {
return invoke("set_azure_issue_state", { baseUrl, username, token, repository, number, state });
}
+7 -5
View File
@@ -104,7 +104,7 @@
.issue-center .board-tools {display:flex;align-items:center;gap:10px;flex-shrink:0}
.issue-center .board-tools .workspace-search {width:360px;max-width:100%}
.issue-center .board-notice {font-size:11px;line-height:1.6;padding:0 24px 12px;color:var(--issue-muted);flex-shrink:0}
.issue-center .board-body {position:relative;display:flex;flex:1;min-height:0;overflow:hidden}
.issue-center .board-body {display:flex;flex:1;min-height:0;overflow:hidden}
.issue-center .board-columns {display:flex;flex:1;min-width:0;gap:14px;padding:0 24px 20px;overflow:auto;align-items:stretch}
.issue-center .board-column {display:flex;flex:1 0 278px;max-width:400px;min-width:0;flex-direction:column;border:1px solid var(--issue-line);border-radius:0;background:transparent;overflow:hidden}
.issue-center .board-column>header {display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:34px;padding:8px 14px;flex-shrink:0;border-bottom:1px solid var(--issue-line);background:color-mix(in srgb,var(--color-surface) 35%,transparent)}
@@ -112,10 +112,10 @@
.issue-center .column-dot {width:13px;height:13px;flex:0 0 13px;border:2px solid var(--status-color);border-radius:50%;background:transparent}
.issue-center .column-count {color:var(--issue-muted);font-size:11px;white-space:nowrap}
.issue-center .wip-limit {font-size:10px}
.issue-center .column-cards {padding:0;min-height:0;overflow:auto;flex:1}
.issue-center .board-card {display:flex;flex-direction:column;align-items:flex-start;gap:7px;width:100%;text-align:left;padding:12px 14px;margin:0;border:0;border-bottom:1px solid var(--issue-line);border-radius:0;box-shadow:none;background:transparent;color:var(--color-ink);overflow-wrap:anywhere}
.issue-center .board-card:hover {background:color-mix(in srgb,var(--color-surface-hover) 55%,transparent)}
.issue-center .board-card.chosen {background:color-mix(in srgb,var(--color-accent) 9%,transparent);box-shadow:inset 2px 0 var(--color-accent)}
.issue-center .column-cards {display:flex;flex-direction:column;gap:8px;padding:10px;min-height:0;overflow:auto;flex:1}
.issue-center .board-card {display:flex;flex-shrink:0;flex-direction:column;align-items:flex-start;gap:5px;width:100%;text-align:left;padding:9px 11px;margin:0;border:1px solid var(--issue-line);border-radius:0;box-shadow:0 1px 2px rgb(0 0 0 / 12%);background:var(--color-surface);color:var(--color-ink);overflow-wrap:anywhere}
.issue-center .board-card:hover {background:var(--color-surface-hover);border-color:color-mix(in srgb,var(--color-ink-dim) 55%,var(--issue-line))}
.issue-center .board-card.chosen {background:color-mix(in srgb,var(--color-accent) 9%,var(--color-surface));border-color:var(--color-accent);box-shadow:inset 2px 0 var(--color-accent)}
.issue-center .board-card strong {font-size:13px;line-height:1.45;font-weight:550}
.issue-center .card-reference {display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:11px;line-height:1.4}
.issue-center .board-card .issue-person {margin-top:3px;font-size:11px}
@@ -251,3 +251,5 @@
.issue-center .board-directory {padding:14px 16px 10px;gap:8px}
.issue-center .board-discovery-notice {padding:8px 16px}
}
.issue-center .issue-state-action {width:100%;margin-top:8px;justify-content:center}