refactor(ui): modernize status bar and component structure
This commit introduces a comprehensive overhaul of the application's UI, focusing on modernizing the global workspace status bar and improving overall theming consistency. Several components were refactored to simplify prop handling and improve separation of concerns, particularly within the TitleBar and RepoToolbar. The CSS includes extensive new variables and styles for better visual fidelity across light and dark themes. - Overhauled the main application footer to display version, branch status, and sync metrics. - Added global CSS variables and component styling for a modern look. - Simplified repository state management by removing redundant props from TitleBar.
This commit is contained in:
+16
-5
@@ -1,5 +1,6 @@
|
||||
<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";
|
||||
@@ -366,6 +367,7 @@
|
||||
let fileHistoryResizeStartWidth = 0;
|
||||
let fileHistoryCollapsed = true;
|
||||
let themeMediaQuery: MediaQueryList | undefined;
|
||||
let appVersion = "";
|
||||
|
||||
// ── Derived ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -427,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(() => {
|
||||
@@ -3526,11 +3529,6 @@
|
||||
|
||||
<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}
|
||||
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||
onOpenHelp={openHelp}
|
||||
language={appLanguage}
|
||||
@@ -4099,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>
|
||||
|
||||
|
||||
+389
-3
@@ -2145,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; }
|
||||
@@ -4974,6 +4974,290 @@
|
||||
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(255,255,255,0.5);
|
||||
}
|
||||
@@ -5480,3 +5764,105 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
|
||||
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
|
||||
<div class="repo-toolbar-sync">
|
||||
<span class="repo-toolbar-group-label">Sync</span>
|
||||
<div class="repo-action-group repo-sync-actions">
|
||||
<button
|
||||
class="repo-action fetch"
|
||||
|
||||
+2
-42
@@ -1,15 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { CircleHelp, GitBranch, Minus, Settings, 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 onOpenHelp: () => void = () => {};
|
||||
export let onOpenSettings: () => void = () => {};
|
||||
export let language: "en" | "de" = "en";
|
||||
@@ -17,7 +11,6 @@
|
||||
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
||||
let isMaximized = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
let appVersion = "";
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
@@ -29,12 +22,6 @@
|
||||
} catch {
|
||||
win = null;
|
||||
}
|
||||
|
||||
try {
|
||||
appVersion = await getVersion();
|
||||
} catch {
|
||||
appVersion = "";
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -61,36 +48,9 @@
|
||||
<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>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="tb-no-repo" data-tauri-drag-region>No repository open</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">
|
||||
|
||||
@@ -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>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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}
|
||||
</header>
|
||||
<div class="status-file-list">
|
||||
{#each changedFiles.filter((file) => file.unstaged !== null) as file (`unstaged:${fileKey(file)}`)}
|
||||
{@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"}
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user