feat(status-panel): add tree view for changes

The body:
Introduces a tree view for the status panel in addition to the
existing list view. A new toggle allows switching between modes.

It builds a hierarchical tree of folders and files from changed
paths and renders both unstaged and staged sections with
collapsible folders and indentation.

Adds responsive styling so the view switch condenses to icons on
narrow screens and includes styles for folders, chevrons, and
folder counts.

- Build and sort a tree structure from file paths.
- Render folders and files with indentation in tree view.
- Add responsive styling and a view switch for status changes.
This commit is contained in:
Christoph Brandau
2026-08-17 21:42:32 +02:00
parent fac22ff173
commit 8b559fa15c
2 changed files with 321 additions and 27 deletions
+149
View File
@@ -6191,6 +6191,80 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
background: var(--color-surface-dim);
}
.status-toolbar .danger { margin-left: auto; }
.status-panel > .section-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
}
.status-panel > .section-head > :first-child { justify-self: start; }
.status-panel > .section-head > .status-head-actions {
justify-self: end;
max-width: 100%;
}
.status-view-switch {
display: inline-grid;
grid-template-columns: repeat(2, auto);
align-items: center;
min-height: 26px;
padding: 2px;
border: 1px solid var(--color-border);
border-radius: 5px;
background: var(--color-surface-dim);
}
.status-view-switch button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-width: 0;
min-height: 20px;
padding: 0 6px;
border: 0;
border-radius: 3px;
color: var(--color-ink-faint);
background: transparent;
font-size: 9.5px;
font-weight: 700;
}
.status-view-switch button:hover:not(:disabled) {
color: var(--color-ink);
background: var(--color-surface-hover);
}
.status-view-switch button.active {
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 13%, var(--color-surface-solid));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.status-view-icon {
position: relative;
display: inline-block;
flex: 0 0 auto;
width: 12px;
height: 10px;
}
.status-view-icon.list-icon {
background: repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px 4px);
}
.status-view-icon.tree-icon::before {
content: "";
position: absolute;
top: 1px;
left: 1px;
width: 3px;
height: 3px;
background: currentColor;
box-shadow: 8px 0 0 currentColor, 8px 7px 0 currentColor;
}
.status-view-icon.tree-icon::after {
content: "";
position: absolute;
top: 2px;
bottom: 1px;
left: 2px;
width: 8px;
border-bottom: 1px solid currentColor;
border-left: 1px solid currentColor;
}
.status-panel { container-type: inline-size; }
.status-lanes {
display: grid;
@@ -6295,6 +6369,7 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.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-row.status-tree-file { padding-left: calc(12px + var(--tree-indent, 0px)); }
.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; }
@@ -6304,6 +6379,75 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.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-folder-row {
display: grid;
grid-template-columns: 14px 15px minmax(0, 1fr) auto;
align-items: center;
gap: 5px;
width: 100%;
min-height: 32px;
padding: 2px 12px 2px calc(12px + var(--tree-indent, 0px));
border: 0;
border-bottom: 1px solid var(--color-border-subtle);
border-radius: 0;
color: var(--color-ink-muted);
background: color-mix(in srgb, var(--color-surface-dim) 64%, transparent);
text-align: left;
}
.status-folder-row:hover:not(:disabled) {
color: var(--color-ink);
background: var(--color-surface-hover);
}
.status-tree-chevron {
width: 6px;
height: 6px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(-45deg);
transition: transform 120ms ease;
}
.status-tree-chevron.expanded { transform: rotate(45deg) translate(-1px, -1px); }
.status-folder-icon {
position: relative;
width: 14px;
height: 10px;
margin-top: 2px;
border: 1px solid currentColor;
border-radius: 2px;
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 7%, transparent);
}
.status-folder-icon::before {
content: "";
position: absolute;
top: -4px;
left: 1px;
width: 7px;
height: 4px;
border: 1px solid currentColor;
border-bottom: 0;
border-radius: 2px 2px 0 0;
}
.status-folder-icon.open { background: color-mix(in srgb, var(--color-primary) 14%, transparent); }
.status-folder-row strong {
overflow: hidden;
color: inherit;
font: 650 11px/1.2 var(--font-mono);
text-overflow: ellipsis;
white-space: nowrap;
}
.status-folder-count {
display: grid;
place-items: center;
min-width: 18px;
height: 18px;
padding-inline: 4px;
border-radius: 4px;
color: var(--color-ink-faint);
background: color-mix(in srgb, var(--color-ink-faint) 9%, transparent);
font-size: 9px;
font-weight: 750;
}
.status-lane-empty {
display: grid;
place-items: center;
@@ -7558,6 +7702,11 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.status-flow-divider span { transform: rotate(90deg); }
}
@container (max-width: 520px) {
.status-view-switch button > span { display: none; }
.status-view-switch button { width: 24px; padding-inline: 0; }
}
@media (max-width: 760px) {
.status-lane-head { align-items: flex-start; flex-direction: column; }
.status-lane-actions { width: 100%; justify-content: flex-start; }