From 8b559fa15cd587f0dbc016769cfa003f3ece3a69 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Mon, 17 Aug 2026 21:42:32 +0200 Subject: [PATCH] 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. --- src/app.css | 149 +++++++++++++++++++ src/lib/components/StatusPanel.svelte | 199 ++++++++++++++++++++++---- 2 files changed, 321 insertions(+), 27 deletions(-) diff --git a/src/app.css b/src/app.css index eecdfa9..f905625 100644 --- a/src/app.css +++ b/src/app.css @@ -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; } diff --git a/src/lib/components/StatusPanel.svelte b/src/lib/components/StatusPanel.svelte index 027cf7a..fac225e 100644 --- a/src/lib/components/StatusPanel.svelte +++ b/src/lib/components/StatusPanel.svelte @@ -1,5 +1,10 @@