@prenta/admin 0.92.0 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +663 -0
- package/design-system/tokens.css +173 -71
- package/design-system/tokens.json +244 -63
- package/design-system/tokens.scss +203 -98
- package/design-system/tokens.tailwind.cjs +93 -38
- package/dist/AdminRoot.d.ts +27 -6
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.js +138 -0
- package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
- package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
- package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/media-block.render.test.js +108 -0
- package/dist/__tests__/components/media-block.render.test.js.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
- package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
- package/dist/__tests__/components/seo-char-count.test.js +24 -6
- package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
- package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
- package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/serp-preview.render.test.js +25 -0
- package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.js +67 -0
- package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
- package/dist/__tests__/lib/media-format.test.d.ts +2 -0
- package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
- package/dist/__tests__/lib/media-format.test.js +70 -0
- package/dist/__tests__/lib/media-format.test.js.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.js +122 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +2 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
- package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-redirect.test.js +26 -0
- package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
- package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
- package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
- package/dist/__tests__/lib/rail-width.test.js +69 -0
- package/dist/__tests__/lib/rail-width.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
- package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.js +73 -0
- package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
- package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
- package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-diff.test.js +230 -0
- package/dist/__tests__/lib/version-diff.test.js.map +1 -0
- package/dist/__tests__/lib/version-history.test.d.ts +2 -0
- package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-history.test.js +125 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
- package/dist/__tests__/styles/packaging.test.d.ts +2 -0
- package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
- package/dist/__tests__/styles/packaging.test.js +31 -0
- package/dist/__tests__/styles/packaging.test.js.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.js +127 -0
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.js +92 -0
- package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
- package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
- package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +30 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-tab.render.test.js +151 -0
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.js +81 -0
- package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
- package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
- package/dist/components/BreakpointPicker.d.ts +18 -0
- package/dist/components/BreakpointPicker.d.ts.map +1 -0
- package/dist/components/BreakpointPicker.js +31 -0
- package/dist/components/BreakpointPicker.js.map +1 -0
- package/dist/components/CanvasEditTip.d.ts +6 -0
- package/dist/components/CanvasEditTip.d.ts.map +1 -0
- package/dist/components/CanvasEditTip.js +22 -0
- package/dist/components/CanvasEditTip.js.map +1 -0
- package/dist/components/EditorRail.d.ts +36 -0
- package/dist/components/EditorRail.d.ts.map +1 -0
- package/dist/components/EditorRail.js +104 -0
- package/dist/components/EditorRail.js.map +1 -0
- package/dist/components/EditorRailTabs.d.ts +32 -0
- package/dist/components/EditorRailTabs.d.ts.map +1 -0
- package/dist/components/EditorRailTabs.js +45 -0
- package/dist/components/EditorRailTabs.js.map +1 -0
- package/dist/components/MediaBlock.d.ts +49 -0
- package/dist/components/MediaBlock.d.ts.map +1 -0
- package/dist/components/MediaBlock.js +50 -0
- package/dist/components/MediaBlock.js.map +1 -0
- package/dist/components/ReflowMeter.d.ts +28 -0
- package/dist/components/ReflowMeter.d.ts.map +1 -0
- package/dist/components/ReflowMeter.js +42 -0
- package/dist/components/ReflowMeter.js.map +1 -0
- package/dist/components/ZoomChip.d.ts +17 -0
- package/dist/components/ZoomChip.d.ts.map +1 -0
- package/dist/components/ZoomChip.js +17 -0
- package/dist/components/ZoomChip.js.map +1 -0
- package/dist/components/breakpoint-icons.d.ts +3 -0
- package/dist/components/breakpoint-icons.d.ts.map +1 -0
- package/dist/components/breakpoint-icons.js +16 -0
- package/dist/components/breakpoint-icons.js.map +1 -0
- package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
- package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
- package/dist/components/page-tab/AuthorPicker.js +95 -0
- package/dist/components/page-tab/AuthorPicker.js.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.js +33 -0
- package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
- package/dist/components/seo/PageAuditPanel.d.ts +14 -0
- package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
- package/dist/components/seo/PageAuditPanel.js +59 -0
- package/dist/components/seo/PageAuditPanel.js.map +1 -0
- package/dist/components/seo/SeoCharCount.d.ts +19 -7
- package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
- package/dist/components/seo/SeoCharCount.js +30 -16
- package/dist/components/seo/SeoCharCount.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts +15 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +118 -21
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SerpPreview.d.ts +20 -0
- package/dist/components/seo/SerpPreview.d.ts.map +1 -0
- package/dist/components/seo/SerpPreview.js +6 -0
- package/dist/components/seo/SerpPreview.js.map +1 -0
- package/dist/hooks/usePageAudit.d.ts +41 -0
- package/dist/hooks/usePageAudit.d.ts.map +1 -0
- package/dist/hooks/usePageAudit.js +68 -0
- package/dist/hooks/usePageAudit.js.map +1 -0
- package/dist/hooks/useReflowGuard.d.ts +47 -0
- package/dist/hooks/useReflowGuard.d.ts.map +1 -0
- package/dist/hooks/useReflowGuard.js +52 -0
- package/dist/hooks/useReflowGuard.js.map +1 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
- package/dist/layout/primitives/AdminShell.js +2 -2
- package/dist/layout/primitives/AdminShell.js.map +1 -1
- package/dist/lib/breakpoints.d.ts +51 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +84 -0
- package/dist/lib/breakpoints.js.map +1 -0
- package/dist/lib/canvas-edit-chrome.d.ts +22 -0
- package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
- package/dist/lib/canvas-edit-chrome.js +81 -0
- package/dist/lib/canvas-edit-chrome.js.map +1 -0
- package/dist/lib/media-format.d.ts +55 -0
- package/dist/lib/media-format.d.ts.map +1 -0
- package/dist/lib/media-format.js +93 -0
- package/dist/lib/media-format.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +45 -0
- package/dist/lib/page-audit-input.d.ts.map +1 -0
- package/dist/lib/page-audit-input.js +53 -0
- package/dist/lib/page-audit-input.js.map +1 -0
- package/dist/lib/page-editor-service.d.ts +12 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +6 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/page-redirect.d.ts +39 -0
- package/dist/lib/page-redirect.d.ts.map +1 -0
- package/dist/lib/page-redirect.js +38 -0
- package/dist/lib/page-redirect.js.map +1 -0
- package/dist/lib/rail-width.d.ts +23 -0
- package/dist/lib/rail-width.d.ts.map +1 -0
- package/dist/lib/rail-width.js +59 -0
- package/dist/lib/rail-width.js.map +1 -0
- package/dist/lib/reflow-baseline.d.ts +45 -0
- package/dist/lib/reflow-baseline.d.ts.map +1 -0
- package/dist/lib/reflow-baseline.js +38 -0
- package/dist/lib/reflow-baseline.js.map +1 -0
- package/dist/lib/reflow-probe.d.ts +159 -0
- package/dist/lib/reflow-probe.d.ts.map +1 -0
- package/dist/lib/reflow-probe.js +228 -0
- package/dist/lib/reflow-probe.js.map +1 -0
- package/dist/lib/seo-service.d.ts +10 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/lib/version-diff.d.ts +50 -0
- package/dist/lib/version-diff.d.ts.map +1 -0
- package/dist/lib/version-diff.js +294 -0
- package/dist/lib/version-diff.js.map +1 -0
- package/dist/lib/version-history.d.ts +73 -0
- package/dist/lib/version-history.d.ts.map +1 -0
- package/dist/lib/version-history.js +126 -0
- package/dist/lib/version-history.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
- package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
- package/dist/views/page-editor/CanvasMetaLine.js +23 -0
- package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +47 -8
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +68 -34
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +21 -2
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +13 -20
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +383 -78
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageTab.d.ts +36 -0
- package/dist/views/page-editor/PageTab.d.ts.map +1 -0
- package/dist/views/page-editor/PageTab.js +65 -0
- package/dist/views/page-editor/PageTab.js.map +1 -0
- package/dist/views/page-editor/ReviewBar.d.ts +21 -0
- package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
- package/dist/views/page-editor/ReviewBar.js +22 -0
- package/dist/views/page-editor/ReviewBar.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +64 -20
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +38 -7
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.js +3 -0
- package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
- package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
- package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
- package/dist/views/page-editor/reflow-context.d.ts +28 -0
- package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
- package/dist/views/page-editor/reflow-context.js +14 -0
- package/dist/views/page-editor/reflow-context.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.js +7 -3
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.js +1 -1
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +66 -26
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.js +27 -15
- package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
- package/dist/views/page-editor/sections/parts.d.ts +0 -1
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
- package/dist/views/page-editor/sections/parts.js +41 -16
- package/dist/views/page-editor/sections/parts.js.map +1 -1
- package/dist/views/page-editor/viewports.d.ts +7 -14
- package/dist/views/page-editor/viewports.d.ts.map +1 -1
- package/dist/views/page-editor/viewports.js +7 -12
- package/dist/views/page-editor/viewports.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +5 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +2 -20
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +4 -1
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +5 -3
- package/src/AdminRoot.tsx +27 -6
- package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
- package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
- package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
- package/src/__tests__/components/media-block.render.test.tsx +126 -0
- package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
- package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
- package/src/__tests__/components/seo-char-count.test.tsx +26 -6
- package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
- package/src/__tests__/lib/breakpoints.test.ts +84 -0
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
- package/src/__tests__/lib/media-format.test.ts +89 -0
- package/src/__tests__/lib/page-audit-input.test.ts +144 -0
- package/src/__tests__/lib/page-editor-service.test.ts +2 -0
- package/src/__tests__/lib/page-redirect.test.ts +29 -0
- package/src/__tests__/lib/rail-width.test.ts +86 -0
- package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
- package/src/__tests__/lib/reflow-probe.test.ts +95 -0
- package/src/__tests__/lib/version-diff.test.ts +278 -0
- package/src/__tests__/lib/version-history.test.ts +166 -0
- package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
- package/src/__tests__/styles/packaging.test.ts +38 -0
- package/src/__tests__/styles/responsive-layer.test.ts +152 -0
- package/src/__tests__/styles/tokens-drift.test.ts +110 -0
- package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
- package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +43 -1
- package/src/__tests__/views/page-tab.render.test.tsx +226 -0
- package/src/__tests__/views/section-class-parity.test.ts +98 -0
- package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
- package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
- package/src/components/BreakpointPicker.tsx +114 -0
- package/src/components/CanvasEditTip.tsx +46 -0
- package/src/components/EditorRail.tsx +219 -0
- package/src/components/EditorRailTabs.tsx +117 -0
- package/src/components/MediaBlock.tsx +223 -0
- package/src/components/ReflowMeter.tsx +125 -0
- package/src/components/ZoomChip.tsx +39 -0
- package/src/components/breakpoint-icons.ts +17 -0
- package/src/components/page-tab/AuthorPicker.tsx +240 -0
- package/src/components/page-tab/DirectiveSwitch.tsx +89 -0
- package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
- package/src/components/seo/PageAuditPanel.tsx +192 -0
- package/src/components/seo/SeoCharCount.tsx +55 -24
- package/src/components/seo/SeoEditorPane.tsx +213 -31
- package/src/components/seo/SerpPreview.tsx +41 -0
- package/src/hooks/usePageAudit.ts +98 -0
- package/src/hooks/useReflowGuard.ts +96 -0
- package/src/layout/primitives/AdminShell.tsx +13 -1
- package/src/lib/breakpoints.ts +107 -0
- package/src/lib/canvas-edit-chrome.ts +82 -0
- package/src/lib/media-format.ts +99 -0
- package/src/lib/page-audit-input.ts +104 -0
- package/src/lib/page-editor-service.ts +18 -0
- package/src/lib/page-redirect.ts +60 -0
- package/src/lib/rail-width.ts +58 -0
- package/src/lib/reflow-baseline.ts +70 -0
- package/src/lib/reflow-probe.ts +299 -0
- package/src/lib/seo-service.ts +10 -0
- package/src/lib/version-diff.ts +376 -0
- package/src/lib/version-history.ts +192 -0
- package/src/styles/prenta-tokens.css +120 -42
- package/src/styles/responsive.css +281 -0
- package/src/styles/theme.css +138 -0
- package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
- package/src/views/page-editor/EditorCanvas.tsx +140 -54
- package/src/views/page-editor/EditorTopBar.tsx +55 -51
- package/src/views/page-editor/PageSectionEditor.tsx +575 -185
- package/src/views/page-editor/PageTab.tsx +433 -0
- package/src/views/page-editor/ReviewBar.tsx +76 -0
- package/src/views/page-editor/SectionInspector.tsx +84 -42
- package/src/views/page-editor/SectionsPanel.tsx +282 -141
- package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
- package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
- package/src/views/page-editor/reflow-context.ts +43 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
- package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
- package/src/views/page-editor/sections/GallerySection.tsx +7 -3
- package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
- package/src/views/page-editor/sections/MissionSection.tsx +4 -2
- package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
- package/src/views/page-editor/sections/SectionRenderer.tsx +115 -34
- package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
- package/src/views/page-editor/sections/parts.tsx +46 -18
- package/src/views/page-editor/viewports.ts +13 -26
- package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
- package/src/views/post-editor/PostSectionEditor.tsx +1 -56
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
|
@@ -6,21 +6,24 @@ import { toast } from 'sonner'
|
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
7
7
|
import { useNavigationBlocker } from '../../router/index.js'
|
|
8
8
|
import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js'
|
|
9
|
-
import {
|
|
9
|
+
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
10
|
+
import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
11
|
+
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
10
12
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
11
|
-
import {
|
|
13
|
+
import { VersionHistoryPanel } from './VersionHistoryPanel.js'
|
|
14
|
+
import { ReviewBar } from './ReviewBar.js'
|
|
15
|
+
import {
|
|
16
|
+
fetchPageVersions,
|
|
17
|
+
restorePageVersion,
|
|
18
|
+
type PageVersion,
|
|
19
|
+
} from '../../lib/version-history.js'
|
|
20
|
+
import { changedSectionIds, diffVersions } from '../../lib/version-diff.js'
|
|
12
21
|
import {
|
|
13
|
-
addSection,
|
|
14
|
-
createSection,
|
|
15
|
-
duplicateSection,
|
|
16
22
|
emptyPage,
|
|
17
23
|
fetchPageForEditor,
|
|
18
24
|
missingPageFields,
|
|
19
|
-
moveSection,
|
|
20
25
|
publishPage,
|
|
21
26
|
PAGES_COLLECTION,
|
|
22
|
-
removeSection,
|
|
23
|
-
reorderSections,
|
|
24
27
|
savePageDraft,
|
|
25
28
|
toggleSectionVisibility,
|
|
26
29
|
updateSectionContent,
|
|
@@ -38,20 +41,33 @@ import {
|
|
|
38
41
|
createPreviewToken,
|
|
39
42
|
} from '../../lib/preview-link.js'
|
|
40
43
|
import { usePublicSiteUrl } from '../../hooks/usePublicSiteUrl.js'
|
|
41
|
-
import type { SectionSettings
|
|
44
|
+
import type { SectionSettings } from './section-types.js'
|
|
42
45
|
import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
|
|
46
|
+
import type { Zoom } from '../../lib/breakpoints.js'
|
|
47
|
+
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
48
|
+
import {
|
|
49
|
+
DEFAULT_DIRECTIVES,
|
|
50
|
+
directivesFromData,
|
|
51
|
+
type RobotsPermissions,
|
|
52
|
+
} from '@prenta/core/seo/robots-directives'
|
|
53
|
+
import { createPublishRedirect, pendingSlugChange } from '../../lib/page-redirect.js'
|
|
54
|
+
import { updateSeoFields } from '../../lib/seo-service.js'
|
|
55
|
+
import { PageTab } from './PageTab.js'
|
|
56
|
+
import { duplicatePage, deletePage } from '../../lib/pages-service.js'
|
|
57
|
+
import { scoreBand, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
58
|
+
import { usePageAudit } from '../../hooks/usePageAudit.js'
|
|
43
59
|
import { hasCanvasRenderer, useAdminSectionRenderers } from './sections/index.js'
|
|
44
60
|
import { EditorTopBar, type SaveState } from './EditorTopBar.js'
|
|
45
|
-
import {
|
|
61
|
+
import { SectionsList } from './SectionsPanel.js'
|
|
46
62
|
import { EditorCanvas } from './EditorCanvas.js'
|
|
63
|
+
import { ReflowContext } from './reflow-context.js'
|
|
47
64
|
import { SitePreviewFrame } from './SitePreviewFrame.js'
|
|
48
65
|
import { useSitePreview } from './useSitePreview.js'
|
|
49
66
|
import { SectionInspector } from './SectionInspector.js'
|
|
50
|
-
import { AddSectionModal } from './AddSectionModal.js'
|
|
51
67
|
import { PageSettingsModal } from './PageSettingsModal.js'
|
|
52
68
|
import { ValidationSummary } from './ValidationSummary.js'
|
|
53
69
|
import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
54
|
-
import {
|
|
70
|
+
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
55
71
|
import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
56
72
|
|
|
57
73
|
interface PageSectionEditorProps {
|
|
@@ -74,16 +90,47 @@ interface PageSectionEditorProps {
|
|
|
74
90
|
onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The lower of the two audit scores — the tab dot should reflect the half that
|
|
95
|
+
* needs work, not an average that hides it.
|
|
96
|
+
*
|
|
97
|
+
* Three-valued on purpose, and it must NOT fall back to another scorer:
|
|
98
|
+
* undefined — the audit has not reported yet (the SEO tab has never been
|
|
99
|
+
* opened). No dot beats a dot from a different model.
|
|
100
|
+
* null — both groups are gated off, so there is genuinely nothing to
|
|
101
|
+
* rate. An earlier version fell back to `computeLiveSeoScore`
|
|
102
|
+
* here, which put a number from the content analyser — different
|
|
103
|
+
* model, different bands — on a tab summarising the audit. That
|
|
104
|
+
* is the disagreement this wiring exists to remove.
|
|
105
|
+
*/
|
|
106
|
+
function weakestAuditScore(audit: PageAuditResult | null): number | null | undefined {
|
|
107
|
+
if (!audit) return undefined
|
|
108
|
+
const scores = [audit.seo.score, audit.geo.score].filter((s): s is number => s !== null)
|
|
109
|
+
return scores.length ? Math.min(...scores) : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deriveCan(session: any): {
|
|
113
|
+
canEdit: boolean
|
|
114
|
+
canPublish: boolean
|
|
115
|
+
canCreateRedirect: boolean
|
|
116
|
+
} {
|
|
78
117
|
const role: string | undefined = session?.user?.role
|
|
79
118
|
// Default closed: when we can't identify a role, hide the edit/publish
|
|
80
119
|
// affordances rather than exposing them. The server independently enforces
|
|
81
120
|
// access on every write, so this is a UX guard — but it should fail safe.
|
|
82
121
|
// (Matches the Post editor's behavior.)
|
|
83
|
-
if (!role) return { canEdit: false, canPublish: false }
|
|
122
|
+
if (!role) return { canEdit: false, canPublish: false, canCreateRedirect: false }
|
|
84
123
|
const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
|
|
85
124
|
const publishers = ['ADMIN', 'EDITOR']
|
|
86
|
-
|
|
125
|
+
// Same roles that may publish a page may create the 301 that follows a
|
|
126
|
+
// slug move. Offering the checkbox to anyone else promises a redirect
|
|
127
|
+
// `POST /redirects` will reject.
|
|
128
|
+
const redirectCreators = ['OWNER', 'ADMIN', 'EDITOR']
|
|
129
|
+
return {
|
|
130
|
+
canEdit: editors.includes(role),
|
|
131
|
+
canPublish: publishers.includes(role),
|
|
132
|
+
canCreateRedirect: redirectCreators.includes(role),
|
|
133
|
+
}
|
|
87
134
|
}
|
|
88
135
|
|
|
89
136
|
export function PageSectionEditor({
|
|
@@ -109,21 +156,90 @@ export function PageSectionEditor({
|
|
|
109
156
|
fieldPath: string
|
|
110
157
|
} | null>(null)
|
|
111
158
|
const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
|
|
159
|
+
const [zoom, setZoom] = useState<Zoom>('fit')
|
|
160
|
+
/**
|
|
161
|
+
* Live audit reported by the SEO pane, built from unsaved form state. Null
|
|
162
|
+
* until that pane has mounted — `audit` below falls back to the saved-record
|
|
163
|
+
* audit so the header chip and tab dot always have a number.
|
|
164
|
+
*/
|
|
165
|
+
const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
|
|
166
|
+
/** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
|
|
167
|
+
const [canvasScale, setCanvasScale] = useState(1)
|
|
168
|
+
/**
|
|
169
|
+
* The live canvas surface, for the reflow guard to clone. State rather than a
|
|
170
|
+
* ref because the context value has to update when it mounts — the canvas
|
|
171
|
+
* unmounts whenever the site-preview frame takes over.
|
|
172
|
+
*/
|
|
173
|
+
const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null)
|
|
174
|
+
/**
|
|
175
|
+
* Section content as it is on the live site. Captured whenever the editor
|
|
176
|
+
* takes delivery of a PUBLISHED page — on load, and after each save and
|
|
177
|
+
* publish, both of which write straight through to the live content.
|
|
178
|
+
*/
|
|
179
|
+
const [baseline, setBaseline] = useState<ReflowBaseline | null>(null)
|
|
180
|
+
/**
|
|
181
|
+
* Path as it is on the live site, captured with the reflow baseline and for
|
|
182
|
+
* the same reason: a slug change only breaks links if something is already
|
|
183
|
+
* live at the old address.
|
|
184
|
+
*/
|
|
185
|
+
const [publishedPath, setPublishedPath] = useState<string | null>(null)
|
|
186
|
+
const [directives, setDirectives] = useState<RobotsPermissions>(DEFAULT_DIRECTIVES)
|
|
187
|
+
/**
|
|
188
|
+
* Defaults ON. The editor changing a live page's slug almost always wants the
|
|
189
|
+
* redirect, and the cost of an unwanted one is a rule they can delete, while
|
|
190
|
+
* the cost of a missing one is 404s discovered by visitors.
|
|
191
|
+
*/
|
|
192
|
+
const [redirectOnPublish, setRedirectOnPublish] = useState(true)
|
|
193
|
+
|
|
194
|
+
// The reflow guard's inputs, gathered in one place. Memoised because a new
|
|
195
|
+
// object identity on every render would restart the debounce in every guarded
|
|
196
|
+
// field's effect — the measurement would then never settle while typing,
|
|
197
|
+
// which is precisely when it is wanted.
|
|
198
|
+
const reflowContext = useMemo(
|
|
199
|
+
() => ({
|
|
200
|
+
canvas: canvasEl,
|
|
201
|
+
sectionId: selectedId,
|
|
202
|
+
baseline,
|
|
203
|
+
device: viewport,
|
|
204
|
+
onProof: setViewport,
|
|
205
|
+
}),
|
|
206
|
+
[canvasEl, selectedId, baseline, viewport],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Take delivery of server state.
|
|
211
|
+
*
|
|
212
|
+
* Every path that receives a fresh page from the API goes through here so the
|
|
213
|
+
* reflow baseline is re-captured with it. Saving a PUBLISHED page writes
|
|
214
|
+
* straight through to the live content — there is no draft/published split —
|
|
215
|
+
* so a save moves the baseline exactly as a publish does. Missing one would
|
|
216
|
+
* leave the guard comparing against content that is no longer on the site.
|
|
217
|
+
*/
|
|
218
|
+
const acceptPage = useCallback((p: EditorPage) => {
|
|
219
|
+
setPage(p)
|
|
220
|
+
setBaseline(snapshotBaseline(p.sections, p.status))
|
|
221
|
+
setPublishedPath(p.status === 'PUBLISHED' ? p.path : null)
|
|
222
|
+
}, [])
|
|
112
223
|
const [dirty, setDirty] = useState(false)
|
|
113
224
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
114
225
|
const [publishing, setPublishing] = useState(false)
|
|
115
226
|
|
|
116
|
-
const [addOpen, setAddOpen] = useState(false)
|
|
117
227
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
118
|
-
|
|
228
|
+
/** Active rail tab. Replaces the old boolean `seoOpen` right-hand aside. */
|
|
229
|
+
const [railTab, setRailTab] = useState<RailTabId>('sections')
|
|
119
230
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
120
|
-
const [
|
|
231
|
+
const [versions, setVersions] = useState<PageVersion[]>([])
|
|
232
|
+
const [versionsLoading, setVersionsLoading] = useState(false)
|
|
233
|
+
const [versionsError, setVersionsError] = useState<string | null>(null)
|
|
234
|
+
/** Index into `versions`; 0 is the newest, which is the current content. */
|
|
235
|
+
const [versionIndex, setVersionIndex] = useState(0)
|
|
236
|
+
const [restoring, setRestoring] = useState(false)
|
|
121
237
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
122
238
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
123
239
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
124
240
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
125
241
|
|
|
126
|
-
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
242
|
+
const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session])
|
|
127
243
|
const customRenderers = useAdminSectionRenderers()
|
|
128
244
|
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
129
245
|
|
|
@@ -148,18 +264,179 @@ export function PageSectionEditor({
|
|
|
148
264
|
)
|
|
149
265
|
const publishReady = Boolean(page) && publishCheck.errors.length === 0
|
|
150
266
|
|
|
151
|
-
const liveSeoScore = useMemo(() => {
|
|
152
|
-
if (!page) return 0
|
|
153
|
-
return computeLiveSeoScore({
|
|
154
|
-
title: page.title,
|
|
155
|
-
slug: page.slug,
|
|
156
|
-
seoTitle: page.seoTitle,
|
|
157
|
-
seoDescription: page.seoDescription,
|
|
158
|
-
sections: page.sections,
|
|
159
|
-
})
|
|
160
|
-
}, [page])
|
|
161
267
|
const publishDisabledReason = publishCheck.errors[0]
|
|
162
268
|
|
|
269
|
+
// Saved-record audit, available from first paint. Both this and the pane's
|
|
270
|
+
// live audit go through `buildPageAuditInput`, so they can differ only in
|
|
271
|
+
// saved-versus-unsaved metadata — never in how a check is decided.
|
|
272
|
+
const {
|
|
273
|
+
audit: savedAudit,
|
|
274
|
+
record: seoRecord,
|
|
275
|
+
reload: reloadAudit,
|
|
276
|
+
} = usePageAudit({
|
|
277
|
+
entityType: 'page',
|
|
278
|
+
entityId: page?.id ?? null,
|
|
279
|
+
sections: page?.sections ?? [],
|
|
280
|
+
slug: page?.slug ?? '',
|
|
281
|
+
fallback: { title: page?.seoTitle, description: page?.seoDescription },
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Seed directives from the saved record. Keyed on the record's identity, not
|
|
285
|
+
// on its fields — re-running on every field change would discard whatever the
|
|
286
|
+
// editor had just toggled and not yet saved.
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!seoRecord) return
|
|
289
|
+
setDirectives(
|
|
290
|
+
directivesFromData({
|
|
291
|
+
noindex: seoRecord.noindex,
|
|
292
|
+
nofollow: seoRecord.nofollow,
|
|
293
|
+
nosnippet: seoRecord.nosnippet,
|
|
294
|
+
noarchive: seoRecord.noarchive,
|
|
295
|
+
maxImagePreview: seoRecord.maxImagePreview,
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
298
|
+
}, [seoRecord?.id])
|
|
299
|
+
|
|
300
|
+
// Unsaved edits win: while the SEO pane is open its number is the truthful
|
|
301
|
+
// one, and every surface reads this single value.
|
|
302
|
+
const audit = liveAudit ?? savedAudit
|
|
303
|
+
|
|
304
|
+
// Content the SEO pane analyses. Hoisted out of the old aside wrapper so the
|
|
305
|
+
// rail can render the pane directly — the aside only ever existed to supply
|
|
306
|
+
// a resizable container, which the rail now is.
|
|
307
|
+
const seoContentHtml = useMemo(
|
|
308
|
+
() => buildEditorContentHtml(page?.sections ?? []),
|
|
309
|
+
[page?.sections],
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
const railTabs = useMemo<RailTab[]>(
|
|
313
|
+
() => [
|
|
314
|
+
{
|
|
315
|
+
id: 'sections',
|
|
316
|
+
label: 'Sections',
|
|
317
|
+
title: 'Page content',
|
|
318
|
+
count: page?.sections.length ?? 0,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'seo',
|
|
322
|
+
label: 'SEO & GEO',
|
|
323
|
+
title: 'Search & AI readiness',
|
|
324
|
+
// The weaker of the two: a page can be perfectly crawlable and still
|
|
325
|
+
// invisible to answer engines, and a dot that averaged them would hide
|
|
326
|
+
// whichever half is failing. `null` means a gate is off, which the tab
|
|
327
|
+
// reports as unrated rather than as a low score.
|
|
328
|
+
score: weakestAuditScore(audit),
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: 'page',
|
|
332
|
+
label: 'Page',
|
|
333
|
+
title: 'Page settings',
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
[page?.sections.length, audit],
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
|
|
340
|
+
|
|
341
|
+
// ─── Version history ─────────────────────────────────────────────────
|
|
342
|
+
// History is a MODE, not a dialog: it overlays the rail and drives the
|
|
343
|
+
// canvas, so the version being reviewed is editor state rather than
|
|
344
|
+
// something owned by a panel that can be unmounted.
|
|
345
|
+
|
|
346
|
+
const loadVersions = useCallback(async (pageId: string) => {
|
|
347
|
+
setVersionsLoading(true)
|
|
348
|
+
setVersionsError(null)
|
|
349
|
+
try {
|
|
350
|
+
setVersions(await fetchPageVersions(PAGES_COLLECTION, pageId))
|
|
351
|
+
} catch (err) {
|
|
352
|
+
setVersions([])
|
|
353
|
+
setVersionsError(err instanceof Error ? err.message : 'Failed to load version history')
|
|
354
|
+
} finally {
|
|
355
|
+
setVersionsLoading(false)
|
|
356
|
+
}
|
|
357
|
+
}, [])
|
|
358
|
+
|
|
359
|
+
const openHistory = useCallback(() => {
|
|
360
|
+
if (!page?.id) return
|
|
361
|
+
setHistoryOpen(true)
|
|
362
|
+
// Always land on "current" so opening history never silently swaps the
|
|
363
|
+
// canvas out from under the editor.
|
|
364
|
+
setVersionIndex(0)
|
|
365
|
+
void loadVersions(page.id)
|
|
366
|
+
}, [page?.id, loadVersions])
|
|
367
|
+
|
|
368
|
+
const closeHistory = useCallback(() => {
|
|
369
|
+
setHistoryOpen(false)
|
|
370
|
+
setVersionIndex(0)
|
|
371
|
+
}, [])
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Index 0 is the current content, which the canvas is already showing — so
|
|
375
|
+
* selecting it is "back to current", not a review. Only an older index puts
|
|
376
|
+
* the editor into review mode.
|
|
377
|
+
*/
|
|
378
|
+
const reviewing = historyOpen && versionIndex > 0
|
|
379
|
+
const reviewVersion = reviewing ? (versions[versionIndex] ?? null) : null
|
|
380
|
+
|
|
381
|
+
// What that version's save CHANGED — diffed against the one before it, which
|
|
382
|
+
// is the question the outlines on the canvas answer.
|
|
383
|
+
const reviewChanges = useMemo(
|
|
384
|
+
() =>
|
|
385
|
+
reviewVersion
|
|
386
|
+
? diffVersions(reviewVersion.sections, versions[versionIndex + 1]?.sections)
|
|
387
|
+
: [],
|
|
388
|
+
[reviewVersion, versions, versionIndex],
|
|
389
|
+
)
|
|
390
|
+
const reviewChangedIds = useMemo(() => changedSectionIds(reviewChanges), [reviewChanges])
|
|
391
|
+
|
|
392
|
+
const handleRestoreVersion = useCallback(
|
|
393
|
+
async (version: PageVersion) => {
|
|
394
|
+
if (!page?.id) return
|
|
395
|
+
setRestoring(true)
|
|
396
|
+
try {
|
|
397
|
+
await restorePageVersion(PAGES_COLLECTION, page.id, version.id)
|
|
398
|
+
// Re-read rather than trusting the snapshot we already hold: the
|
|
399
|
+
// restore goes through the normal update path, so hooks and derived
|
|
400
|
+
// fields may have altered what actually landed.
|
|
401
|
+
const restored = await fetchPageForEditor(page.id)
|
|
402
|
+
acceptPage(restored)
|
|
403
|
+
setSelectedId(null)
|
|
404
|
+
setDirty(false)
|
|
405
|
+
setSaveState('idle')
|
|
406
|
+
setValidation(null)
|
|
407
|
+
closeHistory()
|
|
408
|
+
void loadVersions(page.id)
|
|
409
|
+
toast.success('Version restored')
|
|
410
|
+
} catch (err) {
|
|
411
|
+
toast.error(err instanceof Error ? err.message : 'Failed to restore version')
|
|
412
|
+
} finally {
|
|
413
|
+
setRestoring(false)
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
[page?.id, acceptPage, closeHistory, loadVersions],
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
// ─── Edit permission ─────────────────────────────────────────────────
|
|
420
|
+
/**
|
|
421
|
+
* ADR 0002: a Page-Builder-owned page (it has a `_layout`) compiles its
|
|
422
|
+
* sections from the builder layout on every save, so editing them here would
|
|
423
|
+
* be overwritten. The server independently refuses such edits with 409.
|
|
424
|
+
*/
|
|
425
|
+
const readOnly = page?.builderOwned ?? false
|
|
426
|
+
/**
|
|
427
|
+
* The single gate every writing surface reads.
|
|
428
|
+
*
|
|
429
|
+
* Hoisted above the callbacks rather than derived at render time, because
|
|
430
|
+
* the two that were left behind were the ones that mattered: the site
|
|
431
|
+
* preview's in-place edit handlers close over `canEdit` (the raw role) and
|
|
432
|
+
* so kept accepting edits while a past version was under review — and while
|
|
433
|
+
* a builder-owned page was supposedly read-only. The iframe is a second
|
|
434
|
+
* editing surface, and a lock that only covers the rail is not a lock.
|
|
435
|
+
*/
|
|
436
|
+
const effectiveCanEdit = canEdit && !readOnly && !reviewing
|
|
437
|
+
/** Restoring is not editing: it is allowed precisely while reviewing. */
|
|
438
|
+
const canRestoreVersion = canEdit && !readOnly
|
|
439
|
+
|
|
163
440
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
164
441
|
useEffect(() => {
|
|
165
442
|
let cancelled = false
|
|
@@ -175,7 +452,7 @@ export function PageSectionEditor({
|
|
|
175
452
|
}
|
|
176
453
|
|
|
177
454
|
if (!documentId) {
|
|
178
|
-
|
|
455
|
+
acceptPage(emptyPage(initialParentId ?? null))
|
|
179
456
|
setLoading(false)
|
|
180
457
|
// Start a brand-new page the way WordPress does: name it first. The
|
|
181
458
|
// settings modal opens with the title focused and the slug/path
|
|
@@ -187,7 +464,7 @@ export function PageSectionEditor({
|
|
|
187
464
|
fetchPageForEditor(documentId)
|
|
188
465
|
.then((p) => {
|
|
189
466
|
if (cancelled) return
|
|
190
|
-
|
|
467
|
+
acceptPage(p)
|
|
191
468
|
// Default to a clean canvas (no selection / no inspector) to match
|
|
192
469
|
// the editor's default composition; the inspector opens only once
|
|
193
470
|
// the user picks a section to edit.
|
|
@@ -218,38 +495,11 @@ export function PageSectionEditor({
|
|
|
218
495
|
[],
|
|
219
496
|
)
|
|
220
497
|
|
|
221
|
-
const handleAdd = useCallback(
|
|
222
|
-
(type: SectionTypeId) => {
|
|
223
|
-
const section = createSection(type)
|
|
224
|
-
mutateSections((s) => addSection(s, section))
|
|
225
|
-
setSelectedId(section.id)
|
|
226
|
-
setAddOpen(false)
|
|
227
|
-
},
|
|
228
|
-
[mutateSections],
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
const handleReorder = useCallback(
|
|
232
|
-
(orderedIds: string[]) => mutateSections((s) => reorderSections(s, orderedIds)),
|
|
233
|
-
[mutateSections],
|
|
234
|
-
)
|
|
235
|
-
|
|
236
498
|
const handleToggleVisibility = useCallback(
|
|
237
499
|
(id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
|
|
238
500
|
[mutateSections],
|
|
239
501
|
)
|
|
240
502
|
|
|
241
|
-
const handleDuplicate = useCallback(
|
|
242
|
-
(id: string) => mutateSections((s) => duplicateSection(s, id)),
|
|
243
|
-
[mutateSections],
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
const confirmDelete = useCallback(() => {
|
|
247
|
-
if (!deleteTargetId) return
|
|
248
|
-
mutateSections((s) => removeSection(s, deleteTargetId))
|
|
249
|
-
setSelectedId((cur) => (cur === deleteTargetId ? null : cur))
|
|
250
|
-
setDeleteTargetId(null)
|
|
251
|
-
}, [deleteTargetId, mutateSections])
|
|
252
|
-
|
|
253
503
|
const handleContentChange = useCallback(
|
|
254
504
|
(patch: Record<string, unknown>) => {
|
|
255
505
|
if (!selectedId) return
|
|
@@ -282,6 +532,35 @@ export function PageSectionEditor({
|
|
|
282
532
|
}, [])
|
|
283
533
|
|
|
284
534
|
// ─── Save / publish ──────────────────────────────────────────────────
|
|
535
|
+
/**
|
|
536
|
+
* Persist robots directives. They live on the SEO record, not the document
|
|
537
|
+
* body, so they need their own write — and it has to happen on both save and
|
|
538
|
+
* publish, or a toggle would appear to stick until the next reload.
|
|
539
|
+
*
|
|
540
|
+
* Reported on failure rather than swallowed: a directive that silently failed
|
|
541
|
+
* to save is the same defect as one that never reached the head.
|
|
542
|
+
*/
|
|
543
|
+
const saveDirectives = useCallback(
|
|
544
|
+
async (pageId: string) => {
|
|
545
|
+
const { error } = await updateSeoFields('page', pageId, {
|
|
546
|
+
noindex: !directives.index,
|
|
547
|
+
nofollow: !directives.follow,
|
|
548
|
+
nosnippet: !directives.snippet,
|
|
549
|
+
noarchive: !directives.archive,
|
|
550
|
+
maxImagePreview: directives.maxImagePreview,
|
|
551
|
+
})
|
|
552
|
+
if (error) {
|
|
553
|
+
toast.error(`Search directives were not saved: ${error}`)
|
|
554
|
+
return false
|
|
555
|
+
}
|
|
556
|
+
// The header chip and rail dot read the saved SEO record. Without a
|
|
557
|
+
// reload they keep the pre-save score after noindex/nosnippet flips.
|
|
558
|
+
await reloadAudit()
|
|
559
|
+
return true
|
|
560
|
+
},
|
|
561
|
+
[directives, reloadAudit],
|
|
562
|
+
)
|
|
563
|
+
|
|
285
564
|
const handleSaveDraft = useCallback(async (): Promise<boolean> => {
|
|
286
565
|
if (!page) return false
|
|
287
566
|
const result = validatePage(page, false)
|
|
@@ -294,7 +573,8 @@ export function PageSectionEditor({
|
|
|
294
573
|
setSaveState('saving')
|
|
295
574
|
try {
|
|
296
575
|
const saved = await savePageDraft(page)
|
|
297
|
-
|
|
576
|
+
if (saved.id) await saveDirectives(saved.id)
|
|
577
|
+
acceptPage(saved)
|
|
298
578
|
setDirty(false)
|
|
299
579
|
setSaveState('saved')
|
|
300
580
|
setValidation(null)
|
|
@@ -306,7 +586,7 @@ export function PageSectionEditor({
|
|
|
306
586
|
toast.error(err instanceof Error ? err.message : 'Failed to save draft')
|
|
307
587
|
return false
|
|
308
588
|
}
|
|
309
|
-
}, [page, documentId, onNavigate])
|
|
589
|
+
}, [page, documentId, onNavigate, saveDirectives, acceptPage])
|
|
310
590
|
|
|
311
591
|
// Inline branded preview — Next builder canvas (same origin as admin), not
|
|
312
592
|
// the Astro public apex. "View on site" still uses publicSiteUrl below.
|
|
@@ -375,8 +655,27 @@ export function PageSectionEditor({
|
|
|
375
655
|
if (!page) return
|
|
376
656
|
setPublishing(true)
|
|
377
657
|
try {
|
|
658
|
+
// The move has to be read BEFORE publishing — `acceptPage` moves the
|
|
659
|
+
// published path to the new one, after which there is nothing to detect.
|
|
660
|
+
const move =
|
|
661
|
+
redirectOnPublish && canCreateRedirect ? pendingSlugChange(publishedPath, page.path) : null
|
|
662
|
+
|
|
378
663
|
const published = await publishPage(page)
|
|
379
|
-
|
|
664
|
+
if (published.id) await saveDirectives(published.id)
|
|
665
|
+
|
|
666
|
+
if (move) {
|
|
667
|
+
const { error } = await createPublishRedirect(move)
|
|
668
|
+
if (error) {
|
|
669
|
+
// Loud, and naming both paths: the page has already moved, so a
|
|
670
|
+
// silently-failed redirect leaves exactly the broken links the switch
|
|
671
|
+
// promised to prevent.
|
|
672
|
+
toast.error(`Published, but the redirect ${move.from} → ${move.to} failed: ${error}`)
|
|
673
|
+
} else {
|
|
674
|
+
toast.success(`Redirect created: ${move.from} → ${move.to}`)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
acceptPage(published)
|
|
380
679
|
setDirty(false)
|
|
381
680
|
setSaveState('saved')
|
|
382
681
|
setValidation(null)
|
|
@@ -387,67 +686,92 @@ export function PageSectionEditor({
|
|
|
387
686
|
} finally {
|
|
388
687
|
setPublishing(false)
|
|
389
688
|
}
|
|
390
|
-
}, [
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
689
|
+
}, [
|
|
690
|
+
page,
|
|
691
|
+
documentId,
|
|
692
|
+
onNavigate,
|
|
693
|
+
saveDirectives,
|
|
694
|
+
redirectOnPublish,
|
|
695
|
+
canCreateRedirect,
|
|
696
|
+
publishedPath,
|
|
697
|
+
acceptPage,
|
|
698
|
+
])
|
|
699
|
+
|
|
700
|
+
const handleDuplicate = useCallback(async () => {
|
|
395
701
|
if (!page?.id) return
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
702
|
+
const { id, error } = await duplicatePage(page.id)
|
|
703
|
+
if (error || !id) {
|
|
704
|
+
toast.error(error ?? 'Could not duplicate the page')
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
toast.success('Page duplicated')
|
|
708
|
+
// Navigate to the copy rather than staying put. The duplicate is a draft
|
|
709
|
+
// named "(Copy)", and leaving the editor on the original makes it look as
|
|
710
|
+
// though nothing happened.
|
|
711
|
+
onNavigate(`/pages/${id}/edit`)
|
|
712
|
+
}, [page?.id, onNavigate])
|
|
713
|
+
|
|
714
|
+
const handleTrash = useCallback(async () => {
|
|
715
|
+
if (!page?.id) return
|
|
716
|
+
const { error } = await deletePage(page.id)
|
|
717
|
+
if (error) {
|
|
718
|
+
toast.error(error)
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
toast.success('Page moved to trash')
|
|
722
|
+
onNavigate('/pages')
|
|
723
|
+
}, [page?.id, onNavigate])
|
|
409
724
|
|
|
410
725
|
const handleSeoSaved = useCallback(() => {
|
|
726
|
+
reloadAudit()
|
|
411
727
|
if (!page?.id) return
|
|
412
728
|
fetchPageForEditor(page.id)
|
|
413
|
-
.then((p) =>
|
|
729
|
+
.then((p) => acceptPage(p))
|
|
414
730
|
.catch(() => {
|
|
415
731
|
/* SEO API saved; editor meta will sync on next full load */
|
|
416
732
|
})
|
|
417
|
-
}, [page?.id])
|
|
733
|
+
}, [page?.id, reloadAudit])
|
|
418
734
|
|
|
735
|
+
// The header's SEO chip is a shortcut to the rail's SEO tab. Toggling back
|
|
736
|
+
// returns to Sections rather than leaving the rail on a tab the chip no
|
|
737
|
+
// longer appears to be showing.
|
|
419
738
|
const handleToggleSeo = useCallback(() => {
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
return
|
|
739
|
+
setRailTab((tab) => {
|
|
740
|
+
if (tab !== 'seo') setSelectedId(null)
|
|
741
|
+
return tab === 'seo' ? 'sections' : 'seo'
|
|
423
742
|
})
|
|
424
743
|
}, [])
|
|
425
744
|
|
|
426
745
|
const handleSelectSection = useCallback((id: string | null, fieldPath?: string) => {
|
|
427
|
-
|
|
746
|
+
// Selecting on the canvas is a request to edit that section — show the
|
|
747
|
+
// outline it belongs to, whichever tab was open.
|
|
748
|
+
setRailTab('sections')
|
|
428
749
|
setSelectedId(id)
|
|
429
750
|
setPendingFocusPath(fieldPath ?? null)
|
|
430
751
|
}, [])
|
|
431
752
|
|
|
753
|
+
// `effectiveCanEdit`, not `canEdit`: the site preview is a second editing
|
|
754
|
+
// surface, so a role check alone let it write through both the version-review
|
|
755
|
+
// lock and the builder-owned lock the rest of the editor honours.
|
|
432
756
|
const handleCanvasEdit = useCallback(
|
|
433
757
|
(edit: { sectionId: string; fieldPath: string; value: string }) => {
|
|
434
|
-
if (!
|
|
758
|
+
if (!effectiveCanEdit) return
|
|
435
759
|
setSuppressSectionsEcho((n) => n + 1)
|
|
436
760
|
mutateSections((s) =>
|
|
437
761
|
updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
|
|
438
762
|
)
|
|
439
763
|
setSelectedId(edit.sectionId)
|
|
440
764
|
},
|
|
441
|
-
[
|
|
765
|
+
[effectiveCanEdit, mutateSections],
|
|
442
766
|
)
|
|
443
767
|
|
|
444
768
|
const handleCanvasRequestMedia = useCallback(
|
|
445
769
|
(request: { sectionId: string; fieldPath: string }) => {
|
|
446
|
-
if (!
|
|
770
|
+
if (!effectiveCanEdit) return
|
|
447
771
|
setSelectedId(request.sectionId)
|
|
448
772
|
setCanvasMediaTarget(request)
|
|
449
773
|
},
|
|
450
|
-
[
|
|
774
|
+
[effectiveCanEdit],
|
|
451
775
|
)
|
|
452
776
|
|
|
453
777
|
// After a canvas field click, focus the matching inspector input once painted.
|
|
@@ -580,12 +904,12 @@ export function PageSectionEditor({
|
|
|
580
904
|
|
|
581
905
|
const selected = page.sections.find((s) => s.id === selectedId) ?? null
|
|
582
906
|
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
const
|
|
907
|
+
// `readOnly` / `effectiveCanEdit` / `canRestoreVersion` are computed above,
|
|
908
|
+
// with the hooks, so the callbacks defined there share this one gate.
|
|
909
|
+
|
|
910
|
+
// The canvas shows the reviewed snapshot, but `page.sections` stays the
|
|
911
|
+
// editor's own content throughout — nothing about review mode writes to it.
|
|
912
|
+
const canvasSections = reviewVersion ? reviewVersion.sections : page.sections
|
|
589
913
|
|
|
590
914
|
// Sections whose type has no registered definition (seed/code-owned, e.g.
|
|
591
915
|
// `opal-*`). They are preserved verbatim on save but can't be edited here.
|
|
@@ -620,16 +944,28 @@ export function PageSectionEditor({
|
|
|
620
944
|
publishDisabledReason={publishDisabledReason}
|
|
621
945
|
viewport={viewport}
|
|
622
946
|
onViewport={setViewport}
|
|
947
|
+
{...(sitePreview.active
|
|
948
|
+
? // The site preview is an iframe of the real front end, not a
|
|
949
|
+
// scaled canvas — a zoom percentage there would be a lie.
|
|
950
|
+
{}
|
|
951
|
+
: { zoom, canvasScale, onZoom: setZoom })}
|
|
623
952
|
onBack={() => guardedNavigate('/pages')}
|
|
624
953
|
onPreview={handlePreview}
|
|
625
954
|
onViewOnSite={page.id ? handleViewOnSite : undefined}
|
|
626
955
|
sitePreview={sitePreview.active}
|
|
627
956
|
onToggleSitePreview={page.id && page.slug ? sitePreview.toggle : undefined}
|
|
628
|
-
|
|
957
|
+
historyOpen={historyOpen}
|
|
958
|
+
onHistory={page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
|
|
629
959
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
630
960
|
onOpenSeo={handleToggleSeo}
|
|
631
|
-
seoOpen={
|
|
632
|
-
|
|
961
|
+
seoOpen={railTab === 'seo'}
|
|
962
|
+
// The audit's SEO score, banded by the audit's own scale. Previously
|
|
963
|
+
// this chip showed `computeLiveSeoScore` — a different model — so the
|
|
964
|
+
// header read 53 while the rings beside it read 0 and 57.
|
|
965
|
+
seoScore={audit?.seo.score ?? undefined}
|
|
966
|
+
seoScoreBand={
|
|
967
|
+
typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
|
|
968
|
+
}
|
|
633
969
|
onSaveDraft={handleSaveDraft}
|
|
634
970
|
onPublish={requestPublish}
|
|
635
971
|
onSchedule={page.id ? requestSchedule : undefined}
|
|
@@ -687,88 +1023,152 @@ export function PageSectionEditor({
|
|
|
687
1023
|
/>
|
|
688
1024
|
)}
|
|
689
1025
|
|
|
690
|
-
<
|
|
691
|
-
<
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1026
|
+
<ReflowContext.Provider value={reflowContext}>
|
|
1027
|
+
<div className="prenta-editor-body relative flex flex-1 overflow-hidden">
|
|
1028
|
+
<EditorRail collapsedLabel={activeRailTab.label}>
|
|
1029
|
+
{({ width, dragging, collapse }) => (
|
|
1030
|
+
<>
|
|
1031
|
+
<EditorRailTabs tabs={railTabs} active={railTab} onChange={setRailTab} />
|
|
1032
|
+
<RailHeader
|
|
1033
|
+
title={activeRailTab.title}
|
|
1034
|
+
width={width}
|
|
1035
|
+
dragging={dragging}
|
|
1036
|
+
onCollapse={collapse}
|
|
1037
|
+
/>
|
|
1038
|
+
|
|
1039
|
+
<div
|
|
1040
|
+
role="tabpanel"
|
|
1041
|
+
id={`rail-panel-${railTab}`}
|
|
1042
|
+
aria-labelledby={`rail-tab-${railTab}`}
|
|
1043
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
1044
|
+
>
|
|
1045
|
+
{railTab === 'sections' ? (
|
|
1046
|
+
<SectionsList
|
|
1047
|
+
sections={page.sections}
|
|
1048
|
+
selectedId={selectedId}
|
|
1049
|
+
canEdit={effectiveCanEdit}
|
|
1050
|
+
onSelect={handleSelectSection}
|
|
1051
|
+
onToggleVisibility={handleToggleVisibility}
|
|
1052
|
+
/>
|
|
1053
|
+
) : railTab === 'page' ? (
|
|
1054
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
1055
|
+
<PageTab
|
|
1056
|
+
page={page}
|
|
1057
|
+
canEdit={effectiveCanEdit}
|
|
1058
|
+
publishedPath={publishedPath}
|
|
1059
|
+
siteUrl={publicSiteUrl}
|
|
1060
|
+
directives={directives}
|
|
1061
|
+
onDirectivesChange={(next) => {
|
|
1062
|
+
setDirectives(next)
|
|
1063
|
+
setDirty(true)
|
|
1064
|
+
}}
|
|
1065
|
+
redirectOnPublish={redirectOnPublish}
|
|
1066
|
+
onRedirectOnPublishChange={setRedirectOnPublish}
|
|
1067
|
+
canCreateRedirect={canCreateRedirect}
|
|
1068
|
+
onPageChange={handlePageSettings}
|
|
1069
|
+
onDuplicate={handleDuplicate}
|
|
1070
|
+
onTrash={handleTrash}
|
|
1071
|
+
schemaType={seoRecord?.structuredDataType ?? undefined}
|
|
1072
|
+
brandName={config?.seo?.siteName ?? undefined}
|
|
1073
|
+
/>
|
|
1074
|
+
</div>
|
|
1075
|
+
) : (
|
|
1076
|
+
<SeoEditorPane
|
|
1077
|
+
entityType="page"
|
|
1078
|
+
entityId={page.id}
|
|
1079
|
+
slug={page.slug}
|
|
1080
|
+
auditSections={page.sections}
|
|
1081
|
+
onAuditChange={setLiveAudit}
|
|
1082
|
+
contentHtml={seoContentHtml}
|
|
1083
|
+
draftMeta={{
|
|
1084
|
+
seoTitle: page.seoTitle,
|
|
1085
|
+
seoDescription: page.seoDescription,
|
|
1086
|
+
}}
|
|
1087
|
+
onDraftChange={handlePageSettings}
|
|
1088
|
+
onClose={() => setRailTab('sections')}
|
|
1089
|
+
onSaved={handleSeoSaved}
|
|
1090
|
+
/>
|
|
1091
|
+
)}
|
|
1092
|
+
</div>
|
|
1093
|
+
|
|
1094
|
+
{/* Overlays the rail rather than replacing its contents, so
|
|
1095
|
+
closing history returns to exactly the tab and scroll
|
|
1096
|
+
position it was opened from. */}
|
|
1097
|
+
{historyOpen && (
|
|
1098
|
+
<VersionHistoryPanel
|
|
1099
|
+
versions={versions}
|
|
1100
|
+
loading={versionsLoading}
|
|
1101
|
+
error={versionsError}
|
|
1102
|
+
selectedIndex={versionIndex}
|
|
1103
|
+
onSelect={setVersionIndex}
|
|
1104
|
+
onClose={closeHistory}
|
|
1105
|
+
onRestore={handleRestoreVersion}
|
|
1106
|
+
restoring={restoring}
|
|
1107
|
+
canRestore={canRestoreVersion}
|
|
1108
|
+
/>
|
|
1109
|
+
)}
|
|
1110
|
+
</>
|
|
1111
|
+
)}
|
|
1112
|
+
</EditorRail>
|
|
1113
|
+
|
|
1114
|
+
<div className="flex-1 overflow-hidden">
|
|
1115
|
+
{sitePreview.active ? (
|
|
1116
|
+
<SitePreviewFrame
|
|
1117
|
+
src={sitePreview.url}
|
|
1118
|
+
viewport={viewport}
|
|
1119
|
+
reloadKey={sitePreview.reloadKey}
|
|
1120
|
+
title={`Site preview: ${page.title || 'Untitled page'}`}
|
|
1121
|
+
selectedId={selectedId}
|
|
1122
|
+
sections={page.sections}
|
|
1123
|
+
suppressSectionsEcho={suppressSectionsEcho}
|
|
1124
|
+
onSelectSection={handleSelectSection}
|
|
1125
|
+
onCanvasEdit={handleCanvasEdit}
|
|
1126
|
+
onCanvasRequestMedia={handleCanvasRequestMedia}
|
|
1127
|
+
/>
|
|
1128
|
+
) : (
|
|
1129
|
+
<EditorCanvas
|
|
1130
|
+
sections={canvasSections}
|
|
1131
|
+
selectedId={selectedId}
|
|
1132
|
+
breakpoint={viewport}
|
|
1133
|
+
zoom={zoom}
|
|
1134
|
+
onScaleChange={setCanvasScale}
|
|
1135
|
+
onCanvasElement={setCanvasEl}
|
|
1136
|
+
onSelect={handleSelectSection}
|
|
1137
|
+
structuralTypes={structuralTypes}
|
|
1138
|
+
sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
|
|
1139
|
+
reviewing={reviewing}
|
|
1140
|
+
changedSectionIds={reviewChangedIds}
|
|
1141
|
+
reviewBar={
|
|
1142
|
+
reviewVersion ? (
|
|
1143
|
+
<ReviewBar
|
|
1144
|
+
version={reviewVersion}
|
|
1145
|
+
changedCount={reviewChangedIds.size}
|
|
1146
|
+
onRestore={() => void handleRestoreVersion(reviewVersion)}
|
|
1147
|
+
onBackToCurrent={() => setVersionIndex(0)}
|
|
1148
|
+
restoring={restoring}
|
|
1149
|
+
canRestore={canRestoreVersion}
|
|
1150
|
+
/>
|
|
1151
|
+
) : null
|
|
1152
|
+
}
|
|
1153
|
+
/>
|
|
1154
|
+
)}
|
|
1155
|
+
</div>
|
|
1156
|
+
|
|
1157
|
+
{selected && railTab === 'sections' && !reviewing && (
|
|
1158
|
+
<SectionInspector
|
|
1159
|
+
section={selected}
|
|
1160
|
+
canEdit={effectiveCanEdit}
|
|
1161
|
+
onClose={() => setSelectedId(null)}
|
|
1162
|
+
onContentChange={handleContentChange}
|
|
1163
|
+
onSettingsChange={handleSettingsChange}
|
|
1164
|
+
onMetaChange={handleMetaChange}
|
|
1165
|
+
onToggleVisibility={() => handleToggleVisibility(selected.id)}
|
|
726
1166
|
/>
|
|
727
1167
|
)}
|
|
728
1168
|
</div>
|
|
729
|
-
|
|
730
|
-
{selected && !seoOpen && (
|
|
731
|
-
<SectionInspector
|
|
732
|
-
section={selected}
|
|
733
|
-
canEdit={effectiveCanEdit}
|
|
734
|
-
onClose={() => setSelectedId(null)}
|
|
735
|
-
onContentChange={handleContentChange}
|
|
736
|
-
onSettingsChange={handleSettingsChange}
|
|
737
|
-
onMetaChange={handleMetaChange}
|
|
738
|
-
onToggleVisibility={() => handleToggleVisibility(selected.id)}
|
|
739
|
-
/>
|
|
740
|
-
)}
|
|
741
|
-
|
|
742
|
-
{seoOpen && (
|
|
743
|
-
<EditorSeoAside
|
|
744
|
-
entityType="page"
|
|
745
|
-
entityId={page.id}
|
|
746
|
-
title={page.title}
|
|
747
|
-
slug={page.slug}
|
|
748
|
-
seoTitle={page.seoTitle}
|
|
749
|
-
seoDescription={page.seoDescription}
|
|
750
|
-
sections={page.sections}
|
|
751
|
-
canEdit={effectiveCanEdit}
|
|
752
|
-
onClose={() => setSeoOpen(false)}
|
|
753
|
-
onDraftChange={handlePageSettings}
|
|
754
|
-
onSaved={handleSeoSaved}
|
|
755
|
-
/>
|
|
756
|
-
)}
|
|
757
|
-
</div>
|
|
1169
|
+
</ReflowContext.Provider>
|
|
758
1170
|
</div>
|
|
759
1171
|
|
|
760
|
-
<AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
|
|
761
|
-
|
|
762
|
-
{page.id && (
|
|
763
|
-
<VersionHistory
|
|
764
|
-
collectionSlug="pages"
|
|
765
|
-
documentId={page.id}
|
|
766
|
-
open={historyOpen}
|
|
767
|
-
onClose={() => setHistoryOpen(false)}
|
|
768
|
-
onRestore={handleRestored}
|
|
769
|
-
/>
|
|
770
|
-
)}
|
|
771
|
-
|
|
772
1172
|
<PageSettingsModal
|
|
773
1173
|
open={settingsOpen}
|
|
774
1174
|
page={page}
|
|
@@ -777,16 +1177,6 @@ export function PageSectionEditor({
|
|
|
777
1177
|
onSave={handlePageSettings}
|
|
778
1178
|
/>
|
|
779
1179
|
|
|
780
|
-
<ConfirmDialog
|
|
781
|
-
open={deleteTargetId !== null}
|
|
782
|
-
onClose={() => setDeleteTargetId(null)}
|
|
783
|
-
onConfirm={confirmDelete}
|
|
784
|
-
title="Delete section?"
|
|
785
|
-
description="This removes the section from the page. You can undo by leaving without saving."
|
|
786
|
-
confirmLabel="Delete"
|
|
787
|
-
destructive
|
|
788
|
-
/>
|
|
789
|
-
|
|
790
1180
|
<ConfirmDialog
|
|
791
1181
|
open={confirmPublish}
|
|
792
1182
|
onClose={() => setConfirmPublish(false)}
|