Fix SEO workspace scope preview
This commit is contained in:
parent
d27e6ee43d
commit
1f140c1130
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Activity,
|
Activity,
|
||||||
Archive,
|
Archive,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
ChevronDown,
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
Copy,
|
Copy,
|
||||||
Database,
|
Database,
|
||||||
|
|
@ -220,6 +221,10 @@ function getScanStatusLabel(status: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPageTitle(page: ProjectScanSummary["pages"][number]) {
|
function getPageTitle(page: ProjectScanSummary["pages"][number]) {
|
||||||
|
if (page.scope === "template_block" && page.structureLabel) {
|
||||||
|
return page.structureLabel;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHomePage(page)) {
|
if (isHomePage(page)) {
|
||||||
return page.title || "Главная страница";
|
return page.title || "Главная страница";
|
||||||
}
|
}
|
||||||
|
|
@ -263,6 +268,30 @@ function isIndexableScanPage(page: ProjectScanSummary["pages"][number]) {
|
||||||
return page.scope === "indexable_page";
|
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"]) {
|
function getScanPageScopeLabel(scope: ProjectScanSummary["pages"][number]["scope"]) {
|
||||||
const labels = {
|
const labels = {
|
||||||
admin_page: "админка",
|
admin_page: "админка",
|
||||||
|
|
@ -903,7 +932,11 @@ function getSelectedPagesCount(scan: ProjectScanSummary) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedCoveragePages(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) {
|
function getWorkspaceKey(projectId: string, pageId: string) {
|
||||||
|
|
@ -930,6 +963,7 @@ type WorkspaceTarget = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkspacePreviewMode = "desktop" | "mobile";
|
type WorkspacePreviewMode = "desktop" | "mobile";
|
||||||
|
type WorkspaceDrawerPanel = "fields" | "issues" | "scope";
|
||||||
|
|
||||||
const WORKSPACE_PREVIEW_MODES: Array<{ id: WorkspacePreviewMode; label: string }> = [
|
const WORKSPACE_PREVIEW_MODES: Array<{ id: WorkspacePreviewMode; label: string }> = [
|
||||||
{ id: "desktop", label: "Desktop" },
|
{ id: "desktop", label: "Desktop" },
|
||||||
|
|
@ -1369,6 +1403,9 @@ export function App() {
|
||||||
const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({});
|
const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({});
|
||||||
const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({});
|
const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({});
|
||||||
const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState<Record<string, PreviewMarker[]>>({});
|
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 [pageWorkspaces, setPageWorkspaces] = useState<Record<string, PageWorkspace>>({});
|
||||||
const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({});
|
const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({});
|
||||||
const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({});
|
const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({});
|
||||||
|
|
@ -1557,27 +1594,51 @@ export function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timers: number[] = [];
|
if (activeStageIndex !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [workspaceKey, workspace] of Object.entries(pageWorkspaces)) {
|
const scrollToWorkspace = () => {
|
||||||
const [projectId = "", pageId = ""] = workspaceKey.split(":");
|
document.querySelector(".page-workspace-panel")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
const scanSummary = scanSummaries[projectId];
|
};
|
||||||
const issues = getPageIssues(scanSummary, pageId);
|
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);
|
const targets = buildWorkspaceTargets(workspace, issues);
|
||||||
|
|
||||||
if (targets.length === 0) {
|
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 80));
|
||||||
continue;
|
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 360));
|
||||||
}
|
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000));
|
||||||
|
|
||||||
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 180));
|
|
||||||
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 700));
|
|
||||||
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1600));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
timers.forEach((timer) => window.clearTimeout(timer));
|
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) {
|
function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
|
@ -1599,6 +1660,46 @@ export function App() {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
|
|
||||||
if (!data || typeof data !== "object" || data.type !== "seo-mode:preview-markers") {
|
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") {
|
if (data?.type !== "seo-mode:activate-target") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1619,6 +1720,7 @@ export function App() {
|
||||||
...currentSectionIds,
|
...currentSectionIds,
|
||||||
[workspaceKey]: sectionId || targetId
|
[workspaceKey]: sectionId || targetId
|
||||||
}));
|
}));
|
||||||
|
openWorkspaceDrawer(workspaceKey, "fields");
|
||||||
|
|
||||||
scrollWorkspaceFieldIntoView(workspaceKey, targetId);
|
scrollWorkspaceFieldIntoView(workspaceKey, targetId);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1669,7 +1771,7 @@ export function App() {
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handlePreviewMessage);
|
window.removeEventListener("message", handlePreviewMessage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
|
|
@ -2166,7 +2268,7 @@ export function App() {
|
||||||
async function handleOpenPageWorkspace(
|
async function handleOpenPageWorkspace(
|
||||||
project: ProjectSummary,
|
project: ProjectSummary,
|
||||||
page: ProjectScanSummary["pages"][number],
|
page: ProjectScanSummary["pages"][number],
|
||||||
options: { silent?: boolean } = {}
|
options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const workspaceKey = getWorkspaceKey(project.id, page.pageId);
|
const workspaceKey = getWorkspaceKey(project.id, page.pageId);
|
||||||
|
|
||||||
|
|
@ -2178,10 +2280,19 @@ export function App() {
|
||||||
setProjectActionProjectId(project.id);
|
setProjectActionProjectId(project.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.navigatePreview !== false) {
|
||||||
|
setWorkspacePreviewPageIds((currentPageIds) => ({
|
||||||
|
...currentPageIds,
|
||||||
|
[project.id]: page.pageId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.deferActiveUntilLoaded) {
|
||||||
setActiveWorkspacePageIds((currentPageIds) => ({
|
setActiveWorkspacePageIds((currentPageIds) => ({
|
||||||
...currentPageIds,
|
...currentPageIds,
|
||||||
[project.id]: page.pageId
|
[project.id]: page.pageId
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await openPageWorkspace(project.id, page.pageId);
|
const result = await openPageWorkspace(project.id, page.pageId);
|
||||||
|
|
@ -2206,6 +2317,12 @@ export function App() {
|
||||||
...currentTargetIds,
|
...currentTargetIds,
|
||||||
[workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
|
[workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
|
||||||
}));
|
}));
|
||||||
|
if (options.deferActiveUntilLoaded) {
|
||||||
|
setActiveWorkspacePageIds((currentPageIds) => ({
|
||||||
|
...currentPageIds,
|
||||||
|
[project.id]: page.pageId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
setProjectActionMessage(`Рабочая зона открыта: ${getPageTitle(page)}.`);
|
setProjectActionMessage(`Рабочая зона открыта: ${getPageTitle(page)}.`);
|
||||||
|
|
@ -2320,11 +2437,67 @@ export function App() {
|
||||||
...currentSectionIds,
|
...currentSectionIds,
|
||||||
[workspaceKey]: target.sectionId
|
[workspaceKey]: target.sectionId
|
||||||
}));
|
}));
|
||||||
|
openWorkspaceDrawer(workspaceKey, "fields");
|
||||||
|
|
||||||
scrollWorkspaceFieldIntoView(workspaceKey, target.id);
|
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;
|
const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow;
|
||||||
|
|
||||||
if (!frameWindow) {
|
if (!frameWindow) {
|
||||||
|
|
@ -2340,6 +2513,14 @@ export function App() {
|
||||||
{
|
{
|
||||||
type: "seo-mode:measure-targets",
|
type: "seo-mode:measure-targets",
|
||||||
workspaceKey,
|
workspaceKey,
|
||||||
|
activePageId: workspaceKey.split(":")[1] ?? "",
|
||||||
|
scopePages: scopePages
|
||||||
|
.filter((page) => page.previewTargetSelector)
|
||||||
|
.map((page) => ({
|
||||||
|
pageId: page.pageId,
|
||||||
|
label: getPageTitle(page),
|
||||||
|
selector: page.previewTargetSelector
|
||||||
|
})),
|
||||||
targets: targets
|
targets: targets
|
||||||
.filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0)
|
.filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0)
|
||||||
.map((target) => ({
|
.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) => ({
|
setWorkspacePreviewModes((currentModes) => ({
|
||||||
...currentModes,
|
...currentModes,
|
||||||
[workspaceKey]: mode
|
[workspaceKey]: mode
|
||||||
}));
|
}));
|
||||||
|
|
||||||
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 120);
|
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 120);
|
||||||
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 450);
|
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 450);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkspacePreviewLoad(workspaceKey: string, targets: WorkspaceTarget[]) {
|
function handleWorkspacePreviewLoad(
|
||||||
updateWorkspacePreviewMarkers(workspaceKey, targets);
|
workspaceKey: string,
|
||||||
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 350);
|
targets: WorkspaceTarget[],
|
||||||
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1000);
|
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) {
|
function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) {
|
||||||
|
|
@ -2429,6 +2619,11 @@ export function App() {
|
||||||
delete nextPageIds[project.id];
|
delete nextPageIds[project.id];
|
||||||
return nextPageIds;
|
return nextPageIds;
|
||||||
});
|
});
|
||||||
|
setWorkspacePreviewPageIds((currentPageIds) => {
|
||||||
|
const nextPageIds = { ...currentPageIds };
|
||||||
|
delete nextPageIds[project.id];
|
||||||
|
return nextPageIds;
|
||||||
|
});
|
||||||
setPageWorkspaces((currentWorkspaces) =>
|
setPageWorkspaces((currentWorkspaces) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(currentWorkspaces).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
|
Object.entries(currentWorkspaces).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
|
||||||
|
|
@ -2440,6 +2635,14 @@ export function App() {
|
||||||
setWorkspaceSectionTexts((currentTexts) =>
|
setWorkspaceSectionTexts((currentTexts) =>
|
||||||
Object.fromEntries(Object.entries(currentTexts).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)))
|
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) =>
|
setActiveWorkspaceSectionIds((currentSectionIds) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(currentSectionIds).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
|
Object.entries(currentSectionIds).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
|
||||||
|
|
@ -2516,24 +2719,30 @@ export function App() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPage = scanSummary.pages.find((page) => page.selected);
|
const selectedWorkspacePages = getSelectedCoveragePages(scanSummary);
|
||||||
|
const selectedPage = selectedWorkspacePages[0];
|
||||||
|
|
||||||
if (!selectedPage) {
|
if (!selectedPage) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activePageId = activeWorkspacePageIds[project.id] ?? selectedPage.pageId;
|
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);
|
const workspaceKey = getWorkspaceKey(project.id, pageToOpen.pageId);
|
||||||
|
|
||||||
if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) {
|
if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpenPageWorkspace(project, pageToOpen, { silent: true });
|
void handleOpenPageWorkspace(project, pageToOpen, {
|
||||||
|
navigatePreview: !workspacePreviewPageIds[project.id],
|
||||||
|
silent: true
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]);
|
}, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewPageIds]);
|
||||||
|
|
||||||
const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false;
|
const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false;
|
||||||
const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict;
|
const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict;
|
||||||
|
|
@ -3144,19 +3353,26 @@ export function App() {
|
||||||
null;
|
null;
|
||||||
const activeWorkspaceMarkers = activeWorkspaceKey ? (workspacePreviewMarkers[activeWorkspaceKey] ?? []) : [];
|
const activeWorkspaceMarkers = activeWorkspaceKey ? (workspacePreviewMarkers[activeWorkspaceKey] ?? []) : [];
|
||||||
const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop";
|
const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop";
|
||||||
const activeWorkspaceSectionId =
|
const activeWorkspaceDrawers = activeWorkspaceKey ? (workspaceDrawers[activeWorkspaceKey] ?? []) : [];
|
||||||
activeWorkspaceKey && activeWorkspace
|
const isWorkspacePageMenuOpen = activeWorkspaceKey ? Boolean(workspacePageMenus[activeWorkspaceKey]) : false;
|
||||||
? (activeWorkspaceTarget?.sectionId ?? activeWorkspaceSectionIds[activeWorkspaceKey] ?? activeWorkspace.sections[0]?.id ?? "")
|
const previewWorkspacePageId = workspacePreviewPageIds[project.id] ?? activeWorkspacePageId;
|
||||||
: "";
|
const previewWorkspacePage =
|
||||||
const activeWorkspaceSection =
|
selectedCoveragePages.find((page) => page.pageId === previewWorkspacePageId) ??
|
||||||
activeWorkspace?.sections.find((section) => section.id === activeWorkspaceSectionId) ??
|
activeWorkspacePage;
|
||||||
activeWorkspace?.sections[0] ??
|
const activeWorkspacePageIndex = Math.max(
|
||||||
null;
|
0,
|
||||||
const activeWorkspaceSectionText =
|
selectedCoveragePages.findIndex((page) => page.pageId === activeWorkspacePageId)
|
||||||
activeWorkspaceKey && activeWorkspaceSection
|
);
|
||||||
? (workspaceSectionTexts[activeWorkspaceKey]?.[activeWorkspaceSection.id] ??
|
const activeWorkspacePreviewSourcePath =
|
||||||
activeWorkspaceSection.workingText)
|
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
|
const selectedAuditIssuesAll = scanSummary?.audit
|
||||||
? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues)
|
? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues)
|
||||||
: [];
|
: [];
|
||||||
|
|
@ -4362,7 +4578,7 @@ export function App() {
|
||||||
<section className="page-workspace-panel">
|
<section className="page-workspace-panel">
|
||||||
<div className="page-workspace-heading">
|
<div className="page-workspace-heading">
|
||||||
<div>
|
<div>
|
||||||
<strong>Рабочая зона выбранных посадочных</strong>
|
<strong>Рабочая зона выбранного scope</strong>
|
||||||
<small>Original read-only, working draft сохраняется в Postgres + MinIO.</small>
|
<small>Original read-only, working draft сохраняется в Postgres + MinIO.</small>
|
||||||
</div>
|
</div>
|
||||||
<span>Apply пока не трогаем</span>
|
<span>Apply пока не трогаем</span>
|
||||||
|
|
@ -4388,41 +4604,157 @@ export function App() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{activeWorkspace && activeWorkspacePage && activeWorkspaceKey ? (
|
{activeWorkspace && activeWorkspacePage && activeWorkspaceKey ? (
|
||||||
<div className="workspace-editor-stack">
|
<div className="workspace-editor-stack workspace-cockpit">
|
||||||
<div className="workspace-current-page">
|
<div className="workspace-current-page">
|
||||||
<div>
|
<div>
|
||||||
<span>Выбранная страница</span>
|
<span>
|
||||||
|
Активная страница {activeWorkspacePageIndex + 1}/{selectedCoveragePages.length} · выбранный scope
|
||||||
|
</span>
|
||||||
<strong>{getPageTitle(activeWorkspacePage)}</strong>
|
<strong>{getPageTitle(activeWorkspacePage)}</strong>
|
||||||
<small>{getPagePathLabel(activeWorkspacePage)}</small>
|
<small>{getPagePathLabel(activeWorkspacePage)}</small>
|
||||||
</div>
|
</div>
|
||||||
<em>{activeWorkspaceIssues.length} замечаний</em>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="workspace-preview-layout">
|
<section className={`workspace-preview-panel workspace-cockpit-panel ${activeWorkspaceDrawers.length > 0 ? "drawer-open" : ""}`}>
|
||||||
<section className="workspace-preview-panel">
|
<div className="workspace-cockpit-toolbar">
|
||||||
<div className="workspace-subheading">
|
<div className="workspace-preview-status">
|
||||||
<strong>Зеркало страницы</strong>
|
<span>Замечаний на активной странице: {activeWorkspaceIssues.length}</span>
|
||||||
<small>Оранжевые точки ведут к полям правки ниже.</small>
|
<span>В выбранном scope: {selectedCoverageIssueCount}</span>
|
||||||
|
<span>Warning: {activeWorkspaceIssueCounts.warnings} · Info: {activeWorkspaceIssueCounts.info}</span>
|
||||||
</div>
|
</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) => (
|
{WORKSPACE_PREVIEW_MODES.map((mode) => (
|
||||||
<button
|
<button
|
||||||
className={activeWorkspacePreviewMode === mode.id ? "active" : undefined}
|
className={activeWorkspacePreviewMode === mode.id ? "active" : undefined}
|
||||||
key={mode.id}
|
key={mode.id}
|
||||||
onClick={() => changeWorkspacePreviewMode(activeWorkspaceKey, mode.id, activeWorkspaceTargets)}
|
onClick={() =>
|
||||||
|
changeWorkspacePreviewMode(
|
||||||
|
activeWorkspaceKey,
|
||||||
|
mode.id,
|
||||||
|
activeWorkspaceTargets,
|
||||||
|
selectedCoveragePages
|
||||||
|
)
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{mode.label}
|
{mode.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="workspace-preview-shell">
|
||||||
<div className={`workspace-preview-frame ${activeWorkspacePreviewMode === "mobile" ? "mobile-preview" : "desktop-preview"}`}>
|
<div className={`workspace-preview-frame ${activeWorkspacePreviewMode === "mobile" ? "mobile-preview" : "desktop-preview"}`}>
|
||||||
<iframe
|
<iframe
|
||||||
onLoad={() => handleWorkspacePreviewLoad(activeWorkspaceKey, activeWorkspaceTargets)}
|
onLoad={() =>
|
||||||
|
handleWorkspacePreviewLoad(
|
||||||
|
activeWorkspaceKey,
|
||||||
|
activeWorkspaceTargets,
|
||||||
|
selectedCoveragePages
|
||||||
|
)
|
||||||
|
}
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
previewFrameRefs.current[activeWorkspaceKey] = element;
|
if (element) {
|
||||||
|
previewFrameRefs.current = {
|
||||||
|
[activeWorkspaceKey]: element
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
delete previewFrameRefs.current[activeWorkspaceKey];
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
sandbox="allow-scripts allow-forms allow-popups"
|
sandbox="allow-scripts allow-forms allow-popups"
|
||||||
src={getPreviewPageUrl(project.id, activeWorkspace.page.sourcePath)}
|
src={getPreviewPageUrl(
|
||||||
|
project.id,
|
||||||
|
activeWorkspacePreviewSourcePath,
|
||||||
|
activeWorkspacePreviewFocusSelector
|
||||||
|
)}
|
||||||
title={`Предпросмотр ${getPageTitle(activeWorkspacePage)}`}
|
title={`Предпросмотр ${getPageTitle(activeWorkspacePage)}`}
|
||||||
/>
|
/>
|
||||||
<div className="workspace-preview-markers">
|
<div className="workspace-preview-markers">
|
||||||
|
|
@ -4449,77 +4781,98 @@ export function App() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{activeWorkspaceDrawers.length > 0 ? (
|
||||||
<section className="workspace-page-issues">
|
<div className="workspace-drawer-stack">
|
||||||
<div className="workspace-subheading">
|
{activeWorkspaceDrawers.map((activeWorkspaceDrawer) => (
|
||||||
<strong>Ошибки этой страницы</strong>
|
<aside className="workspace-drawer open" key={`workspace-drawer-${activeWorkspaceDrawer}`}>
|
||||||
<small>{activeWorkspaceIssues.length} шт. из базового аудита</small>
|
<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>
|
</div>
|
||||||
{activeWorkspaceIssues.length > 0 ? (
|
<button
|
||||||
<div className="workspace-issue-list">
|
aria-label="Закрыть панель"
|
||||||
{activeWorkspaceIssues.map((issue) => {
|
className="workspace-drawer-close"
|
||||||
const target = activeWorkspaceTargets.find((item) => item.issues.some((targetIssue) => targetIssue.id === issue.id));
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={!target}
|
className={isActivePage ? "active" : undefined}
|
||||||
key={`workspace-issue-${issue.id}`}
|
disabled={isOpening}
|
||||||
onClick={() => {
|
key={`workspace-scope-${page.pageId}`}
|
||||||
if (!target) return;
|
onClick={() => void handleOpenPageWorkspace(project, page)}
|
||||||
activateWorkspaceTarget(activeWorkspaceKey, target);
|
|
||||||
}}
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span>{getAuditSeverityLabel(issue.severity)}</span>
|
<span>{pageIndex + 1}</span>
|
||||||
<strong>{issue.message}</strong>
|
<strong>{getPageTitle(page)}</strong>
|
||||||
<small>{issue.field ? `Поле: ${issue.field}` : "Медиа/страница"}</small>
|
<small>{getPagePathLabel(page)}</small>
|
||||||
|
<em>{pageIssues.length} замечаний</em>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : activeWorkspaceDrawer === "fields" ? (
|
||||||
<small className="workspace-empty">По этой странице ошибок аудита нет.</small>
|
<div className="workspace-fields-drawer">
|
||||||
)}
|
{activeWorkspaceTargets.length > 0 ? (
|
||||||
</section>
|
activeWorkspaceTargets.map((target) => {
|
||||||
</div>
|
const targetSectionText =
|
||||||
<section className="workspace-section-workbench">
|
workspaceSectionTexts[activeWorkspaceKey]?.[target.sectionId] ?? target.workingText;
|
||||||
<div className="workspace-subheading">
|
|
||||||
<strong>Поля для правки</strong>
|
return (
|
||||||
<small>Title, meta, заголовки, текстовые блоки и media-поля редактируются здесь.</small>
|
<div
|
||||||
</div>
|
|
||||||
<div className="section-workbench-grid">
|
|
||||||
<div className="section-map-list">
|
|
||||||
{activeWorkspaceTargets.map((target) => (
|
|
||||||
<button
|
|
||||||
className={
|
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}
|
id={getWorkspaceFieldDomId(activeWorkspaceKey, target.id)}
|
||||||
onClick={() => activateWorkspaceTarget(activeWorkspaceKey, target)}
|
key={`workspace-field-${target.id}`}
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<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">
|
<div className="workspace-subheading">
|
||||||
<strong>
|
<strong>
|
||||||
{activeWorkspaceTarget.number}. {activeWorkspaceTarget.title}
|
{target.number}. {target.title}
|
||||||
</strong>
|
</strong>
|
||||||
<small>{activeWorkspaceTarget.selector ?? "selector пока не задан"}</small>
|
<small>{target.selector ?? "selector пока не задан"}</small>
|
||||||
</div>
|
</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">
|
<div className="workspace-target-issues">
|
||||||
{activeWorkspaceTarget.issues.map((issue) => (
|
{target.issues.map((issue) => (
|
||||||
<small key={`target-issue-${issue.id}`}>
|
<small key={`target-issue-${target.id}-${issue.id}`}>
|
||||||
{getAuditSeverityLabel(issue.severity)} · {issue.message}
|
{getAuditSeverityLabel(issue.severity)} · {issue.message}
|
||||||
</small>
|
</small>
|
||||||
))}
|
))}
|
||||||
|
|
@ -4527,7 +4880,7 @@ export function App() {
|
||||||
) : null}
|
) : null}
|
||||||
<label>
|
<label>
|
||||||
<span>Original · только чтение</span>
|
<span>Original · только чтение</span>
|
||||||
<pre>{activeWorkspaceTarget.originalText || "Пусто"}</pre>
|
<pre>{target.originalText || "Пусто"}</pre>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Working · редактируется</span>
|
<span>Working · редактируется</span>
|
||||||
|
|
@ -4537,11 +4890,11 @@ export function App() {
|
||||||
...currentTexts,
|
...currentTexts,
|
||||||
[activeWorkspaceKey]: {
|
[activeWorkspaceKey]: {
|
||||||
...(currentTexts[activeWorkspaceKey] ?? {}),
|
...(currentTexts[activeWorkspaceKey] ?? {}),
|
||||||
[activeWorkspaceTarget.sectionId]: event.target.value
|
[target.sectionId]: event.target.value
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
value={activeWorkspaceSectionText}
|
value={targetSectionText}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="workspace-actions">
|
<div className="workspace-actions">
|
||||||
|
|
@ -4564,7 +4917,7 @@ export function App() {
|
||||||
...currentTexts,
|
...currentTexts,
|
||||||
[activeWorkspaceKey]: {
|
[activeWorkspaceKey]: {
|
||||||
...(currentTexts[activeWorkspaceKey] ?? {}),
|
...(currentTexts[activeWorkspaceKey] ?? {}),
|
||||||
[activeWorkspaceTarget.sectionId]: activeWorkspaceTarget.originalText
|
[target.sectionId]: target.originalText
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -4573,18 +4926,47 @@ export function App() {
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Сбросить секцию
|
Сбросить секцию
|
||||||
</button>
|
</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
|
<button
|
||||||
className="tiny-action"
|
disabled={!target}
|
||||||
disabled={isWorkspaceBusy}
|
key={`workspace-issue-${issue.id}`}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
void handleResetPageWorkspace(project.id, activeWorkspace.pageId)
|
if (!target) return;
|
||||||
}
|
activateWorkspaceTarget(activeWorkspaceKey, target);
|
||||||
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} />
|
<span>{getAuditSeverityLabel(issue.severity)}</span>
|
||||||
Сбросить весь draft
|
<strong>{issue.message}</strong>
|
||||||
|
<small>{issue.field ? `Поле: ${issue.field}` : "Медиа/страница"}</small>
|
||||||
|
<p>{explanation.action}</p>
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<small className="workspace-empty">По активной странице ошибок базового аудита нет.</small>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
so project/list scaffolding stays out of the visual hierarchy. */
|
||||||
.stage-3 .project-row {
|
.stage-3 .project-row {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
@ -6327,6 +6327,26 @@ input[type="checkbox"] {
|
||||||
box-shadow: none !important;
|
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-title-row,
|
||||||
.stage-3 .project-row > div > .project-source,
|
.stage-3 .project-row > div > .project-source,
|
||||||
.stage-3 .page-workspace-heading,
|
.stage-3 .page-workspace-heading,
|
||||||
|
|
@ -6344,18 +6364,16 @@ input[type="checkbox"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-3 .workspace-editor-stack {
|
.stage-3 .workspace-editor-stack {
|
||||||
gap: 18px !important;
|
gap: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-current-page {
|
.workspace-current-page {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: flex-end;
|
gap: 4px;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 18px;
|
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-current-page div {
|
.workspace-current-page > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -6371,9 +6389,11 @@ input[type="checkbox"] {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: clamp(24px, 2.2vw, 34px);
|
font-size: clamp(24px, 2.2vw, 34px);
|
||||||
font-weight: 760;
|
font-weight: 760;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: 0;
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
overflow-wrap: anywhere;
|
max-width: 100%;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-current-page small {
|
.workspace-current-page small {
|
||||||
|
|
@ -6383,16 +6403,211 @@ input[type="checkbox"] {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-current-page em {
|
.workspace-cockpit-toolbar {
|
||||||
flex: 0 0 auto;
|
display: flex;
|
||||||
padding: 8px 12px;
|
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);
|
color: var(--ink);
|
||||||
background: #f2f2f4;
|
background: #fff;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
font-size: 13px;
|
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-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,
|
.stage-3 .workspace-preview-panel,
|
||||||
|
|
@ -6406,6 +6621,44 @@ input[type="checkbox"] {
|
||||||
box-shadow: none !important;
|
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 {
|
.stage-3 .workspace-subheading {
|
||||||
align-items: baseline !important;
|
align-items: baseline !important;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
|
|
@ -6430,11 +6683,18 @@ input[type="checkbox"] {
|
||||||
border-radius: 22px !important;
|
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 {
|
.stage-3 .workspace-preview-panel iframe {
|
||||||
width: max(1600px, 100%) !important;
|
width: 100% !important;
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
height: clamp(620px, 72vh, 860px) !important;
|
height: 100% !important;
|
||||||
min-height: 620px !important;
|
min-height: clamp(620px, 68vh, 840px) !important;
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|
@ -6459,7 +6719,238 @@ input[type="checkbox"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-3 .workspace-preview-frame.desktop-preview iframe {
|
.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 {
|
.stage-3 .workspace-page-issues {
|
||||||
|
|
@ -6485,6 +6976,21 @@ input[type="checkbox"] {
|
||||||
transform: none !important;
|
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,
|
||||||
.stage-3 .section-map-card.active:hover {
|
.stage-3 .section-map-card.active:hover {
|
||||||
color: var(--ink) !important;
|
color: var(--ink) !important;
|
||||||
|
|
@ -6524,9 +7030,33 @@ input[type="checkbox"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.workspace-current-page {
|
.workspace-cockpit-toolbar,
|
||||||
|
.workspace-current-page-actions {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ function buildPreviewBridgeScript() {
|
||||||
return `
|
return `
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const state = { workspaceKey: "", targets: [] };
|
const state = { workspaceKey: "", targets: [], scopePages: [], activeScopePageId: "", lastScopeReportAt: 0 };
|
||||||
const layerId = "seo-mode-preview-marker-layer";
|
const layerId = "seo-mode-preview-marker-layer";
|
||||||
let frame = 0;
|
let frame = 0;
|
||||||
let lastActivation = { id: "", at: 0 };
|
let lastActivation = { id: "", at: 0 };
|
||||||
|
|
@ -525,6 +525,73 @@ function buildPreviewBridgeScript() {
|
||||||
return marker;
|
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() {
|
function measure() {
|
||||||
const layer = ensureLayer();
|
const layer = ensureLayer();
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
@ -609,6 +676,8 @@ function buildPreviewBridgeScript() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportActiveScopePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
|
|
@ -620,6 +689,8 @@ function buildPreviewBridgeScript() {
|
||||||
|
|
||||||
state.workspaceKey = data.workspaceKey || "";
|
state.workspaceKey = data.workspaceKey || "";
|
||||||
state.targets = Array.isArray(data.targets) ? data.targets : [];
|
state.targets = Array.isArray(data.targets) ? data.targets : [];
|
||||||
|
state.scopePages = Array.isArray(data.scopePages) ? data.scopePages : [];
|
||||||
|
state.activeScopePageId = data.activePageId || state.activeScopePageId || "";
|
||||||
scheduleMeasure();
|
scheduleMeasure();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue