Polish cutover compare board for denser scan and clearer pass/diff cells.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-21 15:09:36 +08:00
parent 0dc63bb96a
commit 4c3c034c4c
2 changed files with 289 additions and 122 deletions

View file

@ -11285,26 +11285,28 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-board { .bs-cmp-board {
--bs-cmp-surface: rgba(15, 23, 42, 0.72); --bs-cmp-surface: rgba(15, 23, 42, 0.78);
--bs-cmp-border: rgba(148, 163, 184, 0.22); --bs-cmp-border: rgba(148, 163, 184, 0.2);
--bs-cmp-ink: #e2e8f0; --bs-cmp-ink: #e2e8f0;
--bs-cmp-muted: #94a3b8; --bs-cmp-muted: #94a3b8;
--bs-cmp-row: rgba(15, 23, 42, 0.42);
--bs-cmp-row-alt: rgba(30, 41, 59, 0.28);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: min(62vh, 720px); min-height: min(62vh, 720px);
padding: 6px; padding: 8px;
border-radius: 12px; border-radius: 12px;
color: var(--bs-cmp-ink); color: var(--bs-cmp-ink);
background: background:
radial-gradient(1200px 420px at 10% -10%, rgba(37, 99, 235, 0.1), transparent 55%), radial-gradient(1100px 380px at 8% -12%, rgba(37, 99, 235, 0.09), transparent 58%),
rgba(8, 13, 24, 0.35); rgba(8, 13, 24, 0.4);
} }
.bs-cmp-board.is-fullscreen, .bs-cmp-board.is-fullscreen,
.bs-cmp-board:fullscreen { .bs-cmp-board:fullscreen {
--bs-cmp-surface: rgba(15, 23, 42, 0.88); --bs-cmp-surface: rgba(15, 23, 42, 0.92);
background: #0b1220; background: #0b1220;
color: #e2e8f0; color: #e2e8f0;
padding: 12px 14px; padding: 12px 14px;
@ -11316,32 +11318,35 @@ html.login-page--paused .login-page__flare {
.bs-cmp-board__toolbar { .bs-cmp-board__toolbar {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: wrap;
gap: 10px; gap: 8px 10px;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
flex: 0 0 auto; flex: 0 0 auto;
min-height: 36px; min-height: 36px;
padding: 2px 0;
} }
.bs-cmp-board__run-select { .bs-cmp-board__run-select {
flex: 0 1 360px; flex: 0 1 280px;
min-width: 160px; min-width: 140px;
max-width: 420px; max-width: 360px;
height: 32px; height: 32px;
margin: 0; margin: 0;
} }
.bs-cmp-board__run-count { .bs-cmp-board__run-count {
flex: 0 0 auto; flex: 0 0 auto;
font-size: 12px; font-size: 11px;
white-space: nowrap; white-space: nowrap;
opacity: 0.75;
} }
.bs-cmp-board__actions { .bs-cmp-board__actions {
margin-left: auto; margin-left: auto;
flex: 0 0 auto; flex: 0 0 auto;
align-items: center; align-items: center;
gap: 6px;
} }
.bs-cmp-sides { .bs-cmp-sides {
@ -11350,13 +11355,13 @@ html.login-page--paused .login-page__flare {
align-items: center; align-items: center;
gap: 6px 8px; gap: 6px 8px;
min-width: 0; min-width: 0;
flex: 1 1 auto; flex: 1 1 320px;
height: 32px; height: 32px;
padding: 0 8px; padding: 0 8px;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--bs-cmp-border, rgba(148, 163, 184, 0.22)); border: 1px solid var(--bs-cmp-border, rgba(148, 163, 184, 0.22));
background: rgba(15, 23, 42, 0.45); background: rgba(15, 23, 42, 0.5);
overflow: visible; overflow: hidden;
} }
.bs-cmp-sides__side { .bs-cmp-sides__side {
@ -11366,20 +11371,20 @@ html.login-page--paused .login-page__flare {
gap: 6px; gap: 6px;
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
padding: 0 6px; padding: 0 8px;
border-radius: 6px; border-radius: 6px;
height: 24px; height: 24px;
flex: 0 1 auto; flex: 1 1 auto;
} }
.bs-cmp-sides__side.is-before { .bs-cmp-sides__side.is-before {
background: rgba(239, 68, 68, 0.08); background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(248, 113, 113, 0.25); border: 1px solid rgba(248, 113, 113, 0.28);
} }
.bs-cmp-sides__side.is-after { .bs-cmp-sides__side.is-after {
background: rgba(34, 197, 94, 0.08); background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(74, 222, 128, 0.25); border: 1px solid rgba(74, 222, 128, 0.28);
} }
.bs-cmp-sides__tag { .bs-cmp-sides__tag {
@ -11405,9 +11410,9 @@ html.login-page--paused .login-page__flare {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 8rem; max-width: 12rem;
min-width: 0; min-width: 0;
flex: 0 1 auto; flex: 1 1 auto;
line-height: 1.2; line-height: 1.2;
} }
@ -11431,10 +11436,6 @@ html.login-page--paused .login-page__flare {
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.bs-cmp-board__toolbar {
flex-wrap: wrap;
}
.bs-cmp-board__actions { .bs-cmp-board__actions {
margin-left: 0; margin-left: 0;
} }
@ -11445,12 +11446,17 @@ html.login-page--paused .login-page__flare {
height: auto; height: auto;
min-height: 32px; min-height: 32px;
padding: 4px 8px; padding: 4px 8px;
overflow: visible;
}
.bs-cmp-sides__name {
max-width: none;
} }
} }
.bs-cmp-board__body { .bs-cmp-board__body {
display: grid; display: grid;
grid-template-columns: minmax(200px, 248px) minmax(0, 1fr); grid-template-columns: minmax(220px, 268px) minmax(0, 1fr);
gap: 10px; gap: 10px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
@ -11585,9 +11591,10 @@ html.login-page--paused .login-page__flare {
border-radius: 8px; border-radius: 8px;
background: transparent; background: transparent;
color: var(--bs-cmp-ink); color: var(--bs-cmp-ink);
padding: 8px; padding: 7px 8px;
cursor: pointer; cursor: pointer;
line-height: 1.25; line-height: 1.25;
transition: background 0.12s ease, border-color 0.12s ease;
} }
.bs-cmp-nav__item:hover { .bs-cmp-nav__item:hover {
@ -11595,8 +11602,9 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-nav__item.is-active { .bs-cmp-nav__item.is-active {
background: rgba(37, 99, 235, 0.18); background: rgba(37, 99, 235, 0.22);
border-color: rgba(59, 130, 246, 0.45); border-color: rgba(59, 130, 246, 0.5);
box-shadow: inset 2px 0 0 #3b82f6;
} }
.bs-cmp-nav__dot { .bs-cmp-nav__dot {
@ -11606,14 +11614,17 @@ html.login-page--paused .login-page__flare {
background: #64748b; background: #64748b;
justify-self: center; justify-self: center;
align-self: center; align-self: center;
box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.2);
} }
.bs-cmp-nav__item.is-clean .bs-cmp-nav__dot { .bs-cmp-nav__item.is-clean .bs-cmp-nav__dot {
background: #22c55e; background: #22c55e;
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
} }
.bs-cmp-nav__item.has-diff .bs-cmp-nav__dot { .bs-cmp-nav__item.has-diff .bs-cmp-nav__dot {
background: #f59e0b; background: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.22);
} }
.bs-cmp-nav__name { .bs-cmp-nav__name {
@ -11626,6 +11637,10 @@ html.login-page--paused .login-page__flare {
min-width: 0; min-width: 0;
} }
.bs-cmp-nav__item.is-active .bs-cmp-nav__name {
color: #f8fafc;
}
.bs-cmp-main { .bs-cmp-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -11706,13 +11721,13 @@ html.login-page--paused .login-page__flare {
white-space: nowrap; white-space: nowrap;
} }
/* Sheet strip: sheet + kind filters laid out flat (full labels) */ /* Sheet strip: sheet + pass rate + kind filters + search */
.bs-cmp-strip { .bs-cmp-strip {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px 12px; gap: 8px 12px;
min-height: 48px; min-height: 44px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 10px; border-radius: 10px;
border: 1px solid var(--bs-cmp-border); border: 1px solid var(--bs-cmp-border);
@ -11721,11 +11736,17 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-strip.is-ok { .bs-cmp-strip.is-ok {
border-color: rgba(34, 197, 94, 0.35); border-color: rgba(34, 197, 94, 0.32);
background:
linear-gradient(90deg, rgba(34, 197, 94, 0.06), transparent 28%),
var(--bs-cmp-surface);
} }
.bs-cmp-strip.is-warn { .bs-cmp-strip.is-warn {
border-color: rgba(245, 158, 11, 0.4); border-color: rgba(245, 158, 11, 0.38);
background:
linear-gradient(90deg, rgba(245, 158, 11, 0.07), transparent 28%),
var(--bs-cmp-surface);
} }
.bs-cmp-strip__sheet { .bs-cmp-strip__sheet {
@ -11733,7 +11754,7 @@ html.login-page--paused .login-page__flare {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
min-width: 0; min-width: 0;
flex: 1 1 160px; flex: 1 1 180px;
} }
.bs-cmp-strip__sheet-tag { .bs-cmp-strip__sheet-tag {
@ -11763,23 +11784,29 @@ html.login-page--paused .login-page__flare {
.bs-cmp-strip__pass { .bs-cmp-strip__pass {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
gap: 1px; gap: 1px;
min-width: 0; min-width: 0;
padding: 2px 10px;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(8, 13, 24, 0.35);
} }
.bs-cmp-strip__pass-label { .bs-cmp-strip__pass-label {
font-size: 10px; font-size: 10px;
color: var(--bs-cmp-muted); color: var(--bs-cmp-muted);
line-height: 1.2; line-height: 1.2;
white-space: nowrap;
} }
.bs-cmp-strip__pass-value { .bs-cmp-strip__pass-value {
font-size: 20px; font-size: 18px;
font-weight: 800; font-weight: 800;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
letter-spacing: -0.02em; letter-spacing: -0.02em;
line-height: 1.1; line-height: 1.1;
min-width: 4.5ch; min-width: 4.2ch;
} }
.bs-cmp-strip.is-ok .bs-cmp-strip__pass-value { .bs-cmp-strip.is-ok .bs-cmp-strip__pass-value {
@ -11794,24 +11821,28 @@ html.login-page--paused .login-page__flare {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
flex: 2 1 360px; flex: 1 1 280px;
min-width: 0; min-width: 0;
padding: 2px;
border-radius: 8px;
background: rgba(8, 13, 24, 0.28);
} }
.bs-cmp-strip__kind { .bs-cmp-strip__kind {
appearance: none; appearance: none;
flex: 0 0 auto; flex: 0 0 auto;
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
background: rgba(15, 23, 42, 0.55); background: transparent;
color: #cbd5e1; color: #94a3b8;
padding: 5px 10px; padding: 5px 10px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
line-height: 1.35; line-height: 1.35;
white-space: nowrap; white-space: nowrap;
overflow: visible; overflow: visible;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
} }
.bs-cmp-strip__kind b { .bs-cmp-strip__kind b {
@ -11822,13 +11853,21 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-strip__kind:hover { .bs-cmp-strip__kind:hover {
background: rgba(30, 41, 59, 0.9); background: rgba(30, 41, 59, 0.85);
color: #e2e8f0;
} }
.bs-cmp-strip__kind.is-active { .bs-cmp-strip__kind.is-active {
border-color: rgba(59, 130, 246, 0.55); border-color: rgba(59, 130, 246, 0.45);
background: rgba(37, 99, 235, 0.2); background: rgba(37, 99, 235, 0.22);
color: #93c5fd; color: #93c5fd;
font-weight: 600;
}
.bs-cmp-strip__kind--diff.is-active {
border-color: rgba(248, 113, 113, 0.4);
background: rgba(239, 68, 68, 0.14);
color: #fecaca;
} }
.bs-cmp-strip__kind--added b { .bs-cmp-strip__kind--added b {
@ -11844,7 +11883,11 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-strip__kind--unchanged b { .bs-cmp-strip__kind--unchanged b {
color: #94a3b8; color: #4ade80;
}
.bs-cmp-strip__kind--diff b {
color: #f87171;
} }
.bs-cmp-strip__range { .bs-cmp-strip__range {
@ -11897,11 +11940,28 @@ html.login-page--paused .login-page__flare {
.bs-cmp-filter-bar { .bs-cmp-filter-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
flex: 1 1 200px;
min-width: 160px;
max-width: 320px;
margin-left: auto;
min-height: 32px;
}
.bs-cmp-filter-bar .input,
.bs-cmp-filter-bar [data-slot="input"],
.bs-cmp-filter-bar input {
min-width: 0;
width: 100%;
}
.bs-cmp-filter-bar .bs-sheet-count {
flex: 0 0 auto; flex: 0 0 auto;
min-height: 36px; font-size: 11px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
} }
.bs-cmp-kind-pills { .bs-cmp-kind-pills {
@ -11939,8 +11999,9 @@ html.login-page--paused .login-page__flare {
min-height: 240px; min-height: 240px;
max-height: none; max-height: none;
border-color: var(--bs-cmp-border); border-color: var(--bs-cmp-border);
background: rgba(8, 13, 24, 0.35); background: rgba(8, 13, 24, 0.4);
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
border-radius: 10px;
} }
.bs-cmp-result-table.is-loading { .bs-cmp-result-table.is-loading {
@ -11960,6 +12021,12 @@ html.login-page--paused .login-page__flare {
flex: 1 1 100%; flex: 1 1 100%;
} }
.bs-cmp-filter-bar {
flex: 1 1 100%;
max-width: none;
margin-left: 0;
}
.bs-cmp-strip__range { .bs-cmp-strip__range {
margin-left: 0; margin-left: 0;
} }
@ -12001,13 +12068,48 @@ html.login-page--paused .login-page__flare {
.bs-cmp-diff-table thead th { .bs-cmp-diff-table thead th {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 2;
background: rgba(15, 23, 42, 0.96); background: rgba(15, 23, 42, 0.98);
color: #94a3b8; color: #94a3b8;
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.22); box-shadow: 0 1px 0 rgba(148, 163, 184, 0.22);
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
vertical-align: bottom; vertical-align: bottom;
padding: 8px 10px;
}
.bs-cmp-diff-table tbody td {
padding: 7px 10px;
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
}
.bs-cmp-diff-table tbody tr:nth-child(even) td {
background: var(--bs-cmp-row-alt, rgba(30, 41, 59, 0.28));
}
.bs-cmp-diff-table tbody tr:hover td {
background: rgba(37, 99, 235, 0.1);
}
.bs-cmp-sticky-kind {
position: sticky;
left: 0;
z-index: 1;
background: rgba(15, 23, 42, 0.96);
box-shadow: 1px 0 0 rgba(148, 163, 184, 0.16);
}
.bs-cmp-diff-table thead th.bs-cmp-sticky-kind {
z-index: 3;
background: rgba(15, 23, 42, 0.98);
}
.bs-cmp-diff-table tbody tr:nth-child(even) td.bs-cmp-sticky-kind {
background: rgba(22, 32, 51, 0.98);
}
.bs-cmp-diff-table tbody tr:hover td.bs-cmp-sticky-kind {
background: rgba(30, 48, 80, 0.98);
} }
.bs-cmp-th { .bs-cmp-th {
@ -12026,6 +12128,13 @@ html.login-page--paused .login-page__flare {
font-weight: 700; font-weight: 700;
line-height: 1.5; line-height: 1.5;
letter-spacing: 0.02em; letter-spacing: 0.02em;
min-height: 1.5em;
}
.bs-cmp-th__role--spacer {
visibility: hidden;
padding: 0;
background: transparent;
} }
.bs-cmp-th__role--key { .bs-cmp-th__role--key {
@ -12049,11 +12158,11 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-col-key { .bs-cmp-col-key {
background: rgba(59, 130, 246, 0.06); background: rgba(59, 130, 246, 0.05);
} }
.bs-cmp-col-compare { .bs-cmp-col-compare {
background: rgba(245, 158, 11, 0.05); background: rgba(245, 158, 11, 0.04);
} }
.bs-cmp-col-display { .bs-cmp-col-display {
@ -12107,9 +12216,6 @@ html.login-page--paused .login-page__flare {
font-size: 0.75em; font-size: 0.75em;
margin-top: 2px; margin-top: 2px;
} }
.bs-cmp-strip__kind--diff b {
color: #f87171;
}
.bs-cmp-badge { .bs-cmp-badge {
display: inline-block; display: inline-block;
@ -12152,20 +12258,30 @@ html.login-page--paused .login-page__flare {
vertical-align: middle; vertical-align: middle;
} }
.bs-cmp-val-cell--same {
color: #cbd5e1;
}
.bs-cmp-val-cell--same .bs-cmp-val {
color: #cbd5e1;
}
.bs-cmp-val-cell--diff { .bs-cmp-val-cell--diff {
background: rgba(245, 158, 11, 0.08); background: rgba(245, 158, 11, 0.1) !important;
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.18);
} }
.bs-cmp-pair { .bs-cmp-pair {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3px; gap: 2px;
min-width: 4.5rem; min-width: 4.5rem;
padding: 1px 0;
} }
.bs-cmp-pair__row { .bs-cmp-pair__row {
display: grid; display: grid;
grid-template-columns: 1.6em minmax(0, 1fr); grid-template-columns: 1.4em minmax(0, 1fr);
gap: 6px; gap: 6px;
align-items: baseline; align-items: baseline;
} }
@ -12176,15 +12292,20 @@ html.login-page--paused .login-page__flare {
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.02em;
color: #64748b; color: #64748b;
line-height: 1.3; line-height: 1.35;
text-align: center;
border-radius: 3px;
background: rgba(148, 163, 184, 0.12);
} }
.bs-cmp-val-cell--diff .bs-cmp-pair__row:first-child .bs-cmp-pair__tag { .bs-cmp-val-cell--diff .bs-cmp-pair__row:first-child .bs-cmp-pair__tag {
color: #fca5a5; color: #fecaca;
background: rgba(239, 68, 68, 0.18);
} }
.bs-cmp-val-cell--diff .bs-cmp-pair__row:last-child .bs-cmp-pair__tag { .bs-cmp-val-cell--diff .bs-cmp-pair__row:last-child .bs-cmp-pair__tag {
color: #86efac; color: #bbf7d0;
background: rgba(34, 197, 94, 0.18);
} }
.bs-cmp-val { .bs-cmp-val {
@ -12223,11 +12344,13 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-val-cell--pair.is-removed { .bs-cmp-val-cell--pair.is-removed {
background: rgba(239, 68, 68, 0.08); background: rgba(239, 68, 68, 0.1) !important;
box-shadow: inset 0 0 0 1px rgba(248, 113, 113, 0.2);
} }
.bs-cmp-val-cell--pair.is-added { .bs-cmp-val-cell--pair.is-added {
background: rgba(34, 197, 94, 0.08); background: rgba(34, 197, 94, 0.1) !important;
box-shadow: inset 0 0 0 1px rgba(74, 222, 128, 0.2);
} }
.bs-cmp-val-arrow { .bs-cmp-val-arrow {
@ -12237,12 +12360,12 @@ html.login-page--paused .login-page__flare {
color: #94a3b8; color: #94a3b8;
} }
.bs-cmp-row--added { .bs-cmp-row--added td {
background: rgba(34, 197, 94, 0.06); background: rgba(34, 197, 94, 0.05);
} }
.bs-cmp-row--removed { .bs-cmp-row--removed td {
background: rgba(239, 68, 68, 0.06); background: rgba(239, 68, 68, 0.05);
} }
.bs-cmp-row--changed { .bs-cmp-row--changed {
@ -12250,7 +12373,7 @@ html.login-page--paused .login-page__flare {
} }
.bs-cmp-row--unchanged { .bs-cmp-row--unchanged {
opacity: 0.78; opacity: 1;
} }
.bs-cmp-cell--pre { .bs-cmp-cell--pre {

View file

@ -172,6 +172,17 @@ function PairCell(props: {
const post = afterText || "—"; const post = afterText || "—";
const isAdded = kind === "added"; const isAdded = kind === "added";
const isRemoved = kind === "removed"; const isRemoved = kind === "removed";
const isDiff = mismatch || isAdded || isRemoved;
// Matched values: single line (no 前/后 stack) — cuts density on pass-heavy sheets.
if (!isDiff) {
return (
<td className="bs-cmp-val-cell bs-cmp-val-cell--same">
<span className={`bs-cmp-val${beforeText ? "" : " is-empty"}`}>{pre}</span>
</td>
);
}
// Whole-row missing/extra: emphasize the present side; do not strike it out. // Whole-row missing/extra: emphasize the present side; do not strike it out.
const preClass = [ const preClass = [
"bs-cmp-val", "bs-cmp-val",
@ -191,17 +202,21 @@ function PairCell(props: {
.join(" "); .join(" ");
return ( return (
<td <td
className={`bs-cmp-val-cell bs-cmp-val-cell--pair${ className={`bs-cmp-val-cell bs-cmp-val-cell--pair bs-cmp-val-cell--diff${
mismatch || isAdded || isRemoved ? " bs-cmp-val-cell--diff" : "" isAdded ? " is-added" : ""
}${isAdded ? " is-added" : ""}${isRemoved ? " is-removed" : ""}`} }${isRemoved ? " is-removed" : ""}`}
> >
<div className="bs-cmp-pair"> <div className="bs-cmp-pair">
<div className="bs-cmp-pair__row"> <div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{beforeLabel}</span> <span className="bs-cmp-pair__tag" aria-label={beforeLabel}>
{beforeLabel}
</span>
<span className={preClass}>{isAdded ? "—" : pre}</span> <span className={preClass}>{isAdded ? "—" : pre}</span>
</div> </div>
<div className="bs-cmp-pair__row"> <div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{afterLabel}</span> <span className="bs-cmp-pair__tag" aria-label={afterLabel}>
{afterLabel}
</span>
<span className={postClass}>{isRemoved ? "—" : post}</span> <span className={postClass}>{isRemoved ? "—" : post}</span>
</div> </div>
</div> </div>
@ -1184,6 +1199,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const activeSuccess = sheetSuccessOf(activeSheetCard || {}); const activeSuccess = sheetSuccessOf(activeSheetCard || {});
const activePassRate = sheetPassRateOf(activeSheetCard || {}); const activePassRate = sheetPassRateOf(activeSheetCard || {});
const activeAdded = Number(activeSheetCard?.added || 0); const activeAdded = Number(activeSheetCard?.added || 0);
const showFailCol =
kindFilter === "diff" || kindFilter === "all" || kindFilter === "added";
const resultEmptyColSpan =
1 +
(showFailCol ? 1 : 0) +
resultColumns.keys.length +
Math.max(resultColumns.extras.length, 0);
useEffect(() => { useEffect(() => {
const syncFs = () => { const syncFs = () => {
const el = boardRef.current; const el = boardRef.current;
@ -2908,12 +2930,18 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
</strong> </strong>
<span className="muted bs-cmp-strip__sheet-mode"> <span className="muted bs-cmp-strip__sheet-mode">
{activeFail > 0 ? t("bizCompare.kindFail") : t("bizCompare.kindPass")} {activeFail > 0 ? t("bizCompare.kindFail") : t("bizCompare.kindPass")}
{` · ${t("bizCompare.passRateShort")} ${activePassRate}%`}
{activeSheetCard?.mode === "presence" {activeSheetCard?.mode === "presence"
? ` · ${t("bizCompare.presenceShort")}` ? ` · ${t("bizCompare.presenceShort")}`
: ""} : ""}
{` · ${activeSheetCard?.before_count ?? 0}→${activeSheetCard?.after_count ?? 0}`}
</span> </span>
</div> </div>
<div className="bs-cmp-strip__pass" title={t("bizCompare.passRate")}>
<span className="bs-cmp-strip__pass-label">
{t("bizCompare.passRate")}
</span>
<span className="bs-cmp-strip__pass-value">{activePassRate}%</span>
</div>
<div className="bs-cmp-strip__kinds" role="group"> <div className="bs-cmp-strip__kinds" role="group">
{( {(
[ [
@ -2947,20 +2975,16 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
</button> </button>
))} ))}
</div> </div>
<span className="bs-cmp-strip__range muted"> <div className="bs-cmp-filter-bar">
{activeSheetCard?.before_count ?? 0}→{activeSheetCard?.after_count ?? 0} <Input
</span> value={resultKw}
</div> placeholder={t("bizCompare.resultFilterPh")}
onChange={(e) => setResultKw(e.target.value)}
<div className="bs-cmp-filter-bar"> />
<Input <span className="muted bs-sheet-count">
value={resultKw} {diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
placeholder={t("bizCompare.resultFilterPh")} </span>
onChange={(e) => setResultKw(e.target.value)} </div>
/>
<span className="muted bs-sheet-count">
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
</span>
</div> </div>
<div <div
@ -2971,20 +2995,35 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<table className="data-table pt-list-table bs-cmp-diff-table"> <table className="data-table pt-list-table bs-cmp-diff-table">
<thead> <thead>
<tr> <tr>
<th className="bs-cmp-col-kind">{t("bizCompare.colKind")}</th> <th className="bs-cmp-col-kind bs-cmp-sticky-kind">
<th className="bs-cmp-col-fail">{t("bizCompare.colFailFields")}</th> {t("bizCompare.colKind")}
{resultColumns.keys.map((k) => ( </th>
{showFailCol ? (
<th className="bs-cmp-col-fail">{t("bizCompare.colFailFields")}</th>
) : null}
{resultColumns.keys.map((k, ki) => (
<th key={k} className="bs-cmp-col-key"> <th key={k} className="bs-cmp-col-key">
<span className="bs-cmp-th"> <span className="bs-cmp-th">
<span className="bs-cmp-th__role bs-cmp-th__role--key"> {ki === 0 ? (
{t("bizCompare.keyFields")} <span className="bs-cmp-th__role bs-cmp-th__role--key">
</span> {t("bizCompare.keyFields")}
</span>
) : (
<span className="bs-cmp-th__role bs-cmp-th__role--spacer" aria-hidden>
&nbsp;
</span>
)}
<span className="bs-cmp-th__name">{k}</span> <span className="bs-cmp-th__name">{k}</span>
</span> </span>
</th> </th>
))} ))}
{resultColumns.extras.map((f) => { {resultColumns.extras.map((f, fi) => {
const isCmp = resultColumns.compareSet.has(f); const isCmp = resultColumns.compareSet.has(f);
const prev = resultColumns.extras[fi - 1];
const prevCmp = prev
? resultColumns.compareSet.has(prev)
: null;
const showRole = fi === 0 || prevCmp !== isCmp;
return ( return (
<th <th
key={f} key={f}
@ -2993,17 +3032,26 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
} }
> >
<span className="bs-cmp-th"> <span className="bs-cmp-th">
<span {showRole ? (
className={`bs-cmp-th__role ${ <span
isCmp className={`bs-cmp-th__role ${
? "bs-cmp-th__role--compare" isCmp
: "bs-cmp-th__role--display" ? "bs-cmp-th__role--compare"
}`} : "bs-cmp-th__role--display"
> }`}
{isCmp >
? t("bizCompare.compareFields") {isCmp
: t("bizCompare.displayField")} ? t("bizCompare.compareFields")
</span> : t("bizCompare.displayField")}
</span>
) : (
<span
className="bs-cmp-th__role bs-cmp-th__role--spacer"
aria-hidden
>
&nbsp;
</span>
)}
<span className="bs-cmp-th__name">{f}</span> <span className="bs-cmp-th__name">{f}</span>
</span> </span>
</th> </th>
@ -3024,7 +3072,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const isFail = verdict.tone === "fail"; const isFail = verdict.tone === "fail";
return ( return (
<tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}> <tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}>
<td className="bs-cmp-col-kind"> <td className="bs-cmp-col-kind bs-cmp-sticky-kind">
<span className={`bs-cmp-badge bs-cmp-badge--${verdict.tone}`}> <span className={`bs-cmp-badge bs-cmp-badge--${verdict.tone}`}>
{verdict.label} {verdict.label}
</span> </span>
@ -3034,11 +3082,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
</span> </span>
) : null} ) : null}
</td> </td>
<td className="bs-cmp-fail-cell"> {showFailCol ? (
{verdict.tone === "pass" <td className="bs-cmp-fail-cell">
? t("bizCompare.failFieldsEmpty") {verdict.tone === "pass"
: failFieldsLabel(d)} ? t("bizCompare.failFieldsEmpty")
</td> : failFieldsLabel(d)}
</td>
) : null}
{resultColumns.keys.map((k) => ( {resultColumns.keys.map((k) => (
<td key={k} className="bs-cmp-key-cell"> <td key={k} className="bs-cmp-key-cell">
{cellText(d.key?.[k] ?? pre[k] ?? post[k]) || "—"} {cellText(d.key?.[k] ?? pre[k] ?? post[k]) || "—"}
@ -3073,13 +3123,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
})} })}
{runDetail && !diffsLoading && !pagedDiffs.length ? ( {runDetail && !diffsLoading && !pagedDiffs.length ? (
<tr> <tr>
<td <td colSpan={resultEmptyColSpan}>
colSpan={
2 +
resultColumns.keys.length +
Math.max(resultColumns.extras.length, 0)
}
>
<div className="pt-list-empty">{t("bizCompare.resultEmpty")}</div> <div className="pt-list-empty">{t("bizCompare.resultEmpty")}</div>
</td> </td>
</tr> </tr>