Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7d6a6e4f4 | ||
|
|
5aa9d291b6 | ||
|
|
82a5f16a0c | ||
|
|
daa3298c44 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "2026.9.1",
|
||||
"version": "2026.9.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitty",
|
||||
"version": "2026.9.1",
|
||||
"version": "2026.9.2",
|
||||
"dependencies": {
|
||||
"@lucide/svelte": "^1.21.0",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "2026.9.1",
|
||||
"version": "2026.9.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Gitty",
|
||||
"version": "2026.9.1",
|
||||
"version": "2026.9.2",
|
||||
"identifier": "com.gitty",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run prepare:lfs && npm run dev",
|
||||
|
||||
+2
-7
@@ -29,6 +29,7 @@
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
|
||||
import MergeProgressNotice from "./lib/components/MergeProgressNotice.svelte";
|
||||
import MergeBranchDialog from "./lib/components/MergeBranchDialog.svelte";
|
||||
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
||||
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
|
||||
@@ -5500,13 +5501,7 @@
|
||||
{/if}
|
||||
|
||||
{#if workspaceActive && mergeInProgress}
|
||||
<section class="notice conflict" role="status">
|
||||
<GitMerge size={17} aria-hidden="true" />
|
||||
<span>{hasConflicts ? "Merge in progress. Resolve all conflicts, then continue." : "Merge is ready to be completed."}</span>
|
||||
{#if hasConflicts}<button type="button" onclick={openResolveDialog} disabled={isBusy}>Resolve conflicts</button>{/if}
|
||||
<button type="button" onclick={continueMerge} disabled={isBusy || hasConflicts}>Continue</button>
|
||||
<button type="button" onclick={abortMerge} disabled={isBusy}>Abort</button>
|
||||
</section>
|
||||
<MergeProgressNotice conflictCount={conflictedFiles.length} {isBusy} language={appLanguage} onResolve={openResolveDialog} onContinue={continueMerge} onAbort={abortMerge} />
|
||||
{:else if workspaceActive && hasConflicts && !rebaseInProgress && !cherryPickInProgress}
|
||||
<section class="notice conflict" role="alert">
|
||||
<GitMerge size={17} aria-hidden="true" />
|
||||
|
||||
+30
-21
@@ -30,6 +30,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-sync-ahead: #e0a040;
|
||||
--color-sync-behind: #7aacff;
|
||||
--app-color-scheme: dark;
|
||||
--app-bg: #222328;
|
||||
--app-button-bg: #32363d;
|
||||
@@ -743,8 +745,8 @@
|
||||
|
||||
.tb-sync-group { display: inline-flex; align-items: center; gap: 4px; min-width: max-content; }
|
||||
.tb-sync { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 999px; font-size: 11px; font-weight: 800; white-space: nowrap; flex-shrink: 0; }
|
||||
.tb-sync.ahead { color: #e0a040; background: rgba(224,160,64,0.13); }
|
||||
.tb-sync.behind { color: #7aacff; background: rgba(122,172,255,0.13); }
|
||||
.tb-sync.ahead { color: var(--color-sync-ahead); background: color-mix(in srgb, var(--color-sync-ahead) 13%, transparent); }
|
||||
.tb-sync.behind { color: var(--color-sync-behind); background: color-mix(in srgb, var(--color-sync-behind) 13%, transparent); }
|
||||
|
||||
.tb-no-repo { color: var(--color-bar-muted); font-size: 12px; font-style: italic; }
|
||||
|
||||
@@ -842,8 +844,8 @@
|
||||
font-size: 9.5px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.repo-action-count.behind { color: #7aacff; }
|
||||
.repo-action-count.ahead { color: #e0a040; }
|
||||
.repo-action-count.behind { color: var(--color-sync-behind); }
|
||||
.repo-action-count.ahead { color: var(--color-sync-ahead); }
|
||||
.repo-action.sync-primary.publish-local {
|
||||
color: #f0bd6b;
|
||||
background: linear-gradient(180deg, rgba(224,160,64,.1), rgba(224,160,64,.045));
|
||||
@@ -1305,8 +1307,8 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
}
|
||||
.repo-row-meta .ahead { color: #e0a040; background: rgba(224,160,64,0.13); }
|
||||
.repo-row-meta .behind { color: #7aacff; background: rgba(122,172,255,0.13); }
|
||||
.repo-row-meta .ahead { color: var(--color-sync-ahead); background: color-mix(in srgb, var(--color-sync-ahead) 13%, transparent); }
|
||||
.repo-row-meta .behind { color: var(--color-sync-behind); background: color-mix(in srgb, var(--color-sync-behind) 13%, transparent); }
|
||||
.repo-row-icon {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
@@ -1823,8 +1825,8 @@
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sync-stats strong:first-of-type { color: #e0a040; background: rgba(224,160,64,0.13); }
|
||||
.sync-stats strong:last-of-type { color: #7aacff; background: rgba(122,172,255,0.13); }
|
||||
.sync-stats strong:first-of-type { color: var(--color-sync-ahead); background: color-mix(in srgb, var(--color-sync-ahead) 13%, transparent); }
|
||||
.sync-stats strong:last-of-type { color: var(--color-sync-behind); background: color-mix(in srgb, var(--color-sync-behind) 13%, transparent); }
|
||||
.sync-stats span { color: var(--color-ink-dim); background: rgba(94,110,156,0.13); }
|
||||
.sync-stats .sync-local-only {
|
||||
display: inline-flex;
|
||||
@@ -3362,12 +3364,13 @@
|
||||
opacity: 0.08;
|
||||
}
|
||||
.graph-svg path.graph-segment-ahead {
|
||||
stroke: #e0a040;
|
||||
filter: drop-shadow(0 0 3px rgba(224,160,64,0.24));
|
||||
stroke: var(--color-sync-ahead);
|
||||
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-sync-ahead) 24%, transparent));
|
||||
}
|
||||
.graph-svg path.graph-segment-behind {
|
||||
stroke: var(--color-sync-behind);
|
||||
stroke-dasharray: 4 4;
|
||||
filter: drop-shadow(0 0 3px rgba(122,172,255,0.2));
|
||||
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--color-sync-behind) 20%, transparent));
|
||||
}
|
||||
.graph-svg path.graph-ref-connector {
|
||||
opacity: 0.35;
|
||||
@@ -3391,12 +3394,12 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
.graph-dot.ahead {
|
||||
background: #e0a040;
|
||||
box-shadow: 0 0 0 1px rgba(224,160,64,0.42), 0 0 10px rgba(224,160,64,0.16);
|
||||
background: var(--color-sync-ahead);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-sync-ahead) 42%, transparent), 0 0 10px color-mix(in srgb, var(--color-sync-ahead) 16%, transparent);
|
||||
}
|
||||
.graph-dot.behind {
|
||||
background: var(--dot-color, #5a8cf8);
|
||||
box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8), 0 0 10px rgba(122,172,255,0.18);
|
||||
background: var(--color-sync-behind);
|
||||
box-shadow: 0 0 0 1px var(--color-sync-behind), 0 0 10px color-mix(in srgb, var(--color-sync-behind) 18%, transparent);
|
||||
}
|
||||
.graph-dot.tip { width: 14px; height: 14px; box-shadow: 0 0 0 1px var(--dot-color, #5a8cf8); }
|
||||
.graph-dot.merge {
|
||||
@@ -3455,10 +3458,10 @@
|
||||
}
|
||||
.graph-row + .graph-row .commit-body { border-top: 1px solid rgba(226,232,240,0.075); }
|
||||
.graph-row.graph-ahead-row .commit-body {
|
||||
box-shadow: inset 3px 0 0 rgba(224,160,64,0.72);
|
||||
box-shadow: inset 3px 0 0 color-mix(in srgb, var(--color-sync-ahead) 72%, transparent);
|
||||
}
|
||||
.graph-row.graph-behind-row .commit-body {
|
||||
box-shadow: inset 3px 0 0 rgba(122,172,255,0.72);
|
||||
box-shadow: inset 3px 0 0 color-mix(in srgb, var(--color-sync-behind) 72%, transparent);
|
||||
}
|
||||
.graph-row:hover .commit-body { background: rgba(30,39,57,0.72); }
|
||||
.graph-row:hover .graph-svg path { opacity: 1; stroke-width: 2.65; }
|
||||
@@ -6739,8 +6742,8 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
.workspace-status-spacer { flex: 1 1 auto; }
|
||||
.workspace-health > span { width: 8px; height: 8px; border-radius: 50%; background: #d9891b; }
|
||||
.workspace-health.clean > span { background: #2da44e; }
|
||||
.workspace-statusbar .ahead { color: #d9891b; }
|
||||
.workspace-statusbar .behind { color: var(--color-primary); }
|
||||
.workspace-statusbar .ahead { color: var(--color-sync-ahead); }
|
||||
.workspace-statusbar .behind { color: var(--color-sync-behind); }
|
||||
.workspace-statusbar .workspace-local-only {
|
||||
padding: 2px 6px;
|
||||
border: 1px dashed rgba(224,160,64,.42);
|
||||
@@ -7793,11 +7796,11 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
}
|
||||
:root[data-theme="light"] .workspace-branch svg { color: #0b63e6; }
|
||||
:root[data-theme="light"] .workspace-statusbar .ahead {
|
||||
color: #9a5200;
|
||||
color: var(--color-sync-ahead);
|
||||
font-weight: 800;
|
||||
}
|
||||
:root[data-theme="light"] .workspace-statusbar .behind {
|
||||
color: #0755c8;
|
||||
color: var(--color-sync-behind);
|
||||
font-weight: 800;
|
||||
}
|
||||
:root[data-theme="light"] .repo-action.sync-primary.publish-local,
|
||||
@@ -9059,3 +9062,9 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Shared sync colors keep every indicator consistent in the light theme. */
|
||||
:root[data-theme="light"] {
|
||||
--color-sync-ahead: #9a5200;
|
||||
--color-sync-behind: #0755c8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Check, ExternalLink, FileDiff, LoaderCircle, MousePointer2, X } from "@lucide/svelte";
|
||||
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";
|
||||
@@ -60,18 +60,26 @@
|
||||
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("");
|
||||
|
||||
let scopeLabel = $derived(staged ? "Staged changes" : "Unstaged changes");
|
||||
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 = "";
|
||||
@@ -191,6 +199,7 @@
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -266,149 +275,105 @@
|
||||
|
||||
function scrollToHunk(hunkId: string) {
|
||||
const target = patchScroll?.querySelector<HTMLElement>(`[data-hunk-id="${hunkId}"]`);
|
||||
if (patchScroll && target) patchScroll.scrollTop = Math.max(target.offsetTop - 8, 0);
|
||||
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" role="presentation">
|
||||
<div class="dialog line-patch-dialog" role="dialog" aria-modal="true" aria-label="Line patch">
|
||||
<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>
|
||||
<span class="eyebrow">{scopeLabel}</span>
|
||||
<p class="dialog-title" title={displayPath}>{displayPath}</p>
|
||||
</div>
|
||||
<div class="dialog-header-actions line-patch-header-actions">
|
||||
<div class="tool-surface-choice" aria-label={isGerman ? "Diff öffnen mit" : "Open diff with"}>
|
||||
<span>{isGerman ? "Öffnen mit" : "Open with"}</span>
|
||||
<button class="active" type="button" aria-pressed="true" title={isGerman ? "In Gitty anzeigen" : "Show in Gitty"}>
|
||||
<FileDiff size={13} aria-hidden="true" />Gitty
|
||||
</button>
|
||||
<button type="button" onclick={onExternalDiff} disabled={isBusy || isLoading} title={isGerman ? `In ${diffName} öffnen` : `Open in ${diffName}`}>
|
||||
<ExternalLink size={13} aria-hidden="true" /><span>{diffName}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn-sm" type="button" onclick={onRefresh} disabled={isBusy || isLoading}>Refresh</button>
|
||||
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<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 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} aria-hidden="true" />
|
||||
Loading patch...
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="blank-state">{error}</div>
|
||||
{:else if !patch.trim()}
|
||||
<div class="blank-state">No line patch available for this file.</div>
|
||||
{:else if parsed.binary || parsed.hunks.length === 0}
|
||||
<div class="blank-state">This change cannot be split into text lines.</div>
|
||||
<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:active={selectedCount > 0} class="line-patch-selection-bar">
|
||||
<div>
|
||||
<MousePointer2 size={14} aria-hidden="true" />
|
||||
{#if selectedCount > 0}
|
||||
<strong>{selectedCount} {selectedCount === 1 ? "line" : "lines"} selected</strong>
|
||||
<span>Shift-click to select a range.</span>
|
||||
{:else}
|
||||
<strong>Select changed lines</strong>
|
||||
<span>Choose individual additions or deletions below.</span>
|
||||
{/if}
|
||||
<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>
|
||||
{#if selectedCount > 0}
|
||||
<div class="line-patch-selected-actions">
|
||||
<button class="line-patch-hunk-button discard" type="button" onclick={() => applySelected(staged ? "discard-staged" : "discard-unstaged")} disabled={isBusy}>
|
||||
Discard selected
|
||||
</button>
|
||||
<button class="line-patch-hunk-button {staged ? "unstage" : "stage"}" type="button" onclick={() => applySelected(staged ? "unstage" : "stage")} disabled={isBusy}>
|
||||
{staged ? "Unstage selected" : "Stage selected"}
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => { selectedLineIds = new Set(); }} disabled={isBusy}>Clear</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="line-patch-workspace">
|
||||
<div class="line-patch-scroll" bind:this={patchScroll}>
|
||||
{#each parsed.hunks as hunk (hunk.id)}
|
||||
<section class="line-patch-hunk" data-hunk-id={hunk.id}>
|
||||
<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"
|
||||
onclick={() => toggleHunkSelection(hunk)}
|
||||
aria-label={`Select changed lines in ${hunk.header}`}
|
||||
aria-pressed={hunkSelectionState(hunk) === "all"}
|
||||
title="Select all changed lines in this hunk"
|
||||
>
|
||||
{#if hunkSelectionState(hunk) !== "none"}<Check size={12} aria-hidden="true" />{/if}
|
||||
<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>
|
||||
<code>{hunk.header}</code>
|
||||
<strong>{t("Abschnitt", "Hunk")} {index + 1}</strong><code title={hunk.header}>{hunk.header}</code>
|
||||
<div class="line-patch-hunk-actions">
|
||||
{#if staged}
|
||||
<button class="line-patch-hunk-button discard" type="button" onclick={() => applyHunkAction("discard-staged", hunk)} disabled={isBusy}>
|
||||
Discard Hunk
|
||||
</button>
|
||||
<button class="line-patch-hunk-button unstage" type="button" onclick={() => applyHunkAction("unstage", hunk)} disabled={isBusy}>
|
||||
Unstage Hunk
|
||||
</button>
|
||||
{:else}
|
||||
<button class="line-patch-hunk-button discard" type="button" onclick={() => applyHunkAction("discard-unstaged", hunk)} disabled={isBusy}>
|
||||
Discard Hunk
|
||||
</button>
|
||||
<button class="line-patch-hunk-button stage" type="button" onclick={() => applyHunkAction("stage", hunk)} disabled={isBusy}>
|
||||
Stage Hunk
|
||||
</button>
|
||||
{/if}
|
||||
<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"
|
||||
onclick={(event) => toggleLine(event, line)}
|
||||
aria-label={`${selectedLineIds.has(line.id) ? "Deselect" : "Select"} ${line.kind === "add" ? "added" : "deleted"} line ${line.newLine ?? line.oldLine ?? ""}`}
|
||||
aria-pressed={selectedLineIds.has(line.id)}
|
||||
title="Select line (Shift-click for range)"
|
||||
>
|
||||
{#if selectedLineIds.has(line.id)}<Check size={11} aria-hidden="true" />{/if}
|
||||
<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>
|
||||
{: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="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={`Jump to hunk ${index + 1} of ${parsed.hunks.length}`}
|
||||
aria-label={`Jump to hunk ${index + 1} of ${parsed.hunks.length}`}
|
||||
></button>
|
||||
{/each}
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { Check, ChevronRight, GitMerge, X } from "@lucide/svelte";
|
||||
let { conflictCount, isBusy, language, onResolve, onContinue, onAbort }: {
|
||||
conflictCount: number; isBusy: boolean; language: "de" | "en";
|
||||
onResolve: () => void; onContinue: () => void; onAbort: () => void;
|
||||
} = $props();
|
||||
const de = $derived(language === "de");
|
||||
const hasConflicts = $derived(conflictCount > 0);
|
||||
</script>
|
||||
|
||||
<section class="merge-progress" class:ready={!hasConflicts} role="status">
|
||||
<span class="status-icon">{#if hasConflicts}<GitMerge size={18} />{:else}<Check size={18} />{/if}</span>
|
||||
<div class="status-copy">
|
||||
<strong>{hasConflicts ? (de ? "Merge läuft" : "Merge in progress") : (de ? "Bereit zum Abschließen" : "Ready to complete")}</strong>
|
||||
<span>{hasConflicts ? `${conflictCount} ${de ? (conflictCount === 1 ? "Datei mit Konflikten" : "Dateien mit Konflikten") : (conflictCount === 1 ? "file with conflicts" : "files with conflicts")}` : (de ? "Alle Konflikte sind gelöst." : "All conflicts are resolved.")}</span>
|
||||
</div>
|
||||
<div class="merge-actions">
|
||||
{#if hasConflicts}<button class="resolve-action" type="button" onclick={onResolve} disabled={isBusy}><GitMerge size={14} />{de ? "Konflikte lösen" : "Resolve conflicts"}</button>{/if}
|
||||
<button class="continue-action" type="button" onclick={onContinue} disabled={isBusy || hasConflicts} title={hasConflicts ? (de ? "Zuerst alle Konflikte lösen" : "Resolve all conflicts first") : undefined}>{de ? "Fortsetzen" : "Continue"}<ChevronRight size={14} /></button>
|
||||
<span class="action-divider" aria-hidden="true"></span>
|
||||
<button class="abort-action" type="button" onclick={onAbort} disabled={isBusy}><X size={14} />{de ? "Abbrechen" : "Abort"}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.merge-progress{--status-color:var(--color-sync-ahead);display:flex;align-items:center;gap:12px;flex-shrink:0;min-height:50px;padding:8px 14px;border-bottom:1px solid var(--color-border);border-left:3px solid var(--status-color);background:color-mix(in srgb,var(--status-color) 5%,var(--app-bg));color:var(--color-ink);font-size:13px}
|
||||
.merge-progress.ready{--status-color:#68c878}.status-icon{display:flex;align-items:center;color:var(--status-color)}.status-copy{display:flex;align-items:center;gap:14px;min-width:0;flex-wrap:wrap}.status-copy strong{font-size:13px;font-weight:600;white-space:nowrap}.status-copy>span{color:var(--color-ink-muted);font-size:12px}
|
||||
.merge-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-shrink:0}.merge-actions button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:30px;padding:5px 10px;border:1px solid var(--color-border-input);background:transparent;color:var(--color-ink-muted);font-size:12px;font-weight:500;white-space:nowrap}.merge-actions button:hover:not(:disabled){background:var(--color-surface-hover);color:var(--color-ink)}.merge-actions button:disabled{opacity:.4}.merge-actions .resolve-action{border-color:color-mix(in srgb,var(--status-color) 45%,var(--color-border));background:color-mix(in srgb,var(--status-color) 10%,transparent);color:var(--status-color)}.ready .continue-action{color:var(--status-color);border-color:color-mix(in srgb,var(--status-color) 45%,var(--color-border));background:color-mix(in srgb,var(--status-color) 10%,transparent)}.action-divider{height:20px;width:1px;background:var(--color-border);margin:0 3px}.merge-actions .abort-action{border-color:transparent}.merge-actions .abort-action:hover:not(:disabled){color:#ef8080;background:color-mix(in srgb,#ef8080 8%,transparent)}
|
||||
@media(max-width:700px){.merge-progress{flex-wrap:wrap;gap:8px}.status-copy{gap:8px}.merge-actions{width:100%;justify-content:flex-end}.merge-actions button{font-size:11px;padding:5px 8px}}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, Circle, Download, FolderGit2, GitBranch, GitPullRequest, LoaderCircle, Plus, Search, Star, TriangleAlert, Trash2, X } from "@lucide/svelte";
|
||||
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, Circle, Download, FolderGit2, GitBranch, GitPullRequest, LayoutGrid, List, LoaderCircle, Plus, Search, Star, TriangleAlert, Trash2, X } from "@lucide/svelte";
|
||||
import { listIntegrationReviewRequests, listRemotes } from "../git";
|
||||
import { configuredIntegrationSources, integrationCredentialKey } from "../integrations";
|
||||
import type { GitIntegrationSettings, GitIntegrationSource, IntegrationReviewRequest, StoredCredential } from "../types";
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
const STORAGE_KEY = "gitty.dashboard.v1";
|
||||
let query = "";
|
||||
let viewMode: "cards" | "list" = "list";
|
||||
let selectedWorkspace = "";
|
||||
let collapsedCategories = new Set<string>();
|
||||
let workspaces: Workspace[] = [];
|
||||
@@ -92,7 +93,8 @@
|
||||
|
||||
function loadPreferences() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}") as { workspaces?: unknown; assignments?: unknown };
|
||||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}") as { workspaces?: unknown; assignments?: unknown; viewMode?: unknown };
|
||||
viewMode = saved.viewMode === "cards" ? "cards" : "list";
|
||||
if (Array.isArray(saved.workspaces)) {
|
||||
workspaces = saved.workspaces.filter((item): item is Workspace => Boolean(
|
||||
item && typeof item === "object"
|
||||
@@ -111,10 +113,15 @@
|
||||
}
|
||||
|
||||
function savePreferences() {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ workspaces, assignments })); }
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ workspaces, assignments, viewMode })); }
|
||||
catch { /* Preferences remain optional when storage is unavailable. */ }
|
||||
}
|
||||
|
||||
function setViewMode(mode: "cards" | "list") {
|
||||
viewMode = mode;
|
||||
savePreferences();
|
||||
}
|
||||
|
||||
function openWorkspaceDialog() {
|
||||
workspaceName = "";
|
||||
workspaceError = "";
|
||||
@@ -285,8 +292,7 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function pullRequestTitle(repo: DashboardRepository): string {
|
||||
const badge = pullRequestBadges[repo.path];
|
||||
function pullRequestTitle(badge: PullRequestBadgeState | undefined): string {
|
||||
if (!badge || badge.status === "loading") return de ? "Offene Pull Requests werden geladen" : "Loading open pull requests";
|
||||
if (badge.status === "error" || badge.status === "unavailable") return badge.message;
|
||||
return de ? `${badge.count} offene Pull Requests` : `${badge.count} open pull requests`;
|
||||
@@ -314,6 +320,10 @@
|
||||
<Search size={14} />
|
||||
<input bind:value={query} autocomplete="off" spellcheck="false" placeholder={de ? "Repositories suchen …" : "Search repositories …"} aria-label={de ? "Repositories suchen" : "Search repositories"} />
|
||||
</label>
|
||||
<div class="view-switch" role="group" aria-label={de ? "Repository-Ansicht" : "Repository view"}>
|
||||
<button type="button" class:active={viewMode === "list"} aria-pressed={viewMode === "list"} aria-label={de ? "Listenansicht" : "List view"} title={de ? "Listenansicht" : "List view"} onclick={() => setViewMode("list")}><List size={16} /></button>
|
||||
<button type="button" class:active={viewMode === "cards"} aria-pressed={viewMode === "cards"} aria-label={de ? "Kachelansicht" : "Card view"} title={de ? "Kachelansicht" : "Card view"} onclick={() => setViewMode("cards")}><LayoutGrid size={15} /></button>
|
||||
</div>
|
||||
<div class="workspace-tools">
|
||||
<SelectMenu
|
||||
class="workspace-select"
|
||||
@@ -331,7 +341,7 @@
|
||||
|
||||
<div class="dashboard-summary">{repos.length} Repositories <span>·</span> {changedCount} {de ? "mit Änderungen" : "with changes"}</div>
|
||||
|
||||
<div class="dashboard-content">
|
||||
<div class="dashboard-content" class:table-view={viewMode === "list"}>
|
||||
{#each categories as category (category.id)}
|
||||
<section class="dashboard-section">
|
||||
<header class="section-header">
|
||||
@@ -342,28 +352,26 @@
|
||||
</header>
|
||||
{#if !collapsedCategories.has(category.id)}
|
||||
{#if category.repos.length > 0}
|
||||
<div class="repo-grid">
|
||||
<div class="repo-grid" class:list-view={viewMode === "list"} class:tiles-view={viewMode === "cards"}>
|
||||
{#each category.repos as repo (repo.path)}
|
||||
{@const pullRequests = pullRequestBadges[repo.path]}
|
||||
<article class="repo-card" class:open={repo.isOpen}>
|
||||
<button class="card-main" type="button" onclick={() => openCard(repo.path)} disabled={isBusy} title={repo.path}>
|
||||
<span class="card-title"><FolderGit2 size={15} /><strong>{repo.name}</strong></span>
|
||||
<span class="card-path">{repo.path}</span>
|
||||
<span class="card-branch"><GitBranch size={13} />{repo.branch ?? (de ? "Branch unbekannt" : "Unknown branch")}</span>
|
||||
<article class="repo-card" class:open={repo.isOpen} class:favorite-category={category.id === "favorites"}>
|
||||
<button class="card-main" type="button" onclick={() => openCard(repo.path)} disabled={isBusy}>
|
||||
<span class="card-title"><FolderGit2 size={15} /><strong title={repo.path}>{repo.name}</strong></span>
|
||||
<span class="card-branch" title={repo.branch ?? undefined}><GitBranch size={13} />{repo.branch ?? (de ? "Branch unbekannt" : "Unknown branch")}</span>
|
||||
<span class="card-status">
|
||||
{#if !repo.known}<span class="muted"><Circle size={10} />{de ? "Status nicht geladen" : "Status not loaded"}</span>
|
||||
{#if !repo.known}<span class="muted"><Circle class="filled" size={9} />{de ? "Status nicht geladen" : "Status not loaded"}</span>
|
||||
{:else if repo.changed > 0}<span class="changed"><Circle class="filled" size={9} />{repo.changed} {de ? (repo.changed === 1 ? "Änderung" : "Änderungen") : (repo.changed === 1 ? "change" : "changes")}</span>
|
||||
{:else}<span class="clean"><Circle class="filled" size={9} />{de ? "Sauber" : "Clean"}</span>{/if}
|
||||
{#if repo.ahead > 0}<span class="ahead"><ArrowUp size={13} />{repo.ahead} {de ? "voraus" : "ahead"}</span>{/if}
|
||||
{#if repo.behind > 0}<span class="behind"><ArrowDown size={13} />{repo.behind} {de ? "zurück" : "behind"}</span>{/if}
|
||||
</span>
|
||||
<span class="list-sync">{#if repo.known}<span class:ahead={repo.ahead > 0} title={de ? `${repo.ahead} Commits voraus` : `${repo.ahead} commits ahead`}><ArrowUp size={14} />{repo.ahead}</span><span class:behind={repo.behind > 0} title={de ? `${repo.behind} Commits zurück` : `${repo.behind} commits behind`}><ArrowDown size={14} />{repo.behind}</span>{:else}<span class="muted">—</span>{#if viewMode === "cards"}<span class="muted">—</span>{/if}{/if}</span>
|
||||
</button>
|
||||
<div class="card-actions">
|
||||
<button class:has-open-prs={pullRequests?.status === "ready" && pullRequests.count > 0} class:pr-error={pullRequests?.status === "error"} class="pr-badge" type="button" onclick={(event) => openPullRequests(event, repo)} disabled={isBusy || !pullRequests || pullRequests.status !== "ready"} aria-label={`${repo.name}: ${pullRequestTitle(repo)}`} title={pullRequestTitle(repo)}>
|
||||
<button class:has-open-prs={pullRequests?.status === "ready" && pullRequests.count > 0} class:pr-error={pullRequests?.status === "error"} class="pr-badge" type="button" onclick={(event) => openPullRequests(event, repo)} disabled={isBusy || !pullRequests || pullRequests.status !== "ready"} aria-label={`${repo.name}: ${pullRequestTitle(pullRequests)}`} title={pullRequestTitle(pullRequests)}>
|
||||
{#if !pullRequests || pullRequests.status === "loading"}<LoaderCircle class="spin" size={14} />{:else if pullRequests.status === "error"}<TriangleAlert size={14} />{:else}<GitPullRequest size={14} /><span>{pullRequests.status === "ready" ? pullRequests.count : "–"}</span>{/if}
|
||||
</button>
|
||||
<button class:active={repo.favorite} type="button" onclick={(event) => favoriteCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Favorit umschalten" : "Toggle favorite"}`} aria-pressed={repo.favorite}><Star size={15} /></button>
|
||||
{#if repo.isOpen}<button type="button" onclick={(event) => closeCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Schließen" : "Close"}`} title={de ? "Repository schließen" : "Close repository"}><X size={15} /></button>{/if}
|
||||
{#if repo.isOpen && category.id === "open"}<button type="button" onclick={(event) => closeCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Schließen" : "Close"}`} title={de ? "Repository schließen" : "Close repository"}><X size={15} /></button>{/if}
|
||||
{#if category.id === "recent"}<button type="button" onclick={(event) => removeRecentCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Aus Recent entfernen" : "Remove from recent"}`} title={de ? "Aus Recent entfernen" : "Remove from recent"}><X size={15} /></button>{/if}
|
||||
</div>
|
||||
</article>
|
||||
@@ -398,11 +406,74 @@
|
||||
.repo-dashboard{display:flex;flex:1;min-height:0;flex-direction:column;overflow:hidden;color:var(--color-ink);background:var(--app-bg);font-size:12px}button,input{font:inherit}button{color:inherit;cursor:pointer}button:disabled{cursor:default;opacity:.5}
|
||||
.dashboard-header{display:flex;min-height:58px;align-items:center;justify-content:space-between;gap:16px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-header h1{margin:0;font-size:18px;line-height:1.2;font-weight:700}.header-actions,.workspace-tools{display:flex;align-items:center;gap:8px}.header-actions button,.workspace-tools button{display:inline-flex;min-height:30px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}button.primary{border-color:var(--color-primary);background:var(--color-primary);color:#fff}
|
||||
.dashboard-toolbar{display:flex;min-height:48px;align-items:center;gap:8px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-search{display:flex;width:300px;height:30px;align-items:center;gap:8px;padding:0 9px;border:1px solid var(--color-border-input);border-radius:var(--ui-radius-sm);color:var(--color-ink-faint);background:var(--app-input-bg)}.dashboard-search:focus-within{border-color:var(--color-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 14%,transparent)}.dashboard-search :global(svg){flex:0 0 auto}.dashboard-search input{width:100%;min-width:0;height:100%;padding:0;border:0;border-radius:0;outline:0;color:var(--color-ink);background:transparent;box-shadow:none}.dashboard-search input:focus,.dashboard-search input:focus-visible{border:0;outline:0;box-shadow:none}.workspace-tools{margin-left:auto}.workspace-tools :global(.workspace-select){width:240px}.workspace-tools :global(.workspace-select .select-menu-trigger){height:30px;min-height:30px;padding:0 9px;border-color:var(--color-border-input);background:var(--app-input-bg);font-size:12px;font-weight:500}.workspace-tools .workspace-delete{width:30px;min-width:30px;padding:0;border-color:color-mix(in srgb,#df626b 55%,var(--color-border));color:#df747b;background:color-mix(in srgb,#c92f3a 8%,var(--app-button-bg))}.workspace-tools .workspace-delete:hover:not(:disabled){border-color:#df626b;color:#fff;background:#c93b45;box-shadow:0 0 0 2px color-mix(in srgb,#c92f3a 18%,transparent)}
|
||||
.dashboard-summary{padding:8px 20px;color:var(--color-ink-muted)}.dashboard-summary span{padding:0 6px;color:var(--color-ink-faint)}.dashboard-content{flex:1;min-height:0;overflow:auto;padding:0 20px 24px}.dashboard-section+.dashboard-section{margin-top:12px}.section-header{min-height:31px}.section-toggle{display:flex;width:100%;min-height:31px;align-items:center;justify-content:flex-start;gap:7px;padding:0;border:0;background:transparent;color:var(--color-ink);text-align:left}.section-toggle:hover:not(:disabled){border:0;background:transparent;color:var(--color-ink)}.section-toggle :global(svg){flex:0 0 auto;color:var(--color-ink-muted)}.section-toggle strong{font-size:13px;line-height:1.2;font-weight:750}.section-toggle span{color:var(--color-ink-faint);font-weight:400}.section-toggle i{height:1px;flex:1;background:var(--color-border-subtle)}.category-empty{margin:0;padding:9px 12px;border:1px solid var(--color-border-subtle);color:var(--color-ink-faint);background:var(--color-surface)}
|
||||
.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.repo-card{position:relative;min-width:0;height:138px;border:1px solid var(--color-border);background:var(--color-surface)}.repo-card:hover,.repo-card:focus-within{border-color:var(--color-border-input);background:var(--color-surface-hover)}.card-main{display:flex;width:100%;height:100%;flex-direction:column;align-items:flex-start;gap:7px;padding:12px 104px 11px 14px;border:0;background:transparent;text-align:left}.card-main:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.card-title{display:flex;max-width:100%;align-items:center;gap:8px}.card-title strong{overflow:hidden;font-size:14px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.card-path{width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-branch,.card-status,.card-status>span{display:flex;align-items:center;gap:6px}.card-branch{max-width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-status{flex-wrap:wrap;gap:8px 18px;margin-top:auto}.card-status :global(.filled){fill:currentColor}.changed,.behind{color:#eeb94e}.clean{color:#68c878}.ahead{color:var(--color-accent)}.muted{color:var(--color-ink-muted)}
|
||||
.dashboard-summary{padding:8px 20px;color:var(--color-ink-muted)}.dashboard-summary span{padding:0 6px;color:var(--color-ink-faint)}.dashboard-content{flex:1;min-height:0;overflow:auto;padding:0 20px 24px}.dashboard-section+.dashboard-section{margin-top:20px}.section-header{min-height:31px;margin-bottom:6px}.section-toggle{display:flex;width:100%;min-height:31px;align-items:center;justify-content:flex-start;gap:7px;padding:0;border:0;background:transparent;color:var(--color-ink);text-align:left}.section-toggle:hover:not(:disabled){border:0;background:transparent;color:var(--color-ink)}.section-toggle :global(svg){flex:0 0 auto;color:var(--color-ink-muted)}.section-toggle strong{font-size:13px;line-height:1.2;font-weight:750}.section-toggle span{color:var(--color-ink-faint);font-weight:400}.section-toggle i{height:1px;flex:1;background:var(--color-border-subtle)}.category-empty{margin:0;padding:9px 12px;border:1px solid var(--color-border-subtle);color:var(--color-ink-faint);background:var(--color-surface)}
|
||||
.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.repo-card{position:relative;min-width:0;height:138px;border:1px solid var(--color-border);background:var(--color-surface)}.repo-card:hover,.repo-card:focus-within{border-color:var(--color-border-input);background:var(--color-surface-hover)}.card-main{display:flex;width:100%;height:100%;flex-direction:column;align-items:flex-start;gap:7px;padding:12px 104px 11px 14px;border:0;background:transparent;text-align:left}.card-main:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.card-title{display:flex;max-width:100%;align-items:center;gap:8px}.card-title strong{overflow:hidden;font-size:14px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.card-branch,.card-status,.card-status>span{display:flex;align-items:center;gap:6px}.card-branch{max-width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-status{flex-wrap:wrap;gap:8px 18px;margin-top:auto}.card-status :global(.filled){fill:currentColor}.changed{color:#eeb94e}.behind{color:var(--color-sync-behind)}.clean{color:#68c878}.ahead{color:var(--color-sync-ahead)}.muted{color:var(--color-ink-muted)}
|
||||
.card-actions{position:absolute;top:8px;right:8px;z-index:2;display:flex;align-items:center;gap:2px}.card-actions button{display:grid;width:26px;height:26px;place-items:center;padding:0;border:0;color:var(--color-ink-muted);background:transparent}.card-actions button:hover:not(:disabled){color:var(--color-ink);background:var(--color-surface-dim)}.card-actions button.active{color:var(--color-accent)}.card-actions button.active :global(svg){fill:color-mix(in srgb,var(--color-accent) 22%,transparent)}.card-actions .pr-badge{display:flex;width:auto;min-width:31px;grid-template-columns:none;align-items:center;justify-content:center;gap:4px;padding:0 5px;color:var(--color-ink-faint);font-size:11px}.card-actions .pr-badge:disabled{opacity:.72}.card-actions .pr-badge.has-open-prs{color:var(--color-accent)}.card-actions .pr-badge.pr-error{color:#e0a35b}.card-actions .pr-badge:hover:not(:disabled){color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 9%,transparent)}
|
||||
.dashboard-footer{display:flex;min-height:30px;align-items:center;padding:0 20px;border-top:1px solid var(--color-border-subtle);color:var(--color-ink-muted)}
|
||||
.workspace-dialog{width:min(420px,calc(100vw - 32px));margin:auto;padding:0;border:1px solid var(--color-border);color:var(--color-ink);background:var(--app-dialog-bg);box-shadow:var(--app-dialog-shadow);font-size:12px}.workspace-dialog::backdrop{background:var(--app-dialog-backdrop)}.workspace-dialog form>header,.workspace-dialog form>footer{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--app-dialog-chrome)}.workspace-dialog form>header{border-bottom:1px solid var(--color-border)}.workspace-dialog form>footer{justify-content:flex-end;gap:8px;border-top:1px solid var(--color-border)}.workspace-dialog h2{margin:0;font-size:13px}.workspace-dialog header button{width:26px;height:26px;padding:0;border:0;background:transparent}.workspace-dialog form>label,.workspace-dialog form>input,.workspace-dialog fieldset,.dialog-error{margin-right:12px;margin-left:12px}.workspace-dialog form>label{display:block;margin-top:12px;margin-bottom:5px;color:var(--color-ink-muted)}.workspace-dialog form>input{width:calc(100% - 24px);height:30px;padding:0 8px;border:1px solid var(--color-border-input);color:var(--color-ink);background:var(--app-input-bg)}.dialog-error{margin-top:7px;color:#ed9292}.workspace-dialog fieldset{margin-top:14px;margin-bottom:14px;padding:0;border:0}.workspace-dialog legend{margin-bottom:6px;color:var(--color-ink-muted)}.workspace-repos{max-height:190px;overflow:auto;border:1px solid var(--color-border-subtle)}.workspace-repos label{display:grid;grid-template-columns:auto minmax(100px,auto) 1fr;align-items:center;gap:8px;min-height:32px;padding:4px 8px;border-bottom:1px solid var(--color-border-subtle)}.workspace-repos label:last-child{border-bottom:0}.workspace-repos small{overflow:hidden;color:var(--color-ink-faint);text-overflow:ellipsis;white-space:nowrap}.workspace-dialog footer button{display:inline-flex;min-height:28px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}
|
||||
@media(max-width:1000px){.dashboard-toolbar{flex-wrap:wrap;gap:8px}.dashboard-search{flex:1 1 260px}.workspace-tools{margin-left:0}.repo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:680px){.dashboard-header{min-height:auto;align-items:flex-start;flex-direction:column}.header-actions{flex-wrap:wrap}.workspace-tools{width:100%}.workspace-tools :global(.workspace-select){min-width:0;flex:1}.repo-grid{grid-template-columns:1fr}.repo-card{height:132px}}
|
||||
.view-switch{display:flex;flex:0 0 auto;height:30px;border:1px solid var(--color-border-input);background:var(--app-input-bg)}
|
||||
.view-switch button{display:grid;width:32px;min-height:0;height:28px;place-items:center;padding:0;border:0;border-radius:0;color:var(--color-ink-muted);background:transparent}
|
||||
.view-switch button+button{border-left:1px solid var(--color-border-subtle)}
|
||||
.view-switch button.active{color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 12%,var(--app-input-bg));box-shadow:inset 0 -2px var(--color-accent)}
|
||||
.view-switch button:hover{color:var(--color-ink);background:var(--color-surface-hover)}
|
||||
.view-switch button:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}
|
||||
/* Approved list design: shared columns across every category. */
|
||||
.table-view{--repo-list-columns:minmax(180px,1.8fr) minmax(140px,1fr) minmax(150px,1fr) minmax(110px,.75fr)}
|
||||
.table-view .repo-card{display:grid;grid-template-columns:minmax(0,1fr) minmax(170px,22%)}
|
||||
.list-view .card-main{display:grid;grid-template-columns:var(--repo-list-columns);gap:16px;padding:0 18px;align-items:center}
|
||||
.repo-grid.list-view{display:block;border:0}
|
||||
.table-view .repo-card{height:auto;min-height:48px;border:0;border-bottom:1px solid var(--color-border-subtle);background:transparent}
|
||||
.table-view .repo-card:hover,.table-view .repo-card:focus-within{background:color-mix(in srgb,var(--color-accent) 5%,var(--color-surface))}
|
||||
.table-view .repo-card:focus-within{box-shadow:inset 2px 0 var(--color-accent)}
|
||||
.list-view .card-main{height:auto;min-height:48px;min-width:0;justify-content:stretch;align-content:center;text-align:left}
|
||||
.list-view .card-title{min-width:0;gap:9px}.list-view .card-title strong{font-size:12px;font-weight:500}.list-view .card-title :global(svg){flex:none}
|
||||
.list-view .card-branch{min-width:0;font-size:12px}.list-view .card-branch :global(svg){flex:none}
|
||||
.list-view .card-status{margin:0;gap:5px;font-size:12px;flex-wrap:nowrap;white-space:nowrap}.list-view .card-status .muted{font-style:italic}
|
||||
.list-sync{display:flex;align-items:center;gap:18px;color:var(--color-ink-faint);font-size:12px}.list-sync>span{display:flex;align-items:center;gap:5px}
|
||||
.list-view .card-actions{position:static;display:grid;grid-template-columns:minmax(60px,1fr) 40px 40px;gap:0;align-items:center;margin:0}
|
||||
.list-view .card-actions .pr-badge{justify-self:start;justify-content:flex-start;padding:0;gap:8px;color:var(--color-accent)}
|
||||
.list-view .pr-badge span{min-width:18px;padding:1px 4px;border:1px solid var(--color-border);border-radius:2px;color:var(--color-ink-muted);font-size:11px;text-align:center}
|
||||
.list-view .card-actions button{height:28px;width:28px}.list-view .card-actions button :global(svg){width:16px;height:16px}
|
||||
.list-view .card-actions button.active :global(svg){fill:var(--color-accent)}
|
||||
.table-view .category-empty{border:0;border-bottom:1px solid var(--color-border-subtle);background:transparent}
|
||||
.list-view .card-actions .pr-badge{width:auto;min-width:60px}.list-view .card-actions .pr-badge :global(svg){flex:0 0 16px}
|
||||
/* Approved tile layout: repository identity above a shared status footer. */
|
||||
.tiles-view{gap:12px}
|
||||
.tiles-view .repo-card{height:150px;border-radius:3px;background:color-mix(in srgb,var(--color-surface) 35%,var(--app-bg))}
|
||||
.tiles-view .repo-card:focus-within{box-shadow:inset 3px 0 var(--color-accent)}
|
||||
.tiles-view .card-main{display:grid;grid-template-columns:minmax(0,1fr) 104px 42px;grid-template-rows:24px 24px 1fr 30px;gap:0 10px;padding:16px 16px 10px;align-items:center;align-content:stretch}
|
||||
.tiles-view .card-main:after{position:absolute;left:16px;right:16px;bottom:51px;height:1px;background:var(--color-border);content:"";pointer-events:none}
|
||||
.tiles-view .card-title{grid-column:1/-1;grid-row:1;min-width:0;padding:0 60px 0 48px;max-width:100%;display:block}
|
||||
.tiles-view .card-title strong{display:block;font-size:13px;font-weight:650;line-height:22px}
|
||||
.tiles-view .card-title>:global(svg){position:absolute;top:21px;left:18px;width:32px;height:32px;color:var(--color-ink-muted);stroke-width:1.4}
|
||||
.tiles-view .card-branch{grid-column:1/-1;grid-row:2;padding-left:48px;min-width:0;display:block;overflow:hidden;text-overflow:ellipsis;font-size:11px;line-height:22px}
|
||||
.tiles-view .card-branch>:global(svg){display:inline-block;margin-right:6px;vertical-align:-2px}
|
||||
.tiles-view .card-status{grid-column:1;grid-row:4;min-width:0;flex-wrap:nowrap;margin:0;font-size:11px;gap:0;white-space:nowrap}
|
||||
.tiles-view .card-status>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||||
.tiles-view .list-sync{grid-column:2;grid-row:4;gap:16px;font-size:11px}
|
||||
.tiles-view .card-actions{position:static;display:contents}
|
||||
.tiles-view .card-actions button{position:absolute;top:14px;right:14px;width:26px;height:26px}
|
||||
.tiles-view .card-actions button[aria-pressed]{right:44px}
|
||||
.tiles-view .card-actions .pr-badge{top:auto;bottom:12px;right:14px;width:42px;min-width:0;height:26px;gap:6px;padding:0;font-size:12px;color:var(--color-ink-muted)}
|
||||
.tiles-view .card-actions .pr-badge.has-open-prs{color:var(--color-accent)}
|
||||
.tiles-view .card-actions .pr-badge.pr-error{color:#e0a35b}
|
||||
.tiles-view .card-actions .pr-badge>:global(svg){width:16px;height:16px;flex:none}
|
||||
.tiles-view .card-actions button.active>:global(svg){fill:var(--color-accent)}
|
||||
.table-view{--repo-list-columns:minmax(180px,1.8fr) minmax(140px,1fr) minmax(150px,1fr) minmax(110px,.75fr)}
|
||||
.table-view .dashboard-section{min-width:900px}
|
||||
.list-view .card-title strong,.list-view .card-branch,.list-view .card-status{font-size:13px}
|
||||
.list-view .card-actions .pr-badge.pr-error{color:#e0a35b}
|
||||
.list-view .card-actions .pr-badge:disabled{color:var(--color-ink-faint)}
|
||||
@media(min-width:1001px) and (max-width:1150px){.tiles-view{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:420px){.tiles-view .card-main{grid-template-columns:minmax(0,1fr) 82px 34px;gap:0 6px;padding-right:12px;padding-left:12px}.tiles-view .list-sync{gap:8px}.tiles-view .card-title{padding-left:42px}.tiles-view .card-branch{padding-left:42px}.tiles-view .card-title>:global(svg){left:14px;width:28px;height:28px}.tiles-view .card-status{font-size:10px}}
|
||||
.tiles-view .card-title strong{font-size:14px}
|
||||
.tiles-view .card-branch,.tiles-view .card-status,.tiles-view .list-sync{font-size:12px}
|
||||
.tiles-view .card-status .changed,.tiles-view .card-status .clean{color:var(--color-ink-muted)}
|
||||
.tiles-view .card-status .changed>:global(svg){color:#eeb94e;flex:none}.tiles-view .card-status .clean>:global(svg){color:#68c878;flex:none}
|
||||
.tiles-view .favorite-category .card-actions button[aria-pressed]{right:14px}
|
||||
.tiles-view .favorite-category .card-title{padding-right:30px}
|
||||
.list-view .card-status .changed,.list-view .card-status .clean{color:var(--color-ink-muted)}
|
||||
.list-view .card-status .changed>:global(svg){color:#eeb94e}.list-view .card-status .clean>:global(svg){color:#68c878}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { AlertCircle, Check, ExternalLink, GitMerge, LoaderCircle, X } from "@lucide/svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { AlertCircle, Check, ChevronDown, ChevronLeft, ChevronRight, ExternalLink, FileText, GitMerge, LoaderCircle, Pencil, X } from "@lucide/svelte";
|
||||
import type { ConflictChoice, ConflictFile, ConflictPart, GitFileStatus, PreparedResolution } from "../types";
|
||||
|
||||
type ConflictRegion = Extract<ConflictPart, { kind: "conflict" }>;
|
||||
type ResolveSplitRow =
|
||||
| { type: "marker"; conflictIndex: number }
|
||||
| {
|
||||
@@ -53,9 +52,33 @@
|
||||
let resolveContent = $state("");
|
||||
let manualMode = $state(false);
|
||||
let binarySide = $state<"ours" | "theirs" | null>(null);
|
||||
let resolveBeforePane = $state<HTMLDivElement | null>(null);
|
||||
let resolveAfterPane = $state<HTMLDivElement | null>(null);
|
||||
let isSyncingResolveScroll = false;
|
||||
let dialogElement: HTMLDivElement;
|
||||
onMount(() => {
|
||||
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
dialogElement.focus();
|
||||
return () => previousFocus?.focus();
|
||||
});
|
||||
function trapFocus(event: KeyboardEvent) {
|
||||
if (event.key !== "Tab") return;
|
||||
const focusable = Array.from(dialogElement.querySelectorAll<HTMLElement>('button:not(:disabled), textarea:not(:disabled), [tabindex="0"]')).filter(element => element.getClientRects().length > 0);
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (!first) { event.preventDefault(); return; }
|
||||
if (event.shiftKey && (document.activeElement === first || document.activeElement === dialogElement)) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
let comparison = $state<HTMLDivElement | null>(null);
|
||||
let activeConflict = $state(0);
|
||||
let bothMenu = $state<number | null>(null);
|
||||
const t = (de: string, en: string) => isGerman ? de : en;
|
||||
let previewLines = $derived(manualMode ? resolveContent.split("\n") : conflictParts.flatMap(part =>
|
||||
part.kind === "text" ? part.lines : conflictChoices[part.index] == null
|
||||
? [`[${t("Konflikt", "Conflict")} ${part.index + 1} ${t("noch offen", "unresolved")}]`]
|
||||
: buildResolution([part], conflictChoices).split("\n")));
|
||||
function navigateConflict(direction: number) {
|
||||
activeConflict = Math.max(0, Math.min(conflictRegionCount - 1, activeConflict + direction));
|
||||
comparison?.querySelector(`[data-conflict="${activeConflict}"]`)?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const c = conflict;
|
||||
@@ -63,6 +86,8 @@
|
||||
const parts = conflictParts;
|
||||
|
||||
untrack(() => {
|
||||
activeConflict = 0;
|
||||
bothMenu = null;
|
||||
if (!c) {
|
||||
conflictChoices = [];
|
||||
resolveContent = "";
|
||||
@@ -95,7 +120,6 @@
|
||||
});
|
||||
|
||||
let conflictRegionCount = $derived(conflictParts.filter((p) => p.kind === "conflict").length);
|
||||
let conflictRegions = $derived(conflictOnlyParts(conflictParts));
|
||||
let resolveSplitRows = $derived(buildResolveSplitRows(conflictParts));
|
||||
let unresolvedCount = $derived(manualMode ? 0 : conflictChoices.filter((c) => c == null).length);
|
||||
let resolvedContent = $derived(manualMode ? resolveContent : buildResolution(conflictParts, conflictChoices));
|
||||
@@ -147,10 +171,6 @@
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function conflictOnlyParts(parts: ConflictPart[]): ConflictRegion[] {
|
||||
return parts.filter((part): part is ConflictRegion => part.kind === "conflict");
|
||||
}
|
||||
|
||||
function buildResolveSplitRows(parts: ConflictPart[]): ResolveSplitRow[] {
|
||||
const rows: ResolveSplitRow[] = [];
|
||||
let leftNum = 0;
|
||||
@@ -174,7 +194,6 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({ type: "marker", conflictIndex: part.index });
|
||||
const count = Math.max(part.oursLines.length, part.theirsLines.length);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hasOurs = i < part.oursLines.length;
|
||||
@@ -192,36 +211,22 @@
|
||||
rightKind: hasTheirs ? "theirs" : "empty",
|
||||
});
|
||||
}
|
||||
rows.push({ type: "marker", conflictIndex: part.index });
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function syncResolveScroll(source: "before" | "after") {
|
||||
if (isSyncingResolveScroll) return;
|
||||
const sourcePane = source === "before" ? resolveBeforePane : resolveAfterPane;
|
||||
const targetPane = source === "before" ? resolveAfterPane : resolveBeforePane;
|
||||
if (!sourcePane || !targetPane) return;
|
||||
|
||||
isSyncingResolveScroll = true;
|
||||
targetPane.scrollTop = sourcePane.scrollTop;
|
||||
targetPane.scrollLeft = sourcePane.scrollLeft;
|
||||
requestAnimationFrame(() => {
|
||||
isSyncingResolveScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
function legacyConflictParts(): any[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function setConflictChoice(index: number, choice: ConflictChoice) {
|
||||
activeConflict = index;
|
||||
bothMenu = null;
|
||||
const next = [...conflictChoices];
|
||||
next[index] = choice;
|
||||
conflictChoices = next;
|
||||
}
|
||||
|
||||
function setAllConflicts(choice: ConflictChoice) {
|
||||
bothMenu = null;
|
||||
conflictChoices = conflictParts.filter((p) => p.kind === "conflict").map(() => choice);
|
||||
}
|
||||
|
||||
@@ -258,266 +263,121 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dialog-backdrop"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
style="grid-template-rows: auto minmax(0,1fr) auto;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Resolve conflicts"
|
||||
tabindex="-1"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Resolve</span>
|
||||
<h2 class="dialog-title">Conflicts</h2>
|
||||
</div>
|
||||
<div class="dialog-header-actions">
|
||||
{#if conflictTarget}
|
||||
<div class="tool-surface-choice" aria-label={isGerman ? "Konflikt bearbeiten mit" : "Edit conflict with"}>
|
||||
<span>{isGerman ? "Bearbeiten mit" : "Edit with"}</span>
|
||||
<button class="active" type="button" aria-pressed="true" title={isGerman ? "In Gitty bearbeiten" : "Edit in Gitty"}>
|
||||
<GitMerge size={13} aria-hidden="true" />Gitty
|
||||
</button>
|
||||
<button type="button" onclick={() => onExternalMerge(conflictTarget)} disabled={isBusy} title={isGerman ? `In ${mergeName} öffnen` : `Open in ${mergeName}`}>
|
||||
<ExternalLink size={13} aria-hidden="true" /><span>{mergeName}</span>
|
||||
<svelte:window onkeydown={(event) => { if (event.key === "Escape") { if (bothMenu !== null) bothMenu = null; else if (!isBusy) onClose(); } }} onclick={() => { bothMenu = null; }} />
|
||||
|
||||
{#snippet bothChoices(index: number)}
|
||||
<div class="both-control">
|
||||
<button type="button" aria-haspopup="menu" aria-expanded={bothMenu === index} disabled={isBusy} onclick={(event) => { event.stopPropagation(); bothMenu = bothMenu === index ? null : index; }}>
|
||||
{index === -1 ? t("Beide", "Both") : t("Beide übernehmen", "Accept both")}<ChevronDown size={14} />
|
||||
</button>
|
||||
{#if bothMenu === index}
|
||||
<div class="both-menu" role="menu">
|
||||
<button type="button" role="menuitem" onclick={() => index === -1 ? setAllConflicts("both-ot") : setConflictChoice(index, "both-ot")}>{t("Aktuell, dann eingehend", "Current, then incoming")}</button>
|
||||
<button type="button" role="menuitem" onclick={() => index === -1 ? setAllConflicts("both-to") : setConflictChoice(index, "both-to")}>{t("Eingehend, dann aktuell", "Incoming, then current")}</button>
|
||||
</div>
|
||||
{/if}
|
||||
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Konflikte schließen" : "Close conflicts"}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="dialog-backdrop app-chrome-backdrop conflict-backdrop">
|
||||
<div class="conflict-workbench" bind:this={dialogElement} onkeydown={trapFocus} role="dialog" aria-modal="true" aria-label={t("Konflikte lösen", "Resolve conflicts")} tabindex="-1">
|
||||
<header class="workbench-header">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if conflictedFiles.length === 0}
|
||||
<div class="blank-state">All conflicts resolved. Continue the current operation when ready.</div>
|
||||
<div class="empty-message">{t("Alle Konflikte gelöst. Du kannst den aktuellen Vorgang fortsetzen.", "All conflicts resolved. You can continue the current operation.")}</div>
|
||||
{:else}
|
||||
<div class="dialog-body">
|
||||
<aside class="dialog-files" aria-label="Conflicted files">
|
||||
<div class="workbench-body">
|
||||
<aside class="file-sidebar" aria-label={t("Dateien mit Konflikten", "Conflicted files")}>
|
||||
<div class="files-heading">{t("Dateien", "Files")} · {conflictedFiles.length}</div>
|
||||
<div class="file-list">
|
||||
{#each conflictedFiles as file (file.path)}
|
||||
<button
|
||||
class="dialog-file-row"
|
||||
class:active={conflictTarget === file.path}
|
||||
class:prepared={preparedResolutions[file.path] != null}
|
||||
type="button"
|
||||
onclick={() => onSelectFile(file.path)}
|
||||
disabled={isBusy}
|
||||
title={file.path}
|
||||
>
|
||||
<span class={`status-badge ${preparedResolutions[file.path] ? "added" : "conflicted"}`}>
|
||||
{preparedResolutions[file.path] ? "ready" : "conflicted"}
|
||||
</span>
|
||||
<strong>{file.path}</strong>
|
||||
{#if preparedResolutions[file.path]}
|
||||
<Check size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
{@const prepared = preparedResolutions[file.path] != null}
|
||||
<button class="file-item" class:selected={conflictTarget === file.path} type="button" onclick={() => onSelectFile(file.path)} disabled={isBusy} title={file.path}>
|
||||
<FileText size={19} /><span class="file-copy"><strong>{file.path}</strong><small class:ready={prepared}>{prepared ? t("Vorbereitet", "Prepared") : conflictTarget === file.path ? t("In Bearbeitung", "In progress") : t("Offen", "Open")}</small></span>
|
||||
{#if prepared}<Check class="ready" size={20} />{:else}<span class="open-dot"></span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="file-progress">{preparedCount} {t("von", "of")} {conflictedFiles.length} {t("vorbereitet", "prepared")}</div>
|
||||
</aside>
|
||||
|
||||
<div class="resolve-editor" aria-label="Conflict editor">
|
||||
<main class="conflict-main">
|
||||
<header class="file-header">
|
||||
<h3 title={conflictTarget}>{conflictTarget || t("Datei auswählen", "Select a file")}</h3>
|
||||
{#if conflictRegionCount > 0 && !manualMode}
|
||||
<nav aria-label={t("Konfliktnavigation", "Conflict navigation")}><span>{t("Konflikt", "Conflict")} {activeConflict + 1} {t("von", "of")} {conflictRegionCount}</span><button type="button" aria-label={t("Vorheriger Konflikt", "Previous conflict")} disabled={activeConflict === 0} onclick={() => navigateConflict(-1)}><ChevronLeft size={20} /></button><button type="button" aria-label={t("Nächster Konflikt", "Next conflict")} disabled={activeConflict >= conflictRegionCount - 1} onclick={() => navigateConflict(1)}><ChevronRight size={20} /></button></nav>
|
||||
{/if}
|
||||
</header>
|
||||
{#if !conflict}
|
||||
<div class="blank-state">Select a file to resolve.</div>
|
||||
<div class="empty-message">{t("Wähle eine Datei aus, um ihren Konflikt zu lösen.", "Select a file to resolve its conflict.")}</div>
|
||||
{:else if conflict.binary}
|
||||
<div class="resolve-binary">
|
||||
<div class="resolve-binary-note">
|
||||
<AlertCircle size={16} aria-hidden="true" />
|
||||
<span>Binary file — pick which version to keep, then mark it resolved.</span>
|
||||
</div>
|
||||
<div class="resolve-binary-options">
|
||||
<button
|
||||
class="resolve-binary-card ours"
|
||||
class:active={binarySide === "ours"}
|
||||
type="button"
|
||||
onclick={() => { binarySide = "ours"; }}
|
||||
disabled={isBusy || conflict.ours_size == null}
|
||||
>
|
||||
<span class="resolve-side-label">Current (ours)</span>
|
||||
<strong>{formatBytes(conflict.ours_size)}</strong>
|
||||
<span class="resolve-binary-hint">{conflict.ours_size == null ? "Deleted on this side" : "Keep this version"}</span>
|
||||
</button>
|
||||
<button
|
||||
class="resolve-binary-card theirs"
|
||||
class:active={binarySide === "theirs"}
|
||||
type="button"
|
||||
onclick={() => { binarySide = "theirs"; }}
|
||||
disabled={isBusy || conflict.theirs_size == null}
|
||||
>
|
||||
<span class="resolve-side-label">Incoming (theirs)</span>
|
||||
<strong>{formatBytes(conflict.theirs_size)}</strong>
|
||||
<span class="resolve-binary-hint">{conflict.theirs_size == null ? "Deleted on this side" : "Keep this version"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resolve-actions">
|
||||
<span class="resolve-path" title={conflictTarget}>{conflictTarget}</span>
|
||||
{#if currentPrepared}
|
||||
<span class="prepared-tag"><Check size={14} aria-hidden="true" /> Prepared</span>
|
||||
{/if}
|
||||
<button type="button" onclick={handleMarkResolved} disabled={!canMarkResolved}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{currentPrepared ? "Update decision" : "Mark as resolved"}
|
||||
</button>
|
||||
<div class="binary-content"><p><AlertCircle size={18} />{t("Binärdatei – wähle die Version, die erhalten bleiben soll.", "Binary file — choose the version to keep.")}</p>
|
||||
<div class="binary-options">{#each ["ours", "theirs"] as side}
|
||||
{@const size = side === "ours" ? conflict.ours_size : conflict.theirs_size}
|
||||
<button type="button" class:chosen={binarySide === side} disabled={isBusy || size == null} onclick={() => { binarySide = side as "ours" | "theirs"; }}><strong>{side === "ours" ? t("Aktuell (ours)", "Current (ours)") : t("Eingehend (theirs)", "Incoming (theirs)")}</strong><span>{size == null ? t("Auf dieser Seite gelöscht", "Deleted on this side") : formatBytes(size)}</span>{#if binarySide === side}<Check size={20} />{/if}</button>
|
||||
{/each}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="resolve-toolbar">
|
||||
<div class="comparison-toolbar">
|
||||
{#if manualMode}<button type="button" onclick={() => { manualMode = false; }} disabled={isBusy}>{t("Zurück zur Auswahl", "Back to guided")}</button>
|
||||
{:else}<span>{t("Für alle Konflikte", "For all conflicts")}</span><div class="bulk-actions"><button type="button" onclick={() => setAllConflicts("ours")} disabled={isBusy}>{t("Aktuell", "Current")}</button><button type="button" onclick={() => setAllConflicts("theirs")} disabled={isBusy}>{t("Eingehend", "Incoming")}</button>{@render bothChoices(-1)}</div><button class="manual-button" type="button" onclick={enableManualEdit} disabled={isBusy}><Pencil size={16} />{t("Manuell bearbeiten", "Edit manually")}</button>{/if}
|
||||
</div>
|
||||
{#if manualMode}
|
||||
<button type="button" onclick={() => { manualMode = false; }} disabled={isBusy}>Back to guided</button>
|
||||
<textarea class="manual-editor" aria-label={t("Konflikt manuell bearbeiten", "Edit conflict manually")} bind:value={resolveContent} spellcheck="false" disabled={isBusy}></textarea>
|
||||
{:else}
|
||||
<span class="resolve-toolbar-label">Apply to all:</span>
|
||||
<button type="button" onclick={() => setAllConflicts("ours")} disabled={isBusy}>Current</button>
|
||||
<button type="button" onclick={() => setAllConflicts("theirs")} disabled={isBusy}>Incoming</button>
|
||||
<button type="button" onclick={() => setAllConflicts("both-ot")} disabled={isBusy}>Both</button>
|
||||
<button type="button" onclick={enableManualEdit} disabled={isBusy}>Edit manually</button>
|
||||
{/if}
|
||||
<span class="resolve-status">
|
||||
{#if unresolvedCount > 0}
|
||||
<AlertCircle size={14} aria-hidden="true" />
|
||||
{unresolvedCount} of {conflictRegionCount} unresolved
|
||||
{:else if resolveHasMarkers}
|
||||
<AlertCircle size={14} aria-hidden="true" />
|
||||
Conflict markers still present
|
||||
{:else}
|
||||
<Check size={14} aria-hidden="true" />
|
||||
{conflictRegionCount} {conflictRegionCount === 1 ? "conflict" : "conflicts"} resolved
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if manualMode}
|
||||
<textarea
|
||||
class="resolve-textarea"
|
||||
bind:value={resolveContent}
|
||||
spellcheck="false"
|
||||
disabled={isBusy}
|
||||
></textarea>
|
||||
{:else}
|
||||
<div class="resolve-structured">
|
||||
{#if conflictRegions.length > 0}
|
||||
<div class="resolve-conflict-controls" aria-label="Conflict decisions">
|
||||
{#each conflictRegions as part (part.index)}
|
||||
<div class="resolve-conflict-bar" class:unresolved={conflictChoices[part.index] == null}>
|
||||
<span class="resolve-conflict-label">Conflict {part.index + 1}</span>
|
||||
<div class="resolve-choice-buttons">
|
||||
<button type="button" class:active={conflictChoices[part.index] === "ours"} onclick={() => setConflictChoice(part.index, "ours")} disabled={isBusy}>Current</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "theirs"} onclick={() => setConflictChoice(part.index, "theirs")} disabled={isBusy}>Incoming</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "both-ot"} onclick={() => setConflictChoice(part.index, "both-ot")} disabled={isBusy} title="Both - current first">Both C+I</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "both-to"} onclick={() => setConflictChoice(part.index, "both-to")} disabled={isBusy} title="Both - incoming first">Both I+C</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="resolve-split" role="table" aria-label="Full conflict comparison">
|
||||
<div
|
||||
class="resolve-pane"
|
||||
bind:this={resolveBeforePane}
|
||||
aria-label="Current file content"
|
||||
onscroll={() => syncResolveScroll("before")}
|
||||
>
|
||||
<div class="resolve-pane-grid">
|
||||
{#each resolveSplitRows as row, i (`left-${i}`)}
|
||||
<div class="comparison-scroll" bind:this={comparison}>
|
||||
<div class="comparison-grid">
|
||||
<div class="side-heading ours-heading">{t("Aktuell (ours)", "Current (ours)")}</div><div class="side-heading theirs-heading">{t("Eingehend (theirs)", "Incoming (theirs)")}</div>
|
||||
{#each resolveSplitRows as row, i (i)}
|
||||
{#if row.type === "marker"}
|
||||
<div class="resolve-split-marker" class:unresolved={conflictChoices[row.conflictIndex] == null}>Conflict {row.conflictIndex + 1}</div>
|
||||
{@const choice = conflictChoices[row.conflictIndex]}
|
||||
<div class="conflict-decision" data-conflict={row.conflictIndex}>
|
||||
<strong>{t("Konflikt", "Conflict")} {row.conflictIndex + 1}{#if choice == null}<span class="pending"> · {t("Offen", "Open")}</span>{/if}</strong>
|
||||
<button class="accept-ours" class:chosen={choice === "ours"} aria-pressed={choice === "ours"} type="button" disabled={isBusy} onclick={() => setConflictChoice(row.conflictIndex, "ours")}>{t("Aktuell übernehmen", "Accept current")}</button>
|
||||
<button class="accept-theirs" class:chosen={choice === "theirs"} aria-pressed={choice === "theirs"} type="button" disabled={isBusy} onclick={() => setConflictChoice(row.conflictIndex, "theirs")}>{t("Eingehend übernehmen", "Accept incoming")}</button>
|
||||
{@render bothChoices(row.conflictIndex)}
|
||||
{#if choice}<small class="decision-label">{choice === "ours" ? t("Aktuell gewählt", "Current selected") : choice === "theirs" ? t("Eingehend gewählt", "Incoming selected") : choice === "both-ot" ? t("Aktuell → Eingehend", "Current → Incoming") : t("Eingehend → Aktuell", "Incoming → Current")}</small>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="resolve-num" class:ours={row.leftKind === "ours"} class:empty={row.leftKind === "empty"}>{row.leftNum ?? ""}</div>
|
||||
<div
|
||||
class="resolve-cell"
|
||||
class:ours={row.leftKind === "ours"}
|
||||
class:empty={row.leftKind === "empty"}
|
||||
class:dimmed={row.conflictIndex != null && !oursActive(conflictChoices[row.conflictIndex])}
|
||||
>{displayLine(row.leftText ?? "") || " "}</div>
|
||||
<div class="code-line" class:ours-line={row.leftKind === "ours"} class:dimmed={row.conflictIndex != null && conflictChoices[row.conflictIndex] != null && !oursActive(conflictChoices[row.conflictIndex])}><span>{row.leftNum ?? ""}</span><code>{displayLine(row.leftText ?? "") || " "}</code></div>
|
||||
<div class="code-line incoming-line" class:theirs-line={row.rightKind === "theirs"} class:dimmed={row.conflictIndex != null && conflictChoices[row.conflictIndex] != null && !theirsActive(conflictChoices[row.conflictIndex])}><span>{row.rightNum ?? ""}</span><code>{displayLine(row.rightText ?? "") || " "}</code></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resolve-pane"
|
||||
bind:this={resolveAfterPane}
|
||||
aria-label="Incoming file content"
|
||||
onscroll={() => syncResolveScroll("after")}
|
||||
>
|
||||
<div class="resolve-pane-grid">
|
||||
{#each resolveSplitRows as row, i (`right-${i}`)}
|
||||
{#if row.type === "marker"}
|
||||
<div class="resolve-split-marker" class:unresolved={conflictChoices[row.conflictIndex] == null}>Conflict {row.conflictIndex + 1}</div>
|
||||
{:else}
|
||||
<div class="resolve-num" class:theirs={row.rightKind === "theirs"} class:empty={row.rightKind === "empty"}>{row.rightNum ?? ""}</div>
|
||||
<div
|
||||
class="resolve-cell"
|
||||
class:theirs={row.rightKind === "theirs"}
|
||||
class:empty={row.rightKind === "empty"}
|
||||
class:dimmed={row.conflictIndex != null && !theirsActive(conflictChoices[row.conflictIndex])}
|
||||
>{displayLine(row.rightText ?? "") || " "}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if false}
|
||||
{#each legacyConflictParts() as part, partIndex (partIndex)}
|
||||
{#if part.kind === "text"}
|
||||
{#if part.lines.length > 0}
|
||||
<pre class="resolve-context">{#each part.lines as line}<span class="resolve-line context">{displayLine(line) || " "}</span>{/each}</pre>
|
||||
<section class="result-preview" aria-label={t("Ergebnisvorschau", "Result preview")}>
|
||||
<header><strong>{t("Ergebnisvorschau", "Result preview")}</strong><span class:pending={resolveHasMarkers} class:ready={!resolveHasMarkers} aria-live="polite">{manualMode && resolveHasMarkers ? t("Konfliktmarker vorhanden", "Conflict markers remain") : unresolvedCount > 0 ? `${unresolvedCount} ${t(unresolvedCount === 1 ? "Konflikt offen" : "Konflikte offen", unresolvedCount === 1 ? "conflict open" : "conflicts open")}` : t("Alle Konflikte gelöst", "All conflicts resolved")}</span></header>
|
||||
<div class="preview-code">{#each previewLines as line, i}<div class="code-line"><span>{i + 1}</span><code class:pending={line.startsWith(`[${t("Konflikt", "Conflict")} `)}>{displayLine(line) || " "}</code></div>{/each}</div>
|
||||
</section>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="resolve-conflict" class:unresolved={conflictChoices[part.index] == null}>
|
||||
<div class="resolve-conflict-bar">
|
||||
<span class="resolve-conflict-label">Conflict {part.index + 1}</span>
|
||||
<div class="resolve-choice-buttons">
|
||||
<button type="button" class:active={conflictChoices[part.index] === "ours"} onclick={() => setConflictChoice(part.index, "ours")} disabled={isBusy}>Current</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "theirs"} onclick={() => setConflictChoice(part.index, "theirs")} disabled={isBusy}>Incoming</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "both-ot"} onclick={() => setConflictChoice(part.index, "both-ot")} disabled={isBusy} title="Both — current first">Both C+I</button>
|
||||
<button type="button" class:active={conflictChoices[part.index] === "both-to"} onclick={() => setConflictChoice(part.index, "both-to")} disabled={isBusy} title="Both — incoming first">Both I+C</button>
|
||||
{#if conflict}<div class="file-actions"><span>{!canMarkResolved ? t("Alle Konflikte dieser Datei lösen, dann vorbereiten.", "Resolve all conflicts in this file, then prepare it.") : currentPrepared ? t("Diese Datei ist vorbereitet.", "This file is prepared.") : t("Ergebnis prüfen und Datei vorbereiten.", "Review the result and prepare the file.")}</span><button type="button" disabled={!canMarkResolved} onclick={handleMarkResolved}>{currentPrepared ? t("Vorbereitung aktualisieren", "Update preparation") : t("Datei vorbereiten", "Prepare file")}</button></div>{/if}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resolve-side ours" class:dimmed={!oursActive(conflictChoices[part.index])}>
|
||||
<span class="resolve-side-label">Current (ours)</span>
|
||||
<pre class="resolve-lines">{#each part.oursLines as line}<span class="resolve-line ours">{displayLine(line) || " "}</span>{/each}</pre>
|
||||
</div>
|
||||
<div class="resolve-side theirs" class:dimmed={!theirsActive(conflictChoices[part.index])}>
|
||||
<span class="resolve-side-label">Incoming (theirs)</span>
|
||||
<pre class="resolve-lines">{#each part.theirsLines as line}<span class="resolve-line theirs">{displayLine(line) || " "}</span>{/each}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="resolve-actions">
|
||||
<span class="resolve-path" title={conflictTarget}>{conflictTarget}</span>
|
||||
{#if currentPrepared}
|
||||
<span class="prepared-tag"><Check size={14} aria-hidden="true" /> Prepared</span>
|
||||
{/if}
|
||||
<button type="button" onclick={handleMarkResolved} disabled={!canMarkResolved}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{currentPrepared ? "Update decision" : "Mark as resolved"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer">
|
||||
<span class="dialog-footer-info">{preparedCount} of {conflictedFiles.length} prepared</span>
|
||||
<button class="btn-primary" type="button" onclick={onApply} disabled={isBusy || preparedCount === 0}>
|
||||
{#if operation.startsWith("Resolving")}
|
||||
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||
{:else}
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{/if}
|
||||
Apply {preparedCount} resolved
|
||||
</button>
|
||||
</footer>
|
||||
<footer class="workbench-footer"><span>{t("Änderungen werden erst beim Anwenden gespeichert.", "Changes are saved only when applied.")}</span><button class="apply-button" type="button" onclick={onApply} disabled={isBusy || preparedCount === 0}>{#if isBusy}<LoaderCircle class="spin" size={16} />{/if}{preparedCount} {t(preparedCount === 1 ? "vorbereitete Lösung anwenden" : "vorbereitete Lösungen anwenden", preparedCount === 1 ? "prepared resolution to apply" : "prepared resolutions to apply")}</button></footer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.conflict-workbench{--ours:var(--color-accent);--theirs:#b78ae6;display:flex;flex-direction:column;width:min(1700px,100%);height:min(960px,100%);min-width:0;overflow:hidden;border:1px solid var(--color-border-input);border-radius:6px;background:var(--app-dialog-bg);color:var(--color-ink);box-shadow:var(--app-dialog-shadow);font-size:13px}
|
||||
.conflict-workbench button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:30px;padding:5px 10px;border:1px solid var(--color-border-input);border-radius:3px;background:transparent;color:var(--color-ink);font-size:13px;font-weight:400;white-space:nowrap}
|
||||
.conflict-workbench button:hover:not(:disabled){background:var(--color-surface-hover);border-color:var(--color-ink-faint)}.conflict-workbench button:disabled{opacity:.45;cursor:default}.conflict-workbench button:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}
|
||||
.workbench-header{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:52px;padding:10px 16px;border-bottom:1px solid var(--color-border);background:var(--app-dialog-chrome)}h2{display:flex;align-items:center;gap:10px;margin:0;font-size:15px;font-weight:600}.header-actions{display:flex;gap:12px;align-items:center}.header-actions .close-button{border:0;padding:6px}
|
||||
.workbench-body{display:flex;flex:1;min-height:0}.file-sidebar{display:flex;flex-direction:column;flex:0 0 220px;min-width:0;border-right:1px solid var(--color-border);padding:0 10px}.files-heading{padding:14px 10px 12px;font-size:13px;color:var(--color-ink-muted)}.file-list{flex:1;overflow:auto}.file-sidebar .file-item{display:flex;width:100%;gap:12px;padding:10px 10px;border:1px solid transparent;align-items:flex-start;justify-content:flex-start;text-align:left;white-space:normal}.file-sidebar .file-item.selected{background:var(--color-surface);border-left-color:var(--color-accent)}.file-copy{display:grid;gap:5px;flex:1;min-width:0}.file-copy strong{font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.file-copy small{font-size:13px;color:#eda33d}.file-item :global(svg){flex-shrink:0}.open-dot{height:11px;width:11px;background:#eda33d;border-radius:50%;margin-top:5px;flex-shrink:0}.ready,.file-copy small.ready,.file-item :global(svg.ready){color:#68c878}.file-progress{border-top:1px solid var(--color-border);padding:12px 10px;color:var(--color-ink-muted)}
|
||||
.conflict-main{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0}.file-header{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:0 10px;padding:10px 6px;border-bottom:1px solid var(--color-border);min-height:48px}.file-header h3{font-size:14px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0}.file-header nav{display:flex;align-items:center;gap:12px;white-space:nowrap}.file-header nav span{margin-right:8px}.file-header nav button{padding:7px 9px}
|
||||
.comparison-toolbar{display:flex;gap:10px;align-items:center;padding:8px 12px;flex-wrap:wrap}.comparison-toolbar>span{color:var(--color-ink-muted)}.bulk-actions{display:flex}.bulk-actions>button+button{border-left:0}.comparison-toolbar .manual-button{margin-left:auto;border:0;color:var(--ours);padding-right:0}
|
||||
.comparison-scroll{flex:1.25;min-height:150px;overflow:auto;margin:0 10px;border:1px solid var(--color-border)}.comparison-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-content:start;min-width:760px}.side-heading{position:sticky;top:0;z-index:2;background:var(--app-dialog-bg);padding:7px 10px;border-top:2px solid var(--ours);font-size:13px}.theirs-heading{border-color:var(--theirs);border-left:1px solid var(--color-border)}
|
||||
.code-line{display:flex;min-width:0;line-height:20px;min-height:20px;font-family:var(--font-mono);font-size:13px}.code-line>span{flex:0 0 44px;text-align:right;padding-right:14px;color:var(--color-ink-faint);user-select:none}.code-line code{font:inherit;white-space:pre;overflow-x:auto;min-width:0;flex:1;padding-right:12px}.incoming-line{border-left:1px solid var(--color-border)}.ours-line{background:color-mix(in srgb,var(--ours) 13%,transparent)}.theirs-line{background:color-mix(in srgb,var(--theirs) 13%,transparent)}.dimmed code{opacity:.45}
|
||||
.conflict-decision{grid-column:1/-1;display:flex;align-items:center;flex-wrap:wrap;gap:10px;padding:7px 10px;border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border);background:color-mix(in srgb,var(--color-surface) 45%,var(--app-dialog-bg));scroll-margin-top:36px}.conflict-decision strong{font-weight:500;margin-right:12px}.pending{color:#eda33d}.conflict-decision .accept-ours{color:var(--ours);border-color:color-mix(in srgb,var(--ours) 65%,var(--color-border))}.conflict-decision .accept-theirs{color:var(--theirs);border-color:color-mix(in srgb,var(--theirs) 65%,var(--color-border))}.conflict-decision .chosen{background:color-mix(in srgb,var(--ours) 10%,transparent)}.conflict-decision .accept-theirs.chosen{background:color-mix(in srgb,var(--theirs) 10%,transparent)}.decision-label{color:var(--ours);font-size:12px}.both-control{position:relative}.both-menu{position:absolute;right:0;top:100%;z-index:5;min-width:220px;padding:4px;background:var(--app-dialog-chrome);border:1px solid var(--color-border-input);box-shadow:var(--app-panel-shadow)}.both-menu button{width:100%;border:0;text-align:left;justify-content:flex-start}
|
||||
.result-preview{display:flex;flex:1;min-height:120px;flex-direction:column;margin:10px 10px 0;border:1px solid var(--color-border)}.result-preview header{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:7px 10px;border-bottom:1px solid var(--color-border)}.result-preview strong{font-weight:500;font-size:13px}.preview-code{overflow:auto;flex:1;padding:6px 0}.preview-code code{overflow:visible}.file-actions{display:flex;justify-content:space-between;align-items:center;gap:16px;min-height:48px;padding:8px 12px;border-top:1px solid var(--color-border)}.file-actions>span{color:var(--color-ink-muted)}.file-actions button{background:var(--color-surface)}.workbench-footer{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:10px 16px;border-top:1px solid var(--color-border);min-height:52px}.workbench-footer>span{color:var(--color-ink-muted)}.workbench-footer .apply-button{background:var(--color-primary);border-color:var(--color-primary);color:white;padding:6px 12px;font-weight:500}.workbench-footer .apply-button:hover:not(:disabled){background:var(--color-primary-dark)}
|
||||
.empty-message{flex:1;padding:32px;color:var(--color-ink-muted)}.manual-editor{flex:1.25;min-height:150px;resize:none;margin:0 10px;padding:14px;font:13px/20px var(--font-mono);background:var(--app-input-bg);color:var(--color-ink);border:1px solid var(--color-border-input)}.binary-content{flex:1;padding:24px}.binary-content p{display:flex;gap:10px;align-items:center;color:var(--color-ink-muted)}.binary-options{display:flex;gap:16px;margin-top:24px}.binary-options button{flex:1;flex-direction:column;gap:12px;padding:24px;white-space:normal}.binary-options button.chosen{border-color:var(--ours);background:color-mix(in srgb,var(--ours) 10%,transparent)}
|
||||
.conflict-backdrop{display:flex;align-items:center;justify-content:center}
|
||||
.conflict-workbench{outline:none}.open-dot{border-radius:50%!important}
|
||||
@media(max-width:1100px){.file-sidebar{flex-basis:210px}.conflict-workbench{font-size:13px}.conflict-workbench button{font-size:12px;padding:7px 10px}.file-copy strong{font-size:13px}.comparison-toolbar{gap:10px;padding:10px}.decision-label{display:none}.file-header{padding:12px 4px}.file-header nav{gap:4px}.code-line{font-size:12px}}
|
||||
@media(max-width:700px){.conflict-backdrop{padding:8px}.conflict-workbench{width:100%;height:100%}.workbench-header{padding:10px;gap:8px}h2{font-size:13px;gap:8px}.header-actions{gap:6px}.header-actions button{font-size:11px}.workbench-body{flex-direction:column}.file-sidebar{flex:0 0 auto;border-right:0;border-bottom:1px solid var(--color-border)}.files-heading{font-size:13px;padding:8px}.file-list{display:flex;max-height:80px}.file-sidebar .file-item{flex:0 0 180px;min-width:180px;width:180px;padding:8px}.file-copy small{white-space:nowrap;font-size:12px}.file-progress{display:none}.file-header{min-height:48px;padding:6px}.file-header h3{font-size:13px}.file-header nav span{font-size:11px}.comparison-toolbar{padding:6px;gap:6px}.comparison-scroll{min-height:100px}.result-preview{min-height:100px}.file-actions{min-height:48px;padding:6px;gap:8px}.file-actions>span,.workbench-footer>span{font-size:11px}.workbench-footer{min-height:60px;padding:8px;gap:8px}.workbench-footer .apply-button{white-space:normal;padding:8px;font-size:13px}.result-preview header{padding:6px}.result-preview strong{font-size:13px}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user