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.
380 lines
24 KiB
Svelte
380 lines
24 KiB
Svelte
<script lang="ts">
|
||
import { ArrowDown, ArrowUp, Check, ExternalLink, FileDiff, LoaderCircle, Minus, Plus, RefreshCw, Trash2, X } from "@lucide/svelte";
|
||
import type { GitFileStatus, PatchApplyAction } from "../types";
|
||
|
||
type PatchLineKind = "context" | "add" | "delete" | "meta";
|
||
|
||
interface PatchLine {
|
||
id: string;
|
||
text: string;
|
||
kind: PatchLineKind;
|
||
oldLine: number | null;
|
||
newLine: number | null;
|
||
}
|
||
|
||
interface PatchHunk {
|
||
id: string;
|
||
header: string;
|
||
lines: PatchLine[];
|
||
oldStart: number;
|
||
newStart: number;
|
||
suffix: string;
|
||
}
|
||
|
||
interface ParsedPatch {
|
||
headerLines: string[];
|
||
hunks: PatchHunk[];
|
||
binary: boolean;
|
||
}
|
||
|
||
interface Props {
|
||
file: GitFileStatus;
|
||
staged: boolean;
|
||
patch: string;
|
||
isBusy: boolean;
|
||
isLoading: boolean;
|
||
error: string;
|
||
language?: "en" | "de";
|
||
diffName?: string;
|
||
onClose: () => void;
|
||
onRefresh: () => void | Promise<void>;
|
||
onApply: (action: PatchApplyAction, patch: string, scope: "hunk" | "lines") => void | Promise<void>;
|
||
onExternalDiff: () => void | Promise<void>;
|
||
}
|
||
|
||
let {
|
||
file,
|
||
staged = false,
|
||
patch = "",
|
||
isBusy = false,
|
||
isLoading = false,
|
||
error = "",
|
||
language = "en",
|
||
diffName = "diff tool",
|
||
onClose = () => {},
|
||
onRefresh = () => {},
|
||
onApply = () => {},
|
||
onExternalDiff = () => {},
|
||
}: Props = $props();
|
||
|
||
const isGerman = $derived(language === "de");
|
||
|
||
let parsed = $state<ParsedPatch>({ headerLines: [], hunks: [], binary: false });
|
||
let patchWidth = $state(0);
|
||
let patchScroll = $state<HTMLDivElement | null>(null);
|
||
let selectedLineIds = $state<Set<string>>(new Set());
|
||
let lastSelectedLineId = $state("");
|
||
|
||
const t = (de: string, en: string) => isGerman ? de : en;
|
||
let scopeLabel = $derived(staged ? t("Gestagte Änderungen", "Staged changes") : t("Nicht gestagte Änderungen", "Unstaged changes"));
|
||
let activeHunk = $state(0);
|
||
let hasTextPatch = $derived(!isLoading && !error && !!patch.trim() && !parsed.binary && parsed.hunks.length > 0);
|
||
function clearSelection() { selectedLineIds = new Set(); lastSelectedLineId = ""; }
|
||
let displayPath = $derived(file.old_path ? `${file.old_path} -> ${file.path}` : file.path);
|
||
let selectableLines = $derived(
|
||
parsed.hunks.flatMap((hunk) => hunk.lines.filter((line) => line.kind === "add" || line.kind === "delete")),
|
||
);
|
||
let addedCount = $derived(selectableLines.filter(line => line.kind === "add").length);
|
||
let deletedCount = $derived(selectableLines.filter(line => line.kind === "delete").length);
|
||
let selectedCount = $derived(selectableLines.filter((line) => selectedLineIds.has(line.id)).length);
|
||
|
||
$effect(() => {
|
||
activeHunk = 0;
|
||
parsed = parsePatch(patch);
|
||
selectedLineIds = new Set();
|
||
lastSelectedLineId = "";
|
||
});
|
||
|
||
function parsePatch(input: string): ParsedPatch {
|
||
const normalized = input.replace(/\r\n/g, "\n");
|
||
const lines = normalized.split("\n");
|
||
if (lines[lines.length - 1] === "") lines.pop();
|
||
|
||
const headerLines: string[] = [];
|
||
const hunks: PatchHunk[] = [];
|
||
let current: PatchHunk | null = null;
|
||
let oldCursor = 0;
|
||
let newCursor = 0;
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith("@@ ")) {
|
||
const range = parseHunkHeader(line);
|
||
current = {
|
||
id: `hunk-${hunks.length}`,
|
||
header: line,
|
||
lines: [],
|
||
oldStart: range.oldStart,
|
||
newStart: range.newStart,
|
||
suffix: range.suffix,
|
||
};
|
||
oldCursor = range.oldStart;
|
||
newCursor = range.newStart;
|
||
hunks.push(current);
|
||
continue;
|
||
}
|
||
|
||
if (!current) {
|
||
headerLines.push(line);
|
||
continue;
|
||
}
|
||
|
||
const kind = patchLineKind(line);
|
||
const oldLine = kind === "context" || kind === "delete" ? oldCursor : null;
|
||
const newLine = kind === "context" || kind === "add" ? newCursor : null;
|
||
current.lines.push({
|
||
id: `${current.id}-line-${current.lines.length}`,
|
||
text: line,
|
||
kind,
|
||
oldLine,
|
||
newLine,
|
||
});
|
||
if (oldLine !== null) oldCursor += 1;
|
||
if (newLine !== null) newCursor += 1;
|
||
}
|
||
|
||
return {
|
||
headerLines,
|
||
hunks,
|
||
binary: /(^|\n)(Binary files|GIT binary patch|literal \d+)/.test(normalized),
|
||
};
|
||
}
|
||
|
||
function parseHunkHeader(header: string): { oldStart: number; newStart: number; suffix: string } {
|
||
const match = header.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$/);
|
||
return {
|
||
oldStart: Number(match?.[1] ?? 0),
|
||
newStart: Number(match?.[2] ?? 0),
|
||
suffix: match?.[3] ?? "",
|
||
};
|
||
}
|
||
|
||
function patchLineKind(line: string): PatchLineKind {
|
||
if (line.startsWith("+") && !line.startsWith("+++")) return "add";
|
||
if (line.startsWith("-") && !line.startsWith("---")) return "delete";
|
||
if (line.startsWith(" ")) return "context";
|
||
return "meta";
|
||
}
|
||
|
||
function linePrefix(line: PatchLine): string {
|
||
if (line.kind === "add") return "+";
|
||
if (line.kind === "delete") return "-";
|
||
if (line.kind === "meta") return "\\";
|
||
return " ";
|
||
}
|
||
|
||
function lineBody(line: PatchLine): string {
|
||
if (line.kind === "meta") return line.text;
|
||
return line.text.slice(1);
|
||
}
|
||
|
||
function buildHunkPatch(hunk: PatchHunk): string {
|
||
return `${[...parsed.headerLines, hunk.header, ...hunk.lines.map((line) => line.text)].join("\n")}\n`;
|
||
}
|
||
|
||
async function applyHunkAction(action: PatchApplyAction, hunk: PatchHunk) {
|
||
if (isBusy || isLoading) return;
|
||
await onApply(action, buildHunkPatch(hunk), "hunk");
|
||
}
|
||
|
||
function toggleLine(event: MouseEvent, line: PatchLine) {
|
||
if (isBusy || isLoading || (line.kind !== "add" && line.kind !== "delete")) return;
|
||
const next = new Set(selectedLineIds);
|
||
const selecting = !next.has(line.id);
|
||
if (event.shiftKey && lastSelectedLineId) {
|
||
const start = selectableLines.findIndex((candidate) => candidate.id === lastSelectedLineId);
|
||
const end = selectableLines.findIndex((candidate) => candidate.id === line.id);
|
||
if (start >= 0 && end >= 0) {
|
||
for (const candidate of selectableLines.slice(Math.min(start, end), Math.max(start, end) + 1)) {
|
||
if (selecting) next.add(candidate.id);
|
||
else next.delete(candidate.id);
|
||
}
|
||
}
|
||
} else if (selecting) {
|
||
next.add(line.id);
|
||
} else {
|
||
next.delete(line.id);
|
||
}
|
||
selectedLineIds = next;
|
||
lastSelectedLineId = line.id;
|
||
}
|
||
|
||
function toggleHunkSelection(hunk: PatchHunk) {
|
||
if (isBusy || isLoading) return;
|
||
const changed = hunk.lines.filter((line) => line.kind === "add" || line.kind === "delete");
|
||
const allSelected = changed.length > 0 && changed.every((line) => selectedLineIds.has(line.id));
|
||
const next = new Set(selectedLineIds);
|
||
for (const line of changed) {
|
||
if (allSelected) next.delete(line.id);
|
||
else next.add(line.id);
|
||
}
|
||
selectedLineIds = next;
|
||
lastSelectedLineId = changed[changed.length - 1]?.id ?? "";
|
||
}
|
||
|
||
function hunkSelectionState(hunk: PatchHunk): "none" | "some" | "all" {
|
||
const changed = hunk.lines.filter((line) => line.kind === "add" || line.kind === "delete");
|
||
const count = changed.filter((line) => selectedLineIds.has(line.id)).length;
|
||
return count === 0 ? "none" : count === changed.length ? "all" : "some";
|
||
}
|
||
|
||
function rangePart(start: number, count: number): string {
|
||
return count === 1 ? `${start}` : `${start},${count}`;
|
||
}
|
||
|
||
function buildSelectedHunk(hunk: PatchHunk): string | null {
|
||
if (!hunk.lines.some((line) => selectedLineIds.has(line.id))) return null;
|
||
const output: string[] = [];
|
||
let previousIncluded = false;
|
||
|
||
for (const line of hunk.lines) {
|
||
if (line.kind === "context") {
|
||
output.push(line.text);
|
||
previousIncluded = true;
|
||
} else if (line.kind === "delete") {
|
||
output.push(selectedLineIds.has(line.id) ? line.text : ` ${line.text.slice(1)}`);
|
||
previousIncluded = true;
|
||
} else if (line.kind === "add") {
|
||
if (selectedLineIds.has(line.id)) {
|
||
output.push(line.text);
|
||
previousIncluded = true;
|
||
} else {
|
||
previousIncluded = false;
|
||
}
|
||
} else if (previousIncluded) {
|
||
output.push(line.text);
|
||
}
|
||
}
|
||
|
||
const oldCount = output.filter((line) => line.startsWith(" ") || line.startsWith("-")).length;
|
||
const newCount = output.filter((line) => line.startsWith(" ") || line.startsWith("+")).length;
|
||
const header = `@@ -${rangePart(hunk.oldStart, oldCount)} +${rangePart(hunk.newStart, newCount)} @@${hunk.suffix}`;
|
||
return [header, ...output].join("\n");
|
||
}
|
||
|
||
function buildSelectedPatch(): string {
|
||
const hunks = parsed.hunks.map(buildSelectedHunk).filter((hunk): hunk is string => Boolean(hunk));
|
||
return `${[...parsed.headerLines, ...hunks].join("\n")}\n`;
|
||
}
|
||
|
||
async function applySelected(action: PatchApplyAction) {
|
||
if (isBusy || isLoading || selectedCount === 0) return;
|
||
await onApply(action, buildSelectedPatch(), "lines");
|
||
}
|
||
|
||
function hunkPosition(index: number): number {
|
||
const totalLines = parsed.hunks.reduce((sum, hunk) => sum + Math.max(hunk.lines.length, 1), 0);
|
||
const precedingLines = parsed.hunks.slice(0, index).reduce((sum, hunk) => sum + Math.max(hunk.lines.length, 1), 0);
|
||
return (precedingLines / Math.max(totalLines - 1, 1)) * 100;
|
||
}
|
||
|
||
function hunkKind(hunk: PatchHunk): "add" | "delete" | "mixed" {
|
||
const hasAdd = hunk.lines.some((line) => line.kind === "add");
|
||
const hasDelete = hunk.lines.some((line) => line.kind === "delete");
|
||
return hasAdd && hasDelete ? "mixed" : hasAdd ? "add" : "delete";
|
||
}
|
||
|
||
function scrollToHunk(hunkId: string) {
|
||
const target = patchScroll?.querySelector<HTMLElement>(`[data-hunk-id="${hunkId}"]`);
|
||
if (patchScroll && target) {
|
||
patchScroll.scrollTop += target.getBoundingClientRect().top - patchScroll.getBoundingClientRect().top;
|
||
activeHunk = parsed.hunks.findIndex(hunk => hunk.id === hunkId);
|
||
}
|
||
}
|
||
|
||
function updateActiveHunk() {
|
||
if (!patchScroll) return;
|
||
if (patchScroll.scrollTop > 0 && patchScroll.scrollTop + patchScroll.clientHeight >= patchScroll.scrollHeight - 2) {
|
||
activeHunk = Math.max(0, parsed.hunks.length - 1);
|
||
return;
|
||
}
|
||
const top = patchScroll.getBoundingClientRect().top;
|
||
const sections = Array.from(patchScroll.querySelectorAll<HTMLElement>("[data-hunk-id]"));
|
||
let index = 0;
|
||
sections.forEach((section, i) => { if (section.getBoundingClientRect().top <= top + 2) index = i; });
|
||
activeHunk = index;
|
||
}
|
||
</script>
|
||
|
||
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||
<div class="dialog line-patch-dialog" role="dialog" aria-modal="true" aria-label={t("Geänderte Zeilen", "Changed lines")}>
|
||
<header class="dialog-header">
|
||
<div class="patch-identity"><FileDiff size={23} aria-hidden="true" /><div><p class="dialog-title" title={displayPath}>{displayPath}</p><span class="patch-scope">{scopeLabel}</span></div></div>
|
||
<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 data-dialog-close class="icon-action" type="button" onclick={onClose} disabled={isBusy} aria-label={t("Schließen", "Close")}><X size={16} /></button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="line-patch-body">
|
||
{#if isLoading}
|
||
<div class="blank-state"><LoaderCircle class="spin" size={18} />{t("Änderungen werden geladen …", "Loading changes …")}</div>
|
||
{:else if error}<div class="blank-state" role="alert">{error}</div>
|
||
{:else if !patch.trim()}<div class="blank-state">{t("Keine Änderungen für diese Datei vorhanden.", "No changes available for this file.")}</div>
|
||
{:else if parsed.binary || parsed.hunks.length === 0}<div class="blank-state">{t("Diese Änderung lässt sich nicht in Textzeilen aufteilen.", "This change cannot be split into text lines.")}</div>
|
||
{:else}
|
||
<div class="patch-toolbar">
|
||
<strong>{t("Geänderte Zeilen auswählen", "Select changed lines")}</strong><span class="range-hint">{t("Shift-Klick für einen Bereich", "Shift-click to select a range")}</span>
|
||
<div class="patch-summary"><span class="add-count">+{addedCount}</span><span class="delete-count">−{deletedCount}</span><span>{parsed.hunks.length} {t(parsed.hunks.length === 1 ? "Abschnitt" : "Abschnitte", parsed.hunks.length === 1 ? "hunk" : "hunks")}</span>
|
||
<button class="icon-action" type="button" onclick={() => scrollToHunk(parsed.hunks[activeHunk - 1].id)} disabled={activeHunk === 0} aria-label={t("Vorheriger Abschnitt", "Previous hunk")} title={t("Vorheriger Abschnitt", "Previous hunk")}><ArrowUp size={16} /></button>
|
||
<button class="icon-action" type="button" onclick={() => scrollToHunk(parsed.hunks[activeHunk + 1].id)} disabled={activeHunk >= parsed.hunks.length - 1} aria-label={t("Nächster Abschnitt", "Next hunk")} title={t("Nächster Abschnitt", "Next hunk")}><ArrowDown size={16} /></button>
|
||
</div>
|
||
</div>
|
||
<div class="line-patch-workspace" style={`--patch-visible-width: ${patchWidth}px`}>
|
||
<div class="line-patch-scroll" bind:this={patchScroll} bind:clientWidth={patchWidth} onscroll={updateActiveHunk}>
|
||
{#each parsed.hunks as hunk, index (hunk.id)}
|
||
<section class="line-patch-hunk" data-hunk-id={hunk.id} aria-label={`${t("Abschnitt", "Hunk")} ${index + 1}`}>
|
||
<div class="line-patch-hunk-head">
|
||
<button class:all={hunkSelectionState(hunk) === "all"} class:some={hunkSelectionState(hunk) === "some"} class="line-patch-select-hunk" type="button" role="checkbox" aria-checked={hunkSelectionState(hunk) === "some" ? "mixed" : hunkSelectionState(hunk) === "all"} onclick={() => toggleHunkSelection(hunk)} disabled={isBusy || isLoading} aria-label={`${t("Abschnitt auswählen", "Select hunk")} ${index + 1}`} title={t("Alle geänderten Zeilen dieses Abschnitts auswählen", "Select all changed lines in this hunk")}>
|
||
{#if hunkSelectionState(hunk) === "all"}<Check size={11} />{:else if hunkSelectionState(hunk) === "some"}<Minus size={11} />{/if}
|
||
</button>
|
||
<strong>{t("Abschnitt", "Hunk")} {index + 1}</strong><code title={hunk.header}>{hunk.header}</code>
|
||
<div class="line-patch-hunk-actions">
|
||
<button class="line-patch-hunk-button discard" type="button" onclick={() => applyHunkAction(staged ? "discard-staged" : "discard-unstaged", hunk)} disabled={isBusy}><Trash2 size={14} />{t("Verwerfen", "Discard")}</button>
|
||
<button class="line-patch-hunk-button {staged ? "unstage" : "stage"}" type="button" onclick={() => applyHunkAction(staged ? "unstage" : "stage", hunk)} disabled={isBusy}>{#if staged}<Minus size={14} />{:else}<Plus size={14} />{/if}{staged ? t("Abschnitt unstagen", "Unstage hunk") : t("Abschnitt stagen", "Stage hunk")}</button>
|
||
</div>
|
||
</div>
|
||
<div class="line-patch-lines">
|
||
{#each hunk.lines as line (line.id)}
|
||
<div class:selected={selectedLineIds.has(line.id)} class:selectable={line.kind === "add" || line.kind === "delete"} class={`line-patch-row ${line.kind}`}>
|
||
{#if line.kind === "add" || line.kind === "delete"}
|
||
<button class="line-patch-line-select" type="button" role="checkbox" aria-checked={selectedLineIds.has(line.id)} onclick={(event) => toggleLine(event, line)} disabled={isBusy || isLoading} aria-label={`${line.kind === "add" ? t("Hinzugefügte Zeile", "Added line") : t("Entfernte Zeile", "Deleted line")} ${line.newLine ?? line.oldLine ?? ""}`} title={t("Zeile auswählen (Shift-Klick für Bereich)", "Select line (Shift-click for range)")}>
|
||
{#if selectedLineIds.has(line.id)}<Check size={11} />{/if}
|
||
</button>
|
||
{:else}<span class="line-patch-line-select-placeholder"></span>{/if}
|
||
<span class="line-patch-line-number">{line.oldLine ?? ""}</span><span class="line-patch-line-number">{line.newLine ?? ""}</span><span class="line-patch-prefix">{linePrefix(line)}</span><code>{lineBody(line)}</code>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</section>
|
||
{/each}
|
||
</div>
|
||
<nav class="diff-overview line-patch-overview" aria-label={t("Änderungsübersicht", "Change overview")}>
|
||
{#each parsed.hunks as hunk, index (hunk.id)}<button class="diff-overview-marker {hunkKind(hunk)}" type="button" style={`--marker-position: ${hunkPosition(index)}%`} onclick={() => scrollToHunk(hunk.id)} title={`${t("Zu Abschnitt", "Jump to hunk")} ${index + 1}`} aria-label={`${t("Zu Abschnitt", "Jump to hunk")} ${index + 1}`}></button>{/each}
|
||
</nav>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{#if hasTextPatch}
|
||
<footer class="patch-footer">
|
||
<div class="selection-summary"><span class="selection-symbol" class:has-selection={selectedCount > 0}><Check size={13} /></span><strong aria-live="polite">{selectedCount} {t(selectedCount === 1 ? "Zeile ausgewählt" : "Zeilen ausgewählt", selectedCount === 1 ? "line selected" : "lines selected")}</strong><button class="clear-selection" type="button" onclick={clearSelection} disabled={isBusy || selectedCount === 0}>{t("Auswahl aufheben", "Clear selection")}</button></div>
|
||
<div class="selection-actions"><button class="discard-selection" type="button" onclick={() => applySelected(staged ? "discard-staged" : "discard-unstaged")} disabled={isBusy || selectedCount === 0}>{t("Auswahl verwerfen", "Discard selected")}</button><button class="stage-selection" type="button" onclick={() => applySelected(staged ? "unstage" : "stage")} disabled={isBusy || selectedCount === 0}>{selectedCount} {t(selectedCount === 1 ? "Zeile" : "Zeilen", selectedCount === 1 ? "line" : "lines")} {staged ? t("unstagen", "to unstage") : t("stagen", "to stage")}</button></div>
|
||
</footer>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.line-patch-dialog{width:min(1700px,100%);height:min(960px,100%);grid-template-rows:auto minmax(0,1fr) auto;font-size:13px}
|
||
.dialog-header{padding:12px 18px}.patch-identity{display:flex;align-items:center;gap:12px;min-width:0}.patch-identity>div{min-width:0}.patch-identity :global(svg){flex:none;color:var(--color-ink-muted)}.patch-identity .dialog-title{font-size:15px;line-height:1.4;margin:0;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.patch-scope{display:block;color:var(--color-ink-muted);font-size:12px;margin-top:2px}
|
||
.dialog-header-actions{gap:8px}.dialog-header-actions .external-diff{display:flex;align-items:center;gap:8px;border:0;background:transparent;font-size:12px;color:var(--color-ink-muted);padding:5px 10px}.line-patch-dialog .icon-action{display:inline-flex;align-items:center;justify-content:center;flex:none;width:30px;min-width:30px;height:30px;min-height:30px;padding:0;border:1px solid var(--color-border);background:transparent;color:var(--color-ink-muted)}
|
||
.patch-toolbar{display:flex;align-items:center;gap:20px;padding:8px 18px;min-height:46px;border-bottom:1px solid var(--color-border-subtle);background:var(--app-dialog-bg)}.patch-toolbar strong{font-size:12px;font-weight:600}.range-hint{color:var(--color-ink-faint);font-size:12px}.patch-summary{display:flex;align-items:center;gap:10px;margin-left:auto;color:var(--color-ink-muted);font-size:12px;white-space:nowrap}.patch-summary .add-count{color:var(--code-add-text)}.patch-summary .delete-count{color:var(--code-delete-text);margin-right:10px}
|
||
.line-patch-scroll{position:relative}.line-patch-hunk{min-width:100%;width:max-content}.line-patch-hunk-head{width:var(--patch-visible-width, 100%);min-width:0;max-width:100%;height:44px;gap:14px;padding:6px 14px;background:var(--app-dialog-chrome)}.line-patch-hunk-head>strong{font-size:12px;font-weight:600;white-space:nowrap}.line-patch-hunk-head code{font-size:12px;color:var(--color-ink-faint);overflow:hidden;text-overflow:ellipsis;min-width:0}.line-patch-hunk-actions{margin-left:auto;padding-left:10px;gap:8px}.line-patch-hunk-button{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:4px 9px;font-size:12px;font-weight:500;background:transparent}.line-patch-hunk-button.discard{border-color:transparent}.line-patch-hunk-button.discard:hover:not(:disabled){border-color:transparent}
|
||
.line-patch-dialog .line-patch-select-hunk,.line-patch-dialog .line-patch-line-select{display:flex;align-items:center;justify-content:center;width:14px;min-width:14px;max-width:14px;height:14px;min-height:14px;max-height:14px;padding:0;line-height:1;border:1px solid var(--color-ink-faint);background:transparent;box-shadow:none;align-self:center;border-radius:2px!important;color:var(--app-bg);flex:none}
|
||
.line-patch-dialog .line-patch-select-hunk.all,.line-patch-dialog .line-patch-select-hunk.some,.line-patch-dialog .line-patch-line-select[aria-checked="true"]{background:var(--color-accent);border-color:var(--color-accent)}
|
||
.line-patch-lines{font-size:13px}.line-patch-row{grid-template-columns:28px 44px 44px 24px minmax(max-content,1fr);align-items:center;height:22px;min-height:22px;padding:0 14px;line-height:22px}.line-patch-row code{font-size:13px;line-height:22px}.line-patch-line-number{font-size:11px;line-height:22px}.line-patch-row.selected{box-shadow:inset 3px 0 0 var(--color-accent);filter:none}.line-patch-row.add.selected{background:color-mix(in srgb,var(--color-accent) 5%,var(--code-add-bg))}.line-patch-row.delete.selected{background:color-mix(in srgb,var(--color-accent) 5%,var(--code-delete-bg))}
|
||
.patch-footer{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:60px;padding:10px 18px;border-top:1px solid var(--color-border);background:var(--app-dialog-chrome)}.selection-summary,.selection-actions{display:flex;align-items:center;gap:12px}.selection-summary strong{font-size:12px;font-weight:600;white-space:nowrap}.selection-symbol{display:flex;width:16px;height:16px;align-items:center;justify-content:center;border:1px solid var(--color-border-input);color:var(--color-ink-faint)}.selection-symbol.has-selection{background:var(--color-accent);color:var(--app-bg);border-color:var(--color-accent)}.patch-footer button{min-height:30px;padding:5px 12px;font-size:12px;white-space:nowrap}.patch-footer .clear-selection{border:0;background:transparent;color:var(--color-ink-muted)}.patch-footer .discard-selection{border:1px solid color-mix(in srgb,var(--code-delete-text) 60%,var(--color-border));color:var(--code-delete-text);background:transparent}.patch-footer .stage-selection{background:var(--color-primary);border-color:var(--color-primary);color:white}.patch-footer .stage-selection:hover:not(:disabled){background:var(--color-primary-dark)}
|
||
@media(max-width:850px){.range-hint{display:none}.patch-toolbar{gap:10px}.patch-footer{flex-wrap:wrap}.selection-actions{margin-left:auto}.line-patch-hunk-head{gap:8px}.line-patch-hunk-head code{max-width:180px}.dialog-header-actions .external-diff{max-width:180px;overflow:hidden}}
|
||
@media(max-width:560px){.dialog-header{padding:10px}.patch-identity{gap:8px}.patch-identity .dialog-title{font-size:13px}.external-diff span{display:none}.dialog-header-actions .external-diff{width:30px;min-width:30px;height:30px;padding:0;justify-content:center;overflow:visible}.patch-toolbar{flex-wrap:wrap;padding:8px}.patch-summary{margin-left:0}.patch-footer{padding:8px}.selection-summary{gap:8px}.selection-actions{width:100%;justify-content:flex-end}.line-patch-hunk-head{height:auto;min-height:44px;flex-wrap:wrap}.line-patch-hunk-actions{width:100%;justify-content:flex-end}.line-patch-hunk-head code{max-width:160px}}
|
||
</style>
|