Merge git-panel-layout-fixes: right-pinned worktree actions, count on the heading

This commit is contained in:
Yaojia Wang
2026-07-29 18:30:09 +02:00
4 changed files with 62 additions and 19 deletions

View File

@@ -100,8 +100,16 @@ function renderCommitRow(c: CommitLogEntry): HTMLElement {
} }
/** Render a GitLogResult into a container (clears first). Empty → an inert note. */ /** Render a GitLogResult into a container (clears first). Empty → an inert note. */
export function renderGitLog(container: HTMLElement, log: GitLogResult): void { export function renderGitLog(
container: HTMLElement,
log: GitLogResult,
labelEl?: HTMLElement,
): void {
container.textContent = '' container.textContent = ''
// Design: the count sits ON the section heading, not on a line of its own —
// it qualifies "Recent commits", it is not a separate statement.
const badgeHost = labelEl ?? container
badgeHost.querySelector('.proj-commitlog-count')?.remove()
if (log.commits.length === 0) { if (log.commits.length === 0) {
container.append(el('div', 'proj-empty', 'No commits yet.')) container.append(el('div', 'proj-empty', 'No commits yet.'))
return return
@@ -110,9 +118,9 @@ export function renderGitLog(container: HTMLElement, log: GitLogResult): void {
// readable before the eye has walked the rows looking for rails. // readable before the eye has walked the rows looking for rails.
const unpushedCount = log.commits.filter((c) => c.unpushed === true).length const unpushedCount = log.commits.filter((c) => c.unpushed === true).length
if (unpushedCount > 0 && log.upstream !== undefined) { if (unpushedCount > 0 && log.upstream !== undefined) {
const badge = el('div', 'proj-commitlog-count', `${unpushedCount} unpushed`) const badge = el('span', 'proj-commitlog-count', `${unpushedCount} unpushed`)
badge.title = `${unpushedCount} commit(s) not on ${log.upstream}` badge.title = `${unpushedCount} commit(s) not on ${log.upstream}`
container.append(badge) badgeHost.append(badge)
} }
const list = el('div', 'proj-commitlog-list') const list = el('div', 'proj-commitlog-list')
@@ -150,7 +158,11 @@ export interface GitLogHandle {
* the log, then swap in the rows. A fetch failure degrades to a short inert * the log, then swap in the rows. A fetch failure degrades to a short inert
* message (never throws). destroy() removes the node and cancels the swap. * message (never throws). destroy() removes the node and cancels the swap.
*/ */
export function mountGitLog(container: HTMLElement, repoPath: string): GitLogHandle { export function mountGitLog(
container: HTMLElement,
repoPath: string,
labelEl?: HTMLElement,
): GitLogHandle {
let destroyed = false let destroyed = false
container.textContent = '' container.textContent = ''
@@ -164,7 +176,7 @@ export function mountGitLog(container: HTMLElement, repoPath: string): GitLogHan
container.append(el('div', 'proj-empty', 'Could not read recent commits.')) container.append(el('div', 'proj-empty', 'Could not read recent commits.'))
return return
} }
renderGitLog(container, log) renderGitLog(container, log, labelEl)
})() })()
return { return {

Binary file not shown.

View File

@@ -1725,18 +1725,17 @@ body {
.proj-commitlog-count { .proj-commitlog-count {
font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 11.5px; font-size: 11.5px;
font-variant-numeric: tabular-nums;
color: var(--accent); color: var(--accent);
margin: -4px 0 6px; text-transform: none;
letter-spacing: 0;
margin-left: 10px;
} }
/* ── w6/G5: worktree rows as two-line cards ──────────────────────────────── */ /* ── w6/G5: worktree rows as two-line cards ──────────────────────────────── */
.proj-wt-row { /* NOTE: the row's own display/grid lives with the ORIGINAL .proj-wt-row rule
display: grid; further down. Declaring it twice let the later copy silently win and the
grid-template-columns: 1fr auto; Open button never reached the right edge. One rule per selector. */
gap: 14px;
align-items: center;
}
.proj-wt-row-current { .proj-wt-row-current {
border-color: rgba(227, 166, 74, 0.36); border-color: rgba(227, 166, 74, 0.36);
background: linear-gradient(90deg, var(--accent-soft), transparent 46%); background: linear-gradient(90deg, var(--accent-soft), transparent 46%);
@@ -2349,10 +2348,12 @@ body {
gap: 6px; gap: 6px;
} }
.proj-wt-row { .proj-wt-row {
display: flex; /* w6: two-line card — content column, then the actions rail pinned right. */
display: grid;
grid-template-columns: 1fr auto;
align-items: center; align-items: center;
gap: 10px; gap: 14px;
padding: 9px 12px; padding: 11px 14px;
background: var(--surface-2); background: var(--surface-2);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;

View File

@@ -217,14 +217,44 @@ describe('renderGitLog — unpushed count beside the heading (w6 design)', () =>
return { hash, at: Date.now(), subject, ...(unpushed === true ? { unpushed: true } : {}) } return { hash, at: Date.now(), subject, ...(unpushed === true ? { unpushed: true } : {}) }
} }
it('states the count so it is readable before scanning the rows', () => { it('puts the count ON the heading when one is supplied, not on its own line', () => {
const host = document.createElement('div') const host = document.createElement('div')
renderGitLog(host, { const label = document.createElement('div')
label.textContent = 'Recent commits'
renderGitLog(
host,
{
commits: [entry('a1', 'x', true), entry('b2', 'y', true), entry('c3', 'z')], commits: [entry('a1', 'x', true), entry('b2', 'y', true), entry('c3', 'z')],
truncated: false, truncated: false,
upstream: 'origin/develop', upstream: 'origin/develop',
},
label,
)
expect(label.querySelector('.proj-commitlog-count')!.textContent).toContain('2')
expect(host.querySelector('.proj-commitlog-count')).toBeNull()
}) })
expect(host.querySelector('.proj-commitlog-count')!.textContent).toContain('2')
it('does not stack duplicate counts across re-renders', () => {
const host = document.createElement('div')
const label = document.createElement('div')
const log = {
commits: [entry('a1', 'x', true)],
truncated: false,
upstream: 'origin/develop',
}
renderGitLog(host, log, label)
renderGitLog(host, log, label)
expect(label.querySelectorAll('.proj-commitlog-count').length).toBe(1)
})
it('falls back to the list container when no heading is given', () => {
const host = document.createElement('div')
renderGitLog(host, {
commits: [entry('a1', 'x', true)],
truncated: false,
upstream: 'origin/develop',
})
expect(host.querySelector('.proj-commitlog-count')).not.toBeNull()
}) })
it('omits the count when nothing is unpushed', () => { it('omits the count when nothing is unpushed', () => {