feat(ui): add init repository dialog

Add a modal dialog to initialize a new Git repository, replacing the
previous prompt-based flow. It integrates with the app state and invokes
the initialization operation then opens the repository on confirmation.
Also adds styles, keyboard handling, accessibility hints, a folder
browser, initial-branch input, and German/English labels with busy state.

- New dialog component with folder picker, branch input, and i18n.
- Wire dialog open/close and confirm handler to init and open actions.
- Add CSS rules for dialog layout, controls, and error/busy states.
This commit is contained in:
2026-09-01 21:13:46 +02:00
parent 90df347e4a
commit add98f962e
3 changed files with 207 additions and 4 deletions
+23 -4
View File
@@ -26,6 +26,7 @@
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
@@ -313,6 +314,7 @@
let repoStatusCache: Record<string, RepoTab> = {};
let repoSearch = "";
let cloneDialogOpen = false;
let initRepositoryDialogOpen = false;
let pullStrategy: PullStrategy = (localStorage.getItem("gitlite.pullStrategy") as PullStrategy) || "merge";
let selectedRemote = localStorage.getItem("gitlite.selectedRemote") || "";
let remoteActionForceWithLease = false;
@@ -3926,10 +3928,17 @@
}
async function initializeRepository() {
const selected = await openDialog({ directory: true, multiple: false, title: "Choose an empty or existing folder" });
if (typeof selected !== "string") return;
const branch = window.prompt("Initial branch name", "main")?.trim(); if (!branch) return;
await runOperation("Initializing repository", async () => { await initRepository(selected, branch); await openRepo(selected); });
initRepositoryDialogOpen = true;
}
async function confirmInitializeRepository(path: string, branch: string) {
const selected = path.trim();
if (!selected) return;
await runOperation("Initializing repository", async () => {
await initRepository(selected, branch);
initRepositoryDialogOpen = false;
await openRepo(selected);
});
}
async function revertHistoryCommit(commit: GitCommit) {
@@ -6090,6 +6099,16 @@
/>
{/if}
<!-- Initialize repository dialog -->
{#if initRepositoryDialogOpen}
<InitRepositoryDialog
isBusy={operation === "Initializing repository"}
language={appLanguage}
onInit={confirmInitializeRepository}
onClose={() => { if (!isBusy) initRepositoryDialogOpen = false; }}
/>
{/if}
<!-- Full-screen overlay while a repository is being opened -->
{#if openingRepo}
<RepoLoadingOverlay repoName={repoDisplayName} />
+35
View File
@@ -3878,6 +3878,13 @@
max-height: calc(100vh - 32px);
overflow: auto;
}
.init-repository-dialog {
display: block;
width: min(540px, calc(100vw - 32px));
height: auto;
max-height: calc(100vh - 32px);
overflow: auto;
}
.rename-branch-dialog {
display: block;
width: min(520px, calc(100vw - 32px));
@@ -4172,6 +4179,34 @@
gap: 14px;
padding: 16px;
}
.init-repository-header { padding: 16px 18px; }
.init-repository-heading { display: flex; align-items: center; gap: 12px; }
.init-repository-heading h2 { margin: 2px 0 0; color: var(--color-ink); font-size: 15px; line-height: 1.25; }
.init-repository-icon {
display: grid;
place-items: center;
width: 36px;
height: 36px;
flex: 0 0 auto;
border: 1px solid color-mix(in srgb, var(--color-accent) 34%, var(--color-border));
border-radius: 9px;
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface-raised));
}
.init-repository-form { display: flex; flex-direction: column; gap: 16px; padding: 18px; }
.init-repository-description { margin: 0; color: var(--color-ink-dim); font-size: 11.5px; line-height: 1.5; }
.init-repository-field > span { font-size: 9.5px; }
.init-repository-field > div { position: relative; align-items: center; }
.init-repository-field > div > svg { position: absolute; left: 11px; z-index: 1; color: var(--color-ink-faint); pointer-events: none; }
.init-repository-field > div > input { height: 36px; padding-left: 36px; font-family: var(--font-mono); font-size: 12px; }
.init-repository-field small { color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; }
.init-repository-path-field { padding: 12px; border: 1px solid var(--color-border-subtle); border-radius: 9px; background: var(--color-surface-raised); }
.init-repository-path-control { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
.init-repository-path-control > input { grid-column: 1; }
.init-repository-browse { grid-column: 2; display: inline-flex; align-items: center; gap: 6px; height: 36px; font-size: 11.5px; white-space: nowrap; }
.init-repository-error { color: var(--color-danger, #ff6b78) !important; }
.init-repository-actions { padding-top: 2px; }
.init-repository-actions > button { font-size: 11.5px; }
.rename-branch-form {
display: flex;
flex-direction: column;
@@ -0,0 +1,149 @@
<script lang="ts">
import { onMount } from "svelte";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { Folder, FolderOpen, GitBranch, LoaderCircle, Plus, X } from "@lucide/svelte";
import type { AppLanguage } from "../types";
interface Props {
isBusy: boolean;
language: AppLanguage;
onInit: (path: string, branch: string) => void;
onClose: () => void;
}
let {
isBusy = false,
language = "en",
onInit = () => {},
onClose = () => {},
}: Props = $props();
let path = $state("");
let branch = $state("main");
let browseError = $state("");
const isGerman = $derived(language === "de");
const pathPlaceholder = navigator.userAgent.includes("Windows")
? "C:\\Projects\\my-repository"
: "/home/user/projects/my-repository";
function submit(event: SubmitEvent) {
event.preventDefault();
const targetPath = path.trim();
const branchName = branch.trim();
if (!targetPath || !branchName || isBusy) return;
onInit(targetPath, branchName);
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && !isBusy) onClose();
}
async function chooseFolder() {
if (isBusy) return;
browseError = "";
try {
const selected = await openDialog({
title: isGerman ? "Ordner für das neue Repository auswählen" : "Select folder for the new repository",
directory: true,
multiple: false,
defaultPath: path.trim() || undefined,
});
if (typeof selected === "string") path = selected;
} catch (error) {
browseError = error instanceof Error ? error.message : String(error);
}
}
onMount(() => {
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
});
</script>
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
<div
class="dialog init-repository-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="init-repository-title"
tabindex="-1"
>
<header class="dialog-header init-repository-header">
<div class="init-repository-heading">
<span class="init-repository-icon" aria-hidden="true"><Plus size={18} /></span>
<div>
<span class="eyebrow">{isGerman ? "Neues Repository" : "New repository"}</span>
<h2 id="init-repository-title">{isGerman ? "Repository initialisieren" : "Initialize repository"}</h2>
</div>
</div>
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title={isGerman ? "Schließen" : "Close"}>
<X size={18} aria-hidden="true" />
</button>
</header>
<form class="init-repository-form" onsubmit={submit}>
<p class="init-repository-description">
{isGerman
? "GitLite richtet in diesem Ordner ein neues Git-Repository ein. Vorhandene Dateien bleiben unverändert."
: "GitLite will create a new Git repository in this folder. Existing files will remain unchanged."}
</p>
<label class="new-branch-field init-repository-field init-repository-path-field">
<span>{isGerman ? "Zielordner" : "Repository folder"}</span>
<div class="init-repository-path-control">
<Folder size={16} aria-hidden="true" />
<!-- svelte-ignore a11y_autofocus -->
<input
bind:value={path}
autocomplete="off"
spellcheck="false"
placeholder={pathPlaceholder}
disabled={isBusy}
autofocus
aria-describedby="init-repository-path-hint"
/>
<button class="btn-secondary init-repository-browse" type="button" onclick={chooseFolder} disabled={isBusy}>
<FolderOpen size={15} aria-hidden="true" />
{isGerman ? "Auswählen…" : "Browse…"}
</button>
</div>
<small id="init-repository-path-hint">
{isGerman ? "Der Ordner wird angelegt, falls er noch nicht existiert." : "The folder will be created if it does not exist."}
</small>
{#if browseError}<small class="init-repository-error" role="alert">{browseError}</small>{/if}
</label>
<label class="new-branch-field init-repository-field">
<span>{isGerman ? "Name des ersten Branches" : "Initial branch name"}</span>
<div>
<GitBranch size={16} aria-hidden="true" />
<input
bind:value={branch}
autocomplete="off"
spellcheck="false"
placeholder="main"
disabled={isBusy}
aria-describedby="init-repository-branch-hint"
/>
</div>
<small id="init-repository-branch-hint">
{isGerman ? "Du kannst den Branch später jederzeit umbenennen." : "You can rename the branch at any time."}
</small>
</label>
<div class="new-branch-actions init-repository-actions">
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>
{isGerman ? "Abbrechen" : "Cancel"}
</button>
<button class="btn-primary" type="submit" disabled={isBusy || path.trim().length === 0 || branch.trim().length === 0}>
{#if isBusy}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{:else}
<Plus size={16} aria-hidden="true" />
{/if}
{isGerman ? "Repository erstellen" : "Create repository"}
</button>
</div>
</form>
</div>
</div>