Files
GitLite/src/lib/components/LinePatchDialog.svelte
T
Christoph d26d2ac982 refactor(dialogs): add unified dialog header chrome and opt-in markup
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.
2026-09-11 22:50:31 +02:00

380 lines
24 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>