diff --git a/clarity.controlplane/src/index.css b/clarity.controlplane/src/index.css index b563624..e1feaa3 100644 --- a/clarity.controlplane/src/index.css +++ b/clarity.controlplane/src/index.css @@ -804,30 +804,116 @@ body { .opc-sdlc-pipeline { display: flex; - align-items: center; - flex-wrap: wrap; - gap: 0.2rem; - margin-bottom: 0.35rem; -} - -.opc-sdlc-stage-item { - display: flex; - align-items: center; - gap: 0.2rem; + align-items: flex-start; + flex-wrap: nowrap; + gap: 0; + overflow-x: auto; + padding-bottom: 0.25rem; } .opc-sdlc-arrow { color: #8f99a8; - font-size: 0.8rem; + font-size: 1rem; font-weight: 600; - margin: 0 0.1rem; + flex-shrink: 0; + align-self: center; + margin: 0 0.4rem; user-select: none; } -.opc-sdlc-furthest { - font-size: 0.75rem; +/* Individual branch box */ +.opc-sdlc-box { + flex: 1 1 140px; + min-width: 130px; + max-width: 200px; + display: flex; + flex-direction: column; + border: 1px solid #dce0e6; + border-radius: 6px; + background: #fff; + overflow: hidden; + flex-shrink: 0; +} + +.opc-sdlc-box--reached { + border-width: 2px; +} + +.opc-sdlc-box-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.3rem 0.5rem; + background: #f6f7f9; + border-bottom: 1px solid #e5e8eb; + flex-shrink: 0; +} + +.opc-sdlc-box-count { + font-size: 0.68rem; color: #738091; - margin-top: 0.3rem; + background: #e5e8eb; + border-radius: 10px; + padding: 0 6px; + line-height: 1.5; +} + +/* Scrollable body */ +.opc-sdlc-box-body { + flex: 1; + overflow-y: auto; + max-height: 140px; + min-height: 60px; + padding: 0.3rem 0.4rem; + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.opc-sdlc-sha-row { + display: flex; + align-items: baseline; + gap: 0.35rem; + padding: 0.1rem 0.2rem; + border-radius: 3px; + opacity: 0.35; +} + +.opc-sdlc-sha-row--reached { + opacity: 1; +} + +.opc-sdlc-sha { + font-family: 'Consolas', 'Courier New', monospace; + font-size: 0.7rem; + color: #2d72d2; + flex-shrink: 0; +} + +.opc-sdlc-sha-msg { + font-size: 0.68rem; + color: #4a5568; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.opc-sdlc-box-empty { + font-size: 0.7rem; + color: #a3acb6; + font-style: italic; + padding: 0.2rem 0; +} + +.opc-sdlc-box-pending { + font-size: 0.68rem; + color: #a3acb6; + font-style: italic; + margin-top: auto; + padding-top: 0.25rem; + border-top: 1px dashed #e5e8eb; } /* Commits section labels */ diff --git a/clarity.controlplane/src/opc/OpcPage.tsx b/clarity.controlplane/src/opc/OpcPage.tsx index 920f4f9..82df434 100644 --- a/clarity.controlplane/src/opc/OpcPage.tsx +++ b/clarity.controlplane/src/opc/OpcPage.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useCallback } from 'react'; +import { useState, useMemo, useEffect, useCallback, Fragment } from 'react'; import { GitCommitDrawer } from '../components/GitCommitDrawer'; import { Button, Callout, Divider, Drawer, FormGroup, @@ -79,15 +79,6 @@ const SDLC_STAGES: { branch: string; label: string; intent: Intent }[] = [ { branch: 'main', label: 'Production', intent: Intent.SUCCESS }, ]; -function deriveSdlcSummary(coverage: BranchCoverage[]): { label: string; intent: Intent } | null { - for (let i = SDLC_STAGES.length - 1; i >= 0; i--) { - const stage = SDLC_STAGES[i]; - const hit = coverage.find(c => c.branch === stage.branch); - if (hit?.contains) return { label: stage.label, intent: stage.intent }; - } - return null; -} - // Aggregate per-repo branch coverage into a single view. // A stage is "reached" only when every repo that recognised at least one hash // reports contains=true for that branch. Repos that recognised no hashes are @@ -487,42 +478,48 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) { {/* SDLC Delivery Chain */} {coverage.length > 0 && (() => { - const summary = deriveSdlcSummary(coverage); + const allCommits = [ + ...autoCommits, + ...pinned.map(p => ({ repoKey: 'pinned', hash: p.hash, shortHash: p.shortHash, author: p.pinnedBy, date: p.pinnedAt, subject: p.subject, files: [] })), + ].filter((c, i, a) => a.findIndex(x => x.hash === c.hash) === i); + return (
{c.shortHash}
+ {c.subject}
+