diff --git a/README.md b/README.md index 8ffeed0..966ad1a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ **[Roadmap](./ROADMAP.md)** · **[Commercial license](./COMMERCIAL.md)** +The website includes a keyboard-operable, resizable demo of Strand's current +Work, Local Changes, Review, Pull Requests, and All Commits surfaces. + Keyboard-first: almost every action is operable from the keyboard alone — never keyboard-only, the mouse stays first-class. Shortcuts are listed below and **every global one is rebindable** in Settings → Keyboard. diff --git a/ROADMAP.md b/ROADMAP.md index 1343da2..5db6e38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2442,6 +2442,16 @@ error reporting. Selective-stash messages now have explicit regression coverage. The fixes retain snapshot-driven refreshes rather than adding polling or recursive scans to hot paths. +**Website app replica synchronized (2026-07-29):** The landing-page demo now +matches the current Strand shell instead of the earlier three-view snapshot: +Workspaces and network options are present, Pull Requests joins the primary +sidebar, and Work, Local Changes, Review, Pull Requests, and All Commits all +run in place. Work file/terminal tabs, Git/Files navigation, +repository/workspace Review scope, PR selection, review notes, baseline and +file actions, command-palette destinations, plus pointer and keyboard pane +resizing are interactive. Desktop and 900/680 px browser passes found no +runtime warnings or horizontal overflow. + --- ## Cross-cutting tracks (run in parallel with all milestones) diff --git a/TASKS.md b/TASKS.md index 462b4b3..8312b7d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2095,7 +2095,11 @@ quick-wins from that audit already landed (see ROADMAP changelog). `og-image.svg` source and rendered `og-image.png`, with Open Graph + Twitter metadata added 2026-07-18. The 2026-07-26 polish pass replaced the generic download buttons with responsive Apple/Windows/Linux release cards and - platform-aware highlighting while retaining the releases-page fallback.) + platform-aware highlighting while retaining the releases-page fallback. + The 2026-07-29 app-replica sync added current Workspaces/network/sidebar + chrome, all five primary views, Work file/terminal tabs, pull-request + selection, repository/workspace Review scope, notes/baseline/file actions, + and pointer + keyboard pane resizing; desktop/narrow browser QA is clean.) - ☑ User-guide docs on the website (2026-07-08: `website/docs/` — nine fact-checked markdown pages + `manifest.json`, rendered client-side by `docs/index.html`/`docs.js` with vendored `marked.min.js`; no build step — diff --git a/website/README.md b/website/README.md index 9cfeaeb..d20e7c5 100644 --- a/website/README.md +++ b/website/README.md @@ -21,14 +21,16 @@ Preview locally with `pnpm site` from the repo root (serves on choices visible. - `fonts/` — Geist + JetBrains Mono woff2, copied from `ui/public/fonts` (self-hosted, latin subsets only). -- The hero window is a replica of the actual app shell (topbar / sidebar / - Review toolbar / queue tree / diff / statusbar), built to the real metrics +- The hero window is a replica of the actual app shell (workspace/repo tabs, + network and stash controls, sidebar, Work tabs, Review toolbar and queue, + pull requests, commit graph, diff, and statusbar), built to the real metrics in `ui/src/styles/chrome.css` and `features.css`, with syntax colors taken from the app's `pierre-dark` Shiki theme (`@pierre/theme`). It's a working - demo: the sidebar switches Local Changes / Review / All Commits views, tree - folders collapse, `j`/`k`/`space` drive the queue (and the commit graph), - and the commit bar "commits". If the app's chrome changes materially, - re-sync the mock against fresh screenshots. + demo: all five primary sidebar destinations switch in place; file/terminal + tabs, Git/Files, repository/workspace Review scope, PR selection, file + actions, review notes, baselines, tree folders, keyboard navigation, commit + form, and both pane resizers respond like their app counterparts. If the + app's chrome changes materially, re-sync the mock against fresh screenshots. - ⌘K opens a page-level clone of the app's command palette (same grouped / fuzzy / highlighted UI): it scrolls to page sections, switches the demo views, sets the accent, and opens GitHub / X. Items live in `ITEMS` in diff --git a/website/index.html b/website/index.html index 8fbf575..ca14412 100644 --- a/website/index.html +++ b/website/index.html @@ -106,37 +106,44 @@

Agents write the code.
Someone has to review it.
-
+
- acme/api - + + + + - - 0 - 3 + + + + - Stash - + + - feature/auth-retry - Quick Launch⌘K + +
+
acme/apiReview· 8 unstaged files - - - - - + + + + + + + + +
+ +
+
+ + + +
+
+
src/auth/session.ts
+
import { getToken } from './token'
+import { withRetry } from './retry'
+
+export async function getSession(url: string) {
+  const token = await getToken()
+  const headers = { Authorization: `Bearer ${token}` }
+  const res = await withRetry(() => fetch(url, { headers }))
+  return res.json()
+}
+
+
+

acme/api on feature/auth-retry

+

pnpm test

+

✓ 18 tests passed in 412ms

+

+
+
+ +
+
+
Open pull requests
+ + + +
+
+
OpenRetry transient auth failures
+

