Compare commits

..
2 Commits
Author SHA1 Message Date
Christoph a7558e457d feat(compare): add German localization and revamp compare UI
Pass the application language into compare dialogs and add German
translations so titles, buttons and helper text render appropriately.
Refactor dialog markup to introduce a compact header with a GitCompare
icon, show changed-file counts, and surface contextual help and warnings.
Adjust and extend CSS for spacing, layout and responsive behavior.

- Pass app language into compare components and derive isGerman flag
- Replace icons and restructure headers; add counts, help and warnings
- Update styles for sizing, spacing, responsive rules and new classes
2026-08-15 18:39:10 +02:00
Christoph 412e8d19b5 feat(ui): add SelectMenu component and replace native selects
Introduce a reusable SelectMenu component and replace native select
controls across the UI. This centralizes select behavior and styling,
enabling grouped options, placeholders, and a consistent popup
interaction. Add comprehensive CSS and light-theme tweaks, and update
rebase action styling to integrate the new control.

- Add a unified SelectMenu component and wire change handlers.
- Implement .select-menu styles, popup behavior, and theme overrides.
- Replace ad-hoc native selects in dialogs and rebase UI for consistency.
2026-08-15 18:26:32 +02:00
14 changed files with 494 additions and 190 deletions
+3 -1
View File
@@ -5537,6 +5537,7 @@
{canCompare}
{isBusy}
{operation}
language={appLanguage}
onCompareFromChange={(val) => { compareFrom = val; }}
onCompareToChange={(val) => { compareTo = val; }}
onCompare={compareSelectedTargets}
@@ -5551,10 +5552,11 @@
{comparison}
{selectedDiffPath}
{isBusy}
language={appLanguage}
fromLabel={comparisonFromLabel}
toLabel={comparisonToLabel}
highlightQuery={diffHighlightQuery}
restoreLabel={pendingRestoreFile ? "Restore file" : ""}
restoreLabel={pendingRestoreFile ? (appLanguage === "de" ? "Datei wiederherstellen" : "Restore file") : ""}
onClose={closeCompareDialog}
onRestore={restorePreviewedCommitFile}
onSelectFile={selectDiffFile}
+179 -14
View File
@@ -225,6 +225,114 @@
.btn-sm { min-height: 26px; padding: 0 8px; font-size: 12px; }
.select-menu { position: relative; width: 100%; min-width: 0; }
.select-menu-trigger {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
width: 100%;
height: 34px;
min-height: 34px;
padding: 0 9px 0 11px;
border: 1px solid var(--color-border-input);
border-radius: 6px;
color: var(--color-ink);
background: var(--color-surface-raised);
font-size: 13px;
text-align: left;
}
.select-menu-value { display: flex; align-items: center; min-width: 0; gap: 7px; overflow: hidden; }
.select-menu-value > span { overflow: hidden; color: var(--color-ink); font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.select-menu-value > small {
flex: 0 0 auto;
padding: 2px 5px;
border: 1px solid color-mix(in srgb, var(--color-accent) 28%, transparent);
border-radius: 4px;
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
font-size: 9px;
font-weight: 850;
letter-spacing: 0.025em;
}
.select-menu-value.placeholder > span { color: var(--color-ink-muted); font-weight: 600; }
.select-menu-trigger svg { color: var(--color-ink-faint); transition: transform 120ms ease; }
.select-menu.open .select-menu-trigger {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(90, 140, 248, 0.18);
}
.select-menu.open .select-menu-trigger svg { transform: rotate(180deg); }
.select-menu-popup {
position: fixed;
z-index: 1000;
display: grid;
gap: 2px;
padding: 5px;
overflow: auto;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-surface-solid);
box-shadow: 0 18px 50px rgba(0,0,0,0.5);
}
.select-menu-group {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 5px 3px 2px;
padding: 7px 5px 5px;
border-top: 1px solid var(--color-border-subtle);
color: var(--color-accent);
font-size: 10.5px;
font-weight: 900;
letter-spacing: 0.065em;
text-transform: uppercase;
}
.select-menu-group:first-child { margin-top: 0; border-top: 0; }
.select-menu-group small {
min-width: 18px;
padding: 1px 5px;
border-radius: 999px;
color: var(--color-ink);
background: var(--color-surface-hover);
font-size: 9px;
letter-spacing: 0;
text-align: center;
}
.select-menu-option {
display: grid;
grid-template-columns: minmax(0, 1fr) 14px;
align-items: center;
justify-content: initial;
width: 100%;
min-height: 30px;
padding: 6px 8px;
border-color: transparent;
border-radius: 6px;
color: var(--color-ink);
background: transparent;
font-size: 12px;
font-weight: 750;
text-align: left;
}
.select-menu-option > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.select-menu-option svg { color: var(--color-primary); }
.select-menu-option:hover:not(:disabled), .select-menu-option.active:not(:disabled) {
border-color: var(--color-border-subtle);
color: var(--color-ink);
background: rgba(255,255,255,0.06);
}
.select-menu-option.selected { color: var(--color-ink); }
.select-menu-option:disabled { cursor: not-allowed; opacity: 0.42; }
.compare-target-select .select-menu-trigger {
height: 38px;
min-height: 38px;
padding: 0 11px;
border-color: var(--color-border);
font-size: 12px;
font-weight: 700;
}
.btn-primary {
border-color: rgba(111, 140, 255, 0.72);
color: #ffffff;
@@ -3222,17 +3330,16 @@
.compare-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) 40px minmax(0, 1fr);
align-items: end;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--color-border-subtle);
gap: 10px;
}
.compare-field { display: grid; gap: 4px; min-width: 0; }
.compare-field span { color: var(--color-ink-faint); font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
.compare-arrow { margin-bottom: 6px; color: var(--color-ink-faint); }
.compare-target-help {
margin: 12px;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--color-accent) 20%, var(--color-border-subtle));
border-radius: 8px;
@@ -3241,6 +3348,9 @@
font-size: 11.5px;
line-height: 1.5;
}
.compare-target-help svg { flex: 0 0 auto; margin-top: 1px; color: var(--color-accent); }
.compare-target-warning { border-color: color-mix(in srgb, #e0a040 30%, var(--color-border-subtle)); background: color-mix(in srgb, #e0a040 7%, var(--color-surface-raised)); }
.compare-target-warning svg { color: #e0a040; }
.compare-summary { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; padding: 10px 12px; }
.compare-range { display: flex; align-items: center; gap: 7px; color: var(--color-ink-muted); }
@@ -3386,6 +3496,21 @@
width: min(1560px, calc(100vw - 32px));
height: min(940px, calc(100vh - 32px));
}
.compare-dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-height: 70px;
padding: 14px 18px;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--app-dialog-chrome);
}
.compare-dialog-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
.compare-dialog-title > div { min-width: 0; }
.compare-dialog-mark { display: grid; place-items: center; flex: 0 0 auto; width: 38px; height: 38px; border: 1px solid color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
.compare-dialog-title h2 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
.compare-dialog-title p { margin: 3px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.35; }
.global-search-dialog {
width: min(1180px, calc(100vw - 32px));
height: min(840px, calc(100vh - 32px));
@@ -3401,12 +3526,22 @@
height: min(860px, calc(100vh - 32px));
}
.compare-select-dialog {
display: block;
grid-template-rows: auto minmax(0, 1fr);
width: min(720px, calc(100vw - 32px));
height: auto;
max-height: calc(100vh - 32px);
overflow: auto;
overflow: hidden;
}
.compare-select-shell { display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
.compare-select-body { display: grid; align-content: start; gap: 12px; min-height: 0; padding: 18px 20px 20px; overflow: auto; }
.compare-target-panel { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 12px; background: color-mix(in srgb, var(--app-settings-row-bg) 72%, transparent); }
.compare-target-heading h3 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
.compare-target-heading p { margin: 4px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.45; }
.compare-arrow-shell { display: grid; place-items: center; width: 40px; height: 40px; border: 1px solid var(--color-border-subtle); border-radius: 9px; color: var(--color-accent); background: var(--color-surface-raised); }
.compare-select-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; padding: 11px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.compare-select-footer > span { color: var(--color-ink-faint); font-size: 9.5px; }
.compare-select-footer > div { display: flex; gap: 8px; }
.compare-select-footer button { min-height: 32px; }
.interactive-rebase-dialog,
.reflog-dialog {
grid-template-rows: auto minmax(0, 1fr) auto;
@@ -3463,11 +3598,12 @@
border-radius: 5px;
background: var(--code-surface-subtle);
}
.rebase-action { height: 30px; font-family: var(--font-mono); font-weight: 800; }
.rebase-action.pick { color: var(--code-add-strong); }
.rebase-action.reword { color: var(--code-hunk-text); }
.rebase-action.squash, .rebase-action.fixup { color: #96620f; }
.rebase-action.drop { color: var(--code-delete-strong); }
.rebase-action { font-family: var(--font-mono); font-weight: 800; }
.rebase-action .select-menu-trigger { height: 30px; min-height: 30px; font-family: inherit; font-weight: inherit; }
.rebase-action.pick .select-menu-trigger { color: var(--code-add-strong); }
.rebase-action.reword .select-menu-trigger { color: var(--code-hunk-text); }
.rebase-action.squash .select-menu-trigger, .rebase-action.fixup .select-menu-trigger { color: #96620f; }
.rebase-action.drop .select-menu-trigger { color: var(--code-delete-strong); }
.rebase-plan-row > code { color: var(--color-accent); font-family: var(--font-mono); font-size: 11px; font-weight: 800; }
.rebase-commit-copy { display: grid; gap: 3px; min-width: 0; }
.rebase-commit-copy strong { overflow: hidden; color: var(--color-ink); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
@@ -3964,6 +4100,8 @@
.compare-dialog .dialog-body { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); }
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.compare-files-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; margin: -8px -8px 4px; padding: 8px 12px; border-bottom: 1px solid var(--color-border-subtle); color: var(--color-ink-dim); font-size: 10px; font-weight: 850; letter-spacing: .05em; text-transform: uppercase; }
.compare-files-head strong { display: grid; place-items: center; min-width: 22px; height: 20px; padding-inline: 5px; border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); font-size: 9px; }
.dialog-file-row {
display: grid;
@@ -6512,6 +6650,25 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
box-shadow: 0 18px 50px rgba(28,44,74,0.18);
}
:root[data-theme="light"] .select-menu-popup {
box-shadow: 0 18px 50px rgba(28,44,74,0.18);
}
:root[data-theme="light"] .select-menu-value > span,
:root[data-theme="light"] .select-menu-option,
:root[data-theme="light"] .select-menu-option.selected {
color: var(--color-ink);
}
:root[data-theme="light"] .select-menu-value.placeholder > span {
color: var(--color-ink-muted);
}
:root[data-theme="light"] .select-menu-option:hover:not(:disabled),
:root[data-theme="light"] .select-menu-option.active:not(:disabled) {
background: var(--color-surface-hover);
}
:root[data-theme="light"] .dialog-file-row:hover,
:root[data-theme="light"] .branch-row:hover,
:root[data-theme="light"] .explorer-row:hover,
@@ -6806,7 +6963,15 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.repo-summary { height: 40px; }
.change-lanes { grid-template-columns: 1fr; }
.compare-form { grid-template-columns: 1fr; }
.compare-arrow { display: none; }
.compare-arrow-shell { width: 100%; height: 30px; }
.compare-arrow-shell svg { transform: rotate(90deg); }
.compare-dialog-head { min-height: 60px; padding: 10px 12px; }
.compare-dialog-mark { width: 34px; height: 34px; }
.compare-dialog-title h2 { font-size: 15px; }
.compare-select-body { padding: 14px; }
.compare-target-panel { padding: 13px; }
.compare-select-footer > span { display: none; }
.compare-select-footer { justify-content: flex-end; }
.dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.compare-dialog .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.global-search-options { grid-template-columns: 1fr; }
@@ -1,6 +1,7 @@
<script lang="ts">
import { GitCommitHorizontal, LoaderCircle, Sparkles, X } from "@lucide/svelte";
import type { AiCommitPlan } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
plan: AiCommitPlan;
@@ -62,9 +63,7 @@
<div class="split-files">
{#each group.files as file}
<div><code>{file}</code>
<select value={groupIndex} onchange={(event) => moveFile(file, groupIndex, Number(event.currentTarget.value))} disabled={isApplying}>
{#each draft.groups as _, target}<option value={target}>Commit {target + 1}</option>{/each}
</select>
<SelectMenu class="split-file-target" value={String(groupIndex)} options={draft.groups.map((_, target) => ({ value: String(target), label: `Commit ${target + 1}` }))} disabled={isApplying} onChange={(value) => moveFile(file, groupIndex, Number(value))} />
</div>
{/each}
</div>
@@ -98,6 +97,7 @@
.split-files{display:grid;gap:5px}
.split-files>div{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;background:var(--color-surface)}
code{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;color:var(--color-ink-muted);font-size:11px;white-space:nowrap}
select{height:28px;border:1px solid var(--color-border-subtle);border-radius:5px;background:var(--color-surface-raised);color:var(--color-ink);font-size:11px}
:global(.split-file-target){width:110px;flex:0 0 110px}
:global(.split-file-target .select-menu-trigger){height:28px;min-height:28px;font-size:11px}
.dialog-footer p.invalid{color:#e0a040}
</style>
+2 -5
View File
@@ -3,6 +3,7 @@
import { AlertCircle, Bot, Check, Cpu, Eye, EyeOff, Gauge, Globe, Key, LoaderCircle, Sparkles, X, Zap } from "@lucide/svelte";
import { credDelete, credLoad, credSave } from "../git";
import type { AiSettings, CommitAiLocalProfile, CommitAiProvider, LocalModelOption } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
settings: AiSettings;
@@ -200,11 +201,7 @@
</div>
<label class="cred-field">
<span class="cred-field-label">Model</span>
<select bind:value={localModelId}>
{#each localModels as option (option.id)}
<option value={option.id}>{option.label} {formatSize(option.approx_size_mb)}</option>
{/each}
</select>
<SelectMenu value={localModelId} options={localModels.map((option) => ({ value: option.id, label: `${option.label} - ${formatSize(option.approx_size_mb)}` }))} onChange={(value) => { localModelId = value; }} />
</label>
<div class="cred-token-hint">
<AlertCircle size={13} aria-hidden="true" />
+14 -13
View File
@@ -37,6 +37,7 @@
ExternalToolsSettings,
ToolOpenMode,
} from "../types";
import SelectMenu from "./SelectMenu.svelte";
type SettingsPage = "general" | "tools";
@@ -390,17 +391,17 @@
<label class="tool-default-field">
<span>{isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}</span>
<select value={tools[activeToolKind].preset} onchange={(event) => changePreset(activeToolKind, event.currentTarget.value)} aria-label={isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}>
{#if availablePresets(activeToolKind).length > 0}
<optgroup label={isGerman ? "Installiert" : "Installed"}>
{#each availablePresets(activeToolKind) as preset}<option value={preset.id}> {preset.label}</option>{/each}
</optgroup>
{/if}
<optgroup label={isGerman ? "Weitere unterstützte Programme" : "Other supported applications"}>
{#each otherPresets(activeToolKind) as preset}<option value={preset.id}>{preset.label}</option>{/each}
</optgroup>
<option value="custom">{isGerman ? "Eigenes Programm auswählen…" : "Choose a custom application…"}</option>
</select>
<SelectMenu
class="tool-preset-select"
value={tools[activeToolKind].preset}
options={[
...availablePresets(activeToolKind).map((preset) => ({ value: preset.id, label: `Installed - ${preset.label}`, group: isGerman ? "Installiert" : "Installed" })),
...otherPresets(activeToolKind).map((preset) => ({ value: preset.id, label: preset.label, group: isGerman ? "Weitere unterstützte Programme" : "Other supported applications" })),
{ value: "custom", label: isGerman ? "Eigenes Programm auswählen..." : "Choose a custom application..." },
]}
ariaLabel={isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}
onChange={(value) => changePreset(activeToolKind, value)}
/>
<small>{isGerman ? "Diese Auswahl wird gespeichert und für alle passenden Aktionen verwendet." : "This selection is saved and used for every matching action."}</small>
</label>
@@ -504,8 +505,8 @@
.tool-open-mode button span { max-width: 100%; overflow: hidden; font-size: 11px; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.tool-open-mode button small { color: var(--color-ink-faint); font-size: 9px; font-weight: 550; }
.tool-default-field, .tool-advanced-panel label { display: grid; gap: 6px; min-width: 0; color: var(--color-ink-muted); font-size: 10.5px; font-weight: 750; }
.tool-default-field select, .tool-advanced-panel input, .tool-advanced-panel textarea { width: 100%; min-width: 0; border: 1px solid var(--color-border); border-radius: 7px; color: var(--color-ink); background: var(--color-surface-raised); font: inherit; }
.tool-default-field select { height: 38px; padding: 0 11px; font-size: 12px; font-weight: 700; }
.tool-advanced-panel input, .tool-advanced-panel textarea { width: 100%; min-width: 0; border: 1px solid var(--color-border); border-radius: 7px; color: var(--color-ink); background: var(--color-surface-raised); font: inherit; }
:global(.tool-preset-select .select-menu-trigger) { height: 38px; min-height: 38px; padding: 0 11px; border-color: var(--color-border); font-size: 12px; font-weight: 700; }
.tool-default-field small { color: var(--color-ink-faint); font-size: 9.5px; font-weight: 500; }
.tool-usage-callout { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 10px; padding: 10px 11px; border-left: 2px solid var(--color-accent); border-radius: 0 7px 7px 0; background: color-mix(in srgb, var(--color-accent) 6%, transparent); }
.tool-usage-callout span { color: var(--color-accent); font-size: 9.5px; font-weight: 850; text-transform: uppercase; letter-spacing: .04em; }
+2 -5
View File
@@ -11,6 +11,7 @@
X,
} from "@lucide/svelte";
import type { AppLanguage, GitCommit, GitRemote } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
commit: GitCommit;
@@ -196,11 +197,7 @@
<div class="commit-note-sync-controls">
<label>
<span>{text.remote}</span>
<select bind:value={selectedRemote} disabled={isBusy}>
{#each remotes as remote (remote.name)}
<option value={remote.name}>{remote.name}</option>
{/each}
</select>
<SelectMenu value={selectedRemote} options={remotes.map((remote) => ({ value: remote.name, label: remote.name }))} disabled={isBusy} onChange={(value) => { selectedRemote = value; }} />
</label>
<button type="button" onclick={() => onFetch(selectedRemote)} disabled={isBusy || !selectedRemote || hasChanges}>
{#if isBusy}<LoaderCircle class="spin" size={15} aria-hidden="true" />{:else}<Download size={15} aria-hidden="true" />{/if}
+27 -16
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { ArrowRight, FileCode, RotateCcw, X } from "@lucide/svelte";
import { ArrowRight, FileCode, GitCompare, RotateCcw, X } from "@lucide/svelte";
import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";
type SplitRow =
@@ -25,6 +25,7 @@
restoreLabel?: string;
/** When opened from a search hit, the term to highlight on matching lines. */
highlightQuery?: string;
language?: "en" | "de";
onClose: () => void;
onRestore?: () => void;
onSelectFile: (file: GitDiffFile) => void;
@@ -38,11 +39,14 @@
toLabel = "",
restoreLabel = "",
highlightQuery = "",
language = "en",
onClose = () => {},
onRestore = undefined,
onSelectFile = () => {},
}: Props = $props();
let isGerman = $derived(language === "de");
// Needle = first non-empty line of the search query, lowercased for matching.
let highlightNeedle = $derived(
highlightQuery
@@ -225,14 +229,17 @@
>
<div class="dialog compare-dialog" role="dialog" aria-modal="true" aria-label="Branch or commit comparison" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Compare</span>
<h2 class="dialog-range">
<span class="hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
<ArrowRight size={14} aria-hidden="true" />
<span class="hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</h2>
<header class="compare-dialog-head">
<div class="compare-dialog-title">
<span class="compare-dialog-mark"><GitCompare size={18} aria-hidden="true" /></span>
<div>
<h2>{isGerman ? "Änderungen vergleichen" : "Compare changes"}</h2>
<p class="dialog-range">
<span class="hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
<ArrowRight size={14} aria-hidden="true" />
<span class="hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</p>
</div>
</div>
<div class="dialog-header-actions">
{#if restoreLabel && onRestore}
@@ -241,19 +248,23 @@
<span>{restoreLabel}</span>
</button>
{/if}
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Vergleich schließen" : "Close comparison"}>
<X size={18} aria-hidden="true" />
</button>
</div>
</header>
{#if comparison.files.length === 0}
<div class="blank-state">No differences these versions are identical.</div>
<div class="blank-state">{isGerman ? "Keine Unterschiede - diese Versionen sind identisch." : "No differences - these versions are identical."}</div>
{:else}
<div class="dialog-body">
<!-- File list -->
<aside class="dialog-files" aria-label="Changed files">
<aside class="dialog-files" aria-label={isGerman ? "Geänderte Dateien" : "Changed files"}>
<div class="compare-files-head">
<span>{isGerman ? "Geänderte Dateien" : "Changed files"}</span>
<strong>{comparison.files.length}</strong>
</div>
{#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
<button
class="dialog-file-row"
@@ -275,9 +286,9 @@
<!-- Diff pane -->
<div class="dialog-diff">
{#if !selectedFile}
<div class="blank-state">Select a file to see its changes.</div>
<div class="blank-state">{isGerman ? "Wähle eine Datei aus, um ihre Änderungen zu sehen." : "Select a file to see its changes."}</div>
{:else if splitRows.length === 0}
<div class="blank-state">No textual changes for this file.</div>
<div class="blank-state">{isGerman ? "Keine textuellen Änderungen für diese Datei." : "No textual changes for this file."}</div>
{:else}
<!-- Path bar -->
<div class="diff-header">
@@ -292,11 +303,11 @@
<!-- Column headers -->
<div class="split-col-headers">
<div class="split-col-label">
<span>Before</span>
<span>{isGerman ? "Vorher" : "Before"}</span>
<span class="split-col-hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
</div>
<div class="split-col-label">
<span>After</span>
<span>{isGerman ? "Nachher" : "After"}</span>
<span class="split-col-hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</div>
</div>
+5 -20
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { ArrowRight, GitCompare, LoaderCircle } from "@lucide/svelte";
import type { GitCommit, GitCommitComparison } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
commits: GitCommit[];
@@ -40,6 +41,8 @@
event.preventDefault();
onCompare();
}
let commitOptions = $derived(commits.map((item) => ({ value: item.hash, label: commitOptionLabel(item), group: "Commits" })));
</script>
<section class="compare-panel panel" aria-label="Compare commits">
@@ -61,32 +64,14 @@
<form class="compare-form" onsubmit={handleSubmit}>
<label class="compare-field">
<span>From (older)</span>
<select
value={compareFrom}
onchange={(e) => onCompareFromChange((e.target as HTMLSelectElement).value)}
disabled={isBusy}
>
<option value="" disabled>Select a commit</option>
{#each commits as item (item.hash)}
<option value={item.hash}>{commitOptionLabel(item)}</option>
{/each}
</select>
<SelectMenu value={compareFrom} options={commitOptions} placeholder="Select a commit" showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
</label>
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
<label class="compare-field">
<span>To (newer)</span>
<select
value={compareTo}
onchange={(e) => onCompareToChange((e.target as HTMLSelectElement).value)}
disabled={isBusy}
>
<option value="" disabled>Select a commit</option>
{#each commits as item (item.hash)}
<option value={item.hash}>{commitOptionLabel(item)}</option>
{/each}
</select>
<SelectMenu value={compareTo} options={commitOptions} placeholder="Select a commit" showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
</label>
<button class="btn-primary" type="submit" disabled={!canCompare}>
+61 -89
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { ArrowRight, GitCompare, LoaderCircle, X } from "@lucide/svelte";
import { ArrowRight, GitCompare, Info, LoaderCircle, X } from "@lucide/svelte";
import type { GitBranch, GitCommit } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface Props {
commits: GitCommit[];
@@ -10,6 +11,7 @@
canCompare: boolean;
isBusy: boolean;
operation: string;
language?: "en" | "de";
onCompareFromChange: (val: string) => void;
onCompareToChange: (val: string) => void;
onCompare: () => void;
@@ -24,12 +26,15 @@
canCompare = false,
isBusy = false,
operation = "",
language = "en",
onCompareFromChange = () => {},
onCompareToChange = () => {},
onCompare = () => {},
onClose = () => {},
}: Props = $props();
let isGerman = $derived(language === "de");
function commitOptionLabel(item: GitCommit): string {
return `${item.short_hash} - ${item.summary}`;
}
@@ -41,6 +46,11 @@
let localBranches = $derived(branches.filter((branch) => !branch.remote));
let remoteBranches = $derived(branches.filter((branch) => branch.remote));
let targetCount = $derived(branches.length + commits.length);
let compareOptions = $derived([
...localBranches.map((branch) => ({ value: branchValue(branch), label: `${branch.name}${branch.current ? " (current)" : ""}`, group: "Branches - Local" })),
...remoteBranches.map((branch) => ({ value: branchValue(branch), label: branch.name, group: "Branches - Remote" })),
...commits.map((item) => ({ value: item.hash, label: commitOptionLabel(item), group: "Commits" })),
]);
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
@@ -52,104 +62,66 @@
class="dialog-backdrop"
role="presentation"
>
<div class="dialog compare-select-dialog" role="dialog" aria-modal="true" aria-label="Select branches or commits to compare" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Compare</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Compare branches or commits</h2>
<div class="dialog compare-select-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"} tabindex="-1">
<header class="compare-dialog-head">
<div class="compare-dialog-title">
<span class="compare-dialog-mark"><GitCompare size={18} aria-hidden="true" /></span>
<div>
<h2>{isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}</h2>
<p>{isGerman ? "Zwei Repository-Stände direkt gegenüberstellen" : "Review two repository states side by side"}</p>
</div>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Vergleich schließen" : "Close comparison"}>
<X size={18} aria-hidden="true" />
</button>
</header>
{#if targetCount < 2}
<div class="blank-state">At least two branches or commits are needed to compare.</div>
{:else}
<form class="compare-form" onsubmit={handleSubmit}>
<label class="compare-field">
<span>Base</span>
<select
value={compareFrom}
onchange={(e) => onCompareFromChange((e.target as HTMLSelectElement).value)}
disabled={isBusy}
>
<option value="" disabled>Select a branch or commit</option>
{#if localBranches.length > 0}
<optgroup label="Local branches">
{#each localBranches as branch (branch.name)}
<option value={branchValue(branch)}>{branch.name}{branch.current ? " (current)" : ""}</option>
{/each}
</optgroup>
{/if}
{#if remoteBranches.length > 0}
<optgroup label="Remote branches">
{#each remoteBranches as branch (branch.name)}
<option value={branchValue(branch)}>{branch.name}</option>
{/each}
</optgroup>
{/if}
{#if commits.length > 0}
<optgroup label="Recent commits">
{#each commits as item (item.hash)}
<option value={item.hash}>{commitOptionLabel(item)}</option>
{/each}
</optgroup>
{/if}
</select>
</label>
<form class="compare-select-shell" onsubmit={handleSubmit}>
<div class="compare-select-body">
{#if targetCount < 2}
<div class="blank-state">{isGerman ? "Für einen Vergleich werden mindestens zwei Branches oder Commits benötigt." : "At least two branches or commits are needed to compare."}</div>
{:else}
<section class="compare-target-panel">
<div class="compare-target-heading">
<h3>{isGerman ? "Vergleichsbereich" : "Comparison range"}</h3>
<p>{isGerman ? "Wähle Ausgangspunkt und Ziel des Vergleichs." : "Choose the starting point and target for the comparison."}</p>
</div>
<div class="compare-form">
<label class="compare-field">
<span>{isGerman ? "Ausgangsbasis" : "Base"}</span>
<SelectMenu class="compare-target-select" value={compareFrom} options={compareOptions} placeholder={isGerman ? "Branch oder Commit wählen" : "Select a branch or commit"} showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
</label>
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
<span class="compare-arrow-shell"><ArrowRight size={18} aria-hidden="true" /></span>
<label class="compare-field">
<span>Compare with</span>
<select
value={compareTo}
onchange={(e) => onCompareToChange((e.target as HTMLSelectElement).value)}
disabled={isBusy}
>
<option value="" disabled>Select a branch or commit</option>
{#if localBranches.length > 0}
<optgroup label="Local branches">
{#each localBranches as branch (branch.name)}
<option value={branchValue(branch)}>{branch.name}{branch.current ? " (current)" : ""}</option>
{/each}
</optgroup>
{/if}
{#if remoteBranches.length > 0}
<optgroup label="Remote branches">
{#each remoteBranches as branch (branch.name)}
<option value={branchValue(branch)}>{branch.name}</option>
{/each}
</optgroup>
{/if}
{#if commits.length > 0}
<optgroup label="Recent commits">
{#each commits as item (item.hash)}
<option value={item.hash}>{commitOptionLabel(item)}</option>
{/each}
</optgroup>
{/if}
</select>
</label>
<label class="compare-field">
<span>{isGerman ? "Vergleichen mit" : "Compare with"}</span>
<SelectMenu class="compare-target-select" value={compareTo} options={compareOptions} placeholder={isGerman ? "Branch oder Commit wählen" : "Select a branch or commit"} showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
</label>
</div>
</section>
<button class="btn-primary" type="submit" disabled={!canCompare}>
{#if operation === "Comparing revisions"}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{#if compareFrom && compareTo && compareFrom === compareTo}
<div class="compare-target-help compare-target-warning"><Info size={15} aria-hidden="true" /><span>{isGerman ? "Wähle zwei unterschiedliche Branches oder Commits." : "Select two different branches or commits to compare."}</span></div>
{:else}
<GitCompare size={16} aria-hidden="true" />
<div class="compare-target-help"><Info size={15} aria-hidden="true" /><span>{isGerman ? "Verglichen werden die vollständigen Repository-Stände. Nicht committete Änderungen im Arbeitsverzeichnis sind nicht enthalten." : "The complete repository states are compared. Uncommitted working-tree changes are not included."}</span></div>
{/if}
Compare
</button>
</form>
{#if compareFrom && compareTo && compareFrom === compareTo}
<div class="blank-state">Select two different branches or commits to compare.</div>
{:else}
<div class="compare-target-help">
The two branch tips are compared across the entire repository. Uncommitted working-tree changes are not included.
{/if}
</div>
<footer class="compare-select-footer">
<span>{isGerman ? `${targetCount} Ziele verfügbar` : `${targetCount} targets available`}</span>
<div>
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
<button class="btn-primary" type="submit" disabled={!canCompare || targetCount < 2}>
{#if operation === "Comparing revisions"}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{:else}
<GitCompare size={16} aria-hidden="true" />
{/if}
{isGerman ? "Vergleich öffnen" : "Open comparison"}
</button>
</div>
{/if}
{/if}
</footer>
</form>
</div>
</div>
+2 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import SelectMenu from "./SelectMenu.svelte";
import {
CalendarDays,
FileCode,
@@ -193,12 +194,7 @@
<label class="search-limit">
<span>Results</span>
<select bind:value={limit} disabled={!hasRepository || isBusy || isSearching}>
<option value={100}>100</option>
<option value={250}>250</option>
<option value={500}>500</option>
<option value={1000}>1000</option>
</select>
<SelectMenu value={String(limit)} options={[100, 250, 500, 1000].map((value) => ({ value: String(value), label: String(value) }))} disabled={!hasRepository || isBusy || isSearching} onChange={(value) => { limit = Number(value); }} />
</label>
<button class="btn-primary" type="submit" disabled={!hasRepository || isBusy || isSearching || query.trim().length === 0}>
@@ -1,6 +1,7 @@
<script lang="ts">
import { AlertTriangle, ArrowDown, ArrowUp, GitBranch, LoaderCircle, Play, X } from "@lucide/svelte";
import type { GitBranch as GitBranchInfo, RebaseAction, RebaseCommit, RebasePlanItem } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface PlanRow extends RebaseCommit {
action: RebaseAction;
@@ -41,6 +42,7 @@
));
let invalidReword = $derived(rows.some((row) => row.action === "reword" && !row.message.trim()));
let canStart = $derived(Boolean(base) && rows.length > 0 && keptCount > 0 && !invalidSquash && !invalidReword && !isLoading && !isBusy);
const rebaseActions: RebaseAction[] = ["pick", "reword", "squash", "fixup", "drop"];
function updateAction(index: number, action: RebaseAction) {
rows = rows.map((row, rowIndex) => rowIndex === index ? { ...row, action } : row);
@@ -82,12 +84,7 @@
<section class="rebase-base-bar">
<label>
<span>Rebase <strong>{currentBranch || "current branch"}</strong> onto</span>
<select value={base} onchange={(event) => onBaseChange((event.target as HTMLSelectElement).value)} disabled={isBusy || isLoading}>
<option value="" disabled>Select a base branch</option>
{#each availableBases as branch (branch.name)}
<option value={branch.name}>{branch.remote ? "Remote · " : "Local · "}{branch.name}</option>
{/each}
</select>
<SelectMenu value={base} options={availableBases.map((branch) => ({ value: branch.name, label: `${branch.remote ? "Remote - " : "Local - "}${branch.name}` }))} placeholder="Select a base branch" disabled={isBusy || isLoading} onChange={onBaseChange} />
</label>
<p>Oldest commit first. Reorder commits, then choose how each one should be replayed.</p>
</section>
@@ -110,9 +107,7 @@
<button type="button" onclick={() => move(index, -1)} disabled={isBusy || index === 0} title="Move up"><ArrowUp size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => move(index, 1)} disabled={isBusy || index === rows.length - 1} title="Move down"><ArrowDown size={14} aria-hidden="true" /></button>
</div>
<select class={`rebase-action ${row.action}`} value={row.action} onchange={(event) => updateAction(index, (event.target as HTMLSelectElement).value as RebaseAction)} disabled={isBusy} aria-label={`Action for ${row.short_hash}`}>
<option value="pick">pick</option><option value="reword">reword</option><option value="squash">squash</option><option value="fixup">fixup</option><option value="drop">drop</option>
</select>
<SelectMenu class={`rebase-action ${row.action}`} value={row.action} options={rebaseActions.map((action) => ({ value: action, label: action }))} disabled={isBusy} ariaLabel={`Action for ${row.short_hash}`} onChange={(value) => updateAction(index, value as RebaseAction)} />
<code>{row.short_hash}</code>
<div class="rebase-commit-copy">
{#if row.action === "reword"}
+176
View File
@@ -0,0 +1,176 @@
<script lang="ts">
import { tick } from "svelte";
import { Check, ChevronDown } from "@lucide/svelte";
export interface SelectMenuOption {
value: string;
label: string;
group?: string;
disabled?: boolean;
}
interface Props {
value: string;
options: SelectMenuOption[];
placeholder?: string;
disabled?: boolean;
ariaLabel?: string;
class?: string;
showSelectedGroup?: boolean;
onChange: (value: string) => void;
}
let {
value,
options = [],
placeholder = "Select an option",
disabled = false,
ariaLabel = "",
class: className = "",
showSelectedGroup = false,
onChange,
}: Props = $props();
let root = $state<HTMLDivElement>();
let trigger = $state<HTMLButtonElement>();
let open = $state(false);
let activeIndex = $state(-1);
let menuStyle = $state("");
const menuId = `select-menu-${Math.random().toString(36).slice(2)}`;
let selectedOption = $derived(options.find((option) => option.value === value));
let enabledIndices = $derived(options.map((option, index) => option.disabled ? -1 : index).filter((index) => index >= 0));
function groupCount(group: string): number {
return options.filter((option) => option.group === group).length;
}
function positionMenu() {
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
const viewportGap = 8;
const menuGap = 5;
const desiredHeight = Math.min(300, options.length * 34 + 24);
const spaceBelow = window.innerHeight - rect.bottom - viewportGap;
const spaceAbove = rect.top - viewportGap;
const openAbove = spaceBelow < Math.min(desiredHeight, 180) && spaceAbove > spaceBelow;
const maxHeight = Math.max(96, Math.min(desiredHeight, openAbove ? spaceAbove - menuGap : spaceBelow - menuGap));
const width = Math.max(rect.width, 180);
const left = Math.min(rect.left, window.innerWidth - width - viewportGap);
const top = openAbove ? rect.top - menuGap : rect.bottom + menuGap;
menuStyle = `left:${Math.max(viewportGap, left)}px;${openAbove ? `bottom:${window.innerHeight - top}px;` : `top:${top}px;`}min-width:${width}px;max-width:${Math.max(180, window.innerWidth - viewportGap * 2)}px;max-height:${maxHeight}px;`;
}
async function show() {
if (disabled || enabledIndices.length === 0) return;
const selectedIndex = options.findIndex((option) => option.value === value && !option.disabled);
activeIndex = selectedIndex >= 0 ? selectedIndex : enabledIndices[0];
open = true;
await tick();
positionMenu();
document.getElementById(`${menuId}-option-${activeIndex}`)?.scrollIntoView({ block: "nearest" });
}
function close() {
open = false;
}
function choose(index: number) {
const option = options[index];
if (!option || option.disabled) return;
onChange(option.value);
close();
trigger?.focus();
}
function moveActive(direction: -1 | 1) {
if (enabledIndices.length === 0) return;
const current = enabledIndices.indexOf(activeIndex);
const next = current < 0
? (direction > 0 ? 0 : enabledIndices.length - 1)
: (current + direction + enabledIndices.length) % enabledIndices.length;
activeIndex = enabledIndices[next];
document.getElementById(`${menuId}-option-${activeIndex}`)?.scrollIntoView({ block: "nearest" });
}
function handleKeydown(event: KeyboardEvent) {
if (disabled) return;
if (!open && ["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
event.preventDefault();
void show();
return;
}
if (!open) return;
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
moveActive(event.key === "ArrowDown" ? 1 : -1);
} else if (event.key === "Home" || event.key === "End") {
event.preventDefault();
activeIndex = event.key === "Home" ? enabledIndices[0] : enabledIndices[enabledIndices.length - 1];
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
choose(activeIndex);
} else if (event.key === "Escape" || event.key === "Tab") {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
}
close();
}
}
function handleWindowPointerDown(event: PointerEvent) {
if (open && root && !root.contains(event.target as Node)) close();
}
</script>
<svelte:window onpointerdown={handleWindowPointerDown} onresize={positionMenu} onscroll={positionMenu} />
<div class={`select-menu ${className}`} class:open bind:this={root}>
<button
bind:this={trigger}
class="select-menu-trigger"
type="button"
{disabled}
aria-label={ariaLabel || undefined}
aria-haspopup="listbox"
aria-expanded={open}
aria-controls={open ? menuId : undefined}
onclick={() => open ? close() : void show()}
onkeydown={handleKeydown}
>
<span class="select-menu-value" class:placeholder={!selectedOption} title={selectedOption?.label}>
{#if showSelectedGroup && selectedOption?.group}<small>{selectedOption.group}</small>{/if}
<span>{selectedOption?.label ?? placeholder}</span>
</span>
<ChevronDown size={14} aria-hidden="true" />
</button>
{#if open}
<div id={menuId} class="select-menu-popup" style={menuStyle} role="listbox" aria-label={ariaLabel || undefined}>
{#each options as option, index (`${option.value}:${index}`)}
{#if option.group && (index === 0 || options[index - 1]?.group !== option.group)}
<div class="select-menu-group" role="presentation">
<span>{option.group}</span>
<small>{groupCount(option.group)}</small>
</div>
{/if}
<button
id={`${menuId}-option-${index}`}
class="select-menu-option"
class:active={index === activeIndex}
class:selected={option.value === value}
type="button"
role="option"
aria-selected={option.value === value}
disabled={option.disabled}
onmouseenter={() => { if (!option.disabled) activeIndex = index; }}
onclick={() => choose(index)}
>
<span>{option.label}</span>
{#if option.value === value}<Check size={14} aria-hidden="true" />{/if}
</button>
{/each}
</div>
{/if}
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { Cloud, GitBranch, Plus, Save, Trash2, X } from "@lucide/svelte";
import type { GitRemote, PullStrategy } from "../types";
import SelectMenu from "./SelectMenu.svelte";
export let remotes: GitRemote[] = [];
export let remoteBranches: string[] = [];
@@ -76,8 +77,8 @@
</div>
<div class="sync-fields">
<label><span>{de ? "Remote für Sync" : "Remote used for sync"}</span><select bind:value={draftRemote}><option value="">{de ? "Automatisch wählen" : "Choose automatically"}</option>{#each remotes as remote}<option value={remote.name}>{remote.name}</option>{/each}</select><small>{de ? "Ein Remote ist die gespeicherte Verbindung zu einem Server-Repository." : "A remote is a saved connection to a repository on a server."}</small></label>
<label><span>{de ? `Upstream für ${currentBranch || "aktuellen Branch"}` : `Upstream for ${currentBranch || "current branch"}`}</span><select bind:value={draftUpstream}><option value="">{de ? "Kein Upstream" : "No upstream"}</option>{#each remoteBranches as branch}<option value={branch}>{branch}</option>{/each}</select><small>{de ? "Der Upstream ist der Remote-Branch, mit dem Pull, Push und Ahead/Behind verglichen werden." : "The upstream is the remote branch used by Pull, Push, and Ahead/Behind."}</small></label>
<label><span>{de ? "Remote für Sync" : "Remote used for sync"}</span><SelectMenu value={draftRemote} options={[{ value: "", label: de ? "Automatisch wählen" : "Choose automatically" }, ...remotes.map((remote) => ({ value: remote.name, label: remote.name }))]} onChange={(value) => { draftRemote = value; }} /><small>{de ? "Ein Remote ist die gespeicherte Verbindung zu einem Server-Repository." : "A remote is a saved connection to a repository on a server."}</small></label>
<label><span>{de ? `Upstream für ${currentBranch || "aktuellen Branch"}` : `Upstream for ${currentBranch || "current branch"}`}</span><SelectMenu value={draftUpstream} options={[{ value: "", label: de ? "Kein Upstream" : "No upstream" }, ...remoteBranches.map((branch) => ({ value: branch, label: branch }))]} onChange={(value) => { draftUpstream = value; }} /><small>{de ? "Der Upstream ist der Remote-Branch, mit dem Pull, Push und Ahead/Behind verglichen werden." : "The upstream is the remote branch used by Pull, Push, and Ahead/Behind."}</small></label>
</div>
</section>
+12 -6
View File
@@ -21,6 +21,7 @@
X,
} from "@lucide/svelte";
import type { GitBranch as GitBranchInfo, GitWorktree } from "../types";
import SelectMenu from "./SelectMenu.svelte";
type CreateMode = "existing" | "new" | "detached";
@@ -260,12 +261,17 @@
{#if createMode === "existing"}
<label>
<span>Branch</span>
<select bind:value={selectedBranch} disabled={isBusy}>
<option value="" disabled>Select a local branch</option>
{#each localBranches as branch (branch.name)}
<option value={branch.name} disabled={!branchAvailable(branch.name)}>{branch.name}{!branchAvailable(branch.name) ? " (already checked out)" : ""}</option>
{/each}
</select>
<SelectMenu
value={selectedBranch}
placeholder="Select a local branch"
options={localBranches.map((branch) => ({
value: branch.name,
label: `${branch.name}${!branchAvailable(branch.name) ? " (already checked out)" : ""}`,
disabled: !branchAvailable(branch.name),
}))}
disabled={isBusy}
onChange={(value) => { selectedBranch = value; }}
/>
</label>
{:else if createMode === "new"}
<label>