fix(enterprise): contain panels in scrollable body and rebuild steppers as centered vertical layout

This commit is contained in:
matevip 2026-05-12 11:41:29 +08:00
parent 093925e191
commit 2fc9a5153b
7 changed files with 244 additions and 57 deletions

1
.gitignore vendored
View File

@ -98,7 +98,6 @@ CLAUDE.md
# Codex CLI local artifacts
.codex/
.superpowers/
# Sync tooling local state (generated each run; report is intentionally tracked)
scripts/.*-sync-state.json

View File

@ -269,6 +269,7 @@ function scoreToneFor(score: number): string {
.account-shell {
display: grid;
grid-template-columns: 280px 1fr 340px;
grid-template-rows: minmax(0, 1fr);
gap: 14px;
flex: 1;
min-height: 0;

View File

@ -138,7 +138,19 @@ function riskLabel(r: 'high' | 'medium' | 'low'): string {
</script>
<style scoped>
.approvals-shell { display: flex; flex-direction: column; gap: 14px; }
.approvals-shell {
display: flex;
flex-direction: column;
gap: 14px;
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
padding-bottom: 16px;
}
/* Stop the panels from being shrunk to fit the body let them overflow
into the scroll area instead. */
.approvals-shell > .panel { flex-shrink: 0; }
.panel { padding: 18px 20px; display: flex; flex-direction: column; gap: 14px; }
.panel-head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; flex-wrap: wrap; }

View File

@ -97,7 +97,17 @@ function kindLabel(k: Event['kind']): string { return t(`enterprise.audit.kind.$
</script>
<style scoped>
.audit-shell { display: flex; flex-direction: column; }
.audit-shell {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
padding-bottom: 16px;
}
/* Stop the audit panel from being shrunk to fit the body. */
.audit-shell > .panel { flex-shrink: 0; }
.panel { padding: 18px 20px; display: flex; flex-direction: column; gap: 14px; }
.panel-head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; flex-wrap: wrap; }

View File

@ -86,9 +86,9 @@
</li>
</ol>
<div class="chain-actions">
<button class="btn-secondary">{{ t('enterprise.contract.btnReject') }}</button>
<button class="btn-secondary">{{ t('enterprise.contract.btnRequestRevision') }}</button>
<button class="btn-primary">{{ t('enterprise.contract.btnApprove') }}</button>
<button class="btn-secondary chain-btn">{{ t('enterprise.contract.btnReject') }}</button>
<button class="btn-secondary chain-btn">{{ t('enterprise.contract.btnRequestRevision') }}</button>
<button class="btn-primary chain-btn">{{ t('enterprise.contract.btnApprove') }}</button>
</div>
</footer>
</section>
@ -339,6 +339,10 @@ function statusLabel(s: Case['status']): string {
.review-shell {
display: grid;
grid-template-columns: 280px 1fr 340px;
/* Force the single row to the container height so each pane has a definite
height ceiling; minmax(0, 1fr) lets the pane shrink below its content
intrinsic size so its inner scrollable region kicks in. */
grid-template-rows: minmax(0, 1fr);
gap: 14px;
flex: 1;
min-height: 0;
@ -430,47 +434,105 @@ function statusLabel(s: Case['status']): string {
.clause-list {
display: flex;
flex-direction: column;
gap: 12px;
gap: 18px;
overflow-y: auto;
flex: 1;
padding-right: 4px;
padding-right: 6px;
/* Stop the last card from butting against the approval-chain footer. */
padding-bottom: 8px;
}
.clause-card {
border: 1px solid var(--mc-border-light);
border-radius: 12px;
padding: 14px 16px;
border-radius: 14px;
background: var(--mc-bg-elevated);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
transition: border-color 0.15s, box-shadow 0.18s, transform 0.15s;
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
overflow: hidden;
box-shadow: 0 1px 2px rgba(58, 32, 19, 0.04);
}
.clause-card:hover { box-shadow: 0 4px 14px rgba(58, 32, 19, 0.08); }
.clause-card.risk-high { border-left: 4px solid #ef4444; }
.clause-card.risk-medium { border-left: 4px solid #f59e0b; }
.clause-card.risk-low { border-left: 4px solid #84cc16; }
.clause-card[data-active="true"] { box-shadow: var(--mc-shadow-soft); border-color: var(--mc-primary); }
.clause-card[data-active="true"] {
box-shadow: 0 6px 22px rgba(217, 109, 70, 0.18);
border-color: var(--mc-primary);
}
.clause-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.clause-type { font-size: 13px; font-weight: 600; color: var(--mc-text-primary); }
.clause-loc { font-size: 11px; color: var(--mc-text-tertiary); font-family: var(--mc-font-mono); }
/* === head row — gets its own contrast band so each card has an obvious "start" === */
.clause-head {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
padding: 12px 16px;
background: var(--mc-bg-muted);
border-bottom: 1px solid var(--mc-border-light);
}
.clause-type { font-size: 14px; font-weight: 600; color: var(--mc-text-primary); }
.clause-loc {
font-size: 11px;
color: var(--mc-text-tertiary);
font-family: var(--mc-font-mono);
margin-left: auto;
background: var(--mc-bg-elevated);
padding: 3px 8px;
border-radius: 4px;
border: 1px solid var(--mc-border-light);
}
.clause-quote { font-size: 13px; color: var(--mc-text-primary); line-height: 1.55; margin: 0; padding: 8px 12px; background: var(--mc-bg-muted); border-radius: 8px; font-style: italic; }
.clause-deviation { font-size: 12px; color: var(--mc-text-secondary); display: flex; gap: 8px; align-items: baseline; line-height: 1.5; }
.dev-label, .suggest-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mc-text-tertiary); white-space: nowrap; }
.dev-label { color: #b45309; }
.suggest-label { color: var(--mc-primary-hover); }
/* === body — quote / deviation / suggestion stack with consistent padding === */
.clause-card > .clause-quote,
.clause-card > .clause-deviation,
.clause-card > .clause-suggest { margin: 0 16px; }
.clause-card > .clause-quote { margin-top: 14px; }
.clause-card > .clause-deviation { margin-top: 10px; }
.clause-card > .clause-suggest { margin: 12px 16px 16px; }
.clause-quote {
font-size: 13px;
color: var(--mc-text-primary);
line-height: 1.6;
padding: 10px 14px;
background: var(--mc-bg);
border-radius: 8px;
font-style: italic;
border: 1px dashed var(--mc-border-light);
}
.clause-deviation {
font-size: 12px;
color: var(--mc-text-secondary);
display: flex;
gap: 10px;
align-items: baseline;
line-height: 1.55;
}
.dev-label, .suggest-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--mc-text-tertiary);
white-space: nowrap;
padding: 2px 7px;
border-radius: 4px;
}
.dev-label { color: #b45309; background: #fef3c7; }
.suggest-label { color: var(--mc-primary-hover); background: var(--mc-primary-bg); }
.clause-suggest {
background: var(--mc-primary-bg);
border-radius: 8px;
padding: 10px 12px;
background: linear-gradient(180deg, var(--mc-primary-bg), rgba(246, 226, 215, 0.4));
border: 1px solid rgba(217, 109, 70, 0.18);
border-radius: 10px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
}
.suggest-text { font-size: 13px; color: var(--mc-text-primary); line-height: 1.55; }
.suggest-text { font-size: 13px; color: var(--mc-text-primary); line-height: 1.6; }
/* === approval chain === */
.approval-chain {
@ -481,17 +543,39 @@ function statusLabel(s: Case['status']): string {
gap: 12px;
}
.chain-title { font-size: 11px; font-weight: 700; color: var(--mc-text-tertiary); text-transform: uppercase; letter-spacing: 0.06em; }
.chain-list { list-style: none; margin: 0; padding: 0; display: flex; gap: 0; }
.chain-step { flex: 1; display: flex; gap: 10px; align-items: flex-start; padding-right: 12px; position: relative; }
/* Vertical stepper layout bullet on top, body underneath, connector
between bullets at bullet-vertical-center. Mirrors the overview pipeline
so the two surfaces share a single stepper grammar. */
.chain-list { list-style: none; margin: 0; padding: 0; display: flex; align-items: stretch; }
.chain-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 6px;
padding: 0 8px;
position: relative;
min-width: 0;
}
.chain-step::after {
content: ''; position: absolute; top: 13px; right: 0; left: 38px; height: 2px; background: var(--mc-border-light);
content: '';
position: absolute;
top: 13px;
left: 50%;
right: -50%;
height: 2px;
background: var(--mc-border-light);
z-index: 0;
}
.chain-step.done::after { background: var(--mc-primary); }
.chain-step:last-child::after { display: none; }
.chain-bullet {
width: 26px; height: 26px;
width: 28px; height: 28px;
border-radius: 50%;
background: var(--mc-bg-muted);
background: var(--mc-bg-elevated);
color: var(--mc-text-tertiary);
font-size: 12px;
font-weight: 700;
@ -505,12 +589,15 @@ function statusLabel(s: Case['status']): string {
}
.chain-step.done .chain-bullet { background: var(--mc-primary); color: white; border-color: var(--mc-primary); }
.chain-step.active .chain-bullet { background: var(--mc-primary-bg); color: var(--mc-primary-hover); border-color: var(--mc-primary); }
.chain-body { padding-top: 2px; min-width: 0; }
.chain-who { font-size: 12px; font-weight: 600; color: var(--mc-text-primary); }
.chain-note { font-size: 11px; color: var(--mc-text-secondary); margin-top: 2px; line-height: 1.4; }
.chain-at { font-size: 10px; color: var(--mc-text-tertiary); margin-top: 2px; font-family: var(--mc-font-mono); }
.chain-actions { display: flex; gap: 8px; justify-content: flex-end; }
.chain-body { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px; max-width: 140px; }
.chain-who { font-size: 12px; font-weight: 600; color: var(--mc-text-primary); line-height: 1.3; }
.chain-step.active .chain-who { color: var(--mc-primary-hover); }
.chain-note { font-size: 11px; color: var(--mc-text-secondary); line-height: 1.35; }
.chain-at { font-size: 10px; color: var(--mc-text-tertiary); font-family: var(--mc-font-mono); margin-top: 1px; }
.chain-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.chain-btn { white-space: nowrap; }
.btn-primary, .btn-secondary {
border-radius: 8px;
padding: 7px 14px;

View File

@ -90,13 +90,12 @@
</div>
</header>
<div class="pipeline">
<div class="pipeline-step" v-for="(s, i) in pipeline" :key="s.label">
<div class="step-bullet" :class="{ done: s.done, active: !s.done && i === firstActive }">{{ i + 1 }}</div>
<div class="step-body">
<div class="step-label">{{ s.label }}</div>
<div class="step-desc">{{ s.desc }}</div>
</div>
<div v-if="i < pipeline.length - 1" class="step-connector" :class="{ done: s.done }"></div>
<div class="pipeline-step"
v-for="(s, i) in pipeline" :key="s.label"
:class="{ done: s.done, active: !s.done && i === firstActive }">
<div class="step-bullet">{{ s.done ? '✓' : i + 1 }}</div>
<div class="step-label">{{ s.label }}</div>
<div class="step-desc">{{ s.desc }}</div>
</div>
</div>
</article>
@ -197,7 +196,28 @@ const firstActive = computed(() => pipeline.findIndex(s => !s.done))
</script>
<style scoped>
.overview { display: flex; flex-direction: column; gap: 18px; }
.overview {
display: flex;
flex-direction: column;
gap: 18px;
flex: 1;
min-height: 0;
overflow-y: auto;
/* Inset the scrollbar so it doesn't hug the cards, and give the last
card breathing room from the bottom edge of the body. */
padding-right: 4px;
padding-bottom: 16px;
}
/* Each panel inside an overflow-y flex column must opt out of the default
flex-shrink behaviour, otherwise the browser will compress them to fit
inside the visible body instead of letting them overflow into the
scrollable area. Without this the pipeline panel renders at ~38px tall
(= just its padding) and its content overflows below the visible card. */
.overview > .panel,
.overview > .metric-strip,
.overview > .overview-grid {
flex-shrink: 0;
}
/* === metric strip === */
.metric-strip {
@ -373,13 +393,45 @@ const firstActive = computed(() => pipeline.findIndex(s => !s.done))
.tag-playbook { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
.tag-precedent { background: var(--mc-accent-soft); color: var(--mc-accent); }
/* === pipeline === */
.pipeline { display: flex; gap: 0; }
.pipeline-step { display: flex; gap: 12px; flex: 1; align-items: flex-start; position: relative; }
/* === pipeline ===
Centered horizontal stepper. The connector is a pseudo-element on each
step that spans from the bullet's right edge to the next step's bullet
(i.e. just past its left edge). Pulling it out of the flex flow lets the
label and desc sit cleanly below the bullet without colliding with the
horizontal line. */
.pipeline {
display: flex;
align-items: stretch;
padding: 4px 8px;
}
.pipeline-step {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0 8px;
}
.pipeline-step::after {
/* Connector drawn at the bullet's vertical centre, behind the bullet via
z-index. last-child has no connector. */
content: '';
position: absolute;
top: 16px;
left: 50%;
right: -50%;
height: 2px;
background: var(--mc-border-light);
z-index: 0;
}
.pipeline-step.done::after { background: var(--mc-primary); }
.pipeline-step:last-child::after { display: none; }
.step-bullet {
width: 32px; height: 32px;
border-radius: 50%;
background: var(--mc-bg-muted);
background: var(--mc-bg-elevated);
color: var(--mc-text-tertiary);
font-size: 13px;
font-weight: 700;
@ -387,16 +439,28 @@ const firstActive = computed(() => pipeline.findIndex(s => !s.done))
align-items: center;
justify-content: center;
border: 2px solid var(--mc-border-light);
position: relative;
z-index: 1;
margin-bottom: 10px;
flex-shrink: 0;
}
.step-bullet.done { background: var(--mc-primary); color: white; border-color: var(--mc-primary); }
.step-bullet.active { background: var(--mc-primary-bg); color: var(--mc-primary-hover); border-color: var(--mc-primary); }
.step-body { flex: 1; padding-top: 5px; min-width: 0; padding-right: 12px; }
.step-label { font-size: 13px; font-weight: 600; color: var(--mc-text-primary); }
.step-desc { font-size: 11px; color: var(--mc-text-tertiary); margin-top: 2px; line-height: 1.4; }
.step-connector { position: absolute; top: 16px; left: 32px; right: -12px; height: 2px; background: var(--mc-border-light); }
.step-connector.done { background: var(--mc-primary); }
.pipeline-step:last-child .step-connector { display: none; }
.pipeline-step.done .step-bullet { background: var(--mc-primary); color: white; border-color: var(--mc-primary); }
.pipeline-step.active .step-bullet { background: var(--mc-primary-bg); color: var(--mc-primary-hover); border-color: var(--mc-primary); }
.step-label {
font-size: 13px;
font-weight: 600;
color: var(--mc-text-primary);
line-height: 1.3;
}
.pipeline-step.active .step-label { color: var(--mc-primary-hover); }
.step-desc {
font-size: 11px;
color: var(--mc-text-tertiary);
margin-top: 4px;
line-height: 1.45;
max-width: 180px;
}
@media (max-width: 1100px) {
.overview-grid { grid-template-columns: 1fr; }

View File

@ -63,6 +63,11 @@ function onOpenCase(id: string) {
padding: 28px 32px 24px;
gap: 18px;
background: var(--mc-bg);
/* Constrain the inner body so each tab can decide how to overflow:
vertical-stack pages scroll the body itself, three-pane grid pages
keep each pane scrolling independently. Without this, content that
exceeds the viewport falls outside the layout box and is unreachable. */
overflow: hidden;
}
.enterprise-head { display: flex; flex-direction: column; gap: 4px; }
@ -128,5 +133,14 @@ function onOpenCase(id: string) {
}
.enterprise-tab.active .tab-count { background: var(--mc-primary); color: white; }
.enterprise-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* The body fills the remaining height and gives its single child a strict
height contract via flex. Page roots must declare `flex: 1; min-height: 0;`
so they consume that height and can choose their own overflow strategy. */
.enterprise-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>