#142 by dana · feature/auth-retry into main

+
All checks passed3 checks
+
8 files changed+41−12
+
aifeat(auth): retry transient session failures9c4e7a1
+
test: cover retry exhaustion + backoff timing41d2c9f
+
+
+
Unstaged changes 4/8 reviewed - Discard unreviewed (4) - Pin baseline at HEAD + + +
- Unstaged8 - Stage all + Unstaged8 + - Staged0 + Staged0
@@ -209,19 +266,23 @@

Agents write the code.
Someone has to review it.

+
src/auth/session.ts -2+6 - Note - Stage - Discard - Mark reviewed + + + +
+
-
✎ L13surface the retry count in the error message×
+
✎ L13surface the retry count in the error message
11import { getToken, refreshToken } from './token'
2import { withRetry } from './retry'
23
@@ -345,7 +406,7 @@

Agents write the code.
Someone has to review it. - Commit⌘↵ +

@@ -363,6 +424,7 @@

Agents write the code.
Someone has to review it.

+
feature/auth-retry 3↑ 0↓ diff --git a/website/script.js b/website/script.js index 6aea96b..5ba8b30 100644 --- a/website/script.js +++ b/website/script.js @@ -1,5 +1,5 @@ /* Strand landing — reveals, accent switcher, the interactive app mock - * (Local Changes / Review / All Commits views, collapsible tree), and the + * (Work / Local Changes / Review / Pull Requests / All Commits), and the * ⌘K palette repurposed to drive this page. No dependencies; degrades to a * static page without JS. */ (() => { @@ -111,10 +111,30 @@ const barText = $('#bar-text'); const markBtn = $('#mark-btn'); const discardLink = $('#discard-link'); + const mockToast = $('#mock-toast'); let cur = rows.findIndex((r) => r.classList.contains('sel')); if (cur < 0) cur = 0; let hovering = false; let lastNav = 0; // space is only hijacked right after j/k or while hovering + let toastTimer = 0; + let sessionMode = false; + let discardArmed = false; + let fileDiscardArmed = false; + + function showDemoToast(message) { + clearTimeout(toastTimer); + mockToast.textContent = message; + mockToast.hidden = false; + toastTimer = setTimeout(() => { + mockToast.hidden = true; + }, 1800); + } + + function setStagedCounts(staged) { + $('#lc-unstaged-count').textContent = rows.length - staged; + $('#lc-staged-count').textContent = staged; + $('#sb-counts').textContent = `${rows.length - staged} modified · ${staged} staged`; + } mock.addEventListener('pointerenter', () => (hovering = true)); mock.addEventListener('pointerleave', () => (hovering = false)); @@ -149,6 +169,8 @@ markBtn.classList.toggle('on', row.classList.contains('done')); markBtn.lastChild.textContent = row.classList.contains('done') ? 'Reviewed' : 'Mark reviewed'; $('#d-scroll').scrollTop = 0; + fileDiscardArmed = false; + $('#discard-file-btn').textContent = 'Discard'; } // j/k walks only the rows whose folders are expanded @@ -202,6 +224,128 @@ lastNav = Date.now(); }); + /* Review actions use the same two-step destructive affordance as the app. */ + $('#copy-feedback').addEventListener('click', async () => { + const feedback = `Review feedback for acme/api\n\n- ${rows[cur].dataset.path}: surface the retry count in the error message`; + try { + await navigator.clipboard.writeText(feedback); + showDemoToast('Copied feedback prompt'); + } catch { + showDemoToast('Feedback ready to copy'); + } + }); + $('#baseline-link').addEventListener('click', () => { + sessionMode = !sessionMode; + $('.rv-chip', mock).lastChild.textContent = sessionMode ? 'Session since 9c4e7a1 · now' : 'Unstaged changes'; + $('#baseline-link').textContent = sessionMode ? 'Clear baseline' : 'Pin baseline at HEAD'; + showDemoToast(sessionMode ? 'Baseline pinned at HEAD' : 'Baseline cleared'); + }); + discardLink.addEventListener('click', () => { + if (!discardArmed) { + discardArmed = true; + discardLink.textContent = `Really discard ${rows.filter((r) => !r.classList.contains('done')).length} files?`; + setTimeout(() => { + discardArmed = false; + render(); + }, 3000); + return; + } + discardArmed = false; + showDemoToast('Unreviewed files safety-stashed, then discarded'); + render(); + }); + $('#stage-btn').addEventListener('click', () => { + const row = rows[cur]; + row.classList.add('staged'); + const status = $('.st', row); + status.textContent = 'S'; + status.className = 'st s'; + const staged = rows.filter((r) => r.classList.contains('staged')).length; + setStagedCounts(staged); + showDemoToast(`Staged ${row.dataset.path}`); + }); + $('#stage-all').addEventListener('click', () => { + rows.forEach((row) => { + row.classList.add('staged'); + const status = $('.st', row); + status.textContent = 'S'; + status.className = 'st s'; + }); + setStagedCounts(rows.length); + showDemoToast(`Staged all ${rows.length} files`); + }); + $('#discard-file-btn').addEventListener('click', () => { + if (!fileDiscardArmed) { + fileDiscardArmed = true; + $('#discard-file-btn').textContent = 'Really discard?'; + setTimeout(() => { + fileDiscardArmed = false; + $('#discard-file-btn').textContent = 'Discard'; + }, 2500); + return; + } + fileDiscardArmed = false; + $('#discard-file-btn').textContent = 'Discard'; + showDemoToast(`${rows[cur].dataset.path} safety-stashed, then discarded`); + }); + + const noteEditor = $('#note-editor'); + const noteInput = $('#note-input'); + $('#note-btn').addEventListener('click', () => { + noteEditor.hidden = false; + noteInput.focus(); + }); + function closeNoteEditor() { + noteInput.value = ''; + noteEditor.hidden = true; + } + noteEditor.addEventListener('submit', (e) => { + e.preventDefault(); + const text = noteInput.value.trim(); + if (!text) return; + const shown = diffs.find((d) => d.classList.contains('on')); + let notes = $('.dnotes', shown); + if (!notes) { + notes = document.createElement('div'); + notes.className = 'dnotes'; + shown.prepend(notes); + } + const note = document.createElement('span'); + note.className = 'dnote'; + note.innerHTML = `✎ file${esc(text)}`; + notes.append(note); + let count = $('.q-note', rows[cur]); + if (!count) { + count = document.createElement('span'); + count.className = 'q-note'; + rows[cur].insertBefore(count, $('.st', rows[cur])); + } + count.textContent = `✎${notes.children.length}`; + $('#copy-feedback').textContent = `Copy feedback (${ $$('.dnote', mock).length })`; + closeNoteEditor(); + showDemoToast('Review note added'); + }); + noteInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + noteEditor.requestSubmit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + closeNoteEditor(); + } + }); + $('.d-scroll', mock).addEventListener('click', (e) => { + if (e.target.classList.contains('x')) { + const shown = e.target.closest('.mockdiff'); + e.target.closest('.dnote')?.remove(); + const notes = shown ? $$('.dnote', shown) : []; + const count = $('.q-note', rows[cur]); + if (count && notes.length === 0) count.remove(); + else if (count) count.textContent = `✎${notes.length}`; + $('#copy-feedback').textContent = `Copy feedback (${ $$('.dnote', mock).length })`; + } + }); + /* folders collapse like the app's Pierre tree */ $$('.q-dir', mock).forEach((dir) => { dir.addEventListener('click', () => { @@ -211,10 +355,12 @@ }); }); - /* ── View switching: Local Changes / Review / All Commits ── */ + /* ── View switching: every current primary app destination ── */ const VIEWS = { + work: ['Work', '· 2 open tabs'], local: ['Local Changes', '· 8 files with changes'], review: ['Review', '· 8 unstaged files'], + 'pull-requests': ['Pull Requests', '· 3 open'], commits: ['All Commits', '· feature/auth-retry'], }; const crumbLeaf = $('#crumb-leaf'); @@ -227,6 +373,126 @@ } $$('.side-row[data-view]', mock).forEach((r) => r.addEventListener('click', () => setView(r.dataset.view))); + /* Repository / Workspace Review lens toggle. */ + $$('.seg [data-scope]', mock).forEach((button) => { + button.addEventListener('click', () => { + const scope = button.dataset.scope; + mock.dataset.scope = scope; + $$('.seg [data-scope]', mock).forEach((b) => { + const on = b === button; + b.classList.toggle('on', on); + b.setAttribute('aria-pressed', String(on)); + }); + crumbLeaf.textContent = scope === 'workspace' ? 'Workspace Review' : 'Review'; + crumbNote.textContent = scope === 'workspace' ? '· 2 repos + 1 worktree · 8 files to review' : '· 8 unstaged files'; + $('.rv-chip', mock).lastChild.textContent = scope === 'workspace' ? 'Workspace changes' : (sessionMode ? 'Session since 9c4e7a1 · now' : 'Unstaged changes'); + }); + }); + + /* Stacked/split controls share the app's selected-state contract. */ + $$('[data-diff-mode]', mock).forEach((button) => { + button.addEventListener('click', () => { + mock.dataset.diff = button.dataset.diffMode; + $$('[data-diff-mode]', mock).forEach((b) => b.classList.toggle('on', b === button)); + showDemoToast(button.dataset.diffMode === 'split' ? 'Split diff view' : 'Stacked diff view'); + }); + }); + + /* Work file/terminal peer tabs. */ + $$('.work-tab', mock).forEach((button) => { + button.addEventListener('click', () => { + $$('.work-tab', mock).forEach((b) => { + const on = b === button; + b.classList.toggle('active', on); + b.setAttribute('aria-selected', String(on)); + }); + $$('.work-pane', mock).forEach((pane) => pane.classList.toggle('on', pane.dataset.workPane === button.dataset.workTab)); + }); + }); + $('.work-add', mock).addEventListener('click', () => { + const terminalTab = $('.work-tab[data-work-tab="terminal"]', mock); + terminalTab.click(); + showDemoToast('Terminal ready'); + }); + + /* Pull request list/detail selection. */ + const prData = { + 142: ['Retry transient auth failures', '#142 by dana · feature/auth-retry into main'], + 139: ['Reduce token-cache allocations', '#139 by sam · fix/token-cache into main'], + 136: ['Update API examples', '#136 by alex · docs/api into main'], + }; + $$('.pr-row', mock).forEach((row) => { + row.addEventListener('click', () => { + $$('.pr-row', mock).forEach((r) => { + const on = r === row; + r.classList.toggle('active', on); + r.setAttribute('aria-selected', String(on)); + }); + $('#pr-title').textContent = prData[row.dataset.pr][0]; + $('#pr-meta').textContent = prData[row.dataset.pr][1]; + }); + }); + + /* Git/Files sidebar tabs now switch real content, like the app. */ + const sideScroll = $('.side-scroll', mock); + const sideFilter = $('.side-filter', mock); + const gitSidebar = sideScroll.innerHTML; + const filesSidebar = ` +
Files8
+ + + + + `; + $$('.side-tab', mock).forEach((button) => { + button.addEventListener('click', () => { + $$('.side-tab', mock).forEach((b) => b.classList.toggle('on', b === button)); + const files = button.dataset.side === 'files'; + sideScroll.innerHTML = files ? filesSidebar : gitSidebar; + sideFilter.lastChild.textContent = files ? 'Filter files…' : 'Filter branches, tags…'; + }); + }); + sideScroll.addEventListener('click', (e) => { + const file = e.target.closest('[data-file]'); + if (!file) return; + setView('work'); + $('.work-file-tools span', mock).textContent = file.dataset.file; + }); + + /* Pointer + keyboard resizing mirrors the app's resizable pane contract. */ + function installResize(handle, cssVar, min, max, origin) { + let start = null; + const set = (value) => mock.style.setProperty(cssVar, `${Math.max(min, Math.min(max, value))}px`); + handle.addEventListener('pointerdown', (e) => { + start = { x: e.clientX, value: parseFloat(getComputedStyle(mock).getPropertyValue(cssVar)) || origin }; + handle.classList.add('dragging'); + handle.setPointerCapture?.(e.pointerId); + e.preventDefault(); + }); + window.addEventListener('pointermove', (e) => { + if (!start) return; + set(start.value + e.clientX - start.x); + }); + window.addEventListener('pointerup', () => { + if (!start) return; + start = null; + handle.classList.remove('dragging'); + }); + handle.addEventListener('keydown', (e) => { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; + e.preventDefault(); + const value = parseFloat(getComputedStyle(mock).getPropertyValue(cssVar)) || origin; + set(value + (e.key === 'ArrowRight' ? 12 : -12)); + }); + } + installResize($('.body-resize', mock), '--mock-side', 164, 360, 212); + installResize($('.panes-resize', mock), '--mock-tree', 172, 420, 248); + + /* Visible chrome actions acknowledge the interaction instead of being inert. */ + $$('[data-demo-action]', mock).forEach((button) => { + button.addEventListener('click', () => showDemoToast(button.dataset.demoAction)); + }); + /* commit graph rows: click or j/k selects */ const commitsBox = $('.commits', mock); const cRows = $$('.c-row', mock); @@ -282,7 +548,9 @@ { g: 'Go to', icon: 'down', label: 'Download Strand', kw: 'install dmg msi appimage mac windows linux', run: () => goTo('#download') }, { g: 'Go to', icon: 'goto', label: 'Docs — the user guide', kw: 'documentation manual help guide', run: () => { location.href = 'docs/'; } }, { g: 'Demo', icon: 'demo', label: 'Show: Local Changes', kw: 'staging stage view demo', run: () => showDemo('local') }, + { g: 'Demo', icon: 'demo', label: 'Show: Work', kw: 'file terminal editor view demo', run: () => showDemo('work') }, { g: 'Demo', icon: 'demo', label: 'Show: Review', kw: 'queue view demo', run: () => showDemo('review') }, + { g: 'Demo', icon: 'demo', label: 'Show: Pull Requests', kw: 'github azure pr review view demo', run: () => showDemo('pull-requests') }, { g: 'Demo', icon: 'demo', label: 'Show: All Commits', kw: 'graph log history view demo', run: () => showDemo('commits') }, { g: 'Links', icon: 'link', label: 'View source on GitHub', meta: 'github.com ↗', kw: 'repo code source', run: () => openExt('https://github.com/danielss-dev/strand') }, { g: 'Links', icon: 'link', label: 'Follow @danielss_dev on X', meta: 'x.com ↗', kw: 'twitter social', run: () => openExt('https://x.com/danielss_dev') }, @@ -416,13 +684,16 @@ const t = e.target; if (t && /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(t.tagName)) return; const inCommits = mock.dataset.view === 'commits'; + const inQueue = mock.dataset.view === 'review' || mock.dataset.view === 'local'; if (e.key === 'j') { - inCommits ? moveCommit(1) : move(1); + if (inCommits) moveCommit(1); + else if (inQueue) move(1); flash('j'); } else if (e.key === 'k') { - inCommits ? moveCommit(-1) : move(-1); + if (inCommits) moveCommit(-1); + else if (inQueue) move(-1); flash('k'); - } else if (e.key === ' ' && !inCommits && (hovering || Date.now() - lastNav < 5000)) { + } else if (e.key === ' ' && inQueue && (hovering || Date.now() - lastNav < 5000)) { e.preventDefault(); // don't page-scroll while "in" the mock toggleReviewed(); flash('space'); diff --git a/website/style.css b/website/style.css index 1ce32a6..9c709cf 100644 --- a/website/style.css +++ b/website/style.css @@ -61,7 +61,7 @@ /* ─── Base ─── */ *, *::before, *::after { box-sizing: border-box; } -html { scroll-behavior: smooth; } +html { scroll-behavior: smooth; overflow-x: clip; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } body { @@ -222,6 +222,8 @@ p { margin: 0; } .mock { position: relative; z-index: 1; max-width: 1080px; margin: 0 auto; + --mock-side: 212px; + --mock-tree: 248px; background: var(--bg-base); border: 1px solid var(--border); border-radius: var(--r-lg); @@ -233,6 +235,12 @@ p { margin: 0; } cursor: default; } .mock svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +.mock button { + appearance: none; + border: 0; + margin: 0; + font: inherit; +} .m-spacer { flex: 1; } /* topbar (44px, panel bg — chrome.css .topbar) */ @@ -243,14 +251,24 @@ p { margin: 0; } .traffic { display: flex; gap: 8px; padding: 0 4px 0 2px; } .tdot { width: 12px; height: 12px; border-radius: 50%; } .tdot.c { background: #ff5f57; } .tdot.mi { background: #febc2e; } .tdot.mx { background: #28c840; } +.workspace-btn { + display: flex; align-items: center; gap: 6px; height: 26px; max-width: 148px; + padding: 0 8px; border-radius: 7px; color: var(--text-muted); + background: transparent; cursor: pointer; white-space: nowrap; +} +.workspace-btn:hover { background: var(--bg-hover); color: var(--text); } +.workspace-btn > svg { width: 13px; height: 13px; flex: none; } +.workspace-btn .bchev { width: 10px; height: 10px; } +.workspace-btn span { overflow: hidden; text-overflow: ellipsis; } .repo-tab { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px 0 10px; border-radius: 7px; background: var(--bg-elev); box-shadow: 0 0 0 0.5px var(--border-strong) inset; - font-size: 11px; font-weight: 500; color: var(--text); + font-size: 11px; font-weight: 500; color: var(--text); cursor: pointer; } .repo-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); } -.tab-add { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 6px; color: var(--text-muted); font-size: 15px; } +.tab-add { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 6px; color: var(--text-muted); background: transparent; font-size: 15px; cursor: pointer; } +.tab-add:hover { color: var(--text); background: var(--bg-hover); } .sync-group { display: flex; align-items: center; height: 28px; border-radius: 7px; background: var(--bg-elev); box-shadow: 0 0 0 0.5px var(--border) inset; overflow: hidden; @@ -258,8 +276,9 @@ p { margin: 0; } .sync-btn { display: flex; align-items: center; gap: 6px; padding: 0 10px; height: 100%; color: var(--text-2); font-size: 11px; font-weight: 500; - border-right: 0.5px solid var(--border); + border-right: 0.5px solid var(--border); background: transparent; cursor: pointer; } +.sync-btn:hover { background: var(--bg-hover); color: var(--text); } .sync-btn:last-child { border-right: 0; } .sync-btn svg { width: 13px; height: 13px; } .sync-btn.chev { padding: 0 6px; color: var(--text-muted); } @@ -267,29 +286,31 @@ p { margin: 0; } .branch-btn { display: flex; align-items: center; gap: 7px; padding: 0 10px; height: 28px; border-radius: 7px; background: var(--bg-elev); box-shadow: 0 0 0 0.5px var(--border) inset; - font-size: 11px; font-weight: 500; color: var(--text-2); white-space: nowrap; + font-size: 11px; font-weight: 500; color: var(--text-2); white-space: nowrap; cursor: pointer; } +.branch-btn:hover, .cmd-pill:hover { background: var(--bg-hover); color: var(--text); } .branch-btn svg { width: 13px; height: 13px; } .branch-btn .bchev { width: 11px; height: 11px; opacity: 0.7; } .cmd-pill { display: flex; align-items: center; gap: 8px; padding: 0 8px 0 10px; height: 28px; border-radius: 7px; background: var(--bg-elev); box-shadow: 0 0 0 0.5px var(--border) inset; - color: var(--text-muted); font-size: 11px; white-space: nowrap; + color: var(--text-muted); font-size: 11px; white-space: nowrap; cursor: pointer; } .cmd-pill svg { width: 12px; height: 12px; } .cmd-pill kbd { font-family: var(--font-mono); font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--bg-active); color: var(--text-2); } /* body: sidebar | main */ -.m-body { display: grid; grid-template-columns: 212px 1fr; min-height: 0; } +.m-body { display: grid; grid-template-columns: var(--mock-side) 5px minmax(0, 1fr); min-height: 0; } .m-side { - background: var(--bg-panel); border-right: 1px solid var(--border); + background: var(--bg-panel); display: flex; flex-direction: column; overflow: hidden; } .m-side-primary { padding: 6px 0 4px; border-bottom: 1px solid var(--border); } .side-row { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; margin: 0 6px; border-radius: 6px; - font-size: 12px; color: var(--text-2); white-space: nowrap; + width: calc(100% - 12px); background: transparent; + font-size: 12px; color: var(--text-2); white-space: nowrap; text-align: left; } .side-row.active { background: var(--bg-sel); color: var(--text); } .side-row .ico { width: 13px; height: 13px; flex: none; opacity: 0.75; } @@ -300,10 +321,11 @@ p { margin: 0; } font-family: var(--font-mono); font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 999px; background: var(--accent); color: var(--accent-fg); } +.pr-live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex: none; } .side-tabs { display: flex; padding: 8px 10px 6px; } .side-tab { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; - height: 28px; border-radius: 6px; color: var(--text-muted); + height: 28px; border-radius: 6px; color: var(--text-muted); background: transparent; font-size: 11px; font-weight: 500; position: relative; } .side-tab .ico { width: 12px; height: 12px; } @@ -340,8 +362,19 @@ p { margin: 0; } .crumb .dim { color: var(--text-dim); font-size: 11.5px; } .h-actions { margin-left: auto; display: flex; gap: 4px; flex: none; } .icon-btn { width: 26px; height: 26px; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); } +.icon-btn { background: transparent; cursor: pointer; } +.icon-btn:hover { background: var(--bg-hover); color: var(--text); } .icon-btn.on { background: var(--bg-active); color: var(--text); } .icon-btn svg { width: 13px; height: 13px; } +.seg { + display: inline-flex; align-items: center; padding: 2px; border-radius: 7px; + background: var(--bg-elev); box-shadow: 0 0 0 0.5px var(--border) inset; +} +.seg button { + height: 22px; padding: 0 8px; border-radius: 5px; + color: var(--text-muted); background: transparent; font-size: 10.5px; cursor: pointer; +} +.seg button.on { color: var(--text); background: var(--bg-active); } /* review toolbar (features.css .rv-toolbar) */ .rv-toolbar { @@ -358,13 +391,22 @@ p { margin: 0; } .rv-bar { display: block; width: 90px; height: 4px; border-radius: 2px; background: var(--bg-base); overflow: hidden; } .rv-bar i { display: block; height: 100%; width: 50%; background: var(--add); transition: width 0.16s ease; } .rv-actions { display: flex; align-items: center; gap: 12px; white-space: nowrap; } -.h-link { font-size: 11px; font-weight: 500; color: var(--accent); cursor: pointer; } +.h-link { padding: 0; background: transparent; font-size: 11px; font-weight: 500; color: var(--accent); cursor: pointer; } .h-link:hover { color: var(--accent-2); } .h-link.danger { color: var(--del); } /* panes: queue tree | diff */ -.m-panes { display: grid; grid-template-columns: 248px 1fr; height: 440px; min-height: 0; } -.m-tree { border-right: 1px solid var(--border); padding: 6px; overflow-y: auto; } +.m-panes { display: grid; grid-template-columns: var(--mock-tree) 5px minmax(0, 1fr); height: 440px; min-height: 0; } +.m-tree { padding: 6px; overflow-y: auto; } +.m-resize { + position: relative; background: var(--border); cursor: col-resize; touch-action: none; +} +.m-resize::after { + content: ""; position: absolute; inset: 0 -3px; z-index: 2; +} +.m-resize:hover, .m-resize:focus-visible, .m-resize.dragging { + background: var(--accent); outline: none; +} .q-dir, .q-row { display: flex; align-items: center; gap: 7px; height: 26px; padding: 0 8px; border-radius: 6px; font-size: 12px; color: var(--text-2); white-space: nowrap; @@ -389,7 +431,7 @@ p { margin: 0; } .q-ok { color: var(--add); font-size: 11px; flex: none; } .q-note { font-family: var(--font-mono); font-size: 10px; color: var(--warn); flex: none; } .st { font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; width: 10px; text-align: right; flex: none; } -.st.m { color: var(--mod); } .st.a { color: var(--add); } +.st.m { color: var(--mod); } .st.a { color: var(--add); } .st.s { color: var(--accent); } /* diff pane (features.css .rv-file-head + Pierre diff) */ .m-diff { display: flex; flex-direction: column; min-width: 0; } @@ -401,7 +443,7 @@ p { margin: 0; } .rv-file-head .path { color: var(--text); font-weight: 500; margin-right: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .stat-add { color: var(--add); font-variant-numeric: tabular-nums; } .stat-del { color: var(--del); font-variant-numeric: tabular-nums; } -.h-link-sm { font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-muted); cursor: pointer; white-space: nowrap; } +.h-link-sm { padding: 0; background: transparent; font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-muted); cursor: pointer; white-space: nowrap; } .h-link-sm:hover { color: var(--text); } .h-link-sm.danger { color: var(--del); } .rv-check { @@ -409,10 +451,18 @@ p { margin: 0; } font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-dim); box-shadow: 0 0 0 0.5px var(--border) inset; cursor: pointer; white-space: nowrap; } +.rv-check { border: 0; background: transparent; } .rv-check i { font-style: normal; font-size: 10px; } .rv-check:hover { color: var(--text); background: var(--bg-hover); } .rv-check.on { color: var(--add); box-shadow: 0 0 0 1px color-mix(in srgb, var(--add) 50%, transparent) inset; } .d-scroll { overflow-y: auto; flex: 1; padding: 4px 0 12px; } +.note-editor { padding: 6px 10px; border-bottom: 1px solid var(--border); background: var(--bg-panel); } +.note-editor[hidden] { display: none; } +.note-editor input { + width: 100%; height: 30px; padding: 0 9px; border: 1px solid var(--border-strong); + border-radius: 6px; outline: none; background: var(--bg-base); color: var(--text); font: 11.5px var(--font-ui); +} +.note-editor input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } .mockdiff { display: none; } .mockdiff.on { display: block; animation: diffin 0.18s ease-out; } @@ -429,7 +479,8 @@ p { margin: 0; } border-radius: 6px; padding: 4px 9px; } .dnote .ln { font-family: var(--font-mono); font-size: 10px; font-weight: 500; color: var(--warn); } -.dnote .x { font-style: normal; color: var(--text-dim); margin-left: 2px; } +.dnote .x { padding: 0; background: transparent; color: var(--text-dim); margin-left: 2px; cursor: pointer; } +.dnote .x:hover { color: var(--text); } /* diff lines: dual gutter, bar indicators (the app's `bars` mode), pierre-dark tokens */ .dline { display: grid; grid-template-columns: 36px 36px 1fr; line-height: 22px; } @@ -445,14 +496,21 @@ p { margin: 0; } .dline .c .fn { color: #9d6afb; } .dline .c .num { color: #68cdf2; } .dline .c .typ { color: #d568ea; } .dline .c .cmt { color: #737373; } -/* ─── View switching (Local Changes / Review / All Commits) ─── */ +/* ─── View switching (Work / Local Changes / Review / PRs / Commits) ─── */ .side-row[data-view] { cursor: pointer; } .side-row[data-view]:hover { background: var(--bg-hover); color: var(--text); } .side-row[data-view].active:hover { background: var(--bg-sel); } -.only-review, .only-local, .only-commits { display: none !important; } +.only-work, .only-review, .only-local, .only-pr, .only-commits { display: none !important; } +.mock[data-view="work"] .only-work { display: flex !important; } .mock[data-view="review"] .only-review { display: flex !important; } .mock[data-view="local"] .only-local { display: flex !important; } +.mock[data-view="pull-requests"] .only-pr { display: grid !important; } .mock[data-view="commits"] .only-commits { display: block !important; } +.mock[data-view="work"] .m-mainheader, +.mock[data-view="work"] .m-hints, +.mock[data-view="work"] .only-panes, +.mock[data-view="pull-requests"] .m-hints, +.mock[data-view="pull-requests"] .only-panes, .mock[data-view="commits"] .only-panes, .mock[data-view="commits"] .m-hints { display: none !important; } .mock[data-view="local"] .only-review-i, @@ -462,6 +520,98 @@ p { margin: 0; } .mock[data-view="local"] .m-panes { height: 372px; } .mock[data-view="local"] .lc-commit-bar { display: flex !important; } +/* Work mirrors the app's peer file/terminal tab strip. */ +.work-surface { + height: 540px; min-height: 0; flex-direction: column; background: var(--bg-base); +} +.work-tabs { + height: 36px; flex: none; display: flex; align-items: stretch; + border-bottom: 1px solid var(--border); padding: 0 8px; +} +.work-tab, .work-add { + display: flex; align-items: center; gap: 7px; padding: 0 11px; + background: transparent; color: var(--text-muted); cursor: pointer; position: relative; +} +.work-tab svg { width: 13px; height: 13px; } +.work-tab > span { color: var(--text-dim); margin-left: 4px; } +.work-tab.active { color: var(--text); } +.work-tab.active::after { + content: ""; position: absolute; left: 8px; right: 8px; bottom: -1px; + height: 2px; border-radius: 2px 2px 0 0; background: var(--accent); +} +.work-add { width: 30px; justify-content: center; padding: 0; font-size: 16px; } +.work-tab:hover, .work-add:hover { color: var(--text); background: var(--bg-hover); } +.work-pane { display: none; min-height: 0; flex: 1; } +.work-pane.on { display: block; } +.work-file-tools { + height: 34px; display: flex; align-items: center; padding: 0 14px; + border-bottom: 1px solid var(--border); color: var(--text-2); font-family: var(--font-mono); +} +.work-file-tools span { margin-right: auto; } +.work-file-tools button { + padding: 4px 9px; border-radius: 5px; background: var(--bg-elev); color: var(--text-muted); cursor: pointer; +} +.work-file-tools kbd { margin-left: 5px; font: 9.5px var(--font-mono); color: var(--text-dim); } +.work-file pre { + margin: 0; padding: 18px 20px; height: calc(100% - 34px); overflow: auto; + font: 12px/2 var(--font-mono); color: var(--text-2); tab-size: 2; +} +.work-file code i { color: #ff678d; font-style: normal; } +.work-file code b { color: #5ecc71; font-weight: 400; } +.work-file code em { color: #d568ea; font-style: normal; } +.work-terminal { + padding: 18px 20px; background: #12110e; font: 12px/1.9 var(--font-mono); color: var(--text-2); +} +.work-terminal p { margin: 0; } +.work-terminal b { color: var(--accent); } +.work-terminal i { color: #9d6afb; font-style: normal; } +.work-terminal span { color: var(--accent); } +.work-terminal .term-ok { color: var(--add); } +.term-caret { display: inline-block; width: 7px; height: 14px; background: var(--text); vertical-align: -2px; animation: termblink 1s steps(1) infinite; } +@keyframes termblink { 50% { opacity: 0; } } + +/* Pull Requests follows the app's list/detail master-detail surface. */ +.pr-surface { + height: 502px; min-height: 0; grid-template-columns: minmax(190px, 32%) minmax(0, 1fr); +} +.pr-list { border-right: 1px solid var(--border); overflow-y: auto; padding: 6px; } +.pr-list-head { + height: 30px; display: flex; align-items: center; padding: 0 8px; + color: var(--text-dim); font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; +} +.pr-list-head button { margin-left: auto; background: transparent; color: var(--text-muted); cursor: pointer; } +.pr-row { + width: 100%; display: grid; grid-template-columns: 38px 1fr; gap: 1px 8px; + padding: 8px; border-radius: 6px; background: transparent; color: var(--text-2); + text-align: left; cursor: pointer; +} +.pr-row:hover { background: var(--bg-hover); } +.pr-row.active { background: var(--bg-sel); color: var(--text); } +.pr-row b { grid-row: 1 / 3; color: var(--accent); font: 10px var(--font-mono); padding-top: 2px; } +.pr-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pr-row small { color: var(--text-dim); font-size: 10.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pr-detail { min-width: 0; padding: 16px 18px; overflow-y: auto; } +.pr-detail-head { display: flex; align-items: center; gap: 9px; } +.pr-detail-head strong { font-size: 14px; margin-right: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pr-detail-head button, .pr-summary button { + padding: 5px 10px; border-radius: 6px; background: var(--accent); color: var(--accent-fg); cursor: pointer; font-weight: 600; +} +.pr-state { padding: 2px 7px; border-radius: 999px; background: var(--add-bg); color: var(--add); font-size: 10px; font-weight: 600; } +.pr-detail > p { margin: 7px 0 18px; color: var(--text-dim); font-size: 11px; } +.pr-checks, .pr-summary, .pr-commit { + display: flex; align-items: center; gap: 9px; padding: 10px 12px; + border: 1px solid var(--border); background: var(--bg-panel); +} +.pr-checks { border-radius: 7px 7px 0 0; } +.pr-checks .ok { color: var(--add); } +.pr-checks span:last-child { margin-left: auto; color: var(--text-dim); } +.pr-summary { border-top: 0; border-radius: 0 0 7px 7px; margin-bottom: 16px; } +.pr-summary button { margin-left: auto; } +.pr-commit { border-radius: 6px; margin-top: 6px; background: transparent; } +.pr-commit span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pr-commit code { color: var(--text-dim); font: 10.5px var(--font-mono); } +.pr-commit .ai-chip { flex: none; } + /* collapsible tree folders */ .q-kids.closed { display: none; } .q-dir { cursor: pointer; } @@ -557,6 +707,14 @@ p { margin: 0; } .m-status svg { width: 11px; height: 11px; } .m-status .gear svg { width: 12px; height: 12px; } .sb-sep { color: var(--border-strong); } +.mock-toast { + position: absolute; z-index: 8; right: 14px; bottom: 36px; + max-width: 360px; padding: 8px 12px; border-radius: 7px; + background: var(--bg-elev); color: var(--text-2); + box-shadow: 0 0 0 0.5px var(--border-strong), var(--shadow-2); + font-size: 11.5px; pointer-events: none; +} +.mock-toast[hidden] { display: none; } .try-hint { text-align: center; margin-top: 22px; @@ -1027,7 +1185,10 @@ p { margin: 0; } .stats { grid-template-columns: repeat(2, 1fr); gap: 36px 0; } .stat:nth-child(3) { border-left: none; padding-left: 0; } .m-side { display: none; } + .body-resize { display: none; } .m-body { grid-template-columns: 1fr; } + .workspace-btn span, .workspace-btn .bchev { display: none; } + .workspace-btn { width: 28px; padding: 0; justify-content: center; } .cmd-pill { display: none; } } @media (max-width: 720px) { @@ -1036,7 +1197,10 @@ p { margin: 0; } .section { padding: 90px 0; } .m-panes { grid-template-columns: 1fr; height: auto; } .m-tree { display: none; } + .panes-resize { display: none; } .d-scroll { max-height: 380px; } + .pr-surface { grid-template-columns: 1fr; } + .pr-list { display: none; } .price-grid { grid-template-columns: 1fr; } .download-grid { grid-template-columns: 1fr; max-width: 420px; } .grid { grid-template-columns: 1fr; }