Merge panel-fidelity-leftovers: last four design gaps + readable projects.ts diffs
This commit is contained in:
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# public/projects.ts embeds literal NUL bytes on purpose: the namespace-group
|
||||||
|
# sentinels ACTIVE_GROUP_KEY = '\0active' and OTHER_GROUP_KEY = '\0other' are
|
||||||
|
# prefixed with NUL so they can never collide with a real `First.Second` key.
|
||||||
|
#
|
||||||
|
# git classifies any file containing NUL as binary, so `git diff`/`git show`
|
||||||
|
# print "Binary files differ" and review of this file is impossible. The `diff`
|
||||||
|
# attribute tells git to diff it as text anyway. It does NOT set `text`, so
|
||||||
|
# nothing is line-ending-normalised and the bytes on disk are untouched.
|
||||||
|
#
|
||||||
|
# Same trap in the shell: `grep` prints only "Binary file ... matches" and looks
|
||||||
|
# like a no-match. Use `grep -a` on this file.
|
||||||
|
public/projects.ts diff
|
||||||
Binary file not shown.
@@ -1541,13 +1541,17 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Branch chip */
|
/* Branch chip — same geometry and border as every other chip in the panel. It was
|
||||||
|
the odd one out: no border, no mono face, and 1px tighter all round, so the
|
||||||
|
detail header's branch did not match the branch chips in the worktree list. */
|
||||||
.proj-branch {
|
.proj-branch {
|
||||||
font-size: 11px;
|
font-family: var(--mono-font);
|
||||||
|
font-size: 11.5px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
border-radius: 5px;
|
border: 1px solid rgba(227, 166, 74, 0.36);
|
||||||
padding: 2px 7px;
|
border-radius: 6px;
|
||||||
|
padding: 3px 9px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
@@ -1806,18 +1810,22 @@ body {
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The design has exactly two button roles: filled accent for the action that
|
/* Role 2 of 2 — the ghost. The design has exactly two button roles: filled accent
|
||||||
commits a form (Fetch, Create Worktree), and this ghost for anything that only
|
for the action that commits a form (Fetch, Create Worktree), and this outline for
|
||||||
opens or reveals. Everything in a worktree's action rail is a ghost — including
|
anything that only opens, reveals or toggles. ONE geometry for every one of them:
|
||||||
the ✕, which previously had no rule at all and rendered as a raw grey OS button. */
|
the role had drifted to three different sizes across the panel, all at
|
||||||
|
full-strength --text instead of the muted label the design asks for.
|
||||||
|
Every outline button in the panel is listed here, and nowhere else, so a new one
|
||||||
|
cannot quietly pick its own size again. */
|
||||||
.proj-wt-open,
|
.proj-wt-open,
|
||||||
.proj-wt-remove,
|
.proj-wt-remove,
|
||||||
.proj-wt-prune {
|
.proj-wt-prune,
|
||||||
|
.proj-diff-toggle {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 11.5px;
|
font-size: 12.5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px 12px;
|
padding: 7px 15px;
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
@@ -1825,24 +1833,27 @@ body {
|
|||||||
|
|
||||||
.proj-wt-open:hover,
|
.proj-wt-open:hover,
|
||||||
.proj-wt-remove:hover,
|
.proj-wt-remove:hover,
|
||||||
.proj-wt-prune:hover {
|
.proj-wt-prune:hover,
|
||||||
|
.proj-diff-toggle:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.proj-wt-open:focus-visible,
|
.proj-wt-open:focus-visible,
|
||||||
.proj-wt-remove:focus-visible,
|
.proj-wt-remove:focus-visible,
|
||||||
.proj-wt-prune:focus-visible {
|
.proj-wt-prune:focus-visible,
|
||||||
|
.proj-diff-toggle:focus-visible {
|
||||||
outline: 2px solid var(--text);
|
outline: 2px solid var(--text);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Square icon variant — same role, just not a word. Height matches Open so the
|
/* Square icon variant — same role, just not a word. Keeps the shared height
|
||||||
two sit flush in the rail. */
|
(12.5px line + 7px padding twice) so it sits flush beside Open in the rail
|
||||||
|
rather than stretching the row. */
|
||||||
.proj-wt-remove {
|
.proj-wt-remove {
|
||||||
width: 26px;
|
width: 33px;
|
||||||
padding: 0;
|
padding: 7px 0;
|
||||||
line-height: 24px;
|
line-height: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
@@ -2513,26 +2524,12 @@ body {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Role 2 — ghost, for anything that only reveals or toggles. */
|
/* Role 2 (the ghost, incl. .proj-diff-toggle) is defined once, with the worktree
|
||||||
.proj-diff-toggle {
|
action rail — see "Role 2 of 2" above. Nothing for it here on purpose: a second
|
||||||
font: inherit;
|
block at equal specificity is how the role drifted to three sizes before. */
|
||||||
font-size: 12.5px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 7px 15px;
|
|
||||||
border-radius: 7px;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.proj-diff-toggle:hover {
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.proj-wt-submit:focus-visible,
|
.proj-wt-submit:focus-visible,
|
||||||
.proj-fanout-submit:focus-visible,
|
.proj-fanout-submit:focus-visible {
|
||||||
.proj-diff-toggle:focus-visible {
|
|
||||||
outline: 2px solid var(--text);
|
outline: 2px solid var(--text);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,14 @@ async function readSyncState(repoPath: string): Promise<SyncState> {
|
|||||||
if (counts.ahead !== undefined) state.ahead = counts.ahead
|
if (counts.ahead !== undefined) state.ahead = counts.ahead
|
||||||
if (counts.behind !== undefined) state.behind = counts.behind
|
if (counts.behind !== undefined) state.behind = counts.behind
|
||||||
if (lastFetchMs !== undefined) state.lastFetchMs = lastFetchMs
|
if (lastFetchMs !== undefined) state.lastFetchMs = lastFetchMs
|
||||||
if (headText !== null && parseGitHead(headText) === null) state.detached = true
|
if (headText !== null && parseGitHead(headText) === null) {
|
||||||
|
state.detached = true
|
||||||
|
// A detached HEAD file holds the raw object id and nothing else. It is the
|
||||||
|
// only identity the cell can show — there is no branch name — and it costs
|
||||||
|
// no extra I/O, since the file is already read above for the detached test.
|
||||||
|
const sha = headText.trim()
|
||||||
|
if (/^[0-9a-f]{7,40}$/i.test(sha)) state.head = sha.slice(0, 7)
|
||||||
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ export interface SyncState {
|
|||||||
behind?: number; // commits on @{u} not on HEAD — trust only with a fresh lastFetchMs
|
behind?: number; // commits on @{u} not on HEAD — trust only with a fresh lastFetchMs
|
||||||
lastFetchMs?: number; // FETCH_HEAD mtime; undefined ⇒ never fetched
|
lastFetchMs?: number; // FETCH_HEAD mtime; undefined ⇒ never fetched
|
||||||
detached?: boolean; // HEAD is not on a branch ⇒ no branch, no ahead/behind
|
detached?: boolean; // HEAD is not on a branch ⇒ no branch, no ahead/behind
|
||||||
|
head?: string; // short sha, ONLY when detached — the cell's sole identity
|
||||||
}
|
}
|
||||||
|
|
||||||
/** w6/G7: the git state of ONE worktree, fetched lazily per row.
|
/** w6/G7: the git state of ONE worktree, fetched lazily per row.
|
||||||
|
|||||||
@@ -799,6 +799,18 @@ describe('makeSyncBand (w6 G3)', () => {
|
|||||||
expect(btn.disabled).toBe(true)
|
expect(btn.disabled).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows the short sha on a detached HEAD — it is the only identity left', () => {
|
||||||
|
const b = band({ detached: true, head: 'deadbee' })!
|
||||||
|
expect(b.querySelector('.proj-sync-upstream')!.textContent).toBe('deadbee')
|
||||||
|
expect(b.querySelector('.proj-sync-detached')!.textContent).toBe('detached HEAD')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still renders the detached chip when the sha could not be read', () => {
|
||||||
|
const b = band({ detached: true })!
|
||||||
|
expect(b.querySelector('.proj-sync-upstream')).toBeNull()
|
||||||
|
expect(b.querySelector('.proj-sync-detached')).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('names the upstream so the arrows are not floating numbers', () => {
|
it('names the upstream so the arrows are not floating numbers', () => {
|
||||||
const b = band({ upstream: 'origin/develop', ahead: 9, behind: 0, lastFetchMs: FRESH })!
|
const b = band({ upstream: 'origin/develop', ahead: 9, behind: 0, lastFetchMs: FRESH })!
|
||||||
expect(b.textContent).toContain('origin/develop')
|
expect(b.textContent).toContain('origin/develop')
|
||||||
|
|||||||
@@ -662,6 +662,21 @@ describe('buildProjectDetail — sync state (w6 G1)', { timeout: 30_000 }, () =>
|
|||||||
expect(d!.branch).toBeUndefined()
|
expect(d!.branch).toBeUndefined()
|
||||||
expect(d!.sync!.ahead).toBeUndefined()
|
expect(d!.sync!.ahead).toBeUndefined()
|
||||||
expect(d!.sync!.behind).toBeUndefined()
|
expect(d!.sync!.behind).toBeUndefined()
|
||||||
|
// With no branch name the short sha is the only identity the panel can show.
|
||||||
|
// Read out of the .git/HEAD text already loaded for the detached test — no
|
||||||
|
// extra git call.
|
||||||
|
expect(d!.sync!.head).toBe(stdout.trim().slice(0, 7))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not report a head sha when HEAD is on a branch', async () => {
|
||||||
|
if (!(await gitAvailable())) return
|
||||||
|
const repo = path.join(tmp, 'g1-attached-head')
|
||||||
|
await initRepo(repo)
|
||||||
|
await commit(repo, 'a.txt', 'only')
|
||||||
|
|
||||||
|
const d = await buildProjectDetail(makeCfg({}), repo, [])
|
||||||
|
expect(d!.sync!.detached).not.toBe(true)
|
||||||
|
expect(d!.sync!.head).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('counts dirty files instead of only flagging them', async () => {
|
it('counts dirty files instead of only flagging them', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user