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.
This commit is contained in:
+126
-5
@@ -225,6 +225,107 @@
|
|||||||
|
|
||||||
.btn-sm { min-height: 26px; padding: 0 8px; font-size: 12px; }
|
.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: 40px; min-height: 40px; }
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
border-color: rgba(111, 140, 255, 0.72);
|
border-color: rgba(111, 140, 255, 0.72);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@@ -3463,11 +3564,12 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--code-surface-subtle);
|
background: var(--code-surface-subtle);
|
||||||
}
|
}
|
||||||
.rebase-action { height: 30px; font-family: var(--font-mono); font-weight: 800; }
|
.rebase-action { font-family: var(--font-mono); font-weight: 800; }
|
||||||
.rebase-action.pick { color: var(--code-add-strong); }
|
.rebase-action .select-menu-trigger { height: 30px; min-height: 30px; font-family: inherit; font-weight: inherit; }
|
||||||
.rebase-action.reword { color: var(--code-hunk-text); }
|
.rebase-action.pick .select-menu-trigger { color: var(--code-add-strong); }
|
||||||
.rebase-action.squash, .rebase-action.fixup { color: #96620f; }
|
.rebase-action.reword .select-menu-trigger { color: var(--code-hunk-text); }
|
||||||
.rebase-action.drop { color: var(--code-delete-strong); }
|
.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-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 { 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; }
|
.rebase-commit-copy strong { overflow: hidden; color: var(--color-ink); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
@@ -6512,6 +6614,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);
|
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"] .dialog-file-row:hover,
|
||||||
:root[data-theme="light"] .branch-row:hover,
|
:root[data-theme="light"] .branch-row:hover,
|
||||||
:root[data-theme="light"] .explorer-row:hover,
|
:root[data-theme="light"] .explorer-row:hover,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { GitCommitHorizontal, LoaderCircle, Sparkles, X } from "@lucide/svelte";
|
import { GitCommitHorizontal, LoaderCircle, Sparkles, X } from "@lucide/svelte";
|
||||||
import type { AiCommitPlan } from "../types";
|
import type { AiCommitPlan } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
plan: AiCommitPlan;
|
plan: AiCommitPlan;
|
||||||
@@ -62,9 +63,7 @@
|
|||||||
<div class="split-files">
|
<div class="split-files">
|
||||||
{#each group.files as file}
|
{#each group.files as file}
|
||||||
<div><code>{file}</code>
|
<div><code>{file}</code>
|
||||||
<select value={groupIndex} onchange={(event) => moveFile(file, groupIndex, Number(event.currentTarget.value))} disabled={isApplying}>
|
<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))} />
|
||||||
{#each draft.groups as _, target}<option value={target}>Commit {target + 1}</option>{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +97,7 @@
|
|||||||
.split-files{display:grid;gap:5px}
|
.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)}
|
.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}
|
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}
|
.dialog-footer p.invalid{color:#e0a040}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { AlertCircle, Bot, Check, Cpu, Eye, EyeOff, Gauge, Globe, Key, LoaderCircle, Sparkles, X, Zap } from "@lucide/svelte";
|
import { AlertCircle, Bot, Check, Cpu, Eye, EyeOff, Gauge, Globe, Key, LoaderCircle, Sparkles, X, Zap } from "@lucide/svelte";
|
||||||
import { credDelete, credLoad, credSave } from "../git";
|
import { credDelete, credLoad, credSave } from "../git";
|
||||||
import type { AiSettings, CommitAiLocalProfile, CommitAiProvider, LocalModelOption } from "../types";
|
import type { AiSettings, CommitAiLocalProfile, CommitAiProvider, LocalModelOption } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
settings: AiSettings;
|
settings: AiSettings;
|
||||||
@@ -200,11 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<label class="cred-field">
|
<label class="cred-field">
|
||||||
<span class="cred-field-label">Model</span>
|
<span class="cred-field-label">Model</span>
|
||||||
<select bind:value={localModelId}>
|
<SelectMenu value={localModelId} options={localModels.map((option) => ({ value: option.id, label: `${option.label} - ${formatSize(option.approx_size_mb)}` }))} onChange={(value) => { localModelId = value; }} />
|
||||||
{#each localModels as option (option.id)}
|
|
||||||
<option value={option.id}>{option.label} — {formatSize(option.approx_size_mb)}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="cred-token-hint">
|
<div class="cred-token-hint">
|
||||||
<AlertCircle size={13} aria-hidden="true" />
|
<AlertCircle size={13} aria-hidden="true" />
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
ExternalToolsSettings,
|
ExternalToolsSettings,
|
||||||
ToolOpenMode,
|
ToolOpenMode,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
type SettingsPage = "general" | "tools";
|
type SettingsPage = "general" | "tools";
|
||||||
|
|
||||||
@@ -390,17 +391,17 @@
|
|||||||
|
|
||||||
<label class="tool-default-field">
|
<label class="tool-default-field">
|
||||||
<span>{isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}</span>
|
<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)}`}>
|
<SelectMenu
|
||||||
{#if availablePresets(activeToolKind).length > 0}
|
class="tool-preset-select"
|
||||||
<optgroup label={isGerman ? "Installiert" : "Installed"}>
|
value={tools[activeToolKind].preset}
|
||||||
{#each availablePresets(activeToolKind) as preset}<option value={preset.id}>✓ {preset.label}</option>{/each}
|
options={[
|
||||||
</optgroup>
|
...availablePresets(activeToolKind).map((preset) => ({ value: preset.id, label: `Installed - ${preset.label}`, group: isGerman ? "Installiert" : "Installed" })),
|
||||||
{/if}
|
...otherPresets(activeToolKind).map((preset) => ({ value: preset.id, label: preset.label, group: isGerman ? "Weitere unterstützte Programme" : "Other supported applications" })),
|
||||||
<optgroup label={isGerman ? "Weitere unterstützte Programme" : "Other supported applications"}>
|
{ value: "custom", label: isGerman ? "Eigenes Programm auswählen..." : "Choose a custom application..." },
|
||||||
{#each otherPresets(activeToolKind) as preset}<option value={preset.id}>{preset.label}</option>{/each}
|
]}
|
||||||
</optgroup>
|
ariaLabel={isGerman ? `Standardprogramm für ${toolLabel(activeToolKind)}` : `Default application for ${toolLabel(activeToolKind)}`}
|
||||||
<option value="custom">{isGerman ? "Eigenes Programm auswählen…" : "Choose a custom application…"}</option>
|
onChange={(value) => changePreset(activeToolKind, value)}
|
||||||
</select>
|
/>
|
||||||
<small>{isGerman ? "Diese Auswahl wird gespeichert und für alle passenden Aktionen verwendet." : "This selection is saved and used for every matching action."}</small>
|
<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>
|
</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 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-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, .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-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; }
|
: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-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 { 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; }
|
.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,
|
X,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import type { AppLanguage, GitCommit, GitRemote } from "../types";
|
import type { AppLanguage, GitCommit, GitRemote } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
commit: GitCommit;
|
commit: GitCommit;
|
||||||
@@ -196,11 +197,7 @@
|
|||||||
<div class="commit-note-sync-controls">
|
<div class="commit-note-sync-controls">
|
||||||
<label>
|
<label>
|
||||||
<span>{text.remote}</span>
|
<span>{text.remote}</span>
|
||||||
<select bind:value={selectedRemote} disabled={isBusy}>
|
<SelectMenu value={selectedRemote} options={remotes.map((remote) => ({ value: remote.name, label: remote.name }))} disabled={isBusy} onChange={(value) => { selectedRemote = value; }} />
|
||||||
{#each remotes as remote (remote.name)}
|
|
||||||
<option value={remote.name}>{remote.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
<button type="button" onclick={() => onFetch(selectedRemote)} disabled={isBusy || !selectedRemote || hasChanges}>
|
<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}
|
{#if isBusy}<LoaderCircle class="spin" size={15} aria-hidden="true" />{:else}<Download size={15} aria-hidden="true" />{/if}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ArrowRight, GitCompare, LoaderCircle } from "@lucide/svelte";
|
import { ArrowRight, GitCompare, LoaderCircle } from "@lucide/svelte";
|
||||||
import type { GitCommit, GitCommitComparison } from "../types";
|
import type { GitCommit, GitCommitComparison } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
commits: GitCommit[];
|
commits: GitCommit[];
|
||||||
@@ -40,6 +41,8 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onCompare();
|
onCompare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let commitOptions = $derived(commits.map((item) => ({ value: item.hash, label: commitOptionLabel(item), group: "Commits" })));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="compare-panel panel" aria-label="Compare commits">
|
<section class="compare-panel panel" aria-label="Compare commits">
|
||||||
@@ -61,32 +64,14 @@
|
|||||||
<form class="compare-form" onsubmit={handleSubmit}>
|
<form class="compare-form" onsubmit={handleSubmit}>
|
||||||
<label class="compare-field">
|
<label class="compare-field">
|
||||||
<span>From (older)</span>
|
<span>From (older)</span>
|
||||||
<select
|
<SelectMenu value={compareFrom} options={commitOptions} placeholder="Select a commit" showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
|
||||||
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>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
|
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
|
||||||
|
|
||||||
<label class="compare-field">
|
<label class="compare-field">
|
||||||
<span>To (newer)</span>
|
<span>To (newer)</span>
|
||||||
<select
|
<SelectMenu value={compareTo} options={commitOptions} placeholder="Select a commit" showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
|
||||||
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>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button class="btn-primary" type="submit" disabled={!canCompare}>
|
<button class="btn-primary" type="submit" disabled={!canCompare}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ArrowRight, GitCompare, LoaderCircle, X } from "@lucide/svelte";
|
import { ArrowRight, GitCompare, LoaderCircle, X } from "@lucide/svelte";
|
||||||
import type { GitBranch, GitCommit } from "../types";
|
import type { GitBranch, GitCommit } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
commits: GitCommit[];
|
commits: GitCommit[];
|
||||||
@@ -41,6 +42,11 @@
|
|||||||
let localBranches = $derived(branches.filter((branch) => !branch.remote));
|
let localBranches = $derived(branches.filter((branch) => !branch.remote));
|
||||||
let remoteBranches = $derived(branches.filter((branch) => branch.remote));
|
let remoteBranches = $derived(branches.filter((branch) => branch.remote));
|
||||||
let targetCount = $derived(branches.length + commits.length);
|
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) {
|
function handleSubmit(event: SubmitEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -69,68 +75,14 @@
|
|||||||
<form class="compare-form" onsubmit={handleSubmit}>
|
<form class="compare-form" onsubmit={handleSubmit}>
|
||||||
<label class="compare-field">
|
<label class="compare-field">
|
||||||
<span>Base</span>
|
<span>Base</span>
|
||||||
<select
|
<SelectMenu class="compare-target-select" value={compareFrom} options={compareOptions} placeholder="Select a branch or commit" showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
|
||||||
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>
|
</label>
|
||||||
|
|
||||||
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
|
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
|
||||||
|
|
||||||
<label class="compare-field">
|
<label class="compare-field">
|
||||||
<span>Compare with</span>
|
<span>Compare with</span>
|
||||||
<select
|
<SelectMenu class="compare-target-select" value={compareTo} options={compareOptions} placeholder="Select a branch or commit" showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
|
||||||
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>
|
||||||
|
|
||||||
<button class="btn-primary" type="submit" disabled={!canCompare}>
|
<button class="btn-primary" type="submit" disabled={!canCompare}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
FileCode,
|
FileCode,
|
||||||
@@ -193,12 +194,7 @@
|
|||||||
|
|
||||||
<label class="search-limit">
|
<label class="search-limit">
|
||||||
<span>Results</span>
|
<span>Results</span>
|
||||||
<select bind:value={limit} disabled={!hasRepository || isBusy || isSearching}>
|
<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); }} />
|
||||||
<option value={100}>100</option>
|
|
||||||
<option value={250}>250</option>
|
|
||||||
<option value={500}>500</option>
|
|
||||||
<option value={1000}>1000</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button class="btn-primary" type="submit" disabled={!hasRepository || isBusy || isSearching || query.trim().length === 0}>
|
<button class="btn-primary" type="submit" disabled={!hasRepository || isBusy || isSearching || query.trim().length === 0}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertTriangle, ArrowDown, ArrowUp, GitBranch, LoaderCircle, Play, X } from "@lucide/svelte";
|
import { AlertTriangle, ArrowDown, ArrowUp, GitBranch, LoaderCircle, Play, X } from "@lucide/svelte";
|
||||||
import type { GitBranch as GitBranchInfo, RebaseAction, RebaseCommit, RebasePlanItem } from "../types";
|
import type { GitBranch as GitBranchInfo, RebaseAction, RebaseCommit, RebasePlanItem } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
interface PlanRow extends RebaseCommit {
|
interface PlanRow extends RebaseCommit {
|
||||||
action: RebaseAction;
|
action: RebaseAction;
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
));
|
));
|
||||||
let invalidReword = $derived(rows.some((row) => row.action === "reword" && !row.message.trim()));
|
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);
|
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) {
|
function updateAction(index: number, action: RebaseAction) {
|
||||||
rows = rows.map((row, rowIndex) => rowIndex === index ? { ...row, action } : row);
|
rows = rows.map((row, rowIndex) => rowIndex === index ? { ...row, action } : row);
|
||||||
@@ -82,12 +84,7 @@
|
|||||||
<section class="rebase-base-bar">
|
<section class="rebase-base-bar">
|
||||||
<label>
|
<label>
|
||||||
<span>Rebase <strong>{currentBranch || "current branch"}</strong> onto</span>
|
<span>Rebase <strong>{currentBranch || "current branch"}</strong> onto</span>
|
||||||
<select value={base} onchange={(event) => onBaseChange((event.target as HTMLSelectElement).value)} disabled={isBusy || isLoading}>
|
<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} />
|
||||||
<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>
|
|
||||||
</label>
|
</label>
|
||||||
<p>Oldest commit first. Reorder commits, then choose how each one should be replayed.</p>
|
<p>Oldest commit first. Reorder commits, then choose how each one should be replayed.</p>
|
||||||
</section>
|
</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 === 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>
|
<button type="button" onclick={() => move(index, 1)} disabled={isBusy || index === rows.length - 1} title="Move down"><ArrowDown size={14} aria-hidden="true" /></button>
|
||||||
</div>
|
</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}`}>
|
<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)} />
|
||||||
<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>
|
|
||||||
<code>{row.short_hash}</code>
|
<code>{row.short_hash}</code>
|
||||||
<div class="rebase-commit-copy">
|
<div class="rebase-commit-copy">
|
||||||
{#if row.action === "reword"}
|
{#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">
|
<script lang="ts">
|
||||||
import { Cloud, GitBranch, Plus, Save, Trash2, X } from "@lucide/svelte";
|
import { Cloud, GitBranch, Plus, Save, Trash2, X } from "@lucide/svelte";
|
||||||
import type { GitRemote, PullStrategy } from "../types";
|
import type { GitRemote, PullStrategy } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
export let remotes: GitRemote[] = [];
|
export let remotes: GitRemote[] = [];
|
||||||
export let remoteBranches: string[] = [];
|
export let remoteBranches: string[] = [];
|
||||||
@@ -76,8 +77,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sync-fields">
|
<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 ? "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><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 ? `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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
X,
|
X,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import type { GitBranch as GitBranchInfo, GitWorktree } from "../types";
|
import type { GitBranch as GitBranchInfo, GitWorktree } from "../types";
|
||||||
|
import SelectMenu from "./SelectMenu.svelte";
|
||||||
|
|
||||||
type CreateMode = "existing" | "new" | "detached";
|
type CreateMode = "existing" | "new" | "detached";
|
||||||
|
|
||||||
@@ -260,12 +261,17 @@
|
|||||||
{#if createMode === "existing"}
|
{#if createMode === "existing"}
|
||||||
<label>
|
<label>
|
||||||
<span>Branch</span>
|
<span>Branch</span>
|
||||||
<select bind:value={selectedBranch} disabled={isBusy}>
|
<SelectMenu
|
||||||
<option value="" disabled>Select a local branch</option>
|
value={selectedBranch}
|
||||||
{#each localBranches as branch (branch.name)}
|
placeholder="Select a local branch"
|
||||||
<option value={branch.name} disabled={!branchAvailable(branch.name)}>{branch.name}{!branchAvailable(branch.name) ? " (already checked out)" : ""}</option>
|
options={localBranches.map((branch) => ({
|
||||||
{/each}
|
value: branch.name,
|
||||||
</select>
|
label: `${branch.name}${!branchAvailable(branch.name) ? " (already checked out)" : ""}`,
|
||||||
|
disabled: !branchAvailable(branch.name),
|
||||||
|
}))}
|
||||||
|
disabled={isBusy}
|
||||||
|
onChange={(value) => { selectedBranch = value; }}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
{:else if createMode === "new"}
|
{:else if createMode === "new"}
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
Reference in New Issue
Block a user