Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7558e457d | ||
|
|
412e8d19b5 |
+3
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user