diff --git a/clarity.controlplane/src/components/GitCommitDrawer.tsx b/clarity.controlplane/src/components/GitCommitDrawer.tsx index efdabd4..fb07ebc 100644 --- a/clarity.controlplane/src/components/GitCommitDrawer.tsx +++ b/clarity.controlplane/src/components/GitCommitDrawer.tsx @@ -1,74 +1,127 @@ -import { useEffect, useState, useRef } from 'react'; -import { Button, Drawer, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core'; +import { useEffect, useState } from 'react'; +import { Button, Collapse, Drawer, Icon, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core'; import { html as diff2htmlHtml } from 'diff2html'; import 'diff2html/bundles/css/diff2html.min.css'; -import hljs from 'highlight.js'; -import 'highlight.js/styles/github.css'; -import { getCommitDetail, type CommitDetail } from '../api/opcApi'; +import { getCommitDetail, type CommitDetail, type CommitFile } from '../api/opcApi'; interface Props { hash: string | null; onClose: () => void; } +function fileStatusIntent(status: string): Intent { + if (status === 'added') return Intent.SUCCESS; + if (status === 'deleted') return Intent.DANGER; + if (status === 'renamed') return Intent.WARNING; + return Intent.NONE; +} + +function fileStatusIcon(status: string): string { + if (status === 'added') return 'plus'; + if (status === 'deleted') return 'minus'; + if (status === 'renamed') return 'arrow-right'; + return 'edit'; +} + +function FileDiff({ file }: { file: CommitFile }) { + const [open, setOpen] = useState(true); + + const diffHtml = file.patch + ? diff2htmlHtml(file.patch, { + drawFileList: false, + matching: 'lines', + outputFormat: 'line-by-line', + renderNothingWhenEmpty: true, + }) + : ''; + + const displayPath = file.status === 'renamed' && file.oldPath && file.oldPath !== file.path + ? `${file.oldPath} → ${file.path}` + : file.path; + + return ( +
+ + + {diffHtml + ?
+ :
Binary or empty file — no textual diff available.
+ } + +
+ ); +} + export function GitCommitDrawer({ hash, onClose }: Props) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const diffRef = useRef(null); useEffect(() => { - if (!hash) { setDetail(null); setError(null); return; } - setLoading(true); setDetail(null); setError(null); + if (!hash) { + // Delay clearing so the closing animation doesn't flash blank + const t = setTimeout(() => { setDetail(null); setError(null); }, 300); + return () => clearTimeout(t); + } + setLoading(true); + setError(null); getCommitDetail(hash) - .then(setDetail) + .then(d => { setDetail(d); setError(null); }) .catch(e => setError(String(e))) .finally(() => setLoading(false)); }, [hash]); - // After diff HTML is injected, run highlight.js over code blocks - useEffect(() => { - if (detail && diffRef.current) { - diffRef.current.querySelectorAll('code[class]').forEach(el => { - hljs.highlightElement(el); - }); - } - }, [detail]); - - const combinedPatch = detail?.files.map(f => f.patch).join('\n') ?? ''; - const diffHtml = combinedPatch - ? diff2htmlHtml(combinedPatch, { - drawFileList: true, - matching: 'lines', - outputFormat: 'line-by-line', - renderNothingWhenEmpty: false, - }) - : ''; + const totalAdds = detail?.files.reduce((a, f) => a + f.additions, 0) ?? 0; + const totalDels = detail?.files.reduce((a, f) => a + f.deletions, 0) ?? 0; return ( - {detail.shortHash} - {detail.subject} - - ) : 'Commit Diff'} + title={ + detail ? ( + + {detail.shortHash} + {detail.subject} + + ) : 'Commit Diff' + } size="70%" position="right" className="git-commit-drawer" > -
- {loading && } title="Loading diff…" />} - {error && } + {/* Scrollable body */} +
+ {/* Loading overlay — keeps old content visible while fetching next */} + {loading && ( +
+ +
+ )} - {detail && ( + {error && ( + + )} + + {!error && detail && ( <> {/* Metadata bar */}
- + navigator.clipboard.writeText(detail.hash)} @@ -81,26 +134,33 @@ export function GitCommitDrawer({ hash, onClose }: Props) { {detail.date}
- - +{detail.files.reduce((a, f) => a + f.additions, 0)} + {totalAdds > 0 && ( + +{totalAdds} + )} + {totalDels > 0 && ( + -{totalDels} + )} + + {detail.files.length} file{detail.files.length !== 1 ? 's' : ''} - - -{detail.files.reduce((a, f) => a + f.deletions, 0)} - - {detail.files.length} file{detail.files.length !== 1 ? 's' : ''}
- {/* Commit body if multiline */} + {/* Extended commit message */} {detail.body.trim() !== detail.subject.trim() && (
{detail.body.trim()}
)} - {/* Diff */} - {diffHtml - ?
- : - } + {/* Per-file diffs */} + {detail.files.length === 0 ? ( + + ) : ( +
+ {detail.files.map(f => ( + + ))} +
+ )} )} @@ -109,7 +169,8 @@ export function GitCommitDrawer({ hash, onClose }: Props) { )}
-
+ {/* Footer — sticky at bottom */} +
diff --git a/clarity.controlplane/src/index.css b/clarity.controlplane/src/index.css index 310f67a..9ac0cbc 100644 --- a/clarity.controlplane/src/index.css +++ b/clarity.controlplane/src/index.css @@ -817,10 +817,50 @@ body { } /* ── Git Commit Drawer ──────────────────────────────────────────────────────── */ -.git-commit-drawer .bp5-drawer-header { + +/* Drawer shell: full-height flex column */ +.git-commit-drawer.bp6-drawer { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.git-commit-drawer .bp6-drawer-header { + flex-shrink: 0; padding: 0.75rem 1rem; } +/* + * .gcd-body is the scrollable content area. + * Blueprint v6 renders children directly inside .bp6-drawer — no body wrapper. + */ +.git-commit-drawer .gcd-body { + flex: 1 1 0; /* 0 basis — don't size from content, allow shrink */ + min-height: 0; /* flex children won't shrink past content without this */ + overflow-y: auto; + overflow-x: hidden; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + position: relative; /* loading overlay anchor */ +} + +/* Children of the scroll container must NOT shrink — if they do, content + * never overflows and the scrollbar never appears. */ +.git-commit-drawer .gcd-body > * { + flex-shrink: 0; +} + +/* Footer rendered as last child — sits below the scroll area */ +.git-commit-drawer .gcd-footer { + flex-shrink: 0; + padding: 0.5rem 1rem; + display: flex; + justify-content: flex-end; +} + .git-drawer-title { display: flex; align-items: center; @@ -838,6 +878,95 @@ body { font-family: 'JetBrains Mono', 'Fira Code', monospace; } +/* Loading overlay — keeps old diff visible while fetching next commit */ +.gcd-loading-overlay { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + pointer-events: none; +} + +/* Per-file accordion */ +.gcd-files-list { + display: flex; + flex-direction: column; + gap: 0; + border: 1px solid #dce0e6; + border-radius: 6px; + overflow: hidden; + margin: 0.75rem 0; +} + +.gcd-file-section { + border-bottom: 1px solid #dce0e6; +} + +.gcd-file-section:last-child { + border-bottom: none; +} + +.gcd-file-header { + all: unset; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.45rem 0.75rem; + background: #f6f8fa; + cursor: pointer; + user-select: none; + transition: background 0.1s; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 0.78rem; + color: #1c2127; +} + +.gcd-file-header:hover, +.gcd-file-header--open { + background: #edf2f7; +} + +.gcd-file-chevron { + flex-shrink: 0; + color: #738091; +} + +.gcd-file-status-icon { + flex-shrink: 0; +} + +.gcd-file-path { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.gcd-file-stats { + display: flex; + gap: 0.4rem; + flex-shrink: 0; + font-size: 0.73rem; + font-family: 'JetBrains Mono', 'Fira Code', monospace; +} + +.gcd-adds { color: #1a7f37; font-weight: 600; } +.gcd-dels { color: #cf222e; font-weight: 600; } + +.gcd-no-diff { + padding: 0.6rem 1rem; + font-size: 0.8rem; + color: #738091; + font-style: italic; + background: #fafafa; +} + .git-drawer-subject { font-size: 0.92rem; font-weight: 600; @@ -847,21 +976,6 @@ body { color: #1c2127; } -.git-drawer-body { - flex: 1; - overflow-y: auto; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.git-drawer-footer { - padding: 0.75rem 1rem; - border-top: 1px solid #d3d8de; - display: flex; - justify-content: flex-end; -} .git-commit-meta-bar { display: flex; @@ -928,7 +1042,8 @@ body { font-size: 0.78rem; line-height: 1.45; border-radius: 6px; - overflow: hidden; + overflow-x: auto; /* horizontal scroll for wide diffs, not clip */ + overflow-y: visible; border: 1px solid #d0d7de; } diff --git a/clarity.controlplane/src/opc/OpcPage.tsx b/clarity.controlplane/src/opc/OpcPage.tsx index 67d2366..920f4f9 100644 --- a/clarity.controlplane/src/opc/OpcPage.tsx +++ b/clarity.controlplane/src/opc/OpcPage.tsx @@ -76,7 +76,7 @@ const SDLC_STAGES: { branch: string; label: string; intent: Intent }[] = [ { branch: 'develop', label: 'Dev', intent: Intent.PRIMARY }, { branch: 'staging', label: 'Staging', intent: Intent.WARNING }, { branch: 'uat', label: 'UAT', intent: Intent.DANGER }, - { branch: 'master', label: 'Production', intent: Intent.SUCCESS }, + { branch: 'main', label: 'Production', intent: Intent.SUCCESS }, ]; function deriveSdlcSummary(coverage: BranchCoverage[]): { label: string; intent: Intent } | null {