Fix SEO workspace scope preview

This commit is contained in:
DCCONSTRUCTIONS 2026-06-28 15:47:15 +03:00
parent d27e6ee43d
commit 1f140c1130
3 changed files with 1204 additions and 221 deletions

View File

@ -2,6 +2,7 @@ import {
Activity,
Archive,
CheckCircle2,
ChevronDown,
CircleAlert,
Copy,
Database,
@ -220,6 +221,10 @@ function getScanStatusLabel(status: string) {
}
function getPageTitle(page: ProjectScanSummary["pages"][number]) {
if (page.scope === "template_block" && page.structureLabel) {
return page.structureLabel;
}
if (isHomePage(page)) {
return page.title || "Главная страница";
}
@ -263,6 +268,30 @@ function isIndexableScanPage(page: ProjectScanSummary["pages"][number]) {
return page.scope === "indexable_page";
}
function isWorkspaceScopePage(page: ProjectScanSummary["pages"][number]) {
if (!page.selected) {
return false;
}
if (page.previewKind === "none" && !page.previewSourcePath && !page.previewTargetSelector) {
return false;
}
return true;
}
function getWorkspaceScopeOrder(page: ProjectScanSummary["pages"][number], originalIndex: number) {
if (isHomePage(page)) {
return -1;
}
if (page.structureOrder != null) {
return page.structureOrder;
}
return 1000 + originalIndex;
}
function getScanPageScopeLabel(scope: ProjectScanSummary["pages"][number]["scope"]) {
const labels = {
admin_page: "админка",
@ -903,7 +932,11 @@ function getSelectedPagesCount(scan: ProjectScanSummary) {
}
function getSelectedCoveragePages(scan: ProjectScanSummary) {
return scan.pages.filter((page) => page.selected && isIndexableScanPage(page));
return scan.pages
.map((page, index) => ({ index, page }))
.filter(({ page }) => isWorkspaceScopePage(page))
.sort((left, right) => getWorkspaceScopeOrder(left.page, left.index) - getWorkspaceScopeOrder(right.page, right.index))
.map(({ page }) => page);
}
function getWorkspaceKey(projectId: string, pageId: string) {
@ -930,6 +963,7 @@ type WorkspaceTarget = {
};
type WorkspacePreviewMode = "desktop" | "mobile";
type WorkspaceDrawerPanel = "fields" | "issues" | "scope";
const WORKSPACE_PREVIEW_MODES: Array<{ id: WorkspacePreviewMode; label: string }> = [
{ id: "desktop", label: "Desktop" },
@ -1369,6 +1403,9 @@ export function App() {
const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({});
const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({});
const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState<Record<string, PreviewMarker[]>>({});
const [workspaceDrawers, setWorkspaceDrawers] = useState<Record<string, WorkspaceDrawerPanel[]>>({});
const [workspacePageMenus, setWorkspacePageMenus] = useState<Record<string, boolean>>({});
const [workspacePreviewPageIds, setWorkspacePreviewPageIds] = useState<Record<string, string>>({});
const [pageWorkspaces, setPageWorkspaces] = useState<Record<string, PageWorkspace>>({});
const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({});
const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({});
@ -1557,27 +1594,51 @@ export function App() {
}, []);
useEffect(() => {
const timers: number[] = [];
if (activeStageIndex !== 2) {
return;
}
for (const [workspaceKey, workspace] of Object.entries(pageWorkspaces)) {
const [projectId = "", pageId = ""] = workspaceKey.split(":");
const scanSummary = scanSummaries[projectId];
const issues = getPageIssues(scanSummary, pageId);
const scrollToWorkspace = () => {
document.querySelector(".page-workspace-panel")?.scrollIntoView({ behavior: "smooth", block: "start" });
};
const firstTimer = window.setTimeout(scrollToWorkspace, 120);
const secondTimer = window.setTimeout(scrollToWorkspace, 650);
return () => {
window.clearTimeout(firstTimer);
window.clearTimeout(secondTimer);
};
}, [activeProjectId, activeStageIndex]);
useEffect(() => {
const timers: number[] = [];
const projectId = activeProjectId ?? "";
const scanSummary = projectId ? scanSummaries[projectId] : null;
if (activeStageIndex !== 2 || !projectId || !scanSummary) {
return () => undefined;
}
const scopePages = getSelectedCoveragePages(scanSummary);
const activePageId = activeWorkspacePageIds[projectId] ?? scopePages[0]?.pageId ?? "";
const workspaceKey = activePageId ? getWorkspaceKey(projectId, activePageId) : "";
const workspace = workspaceKey ? pageWorkspaces[workspaceKey] : null;
if (!workspaceKey || !workspace) {
return () => undefined;
}
const issues = getPageIssues(scanSummary, activePageId);
const targets = buildWorkspaceTargets(workspace, issues);
if (targets.length === 0) {
continue;
}
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 180));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 700));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1600));
}
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 80));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 360));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000));
return () => {
timers.forEach((timer) => window.clearTimeout(timer));
};
}, [activeStageIndex, activeWorkspacePageIds, pageWorkspaces, scanSummaries, workspacePreviewModes]);
}, [activeProjectId, activeStageIndex, activeWorkspacePageIds, pageWorkspaces, scanSummaries, workspacePreviewModes]);
function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) {
window.requestAnimationFrame(() => {
@ -1599,6 +1660,46 @@ export function App() {
const data = event.data;
if (!data || typeof data !== "object" || data.type !== "seo-mode:preview-markers") {
if (data?.type === "seo-mode:active-scope-page") {
const sourceWorkspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : "";
const pageId = typeof data.pageId === "string" ? data.pageId : "";
const [projectId = ""] = sourceWorkspaceKey.split(":");
if (!projectId || !pageId) {
return;
}
const nextWorkspaceKey = getWorkspaceKey(projectId, pageId);
if (!pageWorkspaces[nextWorkspaceKey]) {
const project = projects.find((item) => item.id === projectId);
const scanSummary = scanSummaries[projectId];
const page = scanSummary ? getSelectedCoveragePages(scanSummary).find((item) => item.pageId === pageId) : null;
if (project && page && busyWorkspaceKey !== nextWorkspaceKey) {
void handleOpenPageWorkspace(project, page, {
deferActiveUntilLoaded: true,
navigatePreview: false,
silent: true
});
}
return;
}
setActiveWorkspacePageIds((currentPageIds) => {
if (currentPageIds[projectId] === pageId) {
return currentPageIds;
}
return {
...currentPageIds,
[projectId]: pageId
};
});
return;
}
if (data?.type !== "seo-mode:activate-target") {
return;
}
@ -1619,6 +1720,7 @@ export function App() {
...currentSectionIds,
[workspaceKey]: sectionId || targetId
}));
openWorkspaceDrawer(workspaceKey, "fields");
scrollWorkspaceFieldIntoView(workspaceKey, targetId);
return;
@ -1669,7 +1771,7 @@ export function App() {
return () => {
window.removeEventListener("message", handlePreviewMessage);
};
}, []);
}, [busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]);
useEffect(() => {
if (projects.length === 0) {
@ -2166,7 +2268,7 @@ export function App() {
async function handleOpenPageWorkspace(
project: ProjectSummary,
page: ProjectScanSummary["pages"][number],
options: { silent?: boolean } = {}
options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {}
) {
const workspaceKey = getWorkspaceKey(project.id, page.pageId);
@ -2178,10 +2280,19 @@ export function App() {
setProjectActionProjectId(project.id);
}
if (options.navigatePreview !== false) {
setWorkspacePreviewPageIds((currentPageIds) => ({
...currentPageIds,
[project.id]: page.pageId
}));
}
if (!options.deferActiveUntilLoaded) {
setActiveWorkspacePageIds((currentPageIds) => ({
...currentPageIds,
[project.id]: page.pageId
}));
}
try {
const result = await openPageWorkspace(project.id, page.pageId);
@ -2206,6 +2317,12 @@ export function App() {
...currentTargetIds,
[workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
}));
if (options.deferActiveUntilLoaded) {
setActiveWorkspacePageIds((currentPageIds) => ({
...currentPageIds,
[project.id]: page.pageId
}));
}
if (!options.silent) {
setProjectActionMessage(`Рабочая зона открыта: ${getPageTitle(page)}.`);
@ -2320,11 +2437,67 @@ export function App() {
...currentSectionIds,
[workspaceKey]: target.sectionId
}));
openWorkspaceDrawer(workspaceKey, "fields");
scrollWorkspaceFieldIntoView(workspaceKey, target.id);
}
function updateWorkspacePreviewMarkers(workspaceKey: string, targets: WorkspaceTarget[]) {
function openWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
setWorkspaceDrawers((currentDrawers) => {
const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];
if (currentWorkspaceDrawers.includes(drawer)) {
return currentDrawers;
}
return {
...currentDrawers,
[workspaceKey]: [...currentWorkspaceDrawers, drawer]
};
});
}
function toggleWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
setWorkspaceDrawers((currentDrawers) => {
const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];
const nextWorkspaceDrawers = currentWorkspaceDrawers.includes(drawer)
? currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer)
: [...currentWorkspaceDrawers, drawer];
return {
...currentDrawers,
[workspaceKey]: nextWorkspaceDrawers
};
});
}
function closeWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
setWorkspaceDrawers((currentDrawers) => {
const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];
if (!currentWorkspaceDrawers.includes(drawer)) {
return currentDrawers;
}
return {
...currentDrawers,
[workspaceKey]: currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer)
};
});
}
function toggleWorkspacePageMenu(workspaceKey: string) {
setWorkspacePageMenus((currentMenus) => ({
...currentMenus,
[workspaceKey]: !currentMenus[workspaceKey]
}));
}
function updateWorkspacePreviewMarkers(
workspaceKey: string,
targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = []
) {
const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow;
if (!frameWindow) {
@ -2340,6 +2513,14 @@ export function App() {
{
type: "seo-mode:measure-targets",
workspaceKey,
activePageId: workspaceKey.split(":")[1] ?? "",
scopePages: scopePages
.filter((page) => page.previewTargetSelector)
.map((page) => ({
pageId: page.pageId,
label: getPageTitle(page),
selector: page.previewTargetSelector
})),
targets: targets
.filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0)
.map((target) => ({
@ -2358,20 +2539,29 @@ export function App() {
);
}
function changeWorkspacePreviewMode(workspaceKey: string, mode: WorkspacePreviewMode, targets: WorkspaceTarget[]) {
function changeWorkspacePreviewMode(
workspaceKey: string,
mode: WorkspacePreviewMode,
targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = []
) {
setWorkspacePreviewModes((currentModes) => ({
...currentModes,
[workspaceKey]: mode
}));
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 120);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 450);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 120);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 450);
}
function handleWorkspacePreviewLoad(workspaceKey: string, targets: WorkspaceTarget[]) {
updateWorkspacePreviewMarkers(workspaceKey, targets);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 350);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1000);
function handleWorkspacePreviewLoad(
workspaceKey: string,
targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = []
) {
updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 350);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000);
}
function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) {
@ -2429,6 +2619,11 @@ export function App() {
delete nextPageIds[project.id];
return nextPageIds;
});
setWorkspacePreviewPageIds((currentPageIds) => {
const nextPageIds = { ...currentPageIds };
delete nextPageIds[project.id];
return nextPageIds;
});
setPageWorkspaces((currentWorkspaces) =>
Object.fromEntries(
Object.entries(currentWorkspaces).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
@ -2440,6 +2635,14 @@ export function App() {
setWorkspaceSectionTexts((currentTexts) =>
Object.fromEntries(Object.entries(currentTexts).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)))
);
setWorkspaceDrawers((currentDrawers) =>
Object.fromEntries(
Object.entries(currentDrawers).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
)
);
setWorkspacePageMenus((currentMenus) =>
Object.fromEntries(Object.entries(currentMenus).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)))
);
setActiveWorkspaceSectionIds((currentSectionIds) =>
Object.fromEntries(
Object.entries(currentSectionIds).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
@ -2516,24 +2719,30 @@ export function App() {
continue;
}
const selectedPage = scanSummary.pages.find((page) => page.selected);
const selectedWorkspacePages = getSelectedCoveragePages(scanSummary);
const selectedPage = selectedWorkspacePages[0];
if (!selectedPage) {
continue;
}
const activePageId = activeWorkspacePageIds[project.id] ?? selectedPage.pageId;
const pageToOpen = scanSummary.pages.find((page) => page.pageId === activePageId) ?? selectedPage;
const pageToOpen =
selectedWorkspacePages.find((page) => page.pageId === activePageId) ??
selectedPage;
const workspaceKey = getWorkspaceKey(project.id, pageToOpen.pageId);
if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) {
continue;
}
void handleOpenPageWorkspace(project, pageToOpen, { silent: true });
void handleOpenPageWorkspace(project, pageToOpen, {
navigatePreview: !workspacePreviewPageIds[project.id],
silent: true
});
break;
}
}, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]);
}, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewPageIds]);
const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false;
const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict;
@ -3144,19 +3353,26 @@ export function App() {
null;
const activeWorkspaceMarkers = activeWorkspaceKey ? (workspacePreviewMarkers[activeWorkspaceKey] ?? []) : [];
const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop";
const activeWorkspaceSectionId =
activeWorkspaceKey && activeWorkspace
? (activeWorkspaceTarget?.sectionId ?? activeWorkspaceSectionIds[activeWorkspaceKey] ?? activeWorkspace.sections[0]?.id ?? "")
: "";
const activeWorkspaceSection =
activeWorkspace?.sections.find((section) => section.id === activeWorkspaceSectionId) ??
activeWorkspace?.sections[0] ??
null;
const activeWorkspaceSectionText =
activeWorkspaceKey && activeWorkspaceSection
? (workspaceSectionTexts[activeWorkspaceKey]?.[activeWorkspaceSection.id] ??
activeWorkspaceSection.workingText)
const activeWorkspaceDrawers = activeWorkspaceKey ? (workspaceDrawers[activeWorkspaceKey] ?? []) : [];
const isWorkspacePageMenuOpen = activeWorkspaceKey ? Boolean(workspacePageMenus[activeWorkspaceKey]) : false;
const previewWorkspacePageId = workspacePreviewPageIds[project.id] ?? activeWorkspacePageId;
const previewWorkspacePage =
selectedCoveragePages.find((page) => page.pageId === previewWorkspacePageId) ??
activeWorkspacePage;
const activeWorkspacePageIndex = Math.max(
0,
selectedCoveragePages.findIndex((page) => page.pageId === activeWorkspacePageId)
);
const activeWorkspacePreviewSourcePath =
activeWorkspace && previewWorkspacePage
? (previewWorkspacePage.previewSourcePath ?? previewWorkspacePage.sourcePath)
: "";
const activeWorkspacePreviewFocusSelector = previewWorkspacePage?.previewTargetSelector ?? null;
const activeWorkspaceIssueCounts = getAuditIssueCounts(activeWorkspaceIssues);
const selectedCoverageIssueCount = selectedCoveragePages.reduce(
(total, page) => total + getPageIssues(scanSummary, page.pageId).length,
0
);
const selectedAuditIssuesAll = scanSummary?.audit
? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues)
: [];
@ -4362,7 +4578,7 @@ export function App() {
<section className="page-workspace-panel">
<div className="page-workspace-heading">
<div>
<strong>Рабочая зона выбранных посадочных</strong>
<strong>Рабочая зона выбранного scope</strong>
<small>Original read-only, working draft сохраняется в Postgres + MinIO.</small>
</div>
<span>Apply пока не трогаем</span>
@ -4388,41 +4604,157 @@ export function App() {
})}
</div>
{activeWorkspace && activeWorkspacePage && activeWorkspaceKey ? (
<div className="workspace-editor-stack">
<div className="workspace-editor-stack workspace-cockpit">
<div className="workspace-current-page">
<div>
<span>Выбранная страница</span>
<span>
Активная страница {activeWorkspacePageIndex + 1}/{selectedCoveragePages.length} · выбранный scope
</span>
<strong>{getPageTitle(activeWorkspacePage)}</strong>
<small>{getPagePathLabel(activeWorkspacePage)}</small>
</div>
<em>{activeWorkspaceIssues.length} замечаний</em>
</div>
<div className="workspace-preview-layout">
<section className="workspace-preview-panel">
<div className="workspace-subheading">
<strong>Зеркало страницы</strong>
<small>Оранжевые точки ведут к полям правки ниже.</small>
<section className={`workspace-preview-panel workspace-cockpit-panel ${activeWorkspaceDrawers.length > 0 ? "drawer-open" : ""}`}>
<div className="workspace-cockpit-toolbar">
<div className="workspace-preview-status">
<span>Замечаний на активной странице: {activeWorkspaceIssues.length}</span>
<span>В выбранном scope: {selectedCoverageIssueCount}</span>
<span>Warning: {activeWorkspaceIssueCounts.warnings} · Info: {activeWorkspaceIssueCounts.info}</span>
</div>
<div className="workspace-preview-mode-switch" aria-label="Режим зеркала">
<div className="workspace-current-page-actions">
<div className={`workspace-page-control ${isWorkspacePageMenuOpen ? "open" : ""}`}>
<button
aria-expanded={isWorkspacePageMenuOpen}
aria-haspopup="menu"
className="workspace-page-control-trigger"
onClick={() => toggleWorkspacePageMenu(activeWorkspaceKey)}
type="button"
>
<FileText size={14} />
Страница
<em>
{activeWorkspacePageIndex + 1}/{selectedCoveragePages.length}
</em>
<ChevronDown size={14} />
</button>
{isWorkspacePageMenuOpen ? (
<div className="workspace-page-menu" role="menu">
<div className="workspace-page-menu-head">
<strong>Страницы и секции scope</strong>
<small>Переключает preview, ошибки и поля рабочей зоны.</small>
</div>
{selectedCoveragePages.length > 0 ? (
<div className="workspace-page-menu-list">
{selectedCoveragePages.map((page, pageIndex) => {
const pageWorkspaceKey = getWorkspaceKey(project.id, page.pageId);
const isActivePage = activeWorkspacePageId === page.pageId;
const isOpening = busyWorkspaceKey === pageWorkspaceKey;
const pageIssues = getPageIssues(scanSummary, page.pageId);
return (
<button
className={isActivePage ? "active" : undefined}
disabled={isOpening}
key={`workspace-page-menu-${page.pageId}`}
onClick={() => {
setWorkspacePageMenus((currentMenus) => ({
...currentMenus,
[activeWorkspaceKey]: false
}));
void handleOpenPageWorkspace(project, page, { silent: true });
}}
role="menuitem"
type="button"
>
<span>{pageIndex + 1}</span>
<strong>{getPageTitle(page)}</strong>
<small>{getPagePathLabel(page)}</small>
<em>{pageIssues.length} замечаний</em>
</button>
);
})}
</div>
) : (
<small className="workspace-page-menu-empty">
В выбранном scope пока нет рабочих страниц или секций.
</small>
)}
</div>
) : null}
</div>
<button
className={activeWorkspaceDrawers.includes("scope") ? "active" : undefined}
onClick={() => toggleWorkspaceDrawer(activeWorkspaceKey, "scope")}
type="button"
>
<FileText size={14} />
Scope
<em>{selectedCoveragePages.length}</em>
</button>
<button
className={activeWorkspaceDrawers.includes("issues") ? "active" : undefined}
onClick={() => toggleWorkspaceDrawer(activeWorkspaceKey, "issues")}
type="button"
>
<CircleAlert size={14} />
Ошибки
<em>{activeWorkspaceIssues.length}</em>
</button>
<button
className={activeWorkspaceDrawers.includes("fields") ? "active" : undefined}
onClick={() => toggleWorkspaceDrawer(activeWorkspaceKey, "fields")}
type="button"
>
<FileText size={14} />
Поля
<em>{activeWorkspaceTargets.length}</em>
</button>
<div className="workspace-preview-mode-switch" aria-label="Режим зеркала">
{WORKSPACE_PREVIEW_MODES.map((mode) => (
<button
className={activeWorkspacePreviewMode === mode.id ? "active" : undefined}
key={mode.id}
onClick={() => changeWorkspacePreviewMode(activeWorkspaceKey, mode.id, activeWorkspaceTargets)}
onClick={() =>
changeWorkspacePreviewMode(
activeWorkspaceKey,
mode.id,
activeWorkspaceTargets,
selectedCoveragePages
)
}
type="button"
>
{mode.label}
</button>
))}
</div>
</div>
</div>
<div className="workspace-preview-shell">
<div className={`workspace-preview-frame ${activeWorkspacePreviewMode === "mobile" ? "mobile-preview" : "desktop-preview"}`}>
<iframe
onLoad={() => handleWorkspacePreviewLoad(activeWorkspaceKey, activeWorkspaceTargets)}
onLoad={() =>
handleWorkspacePreviewLoad(
activeWorkspaceKey,
activeWorkspaceTargets,
selectedCoveragePages
)
}
ref={(element) => {
previewFrameRefs.current[activeWorkspaceKey] = element;
if (element) {
previewFrameRefs.current = {
[activeWorkspaceKey]: element
};
} else {
delete previewFrameRefs.current[activeWorkspaceKey];
}
}}
sandbox="allow-scripts allow-forms allow-popups"
src={getPreviewPageUrl(project.id, activeWorkspace.page.sourcePath)}
src={getPreviewPageUrl(
project.id,
activeWorkspacePreviewSourcePath,
activeWorkspacePreviewFocusSelector
)}
title={`Предпросмотр ${getPageTitle(activeWorkspacePage)}`}
/>
<div className="workspace-preview-markers">
@ -4449,77 +4781,98 @@ export function App() {
})}
</div>
</div>
</section>
<section className="workspace-page-issues">
<div className="workspace-subheading">
<strong>Ошибки этой страницы</strong>
<small>{activeWorkspaceIssues.length} шт. из базового аудита</small>
{activeWorkspaceDrawers.length > 0 ? (
<div className="workspace-drawer-stack">
{activeWorkspaceDrawers.map((activeWorkspaceDrawer) => (
<aside className="workspace-drawer open" key={`workspace-drawer-${activeWorkspaceDrawer}`}>
<div className="workspace-drawer-header">
<div>
<span>
{activeWorkspaceDrawer === "scope"
? "Выбранный scope"
: activeWorkspaceDrawer === "fields"
? "Поля для правки"
: "Аудит страницы"}
</span>
<strong>
{activeWorkspaceDrawer === "scope"
? `${selectedCoveragePages.length} в scope`
: activeWorkspaceDrawer === "fields"
? `${activeWorkspaceTargets.length} полей`
: `${activeWorkspaceIssues.length} замечаний`}
</strong>
<small>
{activeWorkspaceDrawer === "scope"
? "Дальше анализируются только выбранные страницы и секции."
: activeWorkspaceDrawer === "fields"
? "Клик по точке или карточке открывает связанное поле."
: "Список относится к активной странице, не ко всему сайту."}
</small>
</div>
{activeWorkspaceIssues.length > 0 ? (
<div className="workspace-issue-list">
{activeWorkspaceIssues.map((issue) => {
const target = activeWorkspaceTargets.find((item) => item.issues.some((targetIssue) => targetIssue.id === issue.id));
<button
aria-label="Закрыть панель"
className="workspace-drawer-close"
onClick={() => closeWorkspaceDrawer(activeWorkspaceKey, activeWorkspaceDrawer)}
type="button"
>
<X size={16} />
</button>
</div>
{activeWorkspaceDrawer === "scope" ? (
<div className="workspace-scope-list">
{selectedCoveragePages.map((page, pageIndex) => {
const pageWorkspaceKey = getWorkspaceKey(project.id, page.pageId);
const isOpening = busyWorkspaceKey === pageWorkspaceKey;
const isActivePage = activeWorkspacePageId === page.pageId;
const pageIssues = getPageIssues(scanSummary, page.pageId);
return (
<button
disabled={!target}
key={`workspace-issue-${issue.id}`}
onClick={() => {
if (!target) return;
activateWorkspaceTarget(activeWorkspaceKey, target);
}}
className={isActivePage ? "active" : undefined}
disabled={isOpening}
key={`workspace-scope-${page.pageId}`}
onClick={() => void handleOpenPageWorkspace(project, page)}
type="button"
>
<span>{getAuditSeverityLabel(issue.severity)}</span>
<strong>{issue.message}</strong>
<small>{issue.field ? `Поле: ${issue.field}` : "Медиа/страница"}</small>
<span>{pageIndex + 1}</span>
<strong>{getPageTitle(page)}</strong>
<small>{getPagePathLabel(page)}</small>
<em>{pageIssues.length} замечаний</em>
</button>
);
})}
</div>
) : (
<small className="workspace-empty">По этой странице ошибок аудита нет.</small>
)}
</section>
</div>
<section className="workspace-section-workbench">
<div className="workspace-subheading">
<strong>Поля для правки</strong>
<small>Title, meta, заголовки, текстовые блоки и media-поля редактируются здесь.</small>
</div>
<div className="section-workbench-grid">
<div className="section-map-list">
{activeWorkspaceTargets.map((target) => (
<button
) : activeWorkspaceDrawer === "fields" ? (
<div className="workspace-fields-drawer">
{activeWorkspaceTargets.length > 0 ? (
activeWorkspaceTargets.map((target) => {
const targetSectionText =
workspaceSectionTexts[activeWorkspaceKey]?.[target.sectionId] ?? target.workingText;
return (
<div
className={
activeWorkspaceTarget?.id === target.id ? "section-map-card workspace-target-card active" : "section-map-card workspace-target-card"
activeWorkspaceTarget?.id === target.id
? "section-editor-card workspace-field-card active"
: "section-editor-card workspace-field-card"
}
key={target.id}
onClick={() => activateWorkspaceTarget(activeWorkspaceKey, target)}
type="button"
id={getWorkspaceFieldDomId(activeWorkspaceKey, target.id)}
key={`workspace-field-${target.id}`}
>
<div>
<span>{target.number}</span>
<small>{getSectionKindLabel(target.kind)}</small>
</div>
<strong>{target.title}</strong>
<p>{target.issues[0]?.message ?? target.subtitle}</p>
<em>{target.workingText.length} симв.</em>
</button>
))}
</div>
{activeWorkspaceTarget ? (
<div className="section-editor-card" id={getWorkspaceFieldDomId(activeWorkspaceKey, activeWorkspaceTarget.id)}>
<div className="workspace-subheading">
<strong>
{activeWorkspaceTarget.number}. {activeWorkspaceTarget.title}
{target.number}. {target.title}
</strong>
<small>{activeWorkspaceTarget.selector ?? "selector пока не задан"}</small>
<small>{target.selector ?? "selector пока не задан"}</small>
</div>
{activeWorkspaceTarget.issues.length > 0 ? (
<div className="workspace-field-meta">
<span>{getSectionKindLabel(target.kind)}</span>
<em>{target.workingText.length} симв.</em>
</div>
{target.issues.length > 0 ? (
<div className="workspace-target-issues">
{activeWorkspaceTarget.issues.map((issue) => (
<small key={`target-issue-${issue.id}`}>
{target.issues.map((issue) => (
<small key={`target-issue-${target.id}-${issue.id}`}>
{getAuditSeverityLabel(issue.severity)} · {issue.message}
</small>
))}
@ -4527,7 +4880,7 @@ export function App() {
) : null}
<label>
<span>Original · только чтение</span>
<pre>{activeWorkspaceTarget.originalText || "Пусто"}</pre>
<pre>{target.originalText || "Пусто"}</pre>
</label>
<label>
<span>Working · редактируется</span>
@ -4537,11 +4890,11 @@ export function App() {
...currentTexts,
[activeWorkspaceKey]: {
...(currentTexts[activeWorkspaceKey] ?? {}),
[activeWorkspaceTarget.sectionId]: event.target.value
[target.sectionId]: event.target.value
}
}))
}
value={activeWorkspaceSectionText}
value={targetSectionText}
/>
</label>
<div className="workspace-actions">
@ -4564,7 +4917,7 @@ export function App() {
...currentTexts,
[activeWorkspaceKey]: {
...(currentTexts[activeWorkspaceKey] ?? {}),
[activeWorkspaceTarget.sectionId]: activeWorkspaceTarget.originalText
[target.sectionId]: target.originalText
}
}))
}
@ -4573,18 +4926,47 @@ export function App() {
<RefreshCw size={14} />
Сбросить секцию
</button>
</div>
</div>
);
})
) : (
<small className="workspace-empty">Для этой страницы пока нет полей, связанных с замечаниями.</small>
)}
</div>
) : (
<>
{activeWorkspaceIssues.length > 0 ? (
<div className="workspace-issue-list">
{activeWorkspaceIssues.map((issue) => {
const target = activeWorkspaceTargets.find((item) => item.issues.some((targetIssue) => targetIssue.id === issue.id));
const explanation = getAuditIssueExplanation(issue.code);
return (
<button
className="tiny-action"
disabled={isWorkspaceBusy}
onClick={() =>
void handleResetPageWorkspace(project.id, activeWorkspace.pageId)
}
disabled={!target}
key={`workspace-issue-${issue.id}`}
onClick={() => {
if (!target) return;
activateWorkspaceTarget(activeWorkspaceKey, target);
}}
type="button"
>
<RefreshCw size={14} />
Сбросить весь draft
<span>{getAuditSeverityLabel(issue.severity)}</span>
<strong>{issue.message}</strong>
<small>{issue.field ? `Поле: ${issue.field}` : "Медиа/страница"}</small>
<p>{explanation.action}</p>
</button>
);
})}
</div>
) : (
<small className="workspace-empty">По активной странице ошибок базового аудита нет.</small>
)}
</>
)}
</aside>
))}
</div>
) : null}
</div>

View File

@ -6317,7 +6317,7 @@ input[type="checkbox"] {
}
}
/* Stage 3: keep the page editor clean. The selected page is the object now,
/* Stage 3: page workspace cockpit. The selected page is the object now,
so project/list scaffolding stays out of the visual hierarchy. */
.stage-3 .project-row {
padding: 0 !important;
@ -6327,6 +6327,26 @@ input[type="checkbox"] {
box-shadow: none !important;
}
.stage-3 .project-setup {
gap: 0 !important;
}
.stage-3 .project-setup > .section-heading {
display: none !important;
}
.stage-3 .project-list-card {
padding: 0 !important;
background: transparent !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.stage-3 .project-list {
gap: 0 !important;
}
.stage-3 .project-row > div > .project-title-row,
.stage-3 .project-row > div > .project-source,
.stage-3 .page-workspace-heading,
@ -6344,18 +6364,16 @@ input[type="checkbox"] {
}
.stage-3 .workspace-editor-stack {
gap: 18px !important;
gap: 14px !important;
}
.workspace-current-page {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
display: grid;
gap: 4px;
padding: 0 4px;
}
.workspace-current-page div {
.workspace-current-page > div {
display: grid;
gap: 4px;
min-width: 0;
@ -6371,9 +6389,11 @@ input[type="checkbox"] {
color: var(--ink);
font-size: clamp(24px, 2.2vw, 34px);
font-weight: 760;
letter-spacing: -0.04em;
letter-spacing: 0;
line-height: 1.05;
overflow-wrap: anywhere;
max-width: 100%;
overflow-wrap: normal;
word-break: normal;
}
.workspace-current-page small {
@ -6383,16 +6403,211 @@ input[type="checkbox"] {
overflow-wrap: anywhere;
}
.workspace-current-page em {
flex: 0 0 auto;
padding: 8px 12px;
.workspace-cockpit-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
padding: 0 2px;
}
.workspace-current-page-actions {
position: relative;
z-index: 10;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.workspace-current-page-actions > button,
.workspace-page-control-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 38px;
padding: 0 13px;
color: var(--ink);
background: #f2f2f4;
background: #fff;
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
font-size: 13px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.workspace-current-page-actions > button:hover,
.workspace-page-control-trigger:hover {
border-color: rgba(24, 32, 29, 0.2);
background: #fafafa;
}
.workspace-current-page-actions > button.active,
.workspace-page-control.open .workspace-page-control-trigger {
color: #fff;
background: var(--ink);
border-color: var(--ink);
}
.workspace-current-page-actions > button em,
.workspace-page-control-trigger em {
flex: 0 0 auto;
min-width: 23px;
height: 23px;
display: inline-grid;
place-items: center;
padding: 0 7px;
color: currentColor;
background: rgba(24, 32, 29, 0.06);
border-radius: 999px;
font-size: 12px;
font-style: normal;
font-weight: 560;
font-weight: 800;
}
.workspace-current-page-actions > button.active em,
.workspace-page-control.open .workspace-page-control-trigger em {
background: rgba(255, 255, 255, 0.16);
}
.workspace-page-control {
position: relative;
flex: 0 0 auto;
}
.workspace-page-control-trigger svg:last-child {
transition: transform 160ms ease;
}
.workspace-page-control.open .workspace-page-control-trigger svg:last-child {
transform: rotate(180deg);
}
.workspace-page-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 30;
display: grid;
gap: 10px;
width: min(380px, calc(100vw - 48px));
max-height: min(420px, calc(100vh - 240px));
padding: 12px;
overflow: hidden;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.72)),
rgba(255, 255, 255, 0.94);
border: 1px solid rgba(24, 32, 29, 0.1);
border-radius: 20px;
box-shadow: 0 22px 70px rgba(24, 32, 29, 0.16);
backdrop-filter: blur(30px) saturate(1.08);
-webkit-backdrop-filter: blur(30px) saturate(1.08);
}
.workspace-page-menu-head {
display: grid;
gap: 3px;
padding: 2px 2px 0;
}
.workspace-page-menu-head strong {
color: var(--ink);
font-size: 14px;
font-weight: 780;
line-height: 1.1;
}
.workspace-page-menu-head small,
.workspace-page-menu-empty {
color: var(--muted);
font-size: 12px;
font-weight: 520;
line-height: 1.35;
}
.workspace-page-menu-list {
display: grid;
gap: 7px;
min-height: 0;
overflow: auto;
padding-right: 2px;
}
.workspace-page-menu-list button {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 3px 9px;
width: 100%;
padding: 10px;
text-align: left;
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--line);
border-radius: 14px;
cursor: pointer;
}
.workspace-page-menu-list button:hover,
.workspace-page-menu-list button.active {
background: #fffaf5;
border-color: rgba(242, 122, 26, 0.32);
}
.workspace-page-menu-list button span {
grid-row: span 2;
width: 26px;
height: 26px;
display: grid;
place-items: center;
color: #fff;
background: var(--accent);
border-radius: 999px;
font-size: 12px;
font-weight: 850;
}
.workspace-page-menu-list button strong,
.workspace-page-menu-list button small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.workspace-page-menu-list button strong {
color: var(--ink);
font-size: 13px;
font-weight: 760;
line-height: 1.15;
white-space: nowrap;
}
.workspace-page-menu-list button small {
color: var(--muted);
font-size: 12px;
font-weight: 520;
line-height: 1.3;
white-space: nowrap;
}
.workspace-page-menu-list button em {
grid-row: span 2;
align-self: center;
padding: 4px 8px;
color: var(--muted);
background: #f6f6f7;
border-radius: 999px;
font-size: 11px;
font-style: normal;
font-weight: 760;
white-space: nowrap;
}
.workspace-current-page-actions .workspace-preview-mode-switch {
min-height: 38px;
background: #fff !important;
}
.stage-3 .workspace-preview-panel,
@ -6406,6 +6621,44 @@ input[type="checkbox"] {
box-shadow: none !important;
}
.stage-3 .workspace-cockpit-panel {
position: relative;
gap: 10px !important;
padding: 12px !important;
overflow: hidden;
border-radius: 22px !important;
}
.workspace-preview-status {
display: flex;
flex: 1 1 360px;
flex-wrap: wrap;
gap: 7px;
padding: 0 2px 2px;
}
.workspace-preview-status span {
min-height: 26px;
display: inline-flex;
align-items: center;
padding: 0 9px;
color: var(--muted);
background: #f6f6f7;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
font-weight: 650;
}
.workspace-preview-shell {
position: relative;
min-height: clamp(620px, 68vh, 840px);
background: #fff;
border: 1px solid var(--line);
border-radius: 18px;
overflow: hidden;
}
.stage-3 .workspace-subheading {
align-items: baseline !important;
padding: 0 2px;
@ -6430,11 +6683,18 @@ input[type="checkbox"] {
border-radius: 22px !important;
}
.stage-3 .workspace-cockpit-panel .workspace-preview-frame {
position: absolute !important;
inset: 0;
border: 0 !important;
border-radius: 0 !important;
}
.stage-3 .workspace-preview-panel iframe {
width: max(1600px, 100%) !important;
width: 100% !important;
max-width: none !important;
height: clamp(620px, 72vh, 860px) !important;
min-height: 620px !important;
height: 100% !important;
min-height: clamp(620px, 68vh, 840px) !important;
background: #fff !important;
border: 0 !important;
border-radius: 0 !important;
@ -6459,7 +6719,238 @@ input[type="checkbox"] {
}
.stage-3 .workspace-preview-frame.desktop-preview iframe {
width: max(1600px, 100%) !important;
width: 100% !important;
}
.workspace-drawer-stack {
position: absolute;
inset: 12px;
z-index: 5;
display: flex;
flex-direction: row-reverse;
align-items: stretch;
justify-content: flex-start;
gap: 12px;
pointer-events: none;
}
.workspace-drawer {
position: relative;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
flex: 0 0 calc((100% - 24px) / 3);
min-width: 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.96);
border: 1px solid rgba(24, 32, 29, 0.12);
border-radius: 18px;
box-shadow: 0 22px 68px rgba(24, 32, 29, 0.14);
pointer-events: auto;
animation: workspaceDrawerIn 180ms cubic-bezier(0.2, 0.82, 0.2, 1);
}
.workspace-drawer.open {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
@keyframes workspaceDrawerIn {
from {
opacity: 0;
transform: translateX(18px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.workspace-drawer-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--line);
}
.workspace-drawer-header > div {
display: grid;
gap: 4px;
min-width: 0;
}
.workspace-drawer-header span {
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.workspace-drawer-header strong {
color: var(--ink);
font-size: 22px;
font-weight: 760;
letter-spacing: -0.03em;
line-height: 1;
}
.workspace-drawer-header small {
color: var(--muted);
font-size: 12px;
font-weight: 520;
line-height: 1.35;
}
.workspace-drawer-close {
width: 34px;
height: 34px;
display: grid;
flex: 0 0 auto;
place-items: center;
padding: 0;
color: var(--ink);
background: #f7f7f8;
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
}
.workspace-drawer-close:hover {
background: #efeff1;
}
.workspace-scope-list,
.workspace-fields-drawer,
.workspace-drawer > .workspace-issue-list {
min-height: 0;
overflow: auto;
padding: 12px;
}
.workspace-scope-list {
display: grid;
gap: 8px;
}
.workspace-scope-list button {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 4px 9px;
align-items: start;
width: 100%;
padding: 11px;
text-align: left;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
cursor: pointer;
}
.workspace-scope-list button:hover,
.workspace-scope-list button.active {
background: #fffaf5;
border-color: rgba(242, 122, 26, 0.34);
}
.workspace-scope-list button span {
grid-row: span 2;
width: 26px;
height: 26px;
display: grid;
place-items: center;
color: var(--ink);
background: #f3f3f4;
border-radius: 999px;
font-size: 12px;
font-weight: 850;
}
.workspace-scope-list button.active span {
color: #fff;
background: var(--accent);
}
.workspace-scope-list strong,
.workspace-scope-list small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.workspace-scope-list strong {
color: var(--ink);
font-size: 13px;
font-weight: 760;
white-space: nowrap;
}
.workspace-scope-list small {
color: var(--muted);
font-size: 12px;
font-weight: 520;
white-space: nowrap;
}
.workspace-scope-list em {
grid-row: span 2;
align-self: center;
padding: 4px 8px;
color: var(--muted);
background: #f6f6f7;
border-radius: 999px;
font-size: 11px;
font-style: normal;
font-weight: 760;
white-space: nowrap;
}
.workspace-fields-drawer {
display: grid;
gap: 12px;
}
.workspace-fields-drawer .section-editor-card {
padding: 14px !important;
border-radius: 16px !important;
}
.workspace-fields-drawer .workspace-field-card.active {
background: #fffaf5 !important;
border-color: rgba(242, 122, 26, 0.36) !important;
}
.workspace-field-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.workspace-field-meta span,
.workspace-field-meta em {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 8px;
color: var(--muted);
background: #f5f5f6;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 11px;
font-style: normal;
font-weight: 760;
line-height: 1;
}
.workspace-fields-drawer .section-editor-card pre {
max-height: 140px;
overflow: auto;
}
.workspace-fields-drawer .section-editor-card textarea {
min-height: 170px;
}
.stage-3 .workspace-page-issues {
@ -6485,6 +6976,21 @@ input[type="checkbox"] {
transform: none !important;
}
.stage-3 .workspace-issue-list button {
grid-template-columns: auto minmax(0, 1fr) !important;
padding: 11px !important;
border-radius: 14px !important;
}
.stage-3 .workspace-issue-list button p {
grid-column: 2;
margin: 0;
color: var(--muted);
font-size: 12px;
font-weight: 520;
line-height: 1.35;
}
.stage-3 .section-map-card.active,
.stage-3 .section-map-card.active:hover {
color: var(--ink) !important;
@ -6524,9 +7030,33 @@ input[type="checkbox"] {
}
@media (max-width: 760px) {
.workspace-current-page {
.workspace-cockpit-toolbar,
.workspace-current-page-actions {
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
}
.workspace-current-page-actions,
.workspace-current-page-actions > button,
.workspace-page-control,
.workspace-page-control-trigger,
.workspace-preview-mode-switch {
width: 100%;
}
.workspace-preview-shell {
min-height: 720px;
}
.workspace-drawer-stack {
inset: 8px;
flex-direction: column-reverse;
justify-content: flex-start;
}
.workspace-drawer {
flex: 0 0 min(76%, 620px);
}
}

View File

@ -337,7 +337,7 @@ function buildPreviewBridgeScript() {
return `
<script>
(() => {
const state = { workspaceKey: "", targets: [] };
const state = { workspaceKey: "", targets: [], scopePages: [], activeScopePageId: "", lastScopeReportAt: 0 };
const layerId = "seo-mode-preview-marker-layer";
let frame = 0;
let lastActivation = { id: "", at: 0 };
@ -525,6 +525,73 @@ function buildPreviewBridgeScript() {
return marker;
}
function getElementForSelector(selector) {
if (!selector) return null;
try {
return document.querySelector(selector);
} catch {
return null;
}
}
function reportActiveScopePage() {
if (!state.workspaceKey || !Array.isArray(state.scopePages) || state.scopePages.length === 0) {
return;
}
let activeAtProbe = null;
let nearestBelow = null;
let lastAbove = null;
const probeLine = Math.min(Math.max(window.innerHeight * 0.38, 180), Math.max(180, window.innerHeight - 120));
for (const page of state.scopePages) {
const element = getElementForSelector(page.selector);
if (!element) continue;
const rect = element.getBoundingClientRect();
if (rect.height <= 0 || rect.width <= 0) continue;
if (rect.top <= probeLine && rect.bottom > probeLine) {
if (!activeAtProbe || rect.top > activeAtProbe.top) {
activeAtProbe = { pageId: page.pageId, top: rect.top };
}
continue;
}
if (rect.top > probeLine) {
if (!nearestBelow || rect.top < nearestBelow.top) {
nearestBelow = { pageId: page.pageId, top: rect.top };
}
continue;
}
if (!lastAbove || rect.bottom > lastAbove.bottom) {
lastAbove = { pageId: page.pageId, bottom: rect.bottom };
}
}
const bestPage = activeAtProbe || nearestBelow || lastAbove;
if (!bestPage || bestPage.pageId === state.activeScopePageId) {
return;
}
const now = Date.now();
if (now - state.lastScopeReportAt < 120) {
return;
}
state.activeScopePageId = bestPage.pageId;
state.lastScopeReportAt = now;
window.parent?.postMessage({
type: "seo-mode:active-scope-page",
workspaceKey: state.workspaceKey,
pageId: bestPage.pageId
}, "*");
}
function measure() {
const layer = ensureLayer();
const root = document.documentElement;
@ -609,6 +676,8 @@ function buildPreviewBridgeScript() {
);
});
}
reportActiveScopePage();
}
window.addEventListener("message", (event) => {
@ -620,6 +689,8 @@ function buildPreviewBridgeScript() {
state.workspaceKey = data.workspaceKey || "";
state.targets = Array.isArray(data.targets) ? data.targets : [];
state.scopePages = Array.isArray(data.scopePages) ? data.scopePages : [];
state.activeScopePageId = data.activePageId || state.activeScopePageId || "";
scheduleMeasure();
});