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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
font-size: 11px;
|
||||
font-family: var(--mono-font);
|
||||
font-size: 11.5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 5px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid rgba(227, 166, 74, 0.36);
|
||||
border-radius: 6px;
|
||||
padding: 3px 9px;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
@@ -1806,18 +1810,22 @@ body {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* The design has exactly two button roles: filled accent for the action that
|
||||
commits a form (Fetch, Create Worktree), and this ghost for anything that only
|
||||
opens or reveals. Everything in a worktree's action rail is a ghost — including
|
||||
the ✕, which previously had no rule at all and rendered as a raw grey OS button. */
|
||||
/* Role 2 of 2 — the ghost. The design has exactly two button roles: filled accent
|
||||
for the action that commits a form (Fetch, Create Worktree), and this outline for
|
||||
anything that only opens, reveals or toggles. ONE geometry for every one of them:
|
||||
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-remove,
|
||||
.proj-wt-prune {
|
||||
.proj-wt-prune,
|
||||
.proj-diff-toggle {
|
||||
font: inherit;
|
||||
font-size: 11.5px;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
padding: 7px 15px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
@@ -1825,24 +1833,27 @@ body {
|
||||
|
||||
.proj-wt-open:hover,
|
||||
.proj-wt-remove:hover,
|
||||
.proj-wt-prune:hover {
|
||||
.proj-wt-prune:hover,
|
||||
.proj-diff-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.proj-wt-open: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-offset: 2px;
|
||||
}
|
||||
|
||||
/* Square icon variant — same role, just not a word. Height matches Open so the
|
||||
two sit flush in the rail. */
|
||||
/* Square icon variant — same role, just not a word. Keeps the shared height
|
||||
(12.5px line + 7px padding twice) so it sits flush beside Open in the rail
|
||||
rather than stretching the row. */
|
||||
.proj-wt-remove {
|
||||
width: 26px;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
width: 33px;
|
||||
padding: 7px 0;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
flex: none;
|
||||
}
|
||||
@@ -2513,26 +2524,12 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Role 2 — ghost, for anything that only reveals or toggles. */
|
||||
.proj-diff-toggle {
|
||||
font: inherit;
|
||||
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);
|
||||
}
|
||||
/* Role 2 (the ghost, incl. .proj-diff-toggle) is defined once, with the worktree
|
||||
action rail — see "Role 2 of 2" above. Nothing for it here on purpose: a second
|
||||
block at equal specificity is how the role drifted to three sizes before. */
|
||||
|
||||
.proj-wt-submit:focus-visible,
|
||||
.proj-fanout-submit:focus-visible,
|
||||
.proj-diff-toggle:focus-visible {
|
||||
.proj-fanout-submit:focus-visible {
|
||||
outline: 2px solid var(--text);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -209,7 +209,14 @@ async function readSyncState(repoPath: string): Promise<SyncState> {
|
||||
if (counts.ahead !== undefined) state.ahead = counts.ahead
|
||||
if (counts.behind !== undefined) state.behind = counts.behind
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -395,6 +395,7 @@ export interface SyncState {
|
||||
behind?: number; // commits on @{u} not on HEAD — trust only with a fresh lastFetchMs
|
||||
lastFetchMs?: number; // FETCH_HEAD mtime; undefined ⇒ never fetched
|
||||
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.
|
||||
|
||||
@@ -799,6 +799,18 @@ describe('makeSyncBand (w6 G3)', () => {
|
||||
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', () => {
|
||||
const b = band({ upstream: 'origin/develop', ahead: 9, behind: 0, lastFetchMs: FRESH })!
|
||||
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!.sync!.ahead).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 () => {
|
||||
|
||||
Reference in New Issue
Block a user