mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Fix compare board scroll sticky headers and CSS fullscreen interactions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
28aa7cd87e
commit
83be9110c8
2 changed files with 238 additions and 135 deletions
|
|
@ -11279,9 +11279,26 @@ html.login-page--paused .login-page__flare {
|
|||
max-width: min(96vw, 1600px) !important;
|
||||
}
|
||||
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"],
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body {
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"].bs-cmp-job-body,
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body.bs-cmp-job-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-height: min(90vh, 980px);
|
||||
min-height: min(64vh, 680px);
|
||||
}
|
||||
|
||||
.bs-cmp-board-modal .bs-cmp-job-body > .nm-config-modal__tabs {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog [data-slot="modal-body"].bs-cmp-job-body > .bs-cmp-board,
|
||||
.bs-cmp-board-modal .app-heroui-modal__dialog .modal__body.bs-cmp-job-body > .bs-cmp-board,
|
||||
.bs-cmp-board-modal .bs-cmp-job-body > .bs-cmp-board {
|
||||
flex: 1 1 auto !important;
|
||||
flex-shrink: 1 !important;
|
||||
min-height: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bs-cmp-board {
|
||||
|
|
@ -11304,16 +11321,28 @@ html.login-page--paused .login-page__flare {
|
|||
rgba(8, 13, 24, 0.4);
|
||||
}
|
||||
|
||||
.bs-cmp-board.is-fullscreen,
|
||||
.bs-cmp-board:fullscreen {
|
||||
/* CSS immersive board — avoids native Fullscreen + modal focus-trap breaking clicks */
|
||||
.bs-cmp-board.is-fullscreen {
|
||||
--bs-cmp-surface: rgba(15, 23, 42, 0.92);
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 12000;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
padding: 12px 14px;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
body:has(.bs-cmp-board.is-fullscreen) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bs-cmp-board__toolbar {
|
||||
|
|
@ -11478,6 +11507,7 @@ html.login-page--paused .login-page__flare {
|
|||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
transition: grid-template-columns 0.18s ease;
|
||||
}
|
||||
|
||||
|
|
@ -11726,6 +11756,12 @@ html.login-page--paused .login-page__flare {
|
|||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bs-cmp-main > .pt-list-pager,
|
||||
.bs-cmp-main > .pager {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Run-level overview: never changes when switching sheets */
|
||||
|
|
@ -12075,21 +12111,28 @@ html.login-page--paused .login-page__flare {
|
|||
|
||||
.bs-cmp-result-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 240px;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
border-color: var(--bs-cmp-border);
|
||||
background: rgba(8, 13, 24, 0.4);
|
||||
transition: opacity 0.15s ease;
|
||||
border-radius: 10px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.bs-cmp-result-table.bs-sheet-table {
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-result-table.is-loading {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
/* Keep header/filters clickable; only mute row hit-testing via opacity cue */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bs-cmp-board.is-fullscreen .bs-cmp-result-table,
|
||||
.bs-cmp-board:fullscreen .bs-cmp-result-table {
|
||||
.bs-cmp-board.is-fullscreen .bs-cmp-result-table {
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -12154,19 +12197,59 @@ html.login-page--paused .login-page__flare {
|
|||
.bs-cmp-diff-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
--bs-cmp-group-h: 28px;
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
color: #94a3b8;
|
||||
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.22);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead tr.bs-cmp-group-row th {
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 5px 10px;
|
||||
height: var(--bs-cmp-group-h);
|
||||
box-shadow:
|
||||
0 1px 0 rgba(148, 163, 184, 0.18),
|
||||
inset 0 -1px 0 rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead tr.bs-cmp-field-row th {
|
||||
top: var(--bs-cmp-group-h);
|
||||
z-index: 2;
|
||||
vertical-align: bottom;
|
||||
padding: 8px 10px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.bs-cmp-group--key {
|
||||
background: rgba(37, 99, 235, 0.18) !important;
|
||||
color: #93c5fd !important;
|
||||
border-bottom: 2px solid rgba(59, 130, 246, 0.55);
|
||||
}
|
||||
|
||||
.bs-cmp-group--compare {
|
||||
background: rgba(245, 158, 11, 0.16) !important;
|
||||
color: #fcd34d !important;
|
||||
border-bottom: 2px solid rgba(245, 158, 11, 0.55);
|
||||
}
|
||||
|
||||
.bs-cmp-group--display {
|
||||
background: rgba(148, 163, 184, 0.12) !important;
|
||||
color: #cbd5e1 !important;
|
||||
border-bottom: 2px solid rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table tbody td {
|
||||
|
|
@ -12191,8 +12274,16 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
|
||||
.bs-cmp-diff-table thead th.bs-cmp-sticky-kind {
|
||||
z-index: 3;
|
||||
z-index: 4;
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead th.bs-cmp-col-fail {
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table tbody tr:nth-child(even) td.bs-cmp-sticky-kind {
|
||||
|
|
@ -12203,51 +12294,23 @@ html.login-page--paused .login-page__flare {
|
|||
background: rgba(30, 48, 80, 0.98);
|
||||
}
|
||||
|
||||
.bs-cmp-th {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-th__role {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.02em;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
.bs-cmp-th__role--spacer {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bs-cmp-th__role--key {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.bs-cmp-th__role--compare {
|
||||
background: rgba(245, 158, 11, 0.22);
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
.bs-cmp-th__role--display {
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.bs-cmp-th__name {
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-key {
|
||||
background: rgba(22, 36, 66, 0.98);
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-compare {
|
||||
background: rgba(42, 32, 16, 0.98);
|
||||
}
|
||||
|
||||
.bs-cmp-diff-table thead tr.bs-cmp-field-row th.bs-cmp-col-display {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
}
|
||||
|
||||
.bs-cmp-col-key {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Input, Modal } from "@heroui/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { ListPager } from "../../components/ListPager";
|
||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||
|
|
@ -829,6 +829,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const [pagedDiffs, setPagedDiffs] = useState<DiffRow[]>([]);
|
||||
const [diffsLoading, setDiffsLoading] = useState(false);
|
||||
const boardRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableScrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableScrollPosRef = useRef({ top: 0, left: 0 });
|
||||
const [boardFs, setBoardFs] = useState(false);
|
||||
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||
const tplImportRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
@ -1207,24 +1209,38 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
(showFailCol ? 1 : 0) +
|
||||
resultColumns.keys.length +
|
||||
Math.max(resultColumns.extras.length, 0);
|
||||
useEffect(() => {
|
||||
const syncFs = () => {
|
||||
const el = boardRef.current;
|
||||
setBoardFs(Boolean(el && document.fullscreenElement === el));
|
||||
};
|
||||
document.addEventListener("fullscreenchange", syncFs);
|
||||
return () => document.removeEventListener("fullscreenchange", syncFs);
|
||||
|
||||
const rememberTableScroll = useCallback(() => {
|
||||
const wrap = tableScrollRef.current;
|
||||
if (!wrap) return;
|
||||
tableScrollPosRef.current = { top: wrap.scrollTop, left: wrap.scrollLeft };
|
||||
}, []);
|
||||
|
||||
const toggleBoardFullscreen = async () => {
|
||||
const el = boardRef.current;
|
||||
if (!el) return;
|
||||
try {
|
||||
if (document.fullscreenElement === el) await document.exitFullscreen();
|
||||
else await el.requestFullscreen();
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!boardFs) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
rememberTableScroll();
|
||||
setBoardFs(false);
|
||||
};
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
return () => document.removeEventListener("keydown", onKey, true);
|
||||
}, [boardFs, rememberTableScroll]);
|
||||
|
||||
// Keep table scroll across fullscreen enter/exit (layout swap otherwise jumps to top).
|
||||
useLayoutEffect(() => {
|
||||
const wrap = tableScrollRef.current;
|
||||
if (!wrap) return;
|
||||
const { top, left } = tableScrollPosRef.current;
|
||||
wrap.scrollTop = top;
|
||||
wrap.scrollLeft = left;
|
||||
}, [boardFs]);
|
||||
|
||||
const toggleBoardFullscreen = () => {
|
||||
rememberTableScroll();
|
||||
setBoardFs((v) => !v);
|
||||
};
|
||||
|
||||
const downloadRunTables = async () => {
|
||||
|
|
@ -1689,6 +1705,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
};
|
||||
|
||||
const closeJob = () => {
|
||||
setBoardFs(false);
|
||||
setJobId("");
|
||||
setRuns([]);
|
||||
setRunDetail(null);
|
||||
|
|
@ -2657,6 +2674,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<AppModalShell
|
||||
open={Boolean(jobId)}
|
||||
onClose={closeJob}
|
||||
dismissible={!boardFs}
|
||||
size={jobDetailTab === "result" ? "cover" : "lg"}
|
||||
className={`app-heroui-modal--xl${jobDetailTab === "result" ? " bs-cmp-board-modal" : ""}`}
|
||||
>
|
||||
|
|
@ -2811,7 +2829,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={!runDetail}
|
||||
onPress={() => void toggleBoardFullscreen()}
|
||||
onPress={() => toggleBoardFullscreen()}
|
||||
>
|
||||
{boardFs ? t("bizCompare.exitFullscreen") : t("bizCompare.fullscreen")}
|
||||
</Button>
|
||||
|
|
@ -3020,76 +3038,98 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</div>
|
||||
|
||||
<div
|
||||
ref={tableScrollRef}
|
||||
onScroll={rememberTableScroll}
|
||||
className={`pt-list-table-wrap bs-sheet-table bs-cmp-result-table${
|
||||
diffsLoading ? " is-loading" : ""
|
||||
}`}
|
||||
>
|
||||
<table className="data-table pt-list-table bs-cmp-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="bs-cmp-col-kind bs-cmp-sticky-kind">
|
||||
{t("bizCompare.colKind")}
|
||||
</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">
|
||||
<span className="bs-cmp-th">
|
||||
{ki === 0 ? (
|
||||
<span className="bs-cmp-th__role bs-cmp-th__role--key">
|
||||
{t("bizCompare.keyFields")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bs-cmp-th__role bs-cmp-th__role--spacer" aria-hidden>
|
||||
|
||||
</span>
|
||||
)}
|
||||
<span className="bs-cmp-th__name">{k}</span>
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
{resultColumns.extras.map((f, fi) => {
|
||||
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 (
|
||||
<th
|
||||
key={f}
|
||||
className={
|
||||
isCmp ? "bs-cmp-col-compare" : "bs-cmp-col-display"
|
||||
}
|
||||
>
|
||||
<span className="bs-cmp-th">
|
||||
{showRole ? (
|
||||
<span
|
||||
className={`bs-cmp-th__role ${
|
||||
isCmp
|
||||
? "bs-cmp-th__role--compare"
|
||||
: "bs-cmp-th__role--display"
|
||||
}`}
|
||||
{(() => {
|
||||
const keyCols = resultColumns.keys;
|
||||
const compareCols = resultColumns.extras.filter((f) =>
|
||||
resultColumns.compareSet.has(f),
|
||||
);
|
||||
const displayCols = resultColumns.extras.filter(
|
||||
(f) => !resultColumns.compareSet.has(f),
|
||||
);
|
||||
const hasGroups =
|
||||
keyCols.length + compareCols.length + displayCols.length > 0;
|
||||
return (
|
||||
<>
|
||||
{hasGroups ? (
|
||||
<tr className="bs-cmp-group-row">
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="bs-cmp-col-kind bs-cmp-sticky-kind"
|
||||
>
|
||||
{t("bizCompare.colKind")}
|
||||
</th>
|
||||
{showFailCol ? (
|
||||
<th rowSpan={2} className="bs-cmp-col-fail">
|
||||
{t("bizCompare.colFailFields")}
|
||||
</th>
|
||||
) : null}
|
||||
{keyCols.length ? (
|
||||
<th
|
||||
colSpan={keyCols.length}
|
||||
className="bs-cmp-group bs-cmp-group--key"
|
||||
>
|
||||
{isCmp
|
||||
? t("bizCompare.compareFields")
|
||||
: t("bizCompare.displayField")}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="bs-cmp-th__role bs-cmp-th__role--spacer"
|
||||
aria-hidden
|
||||
{t("bizCompare.keyFields")}
|
||||
</th>
|
||||
) : null}
|
||||
{compareCols.length ? (
|
||||
<th
|
||||
colSpan={compareCols.length}
|
||||
className="bs-cmp-group bs-cmp-group--compare"
|
||||
>
|
||||
|
||||
</span>
|
||||
)}
|
||||
<span className="bs-cmp-th__name">{f}</span>
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{t("bizCompare.compareFields")}
|
||||
</th>
|
||||
) : null}
|
||||
{displayCols.length ? (
|
||||
<th
|
||||
colSpan={displayCols.length}
|
||||
className="bs-cmp-group bs-cmp-group--display"
|
||||
>
|
||||
{t("bizCompare.displayField")}
|
||||
</th>
|
||||
) : null}
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th className="bs-cmp-col-kind bs-cmp-sticky-kind">
|
||||
{t("bizCompare.colKind")}
|
||||
</th>
|
||||
{showFailCol ? (
|
||||
<th className="bs-cmp-col-fail">
|
||||
{t("bizCompare.colFailFields")}
|
||||
</th>
|
||||
) : null}
|
||||
</tr>
|
||||
)}
|
||||
{hasGroups ? (
|
||||
<tr className="bs-cmp-field-row">
|
||||
{keyCols.map((k) => (
|
||||
<th key={k} className="bs-cmp-col-key">
|
||||
<span className="bs-cmp-th__name">{k}</span>
|
||||
</th>
|
||||
))}
|
||||
{compareCols.map((f) => (
|
||||
<th key={f} className="bs-cmp-col-compare">
|
||||
<span className="bs-cmp-th__name">{f}</span>
|
||||
</th>
|
||||
))}
|
||||
{displayCols.map((f) => (
|
||||
<th key={f} className="bs-cmp-col-display">
|
||||
<span className="bs-cmp-th__name">{f}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</thead>
|
||||
<tbody>
|
||||
{pagedDiffs.map((d, i) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue