generated from nhcarrigan/template
1ae440659c
## Summary - **Fix git window "Not a git repository" error** — The working directory received from Claude Code is a WSL Linux path (e.g. `/home/naomi/...`), but git commands were being run as native Windows processes with `.current_dir()`. Windows can't resolve WSL paths, causing `git rev-parse --git-dir` to fail. Fixed by routing git commands through `wsl -- git -C <path>` when the working directory starts with `/`. - **Add syntax highlighting and line numbers to diff view** — Replaced the raw `<pre>` block with a proper `DiffViewer` component featuring: - Old/new line number columns with correct tracking across hunks - Colour-coded gutter (`+`/`-`) with green/red row backgrounds - Syntax highlighting via `highlight.js` using the detected file language, respecting all app themes via `--hljs-*` CSS variables - Styled hunk headers and file headers ## New files - `src/lib/utils/diffParser.ts` — pure diff parsing logic - `src/lib/utils/diffParser.test.ts` — 30 tests covering all line types, line number tracking, and language detection - `src/lib/components/DiffViewer.svelte` — the pretty diff viewer component ✨ This pull request was created with help from Hikari~ 🌸 Reviewed-on: #178 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
1106 lines
27 KiB
Svelte
1106 lines
27 KiB
Svelte
<script lang="ts">
|
||
import { invoke } from "@tauri-apps/api/core";
|
||
import { onMount, onDestroy } from "svelte";
|
||
import { claudeStore } from "$lib/stores/claude";
|
||
import DiffViewer from "$lib/components/DiffViewer.svelte";
|
||
|
||
interface GitFileChange {
|
||
path: string;
|
||
status: string;
|
||
}
|
||
|
||
interface GitStatus {
|
||
is_repo: boolean;
|
||
branch: string | null;
|
||
upstream: string | null;
|
||
ahead: number;
|
||
behind: number;
|
||
staged: GitFileChange[];
|
||
unstaged: GitFileChange[];
|
||
untracked: string[];
|
||
}
|
||
|
||
interface GitBranch {
|
||
name: string;
|
||
is_current: boolean;
|
||
is_remote: boolean;
|
||
}
|
||
|
||
interface GitLogEntry {
|
||
hash: string;
|
||
short_hash: string;
|
||
author: string;
|
||
date: string;
|
||
message: string;
|
||
}
|
||
|
||
export let isOpen = false;
|
||
export let onClose: () => void;
|
||
|
||
let status: GitStatus | null = null;
|
||
let branches: GitBranch[] = [];
|
||
let log: GitLogEntry[] = [];
|
||
let loading = false;
|
||
let error: string | null = null;
|
||
let activeTab: "changes" | "branches" | "history" = "changes";
|
||
let commitMessage = "";
|
||
let showDiff = false;
|
||
let diffContent = "";
|
||
let diffFile: string | null = null;
|
||
let showBranchInput = false;
|
||
let newBranchName = "";
|
||
let actionInProgress = false;
|
||
let workingDir = "";
|
||
|
||
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
claudeStore.currentWorkingDirectory.subscribe((dir) => {
|
||
workingDir = dir;
|
||
});
|
||
|
||
onMount(() => {
|
||
if (isOpen) {
|
||
refresh();
|
||
refreshInterval = setInterval(refresh, 5000);
|
||
}
|
||
});
|
||
|
||
onDestroy(() => {
|
||
if (refreshInterval) {
|
||
clearInterval(refreshInterval);
|
||
}
|
||
});
|
||
|
||
$: if (isOpen && !refreshInterval) {
|
||
refresh();
|
||
refreshInterval = setInterval(refresh, 5000);
|
||
} else if (!isOpen && refreshInterval) {
|
||
clearInterval(refreshInterval);
|
||
refreshInterval = null;
|
||
}
|
||
|
||
async function refresh() {
|
||
if (!workingDir) return;
|
||
|
||
loading = true;
|
||
error = null;
|
||
|
||
try {
|
||
status = await invoke<GitStatus>("git_status", { workingDir: workingDir });
|
||
|
||
if (status.is_repo) {
|
||
branches = await invoke<GitBranch[]>("git_branches", { workingDir: workingDir });
|
||
log = await invoke<GitLogEntry[]>("git_log", {
|
||
workingDir: workingDir,
|
||
limit: 20,
|
||
});
|
||
}
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
async function viewDiff(filePath: string, staged: boolean) {
|
||
try {
|
||
diffContent = await invoke<string>("git_diff", {
|
||
workingDir: workingDir,
|
||
filePath,
|
||
staged,
|
||
});
|
||
diffFile = filePath;
|
||
showDiff = true;
|
||
} catch (e) {
|
||
error = e as string;
|
||
}
|
||
}
|
||
|
||
async function stageFile(filePath: string) {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_stage", { workingDir: workingDir, filePath });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function unstageFile(filePath: string) {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_unstage", { workingDir: workingDir, filePath });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function stageAll() {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_stage_all", { workingDir: workingDir });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function discardFile(filePath: string) {
|
||
if (!confirm(`Discard changes to ${filePath}? This cannot be undone.`)) return;
|
||
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_discard", { workingDir: workingDir, filePath });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function commit() {
|
||
if (!commitMessage.trim()) {
|
||
error = "Please enter a commit message";
|
||
return;
|
||
}
|
||
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_commit", { workingDir: workingDir, message: commitMessage });
|
||
commitMessage = "";
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function push() {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_push", { workingDir: workingDir });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function pull() {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_pull", { workingDir: workingDir });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function fetch() {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_fetch", { workingDir: workingDir });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function checkout(branch: string) {
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_checkout", { workingDir: workingDir, branch });
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
async function createBranch() {
|
||
if (!newBranchName.trim()) return;
|
||
|
||
actionInProgress = true;
|
||
try {
|
||
await invoke("git_create_branch", {
|
||
workingDir: workingDir,
|
||
branchName: newBranchName,
|
||
});
|
||
newBranchName = "";
|
||
showBranchInput = false;
|
||
await refresh();
|
||
} catch (e) {
|
||
error = e as string;
|
||
} finally {
|
||
actionInProgress = false;
|
||
}
|
||
}
|
||
|
||
function getStatusIcon(status: string): string {
|
||
switch (status) {
|
||
case "modified":
|
||
return "✏️";
|
||
case "added":
|
||
return "➕";
|
||
case "deleted":
|
||
return "🗑️";
|
||
case "renamed":
|
||
return "📝";
|
||
default:
|
||
return "❓";
|
||
}
|
||
}
|
||
|
||
function handleKeydown(event: KeyboardEvent) {
|
||
if (event.key === "Escape") {
|
||
if (showDiff) {
|
||
showDiff = false;
|
||
} else {
|
||
onClose();
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<svelte:window on:keydown={handleKeydown} />
|
||
|
||
{#if isOpen}
|
||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||
<div class="git-panel-overlay" on:click={onClose} role="presentation">
|
||
<div
|
||
class="git-panel"
|
||
on:click|stopPropagation
|
||
role="dialog"
|
||
aria-label="Git Panel"
|
||
tabindex="-1"
|
||
>
|
||
<div class="git-panel-header">
|
||
<h2>🔀 Git</h2>
|
||
<button class="close-btn" on:click={onClose} title="Close (Esc)">✕</button>
|
||
</div>
|
||
|
||
{#if !workingDir}
|
||
<div class="git-panel-content">
|
||
<p class="empty-state">No working directory selected</p>
|
||
</div>
|
||
{:else if loading && !status}
|
||
<div class="git-panel-content">
|
||
<p class="loading">Loading git status...</p>
|
||
</div>
|
||
{:else if !status?.is_repo}
|
||
<div class="git-panel-content">
|
||
<p class="empty-state">Not a git repository</p>
|
||
</div>
|
||
{:else}
|
||
<div class="git-info-bar">
|
||
<div class="branch-info">
|
||
<span class="branch-icon">🌿</span>
|
||
<span class="branch-name">{status.branch || "detached"}</span>
|
||
{#if status.upstream}
|
||
<span class="upstream">→ {status.upstream}</span>
|
||
{/if}
|
||
</div>
|
||
<div class="sync-status">
|
||
{#if status.ahead > 0}
|
||
<span class="ahead" title="Commits ahead">↑{status.ahead}</span>
|
||
{/if}
|
||
{#if status.behind > 0}
|
||
<span class="behind" title="Commits behind">↓{status.behind}</span>
|
||
{/if}
|
||
</div>
|
||
<div class="quick-actions">
|
||
<button
|
||
on:click={fetch}
|
||
disabled={actionInProgress}
|
||
title="Fetch all remotes"
|
||
class="icon-btn"
|
||
>
|
||
🔄
|
||
</button>
|
||
<button on:click={pull} disabled={actionInProgress} title="Pull" class="icon-btn">
|
||
⬇️
|
||
</button>
|
||
<button
|
||
on:click={push}
|
||
disabled={actionInProgress || status.ahead === 0}
|
||
title="Push"
|
||
class="icon-btn"
|
||
>
|
||
⬆️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{#if error}
|
||
<div class="error-banner">
|
||
<span>{error}</span>
|
||
<button on:click={() => (error = null)}>✕</button>
|
||
</div>
|
||
{/if}
|
||
|
||
<div class="tab-bar">
|
||
<button
|
||
class="tab"
|
||
class:active={activeTab === "changes"}
|
||
on:click={() => (activeTab = "changes")}
|
||
>
|
||
Changes
|
||
{#if status.staged.length + status.unstaged.length + status.untracked.length > 0}
|
||
<span class="badge"
|
||
>{status.staged.length + status.unstaged.length + status.untracked.length}</span
|
||
>
|
||
{/if}
|
||
</button>
|
||
<button
|
||
class="tab"
|
||
class:active={activeTab === "branches"}
|
||
on:click={() => (activeTab = "branches")}
|
||
>
|
||
Branches
|
||
</button>
|
||
<button
|
||
class="tab"
|
||
class:active={activeTab === "history"}
|
||
on:click={() => (activeTab = "history")}
|
||
>
|
||
History
|
||
</button>
|
||
</div>
|
||
|
||
<div class="git-panel-content">
|
||
{#if activeTab === "changes"}
|
||
{#if status.staged.length > 0}
|
||
<div class="file-section">
|
||
<h3>Staged Changes ({status.staged.length})</h3>
|
||
<div class="file-list">
|
||
{#each status.staged as file (file.path)}
|
||
<div class="file-item staged">
|
||
<span class="status-icon">{getStatusIcon(file.status)}</span>
|
||
<span class="file-path" title={file.path}>{file.path}</span>
|
||
<div class="file-actions">
|
||
<button
|
||
on:click={() => viewDiff(file.path, true)}
|
||
title="View diff"
|
||
class="icon-btn small"
|
||
>
|
||
👁️
|
||
</button>
|
||
<button
|
||
on:click={() => unstageFile(file.path)}
|
||
disabled={actionInProgress}
|
||
title="Unstage"
|
||
class="icon-btn small"
|
||
>
|
||
➖
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if status.unstaged.length > 0}
|
||
<div class="file-section">
|
||
<h3>
|
||
Unstaged Changes ({status.unstaged.length})
|
||
<button
|
||
on:click={stageAll}
|
||
disabled={actionInProgress}
|
||
class="btn-trans-gradient stage-all-btn"
|
||
title="Stage all"
|
||
>
|
||
Stage All
|
||
</button>
|
||
</h3>
|
||
<div class="file-list">
|
||
{#each status.unstaged as file (file.path)}
|
||
<div class="file-item unstaged">
|
||
<span class="status-icon">{getStatusIcon(file.status)}</span>
|
||
<span class="file-path" title={file.path}>{file.path}</span>
|
||
<div class="file-actions">
|
||
<button
|
||
on:click={() => viewDiff(file.path, false)}
|
||
title="View diff"
|
||
class="icon-btn small"
|
||
>
|
||
👁️
|
||
</button>
|
||
<button
|
||
on:click={() => stageFile(file.path)}
|
||
disabled={actionInProgress}
|
||
title="Stage"
|
||
class="icon-btn small"
|
||
>
|
||
➕
|
||
</button>
|
||
<button
|
||
on:click={() => discardFile(file.path)}
|
||
disabled={actionInProgress}
|
||
title="Discard changes"
|
||
class="icon-btn small danger"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if status.untracked.length > 0}
|
||
<div class="file-section">
|
||
<h3>Untracked Files ({status.untracked.length})</h3>
|
||
<div class="file-list">
|
||
{#each status.untracked as file (file)}
|
||
<div class="file-item untracked">
|
||
<span class="status-icon">❓</span>
|
||
<span class="file-path" title={file}>{file}</span>
|
||
<div class="file-actions">
|
||
<button
|
||
on:click={() => stageFile(file)}
|
||
disabled={actionInProgress}
|
||
title="Stage"
|
||
class="icon-btn small"
|
||
>
|
||
➕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0}
|
||
<p class="empty-state">✨ Working tree clean</p>
|
||
{/if}
|
||
|
||
{#if status.staged.length > 0}
|
||
<div class="commit-section">
|
||
<textarea
|
||
bind:value={commitMessage}
|
||
placeholder="Commit message..."
|
||
rows="3"
|
||
disabled={actionInProgress}
|
||
></textarea>
|
||
<button
|
||
on:click={commit}
|
||
disabled={actionInProgress || !commitMessage.trim()}
|
||
class="btn-trans-gradient commit-btn"
|
||
>
|
||
Commit
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
{:else if activeTab === "branches"}
|
||
<div class="branch-section">
|
||
<div class="branch-actions">
|
||
{#if showBranchInput}
|
||
<div class="new-branch-input">
|
||
<input
|
||
type="text"
|
||
bind:value={newBranchName}
|
||
placeholder="New branch name..."
|
||
on:keydown={(e) => e.key === "Enter" && createBranch()}
|
||
/>
|
||
<button
|
||
on:click={createBranch}
|
||
disabled={actionInProgress}
|
||
class="btn-trans-gradient">Create</button
|
||
>
|
||
<button on:click={() => (showBranchInput = false)}>Cancel</button>
|
||
</div>
|
||
{:else}
|
||
<button
|
||
on:click={() => (showBranchInput = true)}
|
||
class="btn-trans-gradient new-branch-btn"
|
||
>
|
||
➕ New Branch
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
|
||
<h3>Local Branches</h3>
|
||
<div class="branch-list">
|
||
{#each branches.filter((b) => !b.is_remote) as branch (branch.name)}
|
||
<div class="branch-item" class:current={branch.is_current}>
|
||
<span class="branch-icon">{branch.is_current ? "✓" : "○"}</span>
|
||
<span class="branch-name">{branch.name}</span>
|
||
{#if !branch.is_current}
|
||
<button
|
||
on:click={() => checkout(branch.name)}
|
||
disabled={actionInProgress}
|
||
class="checkout-btn"
|
||
>
|
||
Checkout
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
|
||
{#if branches.filter((b) => b.is_remote).length > 0}
|
||
<h3>Remote Branches</h3>
|
||
<div class="branch-list">
|
||
{#each branches.filter((b) => b.is_remote) as branch (branch.name)}
|
||
<div class="branch-item remote">
|
||
<span class="branch-icon">☁️</span>
|
||
<span class="branch-name">{branch.name}</span>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{:else if activeTab === "history"}
|
||
<div class="history-list">
|
||
{#each log as entry (entry.hash)}
|
||
<div class="log-entry">
|
||
<div class="log-header">
|
||
<span class="commit-hash" title={entry.hash}>{entry.short_hash}</span>
|
||
<span class="commit-date">{entry.date}</span>
|
||
</div>
|
||
<div class="commit-message">{entry.message}</div>
|
||
<div class="commit-author">by {entry.author}</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
{#if showDiff}
|
||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||
<div class="diff-overlay" on:click={() => (showDiff = false)} role="presentation">
|
||
<div
|
||
class="diff-modal"
|
||
on:click|stopPropagation
|
||
role="dialog"
|
||
aria-label="Diff View"
|
||
tabindex="-1"
|
||
>
|
||
<div class="diff-header">
|
||
<h3>📄 {diffFile}</h3>
|
||
<button on:click={() => (showDiff = false)} title="Close">✕</button>
|
||
</div>
|
||
<div class="diff-content">
|
||
<DiffViewer {diffContent} filePath={diffFile ?? ""} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{/if}
|
||
|
||
<style>
|
||
.git-panel-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.git-panel {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
width: 600px;
|
||
max-width: 90vw;
|
||
max-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.git-panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.git-panel-header h2 {
|
||
margin: 0;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-primary);
|
||
cursor: pointer;
|
||
font-size: 1.2rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.git-info-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 0.75rem 1rem;
|
||
background: var(--bg-tertiary);
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.branch-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.branch-icon {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.branch-name {
|
||
font-weight: 600;
|
||
color: var(--accent-primary);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.upstream {
|
||
color: var(--text-secondary);
|
||
font-size: 0.85rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.sync-status {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ahead,
|
||
.behind {
|
||
font-size: 0.85rem;
|
||
padding: 0.1rem 0.4rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.ahead {
|
||
background: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.behind {
|
||
background: var(--warning-color);
|
||
color: white;
|
||
}
|
||
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.icon-btn {
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.icon-btn:hover:not(:disabled) {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.icon-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.icon-btn.small {
|
||
font-size: 0.85rem;
|
||
padding: 0.15rem 0.35rem;
|
||
}
|
||
|
||
.icon-btn.danger:hover:not(:disabled) {
|
||
background: rgba(255, 0, 0, 0.2);
|
||
}
|
||
|
||
.error-banner {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem 1rem;
|
||
background: rgba(255, 0, 0, 0.1);
|
||
border-bottom: 1px solid var(--error-color);
|
||
color: var(--error-color);
|
||
}
|
||
|
||
.error-banner button {
|
||
background: none;
|
||
border: none;
|
||
color: var(--error-color);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tab-bar {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.tab {
|
||
flex: 1;
|
||
padding: 0.75rem;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--accent-primary);
|
||
border-bottom-color: var(--accent-primary);
|
||
}
|
||
|
||
.badge {
|
||
background: var(--accent-primary);
|
||
color: white;
|
||
font-size: 0.75rem;
|
||
padding: 0.1rem 0.4rem;
|
||
border-radius: 10px;
|
||
margin-left: 0.25rem;
|
||
}
|
||
|
||
.git-panel-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.empty-state,
|
||
.loading {
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
padding: 2rem;
|
||
}
|
||
|
||
.file-section {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.file-section h3 {
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
margin: 0 0 0.5rem 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.stage-all-btn {
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.4rem 0.5rem;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.file-item.staged {
|
||
border-left: 3px solid var(--success-color);
|
||
}
|
||
|
||
.file-item.unstaged {
|
||
border-left: 3px solid var(--warning-color);
|
||
}
|
||
|
||
.file-item.untracked {
|
||
border-left: 3px solid var(--text-secondary);
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.file-path {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.file-item:hover .file-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
.commit-section {
|
||
margin-top: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.commit-section textarea {
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
padding: 0.5rem;
|
||
color: var(--text-primary);
|
||
font-family: inherit;
|
||
resize: vertical;
|
||
}
|
||
|
||
.commit-btn {
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.branch-section h3 {
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
margin: 1rem 0 0.5rem 0;
|
||
}
|
||
|
||
.branch-actions {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.new-branch-btn {
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.new-branch-input {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.new-branch-input input {
|
||
flex: 1;
|
||
background: var(--bg-tertiary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
padding: 0.5rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.new-branch-input button {
|
||
padding: 0.5rem 1rem;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.new-branch-input button:last-of-type {
|
||
background: var(--bg-tertiary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.branch-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.branch-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.branch-item.current {
|
||
background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
|
||
border-left: 3px solid var(--accent-primary);
|
||
}
|
||
|
||
.branch-item.remote {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.branch-item .branch-name {
|
||
flex: 1;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.9rem;
|
||
color: var(--text-primary);
|
||
font-weight: normal;
|
||
}
|
||
|
||
.checkout-btn {
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem 0.5rem;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.branch-item:hover .checkout-btn {
|
||
opacity: 1;
|
||
}
|
||
|
||
.history-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.log-entry {
|
||
padding: 0.75rem;
|
||
background: var(--bg-tertiary);
|
||
border-radius: 4px;
|
||
border-left: 3px solid var(--accent-primary);
|
||
}
|
||
|
||
.log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.commit-hash {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem;
|
||
color: var(--accent-primary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.commit-date {
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.commit-message {
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.commit-author {
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.diff-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1100;
|
||
}
|
||
|
||
.diff-modal {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 8px;
|
||
width: 800px;
|
||
max-width: 95vw;
|
||
max-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.diff-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.75rem 1rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.diff-header h3 {
|
||
margin: 0;
|
||
font-size: 1rem;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.diff-header button {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-primary);
|
||
cursor: pointer;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.diff-content {
|
||
flex: 1;
|
||
overflow: auto;
|
||
margin: 0;
|
||
background: var(--bg-primary);
|
||
}
|
||
</style>
|