Compare commits
14
Commits
2026.7.19
...
735acd2551
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
735acd2551 | ||
|
|
f9c0c00618 | ||
|
|
ff00925b13 | ||
|
|
982dbf136d | ||
|
|
646dcc341e | ||
|
|
5f3e55dcd7 | ||
|
|
fdbff8175e | ||
|
|
a6e7e991dd | ||
|
|
3491b8efb3 | ||
|
|
59a8f34e0e | ||
|
|
286b106baa | ||
|
|
670e7e24fe | ||
|
|
6729d61dca | ||
|
|
b181b384e7 |
@@ -104,7 +104,19 @@
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/**)",
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-runtime-wry-2.11.4/src/**)",
|
||||
"Read(//home/christoph/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.11.5/src/window/**)",
|
||||
"WebSearch"
|
||||
"WebSearch",
|
||||
"Bash(node -p \"require\\('./package.json'\\).version\")",
|
||||
"Bash(bash -n PKGBUILD)",
|
||||
"Bash(makepkg --printsrcinfo)",
|
||||
"Bash(npx tauri *)",
|
||||
"Bash(echo \"---exit code:$?---\")",
|
||||
"Bash(git -C /mnt/data/Development/GitLite status --short)",
|
||||
"Bash(makepkg -f --noconfirm)",
|
||||
"Bash(tar -tf gitty-2026.7.19-1-x86_64.pkg.tar.zst)",
|
||||
"Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)",
|
||||
"Bash(sed -i 's/^pkgrel=1/pkgrel=3/' PKGBUILD)",
|
||||
"Bash(node -e \"const fs=require\\('fs'\\); const version='2026.8.1'; const pkgbuild=fs.readFileSync\\('PKGBUILD','utf8'\\).replace\\(/^pkgver=.*\\\\$/m, 'pkgver='+version\\).replace\\(/^pkgrel=.*\\\\$/m, 'pkgrel=1'\\); fs.writeFileSync\\('PKGBUILD', pkgbuild\\);\")",
|
||||
"Bash(rm PKGBUILD)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ jobs:
|
||||
run: |
|
||||
node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');"
|
||||
|
||||
- name: Update PKGBUILD Version
|
||||
run: |
|
||||
node -e "const fs=require('fs'); const version=require('./package.json').version; const pkgbuild=fs.readFileSync('PKGBUILD','utf8').replace(/^pkgver=.*$/m, 'pkgver='+version).replace(/^pkgrel=.*$/m, 'pkgrel=1'); fs.writeFileSync('PKGBUILD', pkgbuild);"
|
||||
|
||||
- name: Commit and Push Changes
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: powershell
|
||||
@@ -95,7 +99,7 @@ jobs:
|
||||
git config user.email '${{ vars.EMAIL_GIT }}'
|
||||
# npm version also bumps the version inside package-lock.json, so stage it
|
||||
# too -- otherwise it stays as an unstaged change and blocks the rebase.
|
||||
git add package.json package-lock.json src-tauri/tauri.conf.json
|
||||
git add package.json package-lock.json src-tauri/tauri.conf.json PKGBUILD
|
||||
|
||||
git commit -m 'Update version to ${{ github.ref_name }}'
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
@@ -7,3 +7,6 @@
|
||||
~
|
||||
.codex*
|
||||
target
|
||||
|
||||
*.pkg.tar.zst
|
||||
pkg
|
||||
@@ -0,0 +1,66 @@
|
||||
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
|
||||
#
|
||||
# Local/in-tree PKGBUILD: builds straight from this working directory (no
|
||||
# source download, no tauri-bundler/AppImage step) and installs the raw
|
||||
# binary + desktop entry. Run `makepkg -si` from the repo root.
|
||||
|
||||
pkgname=gitty
|
||||
pkgver=2026.7.19
|
||||
pkgrel=1
|
||||
pkgdesc="A lightweight, modern Git client built with Tauri"
|
||||
arch=('x86_64')
|
||||
url="https://git.cbsk-tech.de/Christoph/GitLite"
|
||||
license=('MIT')
|
||||
depends=('webkit2gtk-4.1' 'gtk3' 'git' 'hicolor-icon-theme')
|
||||
makedepends=('rust' 'nodejs' 'npm')
|
||||
options=('!lto')
|
||||
|
||||
source=()
|
||||
sha256sums=()
|
||||
|
||||
# Keeps pkgver in sync with package.json (the release CI bumps that file).
|
||||
pkgver() {
|
||||
cd "$startdir"
|
||||
node -p "require('./package.json').version"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$startdir"
|
||||
npm ci
|
||||
# Build through the Tauri CLI (not a raw `cargo build --release`): it enables
|
||||
# the `custom-protocol` cargo feature that makes the binary load the embedded
|
||||
# `dist/` assets instead of the Vite dev server URL (127.0.0.1:1420), and runs
|
||||
# `beforeBuildCommand` (`npm run build`) for us. `--no-bundle` skips deb/rpm/
|
||||
# appimage packaging (and with it linuxdeploy/FUSE) since pacman owns that here.
|
||||
npm run tauri -- build --no-bundle
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$startdir"
|
||||
|
||||
install -Dm755 "src-tauri/target/release/gitty" "$pkgdir/usr/bin/gitty"
|
||||
|
||||
install -Dm644 "src-tauri/icons/32x32.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/32x32/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/128x128.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/128x128/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/128x128@2x.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/256x256@2/apps/gitty.png"
|
||||
install -Dm644 "src-tauri/icons/icon.png" \
|
||||
"$pkgdir/usr/share/icons/hicolor/512x512/apps/gitty.png"
|
||||
|
||||
install -d "$pkgdir/usr/share/applications"
|
||||
cat > "$pkgdir/usr/share/applications/gitty.desktop" <<-EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Gitty
|
||||
Comment=$pkgdesc
|
||||
Exec=gitty
|
||||
Icon=gitty
|
||||
Terminal=false
|
||||
Categories=Development;RevisionControl;
|
||||
StartupWMClass=gitty
|
||||
EOF
|
||||
|
||||
install -Dm644 "README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"dependencies": {
|
||||
"@lucide/svelte": "^1.21.0",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -27,6 +27,12 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
# Self-updating only makes sense for install methods this app ships itself
|
||||
# (NSIS on Windows, the .app bundle on macOS). On Linux the app is meant to be
|
||||
# installed via the system package manager (e.g. the PKGBUILD), which owns
|
||||
# updates instead — so the updater plugin isn't even compiled in there.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios", target_os = "linux")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
# ── Build profiles ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
"windows"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
|
||||
+11
-3
@@ -33,6 +33,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
|
||||
}
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.maximize();
|
||||
window
|
||||
.show()
|
||||
.map_err(|error| format!("failed to show main window: {error}"))?;
|
||||
@@ -52,7 +53,7 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
let builder =tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||
#[cfg(desktop)]
|
||||
let _ = app
|
||||
@@ -60,7 +61,6 @@ async fn main() {
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
}))
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder::new("A-SH-1344793789")
|
||||
.with_options(tauri_plugin_aptabase::InitOptions {
|
||||
@@ -71,7 +71,15 @@ async fn main() {
|
||||
)
|
||||
.manage(SearchCancellationState::default())
|
||||
.manage(commit_ai::CommitAiEngine::new())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_dialog::init());
|
||||
|
||||
// Linux installs are expected to come from the system package manager (see the
|
||||
// PKGBUILD), which owns updates itself — the self-updater is only wired up for
|
||||
// the platforms whose install method this app ships (NSIS/Windows, .app/macOS).
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
builder
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
open_repository,
|
||||
clone_repository,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Gitty",
|
||||
"version": "0.200.10",
|
||||
"version": "2026.7.19",
|
||||
"identifier": "com.gitty",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
+110
-81
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { AlertCircle, BookOpen, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||
import RepoTabs from "./lib/RepoTabs.svelte";
|
||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||
@@ -21,6 +24,7 @@
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||
import HelpOverlay from "./lib/components/HelpOverlay.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
|
||||
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
||||
@@ -104,6 +108,7 @@
|
||||
|
||||
import type {
|
||||
AiSettings,
|
||||
AppLanguage,
|
||||
AppTheme,
|
||||
AnalyticsSettings,
|
||||
CommitAiPhase,
|
||||
@@ -176,6 +181,7 @@
|
||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||
const APP_THEME_KEY = "gitlite.theme.v1";
|
||||
const APP_LANGUAGE_KEY = "gitlite.language.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
||||
@@ -248,9 +254,11 @@
|
||||
let aiSettings: AiSettings = defaultAiSettings();
|
||||
let aiSettingsOpen = false;
|
||||
let appSettingsOpen = false;
|
||||
let helpOpen = false;
|
||||
let analyticsNoticeOpen = false;
|
||||
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||
let appTheme: AppTheme = loadThemePreference();
|
||||
let appLanguage: AppLanguage = loadLanguagePreference();
|
||||
let localModelOptions: LocalModelOption[] = [];
|
||||
let errorMessage = "";
|
||||
let operation = "";
|
||||
@@ -359,6 +367,7 @@
|
||||
let fileHistoryResizeStartWidth = 0;
|
||||
let fileHistoryCollapsed = true;
|
||||
let themeMediaQuery: MediaQueryList | undefined;
|
||||
let appVersion = "";
|
||||
|
||||
// ── Derived ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -412,6 +421,7 @@
|
||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||
|
||||
$: applyThemePreference(appTheme);
|
||||
$: applyLanguagePreference(appLanguage);
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -419,6 +429,7 @@
|
||||
themeMediaQuery = window.matchMedia("(prefers-color-scheme: light)");
|
||||
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||
void runStartupSequence();
|
||||
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -672,7 +683,7 @@
|
||||
}
|
||||
|
||||
async function autoRefreshTick() {
|
||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen) return;
|
||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
||||
const path = activeRepoPath;
|
||||
autoRefreshInFlight = true;
|
||||
try {
|
||||
@@ -813,6 +824,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadLanguagePreference(): AppLanguage {
|
||||
try {
|
||||
const stored = localStorage.getItem(APP_LANGUAGE_KEY);
|
||||
if (stored === "en" || stored === "de") return stored;
|
||||
} catch {
|
||||
// Local storage is optional; English is the first-start default.
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
function persistLanguagePreference(next: AppLanguage) {
|
||||
try {
|
||||
localStorage.setItem(APP_LANGUAGE_KEY, next);
|
||||
} catch {
|
||||
// Ignore storage quota/private-mode errors.
|
||||
}
|
||||
}
|
||||
|
||||
function applyLanguagePreference(next: AppLanguage) {
|
||||
document.documentElement.lang = next;
|
||||
document.documentElement.dataset.language = next;
|
||||
}
|
||||
|
||||
function applyThemePreference(next: AppTheme) {
|
||||
const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
const resolved = next === "system"
|
||||
@@ -828,13 +862,15 @@
|
||||
if (appTheme === "system") applyThemePreference(appTheme);
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme) {
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextLanguage: AppLanguage) {
|
||||
analyticsSettings = next;
|
||||
appTheme = nextTheme;
|
||||
appLanguage = nextLanguage;
|
||||
persistAnalyticsSettings(next);
|
||||
persistThemePreference(nextTheme);
|
||||
persistLanguagePreference(nextLanguage);
|
||||
appSettingsOpen = false;
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme });
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage });
|
||||
}
|
||||
|
||||
function updateCommitMessage(message: string) {
|
||||
@@ -3259,6 +3295,11 @@
|
||||
trackEvent("global_search_opened");
|
||||
}
|
||||
|
||||
function openHelp() {
|
||||
helpOpen = true;
|
||||
trackEvent("help_opened");
|
||||
}
|
||||
|
||||
async function compareSelectedCommits() {
|
||||
if (!canCompare) return;
|
||||
await runOperation("Comparing commits", async () => {
|
||||
@@ -3449,6 +3490,15 @@
|
||||
// ── Event handlers ─────────────────────────────────────────────────────────
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && helpOpen) {
|
||||
helpOpen = false;
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu();
|
||||
else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
||||
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
@@ -3479,89 +3529,50 @@
|
||||
|
||||
<main class="shell">
|
||||
<TitleBar
|
||||
branch={status?.current_branch ?? ""}
|
||||
ahead={status?.ahead ?? 0}
|
||||
behind={status?.behind ?? 0}
|
||||
repoName={activeRepoPath ? (activeRepoPath.split(/[\\/]/).filter(Boolean).slice(-1)[0] ?? "") : ""}
|
||||
hasRepository={workspaceActive}
|
||||
{isBusy}
|
||||
{operation}
|
||||
{autoRefreshEnabled}
|
||||
{autoRefreshInFlight}
|
||||
onFetch={fetchRepo}
|
||||
onPull={pullRepo}
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={openGlobalSearchDialog}
|
||||
onCompare={openCompareSelect}
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||
onOpenHelp={openHelp}
|
||||
language={appLanguage}
|
||||
/>
|
||||
|
||||
<div class="shell-body">
|
||||
|
||||
<header class="repo-tabbar" aria-label="Repository tabs">
|
||||
<button
|
||||
class="repo-tab management"
|
||||
class:active={activeView === "management"}
|
||||
type="button"
|
||||
onclick={openRepoManagement}
|
||||
disabled={isBusy}
|
||||
title="Repository Management"
|
||||
>
|
||||
<BookOpen size={14} aria-hidden="true" />
|
||||
Repository Management
|
||||
</button>
|
||||
<RepoTabs
|
||||
{activeView}
|
||||
{repoTabs}
|
||||
{isBusy}
|
||||
language={appLanguage}
|
||||
onOpenManagement={openRepoManagement}
|
||||
isActive={(path) => activeView === "repository" && sameRepoPath(activeRepoPath, path)}
|
||||
onSelect={selectRepoTab}
|
||||
onClose={closeRepoTab}
|
||||
onContextMenu={openRepoTabContextMenu}
|
||||
onAdd={chooseRepositoryFolder}
|
||||
/>
|
||||
|
||||
<div class="repo-tabs-scroll">
|
||||
{#each repoTabs as repo (repo.path)}
|
||||
<div
|
||||
class="repo-tab-wrap"
|
||||
class:active={activeView === "repository" && sameRepoPath(activeRepoPath, repo.path)}
|
||||
role="presentation"
|
||||
oncontextmenu={(event) => openRepoTabContextMenu(repo.path, event)}
|
||||
>
|
||||
<button
|
||||
class="repo-tab"
|
||||
type="button"
|
||||
onclick={() => selectRepoTab(repo.path)}
|
||||
disabled={isBusy}
|
||||
title={repo.path}
|
||||
>
|
||||
<FolderOpen size={14} aria-hidden="true" />
|
||||
<span>{repo.name}</span>
|
||||
{#if repo.branch}
|
||||
<strong>{repo.branch}</strong>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="repo-tab-close"
|
||||
type="button"
|
||||
onclick={(event) => closeRepoTab(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
aria-label={`Close ${repo.name}`}
|
||||
title="Close repository tab"
|
||||
>
|
||||
<X size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="repo-tab-add"
|
||||
type="button"
|
||||
onclick={chooseRepositoryFolder}
|
||||
disabled={isBusy}
|
||||
title="Open repository folder"
|
||||
aria-label="Open repository folder"
|
||||
>
|
||||
<Plus size={15} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<!-- Repo actions live under the tab bar and disappear in Repository
|
||||
Management, where none of them are applicable. -->
|
||||
{#if workspaceActive}
|
||||
<RepoToolbar
|
||||
hasRepository={workspaceActive}
|
||||
{isBusy}
|
||||
{operation}
|
||||
ahead={status?.ahead ?? 0}
|
||||
behind={status?.behind ?? 0}
|
||||
{autoRefreshEnabled}
|
||||
{autoRefreshInFlight}
|
||||
language={appLanguage}
|
||||
onFetch={fetchRepo}
|
||||
onPull={pullRepo}
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={openGlobalSearchDialog}
|
||||
onCompare={openCompareSelect}
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if repoTabContextMenu}
|
||||
{@const menu = repoTabContextMenu}
|
||||
@@ -4086,6 +4097,19 @@
|
||||
</aside>
|
||||
</section>
|
||||
{/if}
|
||||
<footer class="workspace-statusbar" aria-label="Application status summary">
|
||||
{#if workspaceActive}
|
||||
<span class:clean={status?.clean} class="workspace-health"><span aria-hidden="true"></span>{status?.clean ? "Working tree clean" : `${changedFiles.length} changed ${changedFiles.length === 1 ? "file" : "files"}`}</span>
|
||||
{/if}
|
||||
<span class="workspace-status-spacer"></span>
|
||||
{#if workspaceActive}
|
||||
<span><GitBranch size={12} aria-hidden="true" />{status?.current_branch ?? "No branch"}</span>
|
||||
<span class="ahead">↑ {status?.ahead ?? 0}</span>
|
||||
<span class="behind">↓ {status?.behind ?? 0}</span>
|
||||
<span class:active={autoRefreshEnabled} class="workspace-auto">Auto <i aria-hidden="true"></i></span>
|
||||
{/if}
|
||||
{#if appVersion}<span class="app-version" title={`Gitty version ${appVersion}`}>Gitty v{appVersion}</span>{/if}
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -4113,11 +4137,16 @@
|
||||
<AppSettingsDialog
|
||||
analytics={analyticsSettings}
|
||||
theme={appTheme}
|
||||
language={appLanguage}
|
||||
onSave={saveAppSettings}
|
||||
onClose={() => { appSettingsOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if helpOpen}
|
||||
<HelpOverlay language={appLanguage} onClose={() => { helpOpen = false; }} />
|
||||
{/if}
|
||||
|
||||
{#if linePatchOpen && linePatchFile}
|
||||
<LinePatchDialog
|
||||
file={linePatchFile}
|
||||
|
||||
+651
-47
@@ -541,6 +541,7 @@
|
||||
|
||||
.shell {
|
||||
--app-titlebar-height: 42px;
|
||||
--app-toolbar-height: 58px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -562,7 +563,7 @@
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
height: var(--app-titlebar-height);
|
||||
background:
|
||||
@@ -639,8 +640,187 @@
|
||||
|
||||
.tb-no-repo { color: var(--color-bar-muted); font-size: 12px; font-style: italic; }
|
||||
|
||||
.titlebar-right { display: flex; align-items: stretch; height: 100%; border-left: 1px solid rgba(255,255,255,0.06); }
|
||||
.titlebar-actions { display: flex; align-items: stretch; }
|
||||
/* App-global actions (help, settings) that must stay reachable in every view,
|
||||
including Repository Management where the repo toolbar is hidden. */
|
||||
.titlebar-globals {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Repository actions are ordered by workflow: sync, inspect/history, utilities. */
|
||||
.repo-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
height: var(--app-toolbar-height);
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 10px;
|
||||
background: rgba(16, 17, 29, 0.9);
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.repo-toolbar-sync {
|
||||
display: grid;
|
||||
grid-template-rows: 11px 34px;
|
||||
gap: 2px;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
.repo-toolbar-group-label {
|
||||
padding-left: 6px;
|
||||
color: var(--color-ink-faint);
|
||||
font-size: 8px;
|
||||
font-weight: 850;
|
||||
line-height: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.repo-action-group {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 34px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 7px;
|
||||
background: rgba(255,255,255,0.018);
|
||||
}
|
||||
.repo-action-group > .repo-action,
|
||||
.repo-action-group > .repo-history-wrap > .repo-action {
|
||||
border-right: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.repo-action-group > .repo-action:last-child,
|
||||
.repo-action-group > .repo-history-wrap:last-child > .repo-action {
|
||||
border-right: 0;
|
||||
}
|
||||
.repo-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 38px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.repo-action:first-child { border-radius: 6px 0 0 6px; }
|
||||
.repo-action:last-child { border-radius: 0 6px 6px 0; }
|
||||
.repo-action:only-child { border-radius: 6px; }
|
||||
.repo-action:hover:not(:disabled),
|
||||
.repo-action.active {
|
||||
color: var(--color-ink);
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
.repo-action:disabled { opacity: 0.34; cursor: not-allowed; }
|
||||
.repo-action.sync-primary { color: var(--color-accent); }
|
||||
.repo-action.sync-primary:hover:not(:disabled) { background: rgba(90,140,248,0.1); }
|
||||
.repo-action-label { font-size: 12px; font-weight: 750; }
|
||||
.repo-action-count {
|
||||
color: currentColor;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9.5px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.repo-action-count.behind { color: #7aacff; }
|
||||
.repo-action-count.ahead { color: #e0a040; }
|
||||
.repo-toolbar-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
margin: 0 1px 2px;
|
||||
background: var(--color-border-subtle);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.repo-toolbar-spacer { flex: 1 1 auto; min-width: 6px; }
|
||||
.repo-inspect-actions,
|
||||
.repo-utility-actions { margin-bottom: 1px; }
|
||||
.repo-history-wrap { position: relative; height: 32px; }
|
||||
.history-trigger { border-radius: 0 6px 6px 0; }
|
||||
.history-trigger .history-chevron { transition: transform 120ms ease; }
|
||||
.history-trigger.active .history-chevron { transform: rotate(180deg); }
|
||||
.repo-history-menu {
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
top: calc(100% + 7px);
|
||||
right: -1px;
|
||||
display: grid;
|
||||
width: 240px;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 9px;
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: 0 16px 42px rgba(0,0,0,0.36);
|
||||
}
|
||||
.repo-history-menu > button {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 7px 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
.repo-history-menu > button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
|
||||
.repo-history-menu > button > svg { color: var(--color-accent); }
|
||||
.repo-history-menu > button > span { display: grid; gap: 2px; min-width: 0; }
|
||||
.repo-history-menu strong { color: inherit; font-size: 11px; }
|
||||
.repo-history-menu small { overflow: hidden; color: var(--color-ink-faint); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.repo-utility-actions { flex: 0 0 auto; }
|
||||
.repo-action.icon-action { width: 36px; padding: 0; }
|
||||
.repo-auto-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 8px 0 10px;
|
||||
border: 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: var(--color-ink-muted);
|
||||
background: transparent;
|
||||
font-size: 11.5px;
|
||||
font-weight: 750;
|
||||
}
|
||||
.repo-auto-toggle:hover { color: var(--color-ink); background: var(--color-surface-hover); }
|
||||
.repo-switch {
|
||||
position: relative;
|
||||
width: 27px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border-input);
|
||||
transition: background 140ms ease;
|
||||
}
|
||||
.repo-switch > span {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.28);
|
||||
transition: transform 140ms ease;
|
||||
}
|
||||
.repo-auto-toggle.active { color: #4eca76; }
|
||||
.repo-auto-toggle.active .repo-switch { background: #26924f; }
|
||||
.repo-auto-toggle.active .repo-switch > span { transform: translateX(12px); }
|
||||
.repo-switch.busy { box-shadow: 0 0 0 3px rgba(78,202,118,0.12); }
|
||||
|
||||
.tb-action {
|
||||
display: flex;
|
||||
@@ -655,6 +835,7 @@
|
||||
color: var(--color-bar-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 120ms ease, color 120ms ease;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
@@ -664,9 +845,9 @@
|
||||
.tb-action.auto-toggle.active:hover:not(:disabled) { color: #6ee090; background: rgba(78,202,118,0.1); }
|
||||
.tb-action-label { font-size: 12px; font-weight: 600; }
|
||||
|
||||
.titlebar-divider { width: 1px; margin: 8px 0; background: rgba(255,255,255,0.08); flex-shrink: 0; }
|
||||
.titlebar-divider { width: 1px; margin: 8px 6px; background: rgba(255,255,255,0.08); flex-shrink: 0; }
|
||||
|
||||
.titlebar-controls { display: flex; align-items: stretch; height: 100%; }
|
||||
.titlebar-controls { display: flex; align-items: stretch; height: 100%; border-left: 1px solid rgba(255,255,255,0.06); }
|
||||
|
||||
.tb-btn {
|
||||
display: flex;
|
||||
@@ -715,10 +896,10 @@
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: stretch;
|
||||
min-height: 40px;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 10px;
|
||||
background: rgba(16, 17, 29, 0.9);
|
||||
background: rgba(13, 15, 25, 0.92);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -726,76 +907,89 @@
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
border-left: 1px solid var(--color-border-subtle);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.repo-tab-wrap {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
flex: 0 1 230px;
|
||||
min-width: 150px;
|
||||
max-width: 260px;
|
||||
position: relative;
|
||||
border-right: 1px solid var(--color-border-subtle);
|
||||
background: rgba(255,255,255,0.015);
|
||||
background: rgba(255,255,255,0.012);
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
.repo-tab-wrap:hover { background: rgba(255,255,255,0.035); }
|
||||
.repo-tab-wrap.active {
|
||||
background: rgba(90, 140, 248, 0.16);
|
||||
box-shadow: inset 0 -2px 0 var(--color-primary);
|
||||
background: linear-gradient(180deg, rgba(90,140,248,0.14), rgba(23,29,43,0.96));
|
||||
box-shadow: inset 0 -3px 0 var(--color-primary);
|
||||
}
|
||||
|
||||
.repo-tab {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 42px;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
padding: 0 10px;
|
||||
max-width: none;
|
||||
padding: 0 8px 0 13px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--color-border-subtle);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-weight: 720;
|
||||
text-align: left;
|
||||
}
|
||||
.repo-tab > svg { flex: 0 0 auto; color: var(--color-ink-faint); }
|
||||
.repo-tab.management {
|
||||
max-width: none;
|
||||
min-width: 190px;
|
||||
min-width: 184px;
|
||||
border: 0;
|
||||
box-shadow: inset 0 -3px 0 transparent;
|
||||
}
|
||||
.repo-tab.active,
|
||||
.repo-tab.management.active {
|
||||
color: var(--color-ink);
|
||||
background: linear-gradient(180deg, rgba(90,140,248,0.14), rgba(23,29,43,0.96));
|
||||
box-shadow: inset 0 -3px 0 var(--color-primary);
|
||||
}
|
||||
.repo-tab.management:hover:not(:disabled) { background: var(--color-surface-hover); }
|
||||
.repo-tab-wrap.active .repo-tab {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.repo-tab-wrap.active .repo-tab > svg,
|
||||
.repo-tab.management.active > svg { color: var(--color-accent); }
|
||||
.repo-tab span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.repo-tab strong {
|
||||
flex: 0 0 auto;
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(94, 110, 156, 0.18);
|
||||
color: var(--color-ink-dim);
|
||||
font-size: 10.5px;
|
||||
font-family: var(--font-mono);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.repo-tab-close,
|
||||
.repo-tab-add {
|
||||
min-height: 38px;
|
||||
min-width: 38px;
|
||||
min-height: 42px;
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--color-ink-faint);
|
||||
}
|
||||
.repo-tab-close { opacity: 0.58; transition: opacity 120ms ease, color 120ms ease, background 120ms ease; }
|
||||
.repo-tab-wrap:hover .repo-tab-close,
|
||||
.repo-tab-wrap.active .repo-tab-close { opacity: 1; }
|
||||
.repo-tab-close:hover:not(:disabled) { color: var(--color-ink); background: var(--color-surface-hover); }
|
||||
.repo-tab-add {
|
||||
min-width: 44px;
|
||||
border-left: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.repo-tab-add:hover:not(:disabled) { color: var(--color-accent); background: var(--color-surface-hover); }
|
||||
|
||||
.repo-management {
|
||||
display: grid;
|
||||
@@ -1951,15 +2145,15 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
padding: 4px 8px 4px calc(8px + var(--depth, 0) * 14px);
|
||||
min-height: 28px;
|
||||
padding: 2px 7px 2px calc(7px + var(--depth, 0) * 14px);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
.explorer-row + .explorer-row { margin-top: 2px; }
|
||||
.explorer-row + .explorer-row { margin-top: 0; }
|
||||
.explorer-row:hover { background: var(--color-surface-hover); border-color: var(--color-border-subtle); }
|
||||
.explorer-row.active { background: rgba(90,140,248,0.1); border-color: rgba(90,140,248,0.28); }
|
||||
.explorer-row.folder { font-weight: 700; }
|
||||
@@ -3083,6 +3277,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-segmented.settings-language {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.settings-toggle-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
@@ -4716,10 +4914,15 @@
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .titlebar-brand,
|
||||
:root[data-theme="light"] .titlebar-right {
|
||||
:root[data-theme="light"] .titlebar-controls {
|
||||
border-color: rgba(34,49,78,0.1);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-toolbar {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(255,255,255,0.78);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .titlebar-brand-icon img {
|
||||
filter: drop-shadow(0 1px 2px rgba(28,44,74,0.18));
|
||||
}
|
||||
@@ -4768,15 +4971,308 @@
|
||||
|
||||
:root[data-theme="light"] .repo-tabbar,
|
||||
:root[data-theme="light"] .repo-management-tools {
|
||||
background: rgba(255,255,255,0.78);
|
||||
background: rgba(247,249,252,0.96);
|
||||
}
|
||||
|
||||
/* 2026 workspace refresh --------------------------------------------------
|
||||
A single, quiet product system shared by management, repository workspace,
|
||||
dialogs and both color themes. Kept as an override layer so existing Git
|
||||
behavior and component ownership stay unchanged. */
|
||||
:root {
|
||||
--ui-radius: 7px;
|
||||
--ui-radius-sm: 5px;
|
||||
--ui-control-height: 32px;
|
||||
--ui-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
|
||||
--app-bg: #0d1118;
|
||||
--app-button-bg: #171d27;
|
||||
--app-input-bg: #101620;
|
||||
--app-panel-highlight: none;
|
||||
--app-panel-shadow: none;
|
||||
--color-surface: #111720;
|
||||
--color-surface-alt: #0d121a;
|
||||
--color-surface-dim: #141b25;
|
||||
--color-surface-hover: #1b2431;
|
||||
--color-surface-raised: #151c27;
|
||||
--color-surface-solid: #111720;
|
||||
--color-border: #2a3546;
|
||||
--color-border-subtle: #222c3a;
|
||||
--color-primary: #4d8dff;
|
||||
--color-primary-dark: #3477ed;
|
||||
--color-accent: #65a2ff;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--ui-shadow: 0 10px 28px rgba(23, 40, 72, 0.09);
|
||||
--app-bg: #f4f6f9;
|
||||
--app-button-bg: #ffffff;
|
||||
--app-input-bg: #ffffff;
|
||||
--app-panel-highlight: none;
|
||||
--app-panel-shadow: none;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f7f8fa;
|
||||
--color-surface-dim: #f8f9fb;
|
||||
--color-surface-hover: #f0f4fa;
|
||||
--color-surface-raised: #ffffff;
|
||||
--color-surface-solid: #ffffff;
|
||||
--color-border: #cfd6e1;
|
||||
--color-border-subtle: #e3e7ed;
|
||||
--color-border-input: #c9d1dd;
|
||||
--color-primary: #0b63e6;
|
||||
--color-primary-dark: #0755c8;
|
||||
--color-accent: #0b63e6;
|
||||
}
|
||||
|
||||
body { background: var(--app-bg); }
|
||||
|
||||
button {
|
||||
min-height: var(--ui-control-height);
|
||||
border-radius: var(--ui-radius-sm);
|
||||
box-shadow: none;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--color-primary) 65%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
input, textarea, select { border-radius: var(--ui-radius-sm); }
|
||||
input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent); }
|
||||
|
||||
.shell { --app-titlebar-height: 40px; --app-toolbar-height: 64px; background: var(--app-bg); }
|
||||
.shell-body { gap: 0; padding: 0; }
|
||||
.titlebar {
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background: var(--color-surface-solid);
|
||||
}
|
||||
.titlebar-brand { min-width: 140px; padding-left: 14px; border-right: 0; font-size: 14px; }
|
||||
.titlebar-brand-icon,
|
||||
.titlebar-brand-icon img { width: 22px; height: 22px; }
|
||||
.titlebar-brand-icon { flex-basis: 22px; }
|
||||
.titlebar-drag { align-self: stretch; min-width: 0; }
|
||||
.titlebar-info { justify-self: center; width: min(480px, 100%); }
|
||||
.titlebar-context { justify-content: center; }
|
||||
.tb-repo, .tb-branch { color: var(--color-ink); font-size: 13px; }
|
||||
.tb-version { color: var(--color-ink-faint); }
|
||||
.titlebar-globals { margin-left: auto; }
|
||||
.titlebar-divider { background: var(--color-border-subtle); }
|
||||
|
||||
.repo-tabbar {
|
||||
min-height: 46px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
background: var(--color-surface-dim);
|
||||
}
|
||||
.repo-tab-wrap { min-height: 45px; border-right: 1px solid var(--color-border-subtle); border-radius: 0; }
|
||||
.repo-tab-wrap:first-child { border-left: 1px solid var(--color-border-subtle); }
|
||||
.repo-tab-wrap.active { background: var(--color-surface-solid); box-shadow: inset 0 -3px 0 var(--color-primary); }
|
||||
.repo-tab { min-height: 45px; padding-left: 14px; font-size: 12.5px; }
|
||||
.repo-tab-add { min-height: 45px; }
|
||||
|
||||
.repo-toolbar {
|
||||
height: var(--app-toolbar-height);
|
||||
padding: 7px 18px 8px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
background: var(--color-surface-solid);
|
||||
}
|
||||
.repo-toolbar-sync { grid-template-rows: 10px 36px; }
|
||||
.repo-toolbar-group-label { padding-left: 1px; font-size: 8.5px; letter-spacing: .13em; }
|
||||
.repo-action-group { height: 36px; border-color: var(--color-border); background: transparent; }
|
||||
.repo-action { height: 34px; min-height: 34px; padding-inline: 15px; font-weight: 650; }
|
||||
.repo-action.sync-primary { color: var(--color-primary); }
|
||||
.repo-toolbar-divider { height: 34px; margin-inline: 6px; }
|
||||
|
||||
.workspace {
|
||||
grid-template-columns: minmax(220px, var(--left-sidebar-width, 280px)) 7px minmax(360px, 1fr) 7px minmax(560px, var(--history-aside-width, 620px));
|
||||
flex: 1 1 0;
|
||||
padding: 0;
|
||||
background: var(--color-border-subtle);
|
||||
row-gap: 0;
|
||||
}
|
||||
.left-sidebar { background: var(--color-surface-solid); }
|
||||
.main-panel { border: 0; border-radius: 0; background: var(--color-surface-solid); }
|
||||
.history-aside { background: var(--color-border-subtle); row-gap: 0; }
|
||||
|
||||
.panel {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--color-surface-solid);
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.left-sidebar .panel + .panel { border-top: 1px solid var(--color-border); }
|
||||
.section-head {
|
||||
min-height: 50px;
|
||||
padding: 8px 12px;
|
||||
border-bottom-color: var(--color-border-subtle);
|
||||
background: var(--color-surface-solid);
|
||||
}
|
||||
.section-head h2 { font-size: 14px; font-weight: 750; }
|
||||
.eyebrow { font-size: 9px; font-weight: 750; letter-spacing: .11em; }
|
||||
.pill { min-height: 19px; padding-inline: 6px; border-radius: 5px; }
|
||||
.pill-count { color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 10%, transparent); }
|
||||
|
||||
.repo-summary { height: 48px; padding-inline: 16px; background: var(--color-surface-dim); }
|
||||
.repo-branch { font-size: 14px; }
|
||||
.repo-path { opacity: .82; }
|
||||
.sync-stats strong, .sync-stats span { border-radius: 5px; background: transparent !important; }
|
||||
.top-section { grid-template-rows: minmax(160px, 1fr) 7px var(--commit-panel-height, 220px); padding: 0; }
|
||||
|
||||
.left-sidebar-resize-handle,
|
||||
.history-resize-handle,
|
||||
.file-history-resize-handle,
|
||||
.panel-resize-handle,
|
||||
.left-panel-resize-handle { background: var(--color-border-subtle); }
|
||||
.left-sidebar-resize-handle::before,
|
||||
.history-resize-handle::before,
|
||||
.file-history-resize-handle::before { width: 1px; height: 48px; background: var(--color-border); }
|
||||
.panel-resize-handle::before { width: 44px; height: 2px; }
|
||||
|
||||
.status-toolbar {
|
||||
min-height: 40px;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background: var(--color-surface-dim);
|
||||
}
|
||||
.status-toolbar .danger { margin-left: auto; }
|
||||
.status-lanes { display: grid; align-content: start; min-height: 0; }
|
||||
.status-lane + .status-lane { border-top: 1px solid var(--color-border); }
|
||||
.status-lane-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 42px;
|
||||
padding: 5px 12px;
|
||||
background: var(--color-surface-dim);
|
||||
}
|
||||
.status-lane-head > div { display: flex; align-items: center; gap: 7px; }
|
||||
.status-lane-head strong { color: var(--color-ink); font-size: 12px; }
|
||||
.status-lane-head span { display: grid; place-items: center; min-width: 20px; height: 20px; border-radius: 5px; color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 10%, transparent); font-size: 10.5px; font-weight: 750; }
|
||||
.status-file-list { padding: 5px 0; }
|
||||
.status-file-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
padding: 3px 8px 3px 12px;
|
||||
border-left: 3px solid transparent;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.status-file-row:last-child { border-bottom: 0; }
|
||||
.status-file-row:hover { background: var(--color-surface-hover); }
|
||||
.status-file-row.selected { background: color-mix(in srgb, var(--color-primary) 9%, var(--color-surface-solid)); }
|
||||
.status-file-row.active { border-left-color: var(--color-primary); }
|
||||
.status-file-main { display: grid; justify-content: start; gap: 1px; min-width: 0; padding: 3px 8px; border: 0; background: transparent; text-align: left; }
|
||||
.status-file-main:hover:not(:disabled) { background: transparent; }
|
||||
.status-file-main strong, .status-file-main span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.status-file-main strong { color: var(--color-ink); font: 650 12px/1.3 var(--font-mono); }
|
||||
.status-file-main span { color: var(--color-ink-faint); font: 10px/1.2 var(--font-mono); }
|
||||
.status-file-actions { display: flex; margin-left: 5px; opacity: 0; transition: opacity 120ms ease; }
|
||||
.status-file-row:hover .status-file-actions, .status-file-row:focus-within .status-file-actions { opacity: 1; }
|
||||
.status-file-actions button { width: 28px; min-width: 28px; min-height: 28px; padding: 0; border-color: transparent; background: transparent; }
|
||||
.status-file-actions .danger:hover:not(:disabled) { color: #e86060; background: rgba(232,96,96,.1); }
|
||||
.status-lane-empty { margin: 0; padding: 14px 16px; color: var(--color-ink-faint); font-size: 11.5px; }
|
||||
.status-badge { min-width: 58px; padding-inline: 5px; border: 0; border-radius: 4px; font-size: 9px; letter-spacing: .04em; }
|
||||
|
||||
.commit-panel { border-top: 1px solid var(--color-border); }
|
||||
.commit-head-actions { display: flex; align-items: center; gap: 5px; }
|
||||
.commit-generate-button { color: var(--color-primary); background: transparent; }
|
||||
.commit-settings-button { width: 30px; min-width: 30px; padding: 0; background: transparent; }
|
||||
.commit-form { padding: 10px 12px 12px; }
|
||||
.commit-form textarea { min-height: 72px; resize: none; }
|
||||
.commit-amend-row { min-height: 34px; }
|
||||
.commit-actions-row .btn-primary { min-height: 38px; border-radius: var(--ui-radius-sm); background: var(--color-primary); box-shadow: none; }
|
||||
.commit-actions-row .btn-primary:hover:not(:disabled) { background: var(--color-primary-dark); }
|
||||
|
||||
.branch-list, .explorer-list, .stash-list { padding: 5px !important; }
|
||||
.branch-row, .explorer-row, .stash-row { border-radius: var(--ui-radius-sm); }
|
||||
.branch-row.current, .explorer-row.active { background: color-mix(in srgb, var(--color-primary) 11%, var(--color-surface-solid)); box-shadow: inset 3px 0 0 var(--color-primary); }
|
||||
.branch-group-toggle { background: transparent; }
|
||||
.explorer-row .status-badge { min-width: 18px; overflow: hidden; color: #d9891b; background: transparent; text-indent: -999px; }
|
||||
.explorer-row .status-badge::after { content: "M"; float: right; text-indent: 0; }
|
||||
|
||||
.history-panel, .file-history-panel { min-height: 0; }
|
||||
.graph-list { background: var(--color-surface-solid); }
|
||||
.graph-gutter { background: var(--color-surface-dim); }
|
||||
.commit-body { border-radius: var(--ui-radius-sm); background: transparent; box-shadow: none; }
|
||||
.graph-row + .graph-row .commit-body { border-top: 1px solid var(--color-border-subtle); }
|
||||
.graph-row:hover .commit-body { background: var(--color-surface-hover); }
|
||||
.graph-row.tip-row .commit-body { background: color-mix(in srgb, var(--color-primary) 7%, var(--color-surface-solid)); }
|
||||
.commit-avatar { border-radius: 50%; }
|
||||
.commit-kind, .commit-branch-chip, .ref-chip { border-radius: 4px !important; }
|
||||
.file-history-row { margin: 0; border: 0; border-bottom: 1px solid var(--color-border-subtle); border-radius: 0; background: transparent; }
|
||||
|
||||
.workspace-statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 30px;
|
||||
padding: 0 14px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-ink-faint);
|
||||
background: var(--color-surface-solid);
|
||||
font-size: 10.5px;
|
||||
}
|
||||
.workspace-statusbar > span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||
.workspace-status-spacer { flex: 1 1 auto; }
|
||||
.workspace-health > span { width: 8px; height: 8px; border-radius: 50%; background: #d9891b; }
|
||||
.workspace-health.clean > span { background: #2da44e; }
|
||||
.workspace-statusbar .ahead { color: #d9891b; }
|
||||
.workspace-statusbar .behind { color: var(--color-primary); }
|
||||
.workspace-auto i { width: 7px; height: 7px; border-radius: 50%; background: var(--color-ink-faint); }
|
||||
.workspace-auto.active i { background: #2da44e; }
|
||||
.app-version {
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid var(--color-border-subtle);
|
||||
color: var(--color-ink-faint);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.repo-management { margin: 14px; border-radius: var(--ui-radius); background: var(--color-surface-solid); box-shadow: var(--ui-shadow); }
|
||||
.repo-management-head, .repo-management-tools { background: var(--color-surface-solid); }
|
||||
.repo-section { border-radius: var(--ui-radius-sm); background: var(--color-surface-solid); }
|
||||
.repo-section > header { background: var(--color-surface-dim); }
|
||||
.repo-row-main:hover:not(:disabled) { background: var(--color-surface-hover); }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.repo-action { padding-inline: 10px; }
|
||||
.repo-toolbar-divider { margin-inline: 2px; }
|
||||
.repo-action-label { display: none; }
|
||||
.repo-action.sync-primary .repo-action-label { display: inline; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shell-body { overflow: auto; }
|
||||
.workspace { grid-template-columns: minmax(190px, 230px) 7px minmax(420px, 1fr); min-height: 760px; }
|
||||
.history-resize-handle, .history-aside { display: none; }
|
||||
.workspace-statusbar { position: sticky; bottom: 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-tab-wrap {
|
||||
background: rgba(244,247,252,0.72);
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-tab-wrap.active {
|
||||
background: rgba(49,95,214,0.1);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-tab-wrap:hover,
|
||||
:root[data-theme="light"] .repo-tab.management:hover:not(:disabled) {
|
||||
background: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-tab.management.active {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .repo-row {
|
||||
@@ -5187,6 +5683,7 @@
|
||||
.branch-actions { flex-direction: column; align-items: flex-end; gap: 4px; }
|
||||
.branch-actions button { width: 100%; }
|
||||
.tb-action-label { display: none; }
|
||||
.repo-action-label.utility-label { display: none; }
|
||||
}
|
||||
|
||||
/* Narrow: single column */
|
||||
@@ -5194,8 +5691,7 @@
|
||||
body { overflow: auto; }
|
||||
.shell-body { min-height: 100%; gap: 6px; }
|
||||
.titlebar-info { padding: 0 8px; }
|
||||
.titlebar-actions .tb-action:not(.auto-toggle):not(:last-child) { display: none; }
|
||||
.tb-action { width: 36px; padding: 0 8px; }
|
||||
.tb-action { padding: 0 9px; }
|
||||
.tb-btn { width: 34px; }
|
||||
.workspace { grid-template-columns: 1fr; gap: 6px; }
|
||||
.left-sidebar-resize-handle { display: none; }
|
||||
@@ -5229,9 +5725,10 @@
|
||||
}
|
||||
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); }
|
||||
.repo-form { grid-template-columns: 1fr; }
|
||||
.repo-tabbar { grid-template-columns: minmax(0, 1fr) auto; }
|
||||
.repo-tab.management { min-width: 0; }
|
||||
.repo-tabs-scroll { grid-column: 1 / -1; order: 2; border-top: 1px solid var(--color-border-subtle); }
|
||||
.repo-tabbar { grid-template-columns: auto minmax(0, 1fr) auto; }
|
||||
.repo-tab.management { min-width: 44px; width: 44px; padding: 0; justify-content: center; }
|
||||
.repo-tab.management span { display: none; }
|
||||
.repo-tabs-scroll { grid-column: auto; order: initial; border-top: 0; }
|
||||
.repo-management-head,
|
||||
.repo-management-tools { align-items: stretch; flex-direction: column; }
|
||||
.repo-management-actions { justify-content: flex-start; }
|
||||
@@ -5261,4 +5758,111 @@
|
||||
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
|
||||
.branch-actions { flex-direction: row; justify-content: flex-start; }
|
||||
.tb-action-label { display: none; }
|
||||
.repo-action-label { display: none; }
|
||||
.repo-action { padding-inline: 8px; }
|
||||
.repo-toolbar { gap: 5px; padding-inline: 6px; }
|
||||
.repo-toolbar-divider { margin-inline: 0; }
|
||||
.repo-auto-toggle { padding-left: 7px; gap: 6px; }
|
||||
}
|
||||
|
||||
/* Keep the refreshed workspace rules authoritative after legacy theme/media
|
||||
compatibility declarations above. */
|
||||
.commit-actions-row .btn-primary,
|
||||
:root[data-theme="light"] .commit-actions-row .btn-primary {
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
background: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
.commit-actions-row .btn-primary:hover:not(:disabled),
|
||||
:root[data-theme="light"] .commit-actions-row .btn-primary:hover:not(:disabled) {
|
||||
border-color: var(--color-primary-dark);
|
||||
color: #fff;
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
:root[data-theme="light"] .panel,
|
||||
:root[data-theme="light"] .main-panel,
|
||||
:root[data-theme="light"] .graph-list,
|
||||
:root[data-theme="light"] .commit-body { box-shadow: none; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.shell-body { overflow: auto; }
|
||||
.workspace {
|
||||
grid-template-columns: minmax(200px, 250px) 7px minmax(480px, 1fr);
|
||||
min-width: 760px;
|
||||
min-height: 680px;
|
||||
}
|
||||
.history-resize-handle,
|
||||
.history-aside { display: none; }
|
||||
.workspace-statusbar { position: sticky; bottom: 0; z-index: 20; }
|
||||
}
|
||||
|
||||
/* Repository workflow order: inspect first, then remote synchronization. */
|
||||
.repo-toolbar > .repo-inspect-actions { order: 1; }
|
||||
.repo-toolbar > .repo-toolbar-divider:not(.utility) { order: 2; }
|
||||
.repo-toolbar > .repo-toolbar-sync {
|
||||
order: 3;
|
||||
display: block;
|
||||
align-self: auto;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.repo-toolbar > .repo-toolbar-spacer { order: 4; }
|
||||
.repo-toolbar > .repo-toolbar-divider.utility { order: 5; }
|
||||
.repo-toolbar > .repo-utility-actions { order: 6; }
|
||||
|
||||
/* Compact change map for long compare and line-patch views. */
|
||||
.split-diff-shell,
|
||||
.line-patch-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 16px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.split-diff-shell .split-diff { min-height: 0; }
|
||||
.line-patch-workspace .line-patch-scroll { min-height: 0; }
|
||||
.diff-overview {
|
||||
position: relative;
|
||||
min-width: 16px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid var(--color-border-subtle);
|
||||
background: var(--code-surface-subtle);
|
||||
}
|
||||
.diff-overview::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 5px 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border-subtle);
|
||||
}
|
||||
.diff-overview-marker {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: var(--marker-position);
|
||||
left: 2px;
|
||||
width: 11px;
|
||||
min-width: 11px;
|
||||
height: 5px;
|
||||
min-height: 5px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
transform: translateY(-50%);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 28%, transparent);
|
||||
}
|
||||
.diff-overview-marker.add { color: var(--code-add-strong); background: var(--code-add-strong); }
|
||||
.diff-overview-marker.delete { color: var(--code-delete-strong); background: var(--code-delete-strong); }
|
||||
.diff-overview-marker.mixed {
|
||||
color: var(--color-accent);
|
||||
background: linear-gradient(90deg, var(--code-delete-strong) 0 48%, var(--code-add-strong) 52% 100%);
|
||||
}
|
||||
.diff-overview-marker:hover:not(:disabled),
|
||||
.diff-overview-marker:focus-visible {
|
||||
width: 13px;
|
||||
left: 1px;
|
||||
filter: brightness(1.18);
|
||||
outline: 1px solid var(--color-ink);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { BookOpen, Database, Plus, X } from "@lucide/svelte";
|
||||
|
||||
interface RepositoryTabItem {
|
||||
path: string;
|
||||
name: string;
|
||||
branch: string | null;
|
||||
}
|
||||
|
||||
export let activeView: "management" | "repository" = "management";
|
||||
export let repoTabs: RepositoryTabItem[] = [];
|
||||
export let isBusy: boolean = false;
|
||||
export let language: "en" | "de" = "en";
|
||||
export let onOpenManagement: () => void = () => {};
|
||||
export let isActive: (path: string) => boolean = () => false;
|
||||
export let onSelect: (path: string) => void | Promise<void> = () => {};
|
||||
export let onClose: (path: string, event: MouseEvent) => void | Promise<void> = () => {};
|
||||
export let onContextMenu: (path: string, event: MouseEvent) => void = () => {};
|
||||
export let onAdd: () => void | Promise<void> = () => {};
|
||||
</script>
|
||||
|
||||
<header class="repo-tabbar" aria-label={language === "de" ? "Repository-Reiter" : "Repository tabs"}>
|
||||
<button
|
||||
class="repo-tab management"
|
||||
class:active={activeView === "management"}
|
||||
type="button"
|
||||
onclick={onOpenManagement}
|
||||
disabled={isBusy}
|
||||
title={language === "de" ? "Repository-Verwaltung" : "Repository Management"}
|
||||
>
|
||||
<BookOpen size={14} aria-hidden="true" />
|
||||
<span>{language === "de" ? "Repository-Verwaltung" : "Repository Management"}</span>
|
||||
</button>
|
||||
|
||||
<div class="repo-tabs-scroll">
|
||||
{#each repoTabs as repo (repo.path)}
|
||||
<div
|
||||
class="repo-tab-wrap"
|
||||
class:active={isActive(repo.path)}
|
||||
role="presentation"
|
||||
oncontextmenu={(event) => onContextMenu(repo.path, event)}
|
||||
>
|
||||
<button
|
||||
class="repo-tab"
|
||||
type="button"
|
||||
onclick={() => onSelect(repo.path)}
|
||||
disabled={isBusy}
|
||||
title={repo.path}
|
||||
>
|
||||
<Database size={15} aria-hidden="true" />
|
||||
<span>{repo.name}</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-tab-close"
|
||||
type="button"
|
||||
onclick={(event) => onClose(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
aria-label={language === "de" ? `${repo.name} schließen` : `Close ${repo.name}`}
|
||||
title={language === "de" ? "Repository-Tab schließen" : "Close repository tab"}
|
||||
>
|
||||
<X size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="repo-tab-add"
|
||||
type="button"
|
||||
onclick={onAdd}
|
||||
disabled={isBusy}
|
||||
title={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
|
||||
aria-label={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
|
||||
>
|
||||
<Plus size={15} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ChevronDown,
|
||||
CloudDownload,
|
||||
Download,
|
||||
FolderOpen,
|
||||
GitCompare,
|
||||
History,
|
||||
ListRestart,
|
||||
LoaderCircle,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Upload,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
export let hasRepository: boolean = false;
|
||||
export let isBusy: boolean = false;
|
||||
export let operation: string = "";
|
||||
export let ahead: number = 0;
|
||||
export let behind: number = 0;
|
||||
export let autoRefreshEnabled: boolean = true;
|
||||
export let autoRefreshInFlight: boolean = false;
|
||||
export let language: "en" | "de" = "en";
|
||||
export let onFetch: () => void = () => {};
|
||||
export let onPull: () => void = () => {};
|
||||
export let onPush: () => void = () => {};
|
||||
export let onRefresh: () => void = () => {};
|
||||
export let onSearch: () => void = () => {};
|
||||
export let onCompare: () => void = () => {};
|
||||
export let onInteractiveRebase: () => void = () => {};
|
||||
export let onReflog: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
|
||||
let historyOpen = false;
|
||||
let toolbarElement: HTMLDivElement;
|
||||
|
||||
$: isGerman = language === "de";
|
||||
|
||||
function runHistoryAction(action: () => void) {
|
||||
historyOpen = false;
|
||||
action();
|
||||
}
|
||||
|
||||
function handleWindowClick(event: MouseEvent) {
|
||||
if (historyOpen && toolbarElement && !toolbarElement.contains(event.target as Node)) {
|
||||
historyOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && historyOpen) {
|
||||
event.stopPropagation();
|
||||
historyOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
|
||||
|
||||
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
|
||||
<div class="repo-toolbar-sync">
|
||||
<div class="repo-action-group repo-sync-actions">
|
||||
<button
|
||||
class="repo-action fetch"
|
||||
onclick={onFetch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Fetch"
|
||||
aria-label="Fetch"
|
||||
>
|
||||
{#if operation === "Fetching"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<CloudDownload size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Fetch</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action sync-primary"
|
||||
onclick={onPull}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Pull"
|
||||
aria-label={behind > 0
|
||||
? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
|
||||
: "Pull"}
|
||||
>
|
||||
{#if operation === "Pulling"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<Download size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Pull</span>
|
||||
{#if behind > 0}<span class="repo-action-count behind">↓{behind}</span>{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action sync-primary"
|
||||
onclick={onPush}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Push"
|
||||
aria-label={ahead > 0
|
||||
? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
|
||||
: "Push"}
|
||||
>
|
||||
{#if operation === "Pushing"}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<Upload size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="repo-action-label">Push</span>
|
||||
{#if ahead > 0}<span class="repo-action-count ahead">↑{ahead}</span>{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-toolbar-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="repo-action-group repo-inspect-actions">
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onSearch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Globale Suche" : "Global search"}
|
||||
aria-label={isGerman ? "Globale Suche" : "Global search"}
|
||||
>
|
||||
<Search size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Suchen" : "Search"}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onCompare}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Commits vergleichen" : "Compare commits"}
|
||||
aria-label={isGerman ? "Commits vergleichen" : "Compare commits"}
|
||||
>
|
||||
<GitCompare size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
|
||||
</button>
|
||||
|
||||
<div class="repo-history-wrap">
|
||||
<button
|
||||
class="repo-action history-trigger"
|
||||
class:active={historyOpen}
|
||||
type="button"
|
||||
onclick={() => { historyOpen = !historyOpen; }}
|
||||
disabled={!hasRepository || isBusy}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={historyOpen}
|
||||
title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
|
||||
>
|
||||
<History size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
|
||||
<ChevronDown class="history-chevron" size={13} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{#if historyOpen}
|
||||
<div class="repo-history-menu" role="menu">
|
||||
<button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
|
||||
<History size={15} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>Reflog</strong>
|
||||
<small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
|
||||
<ListRestart size={15} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
|
||||
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-toolbar-spacer"></div>
|
||||
<div class="repo-toolbar-divider utility" aria-hidden="true"></div>
|
||||
|
||||
<div class="repo-action-group repo-utility-actions">
|
||||
<button
|
||||
class="repo-action"
|
||||
onclick={onOpenInExplorer}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
|
||||
aria-label={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
|
||||
>
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
<span class="repo-action-label utility-label">Explorer</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-action icon-action"
|
||||
onclick={onRefresh}
|
||||
disabled={isBusy || !hasRepository}
|
||||
title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
|
||||
aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
|
||||
>
|
||||
<RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={15} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="repo-auto-toggle"
|
||||
class:active={autoRefreshEnabled}
|
||||
onclick={onToggleAutoRefresh}
|
||||
aria-pressed={autoRefreshEnabled}
|
||||
title={autoRefreshEnabled
|
||||
? (isGerman ? "Auto-Aktualisierung ausschalten" : "Turn auto-refresh off")
|
||||
: (isGerman ? "Auto-Aktualisierung einschalten" : "Turn auto-refresh on")}
|
||||
>
|
||||
<span>{isGerman ? "Auto" : "Auto"}</span>
|
||||
<span class="repo-switch" class:busy={autoRefreshInFlight} aria-hidden="true"><span></span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+48
-228
@@ -1,35 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, History, ListRestart, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
||||
import { CircleHelp, Minus, Settings, X } from "@lucide/svelte";
|
||||
import iconUrl from "../../src-tauri/icons/icon.png";
|
||||
|
||||
export let branch: string = "";
|
||||
export let ahead: number = 0;
|
||||
export let behind: number = 0;
|
||||
export let repoName: string = "";
|
||||
export let hasRepository: boolean = false;
|
||||
export let isBusy: boolean = false;
|
||||
export let operation: string = "";
|
||||
export let autoRefreshEnabled: boolean = true;
|
||||
export let autoRefreshInFlight: boolean = false;
|
||||
export let onFetch: () => void = () => {};
|
||||
export let onPull: () => void = () => {};
|
||||
export let onPush: () => void = () => {};
|
||||
export let onRefresh: () => void = () => {};
|
||||
export let onSearch: () => void = () => {};
|
||||
export let onCompare: () => void = () => {};
|
||||
export let onInteractiveRebase: () => void = () => {};
|
||||
export let onReflog: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
export let onOpenHelp: () => void = () => {};
|
||||
export let onOpenSettings: () => void = () => {};
|
||||
export let language: "en" | "de" = "en";
|
||||
|
||||
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
||||
let isMaximized = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
let appVersion = "";
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
@@ -41,12 +22,6 @@
|
||||
} catch {
|
||||
win = null;
|
||||
}
|
||||
|
||||
try {
|
||||
appVersion = await getVersion();
|
||||
} catch {
|
||||
appVersion = "";
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -73,208 +48,53 @@
|
||||
<img src={iconUrl} alt="" data-tauri-drag-region />
|
||||
</span>
|
||||
<span data-tauri-drag-region>Gitty</span>
|
||||
{#if appVersion}
|
||||
<span class="tb-version" data-tauri-drag-region title="Version {appVersion}">v{appVersion}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Center: repo + branch info -->
|
||||
<div class="titlebar-info" data-tauri-drag-region>
|
||||
{#if hasRepository}
|
||||
<div class="titlebar-context" data-tauri-drag-region>
|
||||
{#if repoName}
|
||||
<span class="tb-repo" data-tauri-drag-region>{repoName}</span>
|
||||
<span class="tb-sep" data-tauri-drag-region aria-hidden="true">/</span>
|
||||
{/if}
|
||||
<GitBranch size={12} aria-hidden="true" />
|
||||
<span class="tb-branch" data-tauri-drag-region title={branch}>{branch}</span>
|
||||
</div>
|
||||
{#if ahead > 0 || behind > 0}
|
||||
<div class="tb-sync-group" aria-label="Branch synchronization status">
|
||||
{#if ahead > 0}
|
||||
<span class="tb-sync ahead" title="{ahead} commits ahead">↑{ahead}</span>
|
||||
{/if}
|
||||
{#if behind > 0}
|
||||
<span class="tb-sync behind" title="{behind} commits behind">↓{behind}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="titlebar-drag" data-tauri-drag-region aria-hidden="true"></div>
|
||||
|
||||
<!-- Right: app-global actions + window controls -->
|
||||
<div class="titlebar-globals">
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenHelp}
|
||||
title={language === "de" ? "Hilfe (Ctrl+/)" : "Help (Ctrl+/)"}
|
||||
aria-label={language === "de" ? "Hilfe öffnen" : "Open help"}
|
||||
>
|
||||
<CircleHelp size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenSettings}
|
||||
title={language === "de" ? "Einstellungen" : "Settings"}
|
||||
aria-label={language === "de" ? "Einstellungen" : "Settings"}
|
||||
>
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
|
||||
<Minus size={12} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
onclick={toggleMaximizeWindow}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
{#if isMaximized}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="tb-no-repo" data-tauri-drag-region>No repository open</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right: actions + window controls -->
|
||||
<div class="titlebar-right">
|
||||
<div class="titlebar-actions" role="toolbar" aria-label="Repository actions">
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenInExplorer}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Open repository in Explorer"
|
||||
aria-label="Open repository in Explorer"
|
||||
>
|
||||
<FolderOpen size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Explorer</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onSearch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Global search"
|
||||
aria-label="Global search"
|
||||
>
|
||||
<Search size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Search</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onCompare}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Compare commits"
|
||||
aria-label="Compare commits"
|
||||
>
|
||||
<GitCompare size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Compare</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onInteractiveRebase}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Interactive rebase"
|
||||
aria-label="Interactive rebase"
|
||||
>
|
||||
<ListRestart size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Rebase</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onReflog}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Reflog"
|
||||
aria-label="Reflog"
|
||||
>
|
||||
<History size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Reflog</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onFetch}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Fetch"
|
||||
aria-label="Fetch"
|
||||
>
|
||||
{#if operation === "Fetching"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<CloudDownload size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Fetch</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onPull}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Pull"
|
||||
aria-label="Pull"
|
||||
>
|
||||
{#if operation === "Pulling"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<Download size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Pull</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onPush}
|
||||
disabled={!hasRepository || isBusy}
|
||||
title="Push"
|
||||
aria-label="Push"
|
||||
>
|
||||
{#if operation === "Pushing"}
|
||||
<LoaderCircle class="spin" size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<Upload size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="tb-action-label">Push</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onRefresh}
|
||||
disabled={isBusy || !hasRepository}
|
||||
title="Refresh"
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class={operation === "Refreshing" ? "spin" : ""}
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="tb-action-label">Refresh</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action auto-toggle"
|
||||
class:active={autoRefreshEnabled}
|
||||
onclick={onToggleAutoRefresh}
|
||||
aria-pressed={autoRefreshEnabled}
|
||||
title={autoRefreshEnabled ? "Auto-refresh on — click to disable" : "Auto-refresh off — click to enable"}
|
||||
aria-label="Toggle auto-refresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class={autoRefreshInFlight ? "spin" : ""}
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="tb-action-label">Auto</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenSettings}
|
||||
title="Settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
|
||||
<Minus size={12} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
onclick={toggleMaximizeWindow}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
{#if isMaximized}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="1" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M1 3v7a1 1 0 0 0 1 1h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Check, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings, AppTheme } from "../types";
|
||||
import { Check, Languages, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings, AppLanguage, AppTheme } from "../types";
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsSettings;
|
||||
theme: AppTheme;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme) => void;
|
||||
language: AppLanguage;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme, language: AppLanguage) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { analytics, theme = "system", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
let { analytics, theme = "system", language = "en", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
|
||||
let analyticsEnabled = $state(true);
|
||||
let selectedTheme = $state<AppTheme>("system");
|
||||
let selectedLanguage = $state<AppLanguage>("en");
|
||||
const isGerman = $derived(selectedLanguage === "de");
|
||||
|
||||
$effect(() => {
|
||||
analyticsEnabled = analytics.enabled;
|
||||
selectedTheme = theme;
|
||||
selectedLanguage = language;
|
||||
});
|
||||
|
||||
function save() {
|
||||
@@ -24,18 +28,18 @@
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
}, selectedTheme);
|
||||
}, selectedTheme, selectedLanguage);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
|
||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Einstellungen" : "Settings"} tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Gitty</span>
|
||||
<h2 class="dialog-title">Settings</h2>
|
||||
<h2 class="dialog-title">{isGerman ? "Einstellungen" : "Settings"}</h2>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -45,23 +49,44 @@
|
||||
<header>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Appearance</span>
|
||||
<h3>Theme</h3>
|
||||
<span class="eyebrow">{isGerman ? "Darstellung" : "Appearance"}</span>
|
||||
<h3>{isGerman ? "Farbschema" : "Theme"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="settings-segmented" role="radiogroup" aria-label="Theme">
|
||||
<div class="settings-segmented" role="radiogroup" aria-label={isGerman ? "Farbschema" : "Theme"}>
|
||||
<label class:active={selectedTheme === "system"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="system" />
|
||||
<span>System</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "light"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="light" />
|
||||
<span>Light</span>
|
||||
<span>{isGerman ? "Hell" : "Light"}</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "dark"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="dark" />
|
||||
<span>Dark</span>
|
||||
<span>{isGerman ? "Dunkel" : "Dark"}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<header>
|
||||
<Languages size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">{isGerman ? "Sprache" : "Language"}</span>
|
||||
<h3>{isGerman ? "App-Sprache" : "App language"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="settings-segmented settings-language" role="radiogroup" aria-label={isGerman ? "App-Sprache" : "App language"}>
|
||||
<label class:active={selectedLanguage === "en"}>
|
||||
<input type="radio" bind:group={selectedLanguage} value="en" />
|
||||
<span>EN · English</span>
|
||||
</label>
|
||||
<label class:active={selectedLanguage === "de"}>
|
||||
<input type="radio" bind:group={selectedLanguage} value="de" />
|
||||
<span>DE · Deutsch</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -71,24 +96,24 @@
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Analytics</span>
|
||||
<h3>Anonymous usage analytics</h3>
|
||||
<h3>{isGerman ? "Anonyme Nutzungsanalyse" : "Anonymous usage analytics"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<label class="settings-toggle-row">
|
||||
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||
<span>
|
||||
<strong>Allow anonymous Aptabase events</strong>
|
||||
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
|
||||
<strong>{isGerman ? "Anonyme Aptabase-Ereignisse erlauben" : "Allow anonymous Aptabase events"}</strong>
|
||||
<small>{isGerman ? "Es werden keine Repository-Pfade, Remotes, Branches, Commit-Nachrichten, Dateinamen, Diffs, Zugangsdaten oder Code übertragen." : "No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent."}</small>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="new-branch-actions">
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
|
||||
<button class="btn-primary" type="submit">
|
||||
<Check size={16} aria-hidden="true" />
|
||||
Save
|
||||
{isGerman ? "Speichern" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -69,17 +69,32 @@
|
||||
<section class="panel commit-panel" aria-label="Commit">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<span class="eyebrow">Commit</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Message</h2>
|
||||
<span class="eyebrow">Create revision</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commit</h2>
|
||||
</div>
|
||||
<div class="commit-head-actions">
|
||||
<span class="pill pill-count">{stagedCount} staged</span>
|
||||
<button
|
||||
class="commit-generate-button"
|
||||
type="button"
|
||||
onclick={onGenerateCommitMessage}
|
||||
disabled={!canGenerate}
|
||||
title={aiButtonTitle(commitAiProvider, commitAiPhase, stagedCount)}
|
||||
>
|
||||
{#if commitAiGenerating || localModelLoading}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<Sparkles size={14} aria-hidden="true" />{/if}
|
||||
Generate
|
||||
</button>
|
||||
<button class="commit-settings-button" type="button" onclick={onOpenAiSettings} disabled={isBusy} title="AI settings" aria-label="AI settings">
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<span class="pill pill-count">{stagedCount} staged</span>
|
||||
</div>
|
||||
|
||||
<form class="commit-form" onsubmit={handleSubmit}>
|
||||
<textarea
|
||||
value={commitMessage}
|
||||
oninput={(e) => onCommitMessageChange((e.target as HTMLTextAreaElement).value)}
|
||||
placeholder="Commit message..."
|
||||
placeholder="Commit message"
|
||||
disabled={!hasRepository || isBusy}
|
||||
></textarea>
|
||||
{#if commitBlockReason}
|
||||
@@ -117,30 +132,6 @@
|
||||
{/if}
|
||||
{amendMode ? "Amend" : "Commit"}
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary commit-ai-button"
|
||||
type="button"
|
||||
onclick={onGenerateCommitMessage}
|
||||
disabled={!canGenerate}
|
||||
title={aiButtonTitle(commitAiProvider, commitAiPhase, stagedCount)}
|
||||
>
|
||||
{#if commitAiGenerating || localModelLoading}
|
||||
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||
{:else}
|
||||
<Sparkles size={16} aria-hidden="true" />
|
||||
{/if}
|
||||
AI
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary commit-ai-settings-button"
|
||||
type="button"
|
||||
onclick={onOpenAiSettings}
|
||||
disabled={isBusy}
|
||||
title="AI settings"
|
||||
aria-label="AI settings"
|
||||
>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
rightNum?: number; rightText?: string; rightKind: "add" | "context" | "empty";
|
||||
};
|
||||
|
||||
interface DiffMarker {
|
||||
start: number;
|
||||
end: number;
|
||||
kind: "add" | "delete" | "mixed";
|
||||
}
|
||||
|
||||
interface Props {
|
||||
comparison: GitCommitComparison;
|
||||
selectedDiffPath: string;
|
||||
@@ -173,10 +179,40 @@
|
||||
return rows;
|
||||
}
|
||||
|
||||
function buildDiffMarkers(rows: SplitRow[]): DiffMarker[] {
|
||||
const markers: DiffMarker[] = [];
|
||||
let current: DiffMarker | null = null;
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
const row = rows[index];
|
||||
if (row.type !== "pair" || (row.leftKind === "context" && row.rightKind === "context")) {
|
||||
current = null;
|
||||
continue;
|
||||
}
|
||||
const kind = row.leftKind === "del" && row.rightKind === "add"
|
||||
? "mixed"
|
||||
: row.rightKind === "add" ? "add" : "delete";
|
||||
if (current && current.end === index - 1 && current.kind === kind) {
|
||||
current.end = index;
|
||||
} else {
|
||||
current = { start: index, end: index, kind };
|
||||
markers.push(current);
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
function scrollToDiffMarker(rowIndex: number) {
|
||||
const ratio = rowIndex / Math.max(splitRows.length - 1, 1);
|
||||
for (const pane of [beforePane, afterPane]) {
|
||||
if (pane) pane.scrollTop = ratio * Math.max(pane.scrollHeight - pane.clientHeight, 0);
|
||||
}
|
||||
}
|
||||
|
||||
let diffByPath = $derived(buildDiffByPath(comparison.patch));
|
||||
let selectedFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
|
||||
let selectedPatch = $derived(selectedFile ? (diffByPath.get(selectedFile.path) ?? "") : "");
|
||||
let splitRows = $derived(buildSplitRows(selectedPatch));
|
||||
let diffMarkers = $derived(buildDiffMarkers(splitRows));
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -262,6 +298,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Split diff grid -->
|
||||
<div class="split-diff-shell">
|
||||
<div class="split-diff" role="table" aria-label="Side-by-side diff">
|
||||
<div
|
||||
class="split-pane"
|
||||
@@ -298,6 +335,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="diff-overview" aria-label="Change overview">
|
||||
{#each diffMarkers as marker, index (`${marker.start}-${marker.end}-${marker.kind}`)}
|
||||
<button
|
||||
class="diff-overview-marker {marker.kind}"
|
||||
type="button"
|
||||
style={`--marker-position: ${(marker.start / Math.max(splitRows.length - 1, 1)) * 100}%`}
|
||||
onclick={() => scrollToDiffMarker(marker.start)}
|
||||
title={`Jump to change ${index + 1} of ${diffMarkers.length}`}
|
||||
aria-label={`Jump to change ${index + 1} of ${diffMarkers.length}`}
|
||||
></button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
let parsed = $state<ParsedPatch>({ headerLines: [], hunks: [], binary: false });
|
||||
let patchScroll = $state<HTMLDivElement | null>(null);
|
||||
|
||||
let scopeLabel = $derived(staged ? "Staged changes" : "Unstaged changes");
|
||||
let displayPath = $derived(file.old_path ? `${file.old_path} -> ${file.path}` : file.path);
|
||||
@@ -118,6 +119,23 @@
|
||||
if (isBusy || isLoading) return;
|
||||
await onApply(action, buildHunkPatch(hunk));
|
||||
}
|
||||
|
||||
function hunkPosition(index: number): number {
|
||||
const totalLines = parsed.hunks.reduce((sum, hunk) => sum + Math.max(hunk.lines.length, 1), 0);
|
||||
const precedingLines = parsed.hunks.slice(0, index).reduce((sum, hunk) => sum + Math.max(hunk.lines.length, 1), 0);
|
||||
return (precedingLines / Math.max(totalLines - 1, 1)) * 100;
|
||||
}
|
||||
|
||||
function hunkKind(hunk: PatchHunk): "add" | "delete" | "mixed" {
|
||||
const hasAdd = hunk.lines.some((line) => line.kind === "add");
|
||||
const hasDelete = hunk.lines.some((line) => line.kind === "delete");
|
||||
return hasAdd && hasDelete ? "mixed" : hasAdd ? "add" : "delete";
|
||||
}
|
||||
|
||||
function scrollToHunk(hunkId: string) {
|
||||
const target = patchScroll?.querySelector<HTMLElement>(`[data-hunk-id="${hunkId}"]`);
|
||||
if (patchScroll && target) patchScroll.scrollTop = Math.max(target.offsetTop - 8, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
@@ -148,9 +166,10 @@
|
||||
{:else if parsed.binary || parsed.hunks.length === 0}
|
||||
<div class="blank-state">This change cannot be split into text lines.</div>
|
||||
{:else}
|
||||
<div class="line-patch-scroll">
|
||||
<div class="line-patch-workspace">
|
||||
<div class="line-patch-scroll" bind:this={patchScroll}>
|
||||
{#each parsed.hunks as hunk (hunk.id)}
|
||||
<section class="line-patch-hunk">
|
||||
<section class="line-patch-hunk" data-hunk-id={hunk.id}>
|
||||
<div class="line-patch-hunk-head">
|
||||
<code>{hunk.header}</code>
|
||||
<div class="line-patch-hunk-actions">
|
||||
@@ -183,6 +202,19 @@
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
<nav class="diff-overview line-patch-overview" aria-label="Change overview">
|
||||
{#each parsed.hunks as hunk, index (hunk.id)}
|
||||
<button
|
||||
class="diff-overview-marker {hunkKind(hunk)}"
|
||||
type="button"
|
||||
style={`--marker-position: ${hunkPosition(index)}%`}
|
||||
onclick={() => scrollToHunk(hunk.id)}
|
||||
title={`Jump to hunk ${index + 1} of ${parsed.hunks.length}`}
|
||||
aria-label={`Jump to hunk ${index + 1} of ${parsed.hunks.length}`}
|
||||
></button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,11 +156,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="panel relative grid grid-rows-[auto_auto_1fr] overflow-hidden" aria-label="Working tree status">
|
||||
<section class="panel status-panel relative grid grid-rows-[auto_auto_1fr] overflow-hidden" aria-label="Working tree status">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<span class="eyebrow">Working tree</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Status</h2>
|
||||
<span class="eyebrow">Workspace</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Changes</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="pill pill-count">{stagedCount} staged</span>
|
||||
@@ -170,26 +170,6 @@
|
||||
|
||||
{#if hasRepository && changedFiles.length > 0}
|
||||
<div class="status-toolbar">
|
||||
<button
|
||||
class="btn-sm"
|
||||
type="button"
|
||||
onclick={onStageAll}
|
||||
disabled={isBusy || !hasUnstaged}
|
||||
title="Stage all unstaged files"
|
||||
>
|
||||
<Check size={14} aria-hidden="true" />
|
||||
Stage all
|
||||
</button>
|
||||
<button
|
||||
class="btn-sm"
|
||||
type="button"
|
||||
onclick={onUnstageAll}
|
||||
disabled={isBusy || !hasStaged}
|
||||
title="Unstage all staged files"
|
||||
>
|
||||
<Undo2 size={14} aria-hidden="true" />
|
||||
Unstage all
|
||||
</button>
|
||||
<button
|
||||
class="btn-sm danger"
|
||||
type="button"
|
||||
@@ -243,79 +223,60 @@
|
||||
{:else if changedFiles.length === 0}
|
||||
<div class="blank-state">No file changes returned.</div>
|
||||
{:else}
|
||||
<div class="overflow-auto p-2">
|
||||
{#each changedFiles as file (`${file.old_path ?? ""}:${file.path}`)}
|
||||
<article
|
||||
class="file-row"
|
||||
class:selected={isStatusSelected(file)}
|
||||
class:active={selectedFilePath === file.path}
|
||||
>
|
||||
<div class="file-title">
|
||||
<button
|
||||
class="file-title-button"
|
||||
type="button"
|
||||
onclick={(event) => handleFileSelect(event, file)}
|
||||
title={`Select ${displayPath(file)} in Explorer`}
|
||||
>
|
||||
<strong>{fileName(file)}</strong>
|
||||
</button>
|
||||
</div>
|
||||
<div class="status-lanes overflow-auto">
|
||||
<section class="status-lane" aria-label="Unstaged changes">
|
||||
<header class="status-lane-head">
|
||||
<div><strong>Unstaged</strong><span>{unstagedCount}</span></div>
|
||||
<button class="btn-sm" type="button" onclick={onStageAll} disabled={isBusy || !hasUnstaged} title="Stage all unstaged files">
|
||||
<Check size={13} aria-hidden="true" /> Stage all
|
||||
</button>
|
||||
</header>
|
||||
<div class="status-file-list">
|
||||
{#each changedFiles.filter((file) => file.unstaged !== null) as file (`unstaged:${fileKey(file)}`)}
|
||||
{@const stageTargets = selectedStageTargets(file)}
|
||||
<article class="status-file-row" class:selected={isStatusSelected(file)} class:active={selectedFilePath === file.path}>
|
||||
<button class="status-file-main" type="button" onclick={(event) => handleFileSelect(event, file)} title={`Select ${displayPath(file)} in Explorer`}>
|
||||
<strong>{fileName(file)}</strong>
|
||||
<span>{displayPath(file)}</span>
|
||||
</button>
|
||||
<span class={`status-badge ${file.unstaged ?? "none"}`}>{statusLabel(file.unstaged)}</span>
|
||||
<div class="status-file-actions">
|
||||
<button type="button" onclick={() => stageFromFile(file)} disabled={isBusy || stageTargets.length === 0} title="Stage file"><Check size={14} aria-hidden="true" /></button>
|
||||
<button type="button" onclick={() => onPatch(file, false)} disabled={isBusy || !canPatch(file.unstaged)} title="Show unstaged details"><FileDiff size={14} aria-hidden="true" /></button>
|
||||
<button class="danger" type="button" onclick={() => discardUnstagedFromFile(file)} disabled={isBusy || stageTargets.length === 0} title="Discard unstaged changes"><RotateCcw size={14} aria-hidden="true" /></button>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
{#if unstagedCount === 0}<p class="status-lane-empty">No unstaged changes.</p>{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="change-lanes">
|
||||
<div class="change-lane" class:inactive={!file.staged}>
|
||||
<div class="lane-header">
|
||||
<span class="lane-name">Staged</span>
|
||||
<span class={`status-badge ${file.staged ?? "none"}`}>{statusLabel(file.staged)}</span>
|
||||
<section class="status-lane" aria-label="Staged changes">
|
||||
<header class="status-lane-head">
|
||||
<div><strong>Staged</strong><span>{stagedCount}</span></div>
|
||||
<button class="btn-sm" type="button" onclick={onUnstageAll} disabled={isBusy || !hasStaged} title="Unstage all staged files">
|
||||
<Undo2 size={13} aria-hidden="true" /> Unstage all
|
||||
</button>
|
||||
</header>
|
||||
<div class="status-file-list">
|
||||
{#each changedFiles.filter((file) => file.staged !== null) as file (`staged:${fileKey(file)}`)}
|
||||
{@const unstageTargets = selectedUnstageTargets(file)}
|
||||
<article class="status-file-row" class:selected={isStatusSelected(file)} class:active={selectedFilePath === file.path}>
|
||||
<button class="status-file-main" type="button" onclick={(event) => handleFileSelect(event, file)} title={`Select ${displayPath(file)} in Explorer`}>
|
||||
<strong>{fileName(file)}</strong>
|
||||
<span>{displayPath(file)}</span>
|
||||
</button>
|
||||
<span class={`status-badge ${file.staged ?? "none"}`}>{statusLabel(file.staged)}</span>
|
||||
<div class="status-file-actions">
|
||||
<button type="button" onclick={() => unstageFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title="Unstage file"><Undo2 size={14} aria-hidden="true" /></button>
|
||||
<button type="button" onclick={() => onPatch(file, true)} disabled={isBusy || !canPatch(file.staged)} title="Show staged details"><FileDiff size={14} aria-hidden="true" /></button>
|
||||
<button class="danger" type="button" onclick={() => discardStagedFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title="Discard staged changes"><RotateCcw size={14} aria-hidden="true" /></button>
|
||||
</div>
|
||||
<div class="lane-actions">
|
||||
{#if file.staged}
|
||||
{@const unstageTargets = selectedUnstageTargets(file)}
|
||||
<button class="btn-sm" type="button" onclick={() => unstageFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title={unstageTargets.length > 1 ? `Unstage ${unstageTargets.length} selected files` : "Unstage file"}>
|
||||
<Undo2 size={14} aria-hidden="true" />
|
||||
{unstageTargets.length > 1 ? `Unstage ${unstageTargets.length}` : "Unstage"}
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => onPatch(file, true)} disabled={isBusy || !canPatch(file.staged)} title="Stage, unstage, or discard selected lines">
|
||||
<FileDiff size={14} aria-hidden="true" />
|
||||
Details
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => discardStagedFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title={unstageTargets.length > 1 ? `Discard staged changes in ${unstageTargets.length} selected files` : "Discard staged changes"}>
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
{unstageTargets.length > 1 ? `Discard ${unstageTargets.length}` : "Discard"}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="quiet">No staged change</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="change-lane" class:inactive={!file.unstaged}>
|
||||
<div class="lane-header">
|
||||
<span class="lane-name">Unstaged</span>
|
||||
<span class={`status-badge ${file.unstaged ?? "none"}`}>{statusLabel(file.unstaged)}</span>
|
||||
</div>
|
||||
<div class="lane-actions">
|
||||
{#if file.unstaged}
|
||||
{@const stageTargets = selectedStageTargets(file)}
|
||||
<button class="btn-sm" type="button" onclick={() => stageFromFile(file)} disabled={isBusy || stageTargets.length === 0} title={stageTargets.length > 1 ? `Stage ${stageTargets.length} selected files` : "Stage file"}>
|
||||
<Check size={14} aria-hidden="true" />
|
||||
{stageTargets.length > 1 ? `Stage ${stageTargets.length}` : "Stage"}
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => onPatch(file, false)} disabled={isBusy || !canPatch(file.unstaged)} title="Stage or discard selected lines">
|
||||
<FileDiff size={14} aria-hidden="true" />
|
||||
Details
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => discardUnstagedFromFile(file)} disabled={isBusy || stageTargets.length === 0} title={stageTargets.length > 1 ? `Discard unstaged changes in ${stageTargets.length} selected files` : "Discard unstaged changes"}>
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
{stageTargets.length > 1 ? `Discard ${stageTargets.length}` : "Discard"}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="quiet">No unstaged change</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</article>
|
||||
{/each}
|
||||
{#if stagedCount === 0}<p class="status-lane-empty">Stage files to include them in the next commit.</p>{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export type CommitAiPhase = "idle" | "loading" | "ready" | "error";
|
||||
export type CommitAiProvider = "local" | "openai" | "anthropic" | "custom";
|
||||
export type CommitAiLocalProfile = "fast" | "balanced" | "detailed";
|
||||
export type AppTheme = "system" | "light" | "dark";
|
||||
export type AppLanguage = "en" | "de";
|
||||
|
||||
export interface CommitAiStatus {
|
||||
phase: CommitAiPhase;
|
||||
|
||||
Reference in New Issue
Block a user