Auth for submodule ops, revision checkout, central confirm dialog, unified sidebar sizing #46

Merged
Christoph merged 10 commits from UI/UX-enhance into main 2026-09-18 08:26:46 +00:00
3 changed files with 41 additions and 3 deletions
Showing only changes of commit b21fc93e10 - Show all commits
+4
View File
@@ -327,12 +327,16 @@
.select-menu-option:hover .select-menu-option-icon { color: var(--color-accent); }
.select-menu-option-icon svg { color: inherit; }
.select-menu-option-meta {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
color: var(--color-ink-faint);
font-size: 9.5px;
font-weight: 650;
letter-spacing: 0;
}
.select-menu-option-meta svg { color: inherit; }
.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);
+29 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import SelectMenu from "./SelectMenu.svelte";
import { onMount } from "svelte";
import { GitPullRequest, X, LoaderCircle, ArrowRight, Sparkles } from "@lucide/svelte";
import { GitBranch, GitPullRequest, LockKeyhole, X, LoaderCircle, ArrowRight, Sparkles } from "@lucide/svelte";
import { credLoad, pullRequestAiGenerate, createIntegrationReviewRequest, listIntegrationRepositories, listIntegrationRepositoryBranches, listRemotes, listBranches } from "../git";
import { integrationCredentialKey } from "../integrations";
import type { AiSettings, GitIntegrationSource, GitIntegrationRepository, IntegrationReviewRequest, StoredCredential } from "../types";
@@ -15,6 +15,26 @@
let dialog: HTMLDialogElement;
let titleInput: HTMLInputElement;
let repositories = $state<GitIntegrationRepository[]>([]);
/** Repository options grouped by owner, like the clone dialog's list. */
const repositoryOptions = $derived(repositories.map((repository) => {
const separator = repository.fullName.lastIndexOf("/");
return {
value: repository.id,
label: separator > 0 ? repository.fullName.slice(separator + 1) : repository.fullName,
group: separator > 0 ? repository.fullName.slice(0, separator) : source.label,
};
}));
function repositoryById(id: string): GitIntegrationRepository | undefined {
return repositories.find((repository) => repository.id === id);
}
function formatUpdatedAt(value: string): string {
if (!value) return "";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "" : new Intl.DateTimeFormat(de ? "de-DE" : "en-US", { dateStyle: "medium" }).format(date);
}
let repositoryId = $state("");
let sourceBranch = $state("");
let targetBranch = $state("");
@@ -129,7 +149,14 @@
<div class="body">
{#if error}<div class="error" role="alert">{error}{#if !repositories.length && !loading}<button type="button" onclick={loadRepositories}>{de ? "Erneut laden" : "Retry"}</button>{/if}</div>{/if}
<div class="repository-field"><div class="field-heading"><span>Repository</span><small>{loading ? "…" : `${repositories.length} ${de ? "verfügbar" : "available"}`}</small></div>
<SelectMenu value={repositoryId} options={repositories.map(repository => ({value:repository.id,label:repository.fullName}))} disabled={loading || busy} ariaLabel="Repository" placeholder={loading ? (de ? "Repositories werden geladen …" : "Loading repositories…") : (de ? "Repository auswählen" : "Select repository")} searchable searchPlaceholder={de ? "Repositories durchsuchen …" : "Search repositories…"} emptyText={de ? "Keine passenden Repositories" : "No matching repositories"} onChange={value => repositoryId = value}/>
<SelectMenu value={repositoryId} options={repositoryOptions} disabled={loading || busy} ariaLabel="Repository" placeholder={loading ? (de ? "Repositories werden geladen …" : "Loading repositories…") : (de ? "Repository auswählen" : "Select repository")} searchable searchPlaceholder={de ? "Repositories durchsuchen …" : "Search repositories…"} emptyText={de ? "Keine passenden Repositories" : "No matching repositories"} showSelectedGroup onChange={value => repositoryId = value}>
{#snippet optionIcon()}<GitBranch size={14} aria-hidden="true" />{/snippet}
{#snippet optionMeta(option)}
{@const repository = repositoryById(option.value)}
{#if repository?.private}<LockKeyhole size={11} aria-label={de ? "Privat" : "Private"} />{/if}
{formatUpdatedAt(repository?.updatedAt ?? "")}
{/snippet}
</SelectMenu>
</div>
{#if !loading && !error && !repositories.length}<p>{de ? "Keine Repositories für diese Integration gefunden." : "No repositories found for this integration."}</p>{/if}
<div class="branches">
+8 -1
View File
@@ -25,6 +25,8 @@
emptyText?: string;
/** Optional icon in front of every option, e.g. a branch or repository mark. */
optionIcon?: Snippet<[SelectMenuOption]>;
/** Optional right-aligned content per option, richer than `option.meta`. */
optionMeta?: Snippet<[SelectMenuOption]>;
onChange: (value: string) => void;
}
@@ -40,6 +42,7 @@
searchPlaceholder = "Search…",
emptyText = "No results",
optionIcon = undefined,
optionMeta = undefined,
onChange,
}: Props = $props();
@@ -195,7 +198,11 @@
>
{#if optionIcon}<span class="select-menu-option-icon">{@render optionIcon(option)}</span>{/if}
<span class="select-menu-option-label">{option.label}</span>
{#if option.meta}<small class="select-menu-option-meta">{option.meta}</small>{/if}
{#if optionMeta}
<small class="select-menu-option-meta">{@render optionMeta(option)}</small>
{:else if option.meta}
<small class="select-menu-option-meta">{option.meta}</small>
{/if}
{#if option.value === value}<Check size={14} aria-hidden="true" />{/if}
</button>
{/each}