Introduce a shared dialog header style (.unified-dialog-header) in app.css and opt dialog components into the new chrome by updating their header markup. Headers now use unified-dialog-icon and unified-dialog-text elements (and import the matching Lucide icons where needed), which standardizes icon placement, title/eyebrow layout, close button styling and responsive behavior. The Command Palette layout was adjusted to include the new header and its grid rows. The CSS is explicitly opt-in (so page/section headers remain unchanged) and includes hover/focus styles and a theme-sensitive close color variable. This commit is a UI refactor only — no API or behavior logic changes.
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 unified-dialog-header">
|
||
<div class="patch-identity unified-dialog-heading"><span class="unified-dialog-icon" aria-hidden="true"><FileDiff size={23} aria-hidden="true" /></span><div class="unified-dialog-text"><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>
|