@prenta/admin 0.92.0 → 0.93.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 +552 -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__/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/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/editor-top-bar.render.test.js +1 -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/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 +29 -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/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/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 +34 -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 +14 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +8 -17
- 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 +275 -53
- 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/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +63 -19
- 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/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.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +49 -24
- 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__/styles/canvas-container-queries.test.ts +103 -0
- package/src/__tests__/styles/packaging.test.ts +38 -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/editor-top-bar.render.test.tsx +1 -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/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 +86 -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/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/styles/prenta-tokens.css +120 -42
- package/src/styles/theme.css +130 -0
- package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
- package/src/views/page-editor/EditorCanvas.tsx +109 -50
- package/src/views/page-editor/EditorTopBar.tsx +35 -43
- package/src/views/page-editor/PageSectionEditor.tsx +419 -148
- package/src/views/page-editor/PageTab.tsx +433 -0
- package/src/views/page-editor/SectionInspector.tsx +82 -41
- package/src/views/page-editor/SectionsPanel.tsx +281 -140
- package/src/views/page-editor/SitePreviewFrame.tsx +3 -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 +69 -29
- 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,17 @@ 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
13
|
import { VersionHistory } from '../../components/VersionHistory.js'
|
|
12
14
|
import {
|
|
13
|
-
addSection,
|
|
14
|
-
createSection,
|
|
15
|
-
duplicateSection,
|
|
16
15
|
emptyPage,
|
|
17
16
|
fetchPageForEditor,
|
|
18
17
|
missingPageFields,
|
|
19
|
-
moveSection,
|
|
20
18
|
publishPage,
|
|
21
19
|
PAGES_COLLECTION,
|
|
22
|
-
removeSection,
|
|
23
|
-
reorderSections,
|
|
24
20
|
savePageDraft,
|
|
25
21
|
toggleSectionVisibility,
|
|
26
22
|
updateSectionContent,
|
|
@@ -38,20 +34,33 @@ import {
|
|
|
38
34
|
createPreviewToken,
|
|
39
35
|
} from '../../lib/preview-link.js'
|
|
40
36
|
import { usePublicSiteUrl } from '../../hooks/usePublicSiteUrl.js'
|
|
41
|
-
import type { SectionSettings
|
|
37
|
+
import type { SectionSettings } from './section-types.js'
|
|
42
38
|
import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
|
|
39
|
+
import type { Zoom } from '../../lib/breakpoints.js'
|
|
40
|
+
import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
|
|
41
|
+
import {
|
|
42
|
+
DEFAULT_DIRECTIVES,
|
|
43
|
+
directivesFromData,
|
|
44
|
+
type RobotsPermissions,
|
|
45
|
+
} from '@prenta/core/seo/robots-directives'
|
|
46
|
+
import { createPublishRedirect, pendingSlugChange } from '../../lib/page-redirect.js'
|
|
47
|
+
import { updateSeoFields } from '../../lib/seo-service.js'
|
|
48
|
+
import { PageTab } from './PageTab.js'
|
|
49
|
+
import { duplicatePage, deletePage } from '../../lib/pages-service.js'
|
|
50
|
+
import { scoreBand, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
51
|
+
import { usePageAudit } from '../../hooks/usePageAudit.js'
|
|
43
52
|
import { hasCanvasRenderer, useAdminSectionRenderers } from './sections/index.js'
|
|
44
53
|
import { EditorTopBar, type SaveState } from './EditorTopBar.js'
|
|
45
|
-
import {
|
|
54
|
+
import { SectionsList } from './SectionsPanel.js'
|
|
46
55
|
import { EditorCanvas } from './EditorCanvas.js'
|
|
56
|
+
import { ReflowContext } from './reflow-context.js'
|
|
47
57
|
import { SitePreviewFrame } from './SitePreviewFrame.js'
|
|
48
58
|
import { useSitePreview } from './useSitePreview.js'
|
|
49
59
|
import { SectionInspector } from './SectionInspector.js'
|
|
50
|
-
import { AddSectionModal } from './AddSectionModal.js'
|
|
51
60
|
import { PageSettingsModal } from './PageSettingsModal.js'
|
|
52
61
|
import { ValidationSummary } from './ValidationSummary.js'
|
|
53
62
|
import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
|
|
54
|
-
import {
|
|
63
|
+
import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
|
|
55
64
|
import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
|
|
56
65
|
|
|
57
66
|
interface PageSectionEditorProps {
|
|
@@ -74,16 +83,47 @@ interface PageSectionEditorProps {
|
|
|
74
83
|
onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The lower of the two audit scores — the tab dot should reflect the half that
|
|
88
|
+
* needs work, not an average that hides it.
|
|
89
|
+
*
|
|
90
|
+
* Three-valued on purpose, and it must NOT fall back to another scorer:
|
|
91
|
+
* undefined — the audit has not reported yet (the SEO tab has never been
|
|
92
|
+
* opened). No dot beats a dot from a different model.
|
|
93
|
+
* null — both groups are gated off, so there is genuinely nothing to
|
|
94
|
+
* rate. An earlier version fell back to `computeLiveSeoScore`
|
|
95
|
+
* here, which put a number from the content analyser — different
|
|
96
|
+
* model, different bands — on a tab summarising the audit. That
|
|
97
|
+
* is the disagreement this wiring exists to remove.
|
|
98
|
+
*/
|
|
99
|
+
function weakestAuditScore(audit: PageAuditResult | null): number | null | undefined {
|
|
100
|
+
if (!audit) return undefined
|
|
101
|
+
const scores = [audit.seo.score, audit.geo.score].filter((s): s is number => s !== null)
|
|
102
|
+
return scores.length ? Math.min(...scores) : null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function deriveCan(session: any): {
|
|
106
|
+
canEdit: boolean
|
|
107
|
+
canPublish: boolean
|
|
108
|
+
canCreateRedirect: boolean
|
|
109
|
+
} {
|
|
78
110
|
const role: string | undefined = session?.user?.role
|
|
79
111
|
// Default closed: when we can't identify a role, hide the edit/publish
|
|
80
112
|
// affordances rather than exposing them. The server independently enforces
|
|
81
113
|
// access on every write, so this is a UX guard — but it should fail safe.
|
|
82
114
|
// (Matches the Post editor's behavior.)
|
|
83
|
-
if (!role) return { canEdit: false, canPublish: false }
|
|
115
|
+
if (!role) return { canEdit: false, canPublish: false, canCreateRedirect: false }
|
|
84
116
|
const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
|
|
85
117
|
const publishers = ['ADMIN', 'EDITOR']
|
|
86
|
-
|
|
118
|
+
// Same roles that may publish a page may create the 301 that follows a
|
|
119
|
+
// slug move. Offering the checkbox to anyone else promises a redirect
|
|
120
|
+
// `POST /redirects` will reject.
|
|
121
|
+
const redirectCreators = ['OWNER', 'ADMIN', 'EDITOR']
|
|
122
|
+
return {
|
|
123
|
+
canEdit: editors.includes(role),
|
|
124
|
+
canPublish: publishers.includes(role),
|
|
125
|
+
canCreateRedirect: redirectCreators.includes(role),
|
|
126
|
+
}
|
|
87
127
|
}
|
|
88
128
|
|
|
89
129
|
export function PageSectionEditor({
|
|
@@ -109,21 +149,84 @@ export function PageSectionEditor({
|
|
|
109
149
|
fieldPath: string
|
|
110
150
|
} | null>(null)
|
|
111
151
|
const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
|
|
152
|
+
const [zoom, setZoom] = useState<Zoom>('fit')
|
|
153
|
+
/**
|
|
154
|
+
* Live audit reported by the SEO pane, built from unsaved form state. Null
|
|
155
|
+
* until that pane has mounted — `audit` below falls back to the saved-record
|
|
156
|
+
* audit so the header chip and tab dot always have a number.
|
|
157
|
+
*/
|
|
158
|
+
const [liveAudit, setLiveAudit] = useState<PageAuditResult | null>(null)
|
|
159
|
+
/** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
|
|
160
|
+
const [canvasScale, setCanvasScale] = useState(1)
|
|
161
|
+
/**
|
|
162
|
+
* The live canvas surface, for the reflow guard to clone. State rather than a
|
|
163
|
+
* ref because the context value has to update when it mounts — the canvas
|
|
164
|
+
* unmounts whenever the site-preview frame takes over.
|
|
165
|
+
*/
|
|
166
|
+
const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null)
|
|
167
|
+
/**
|
|
168
|
+
* Section content as it is on the live site. Captured whenever the editor
|
|
169
|
+
* takes delivery of a PUBLISHED page — on load, and after each save and
|
|
170
|
+
* publish, both of which write straight through to the live content.
|
|
171
|
+
*/
|
|
172
|
+
const [baseline, setBaseline] = useState<ReflowBaseline | null>(null)
|
|
173
|
+
/**
|
|
174
|
+
* Path as it is on the live site, captured with the reflow baseline and for
|
|
175
|
+
* the same reason: a slug change only breaks links if something is already
|
|
176
|
+
* live at the old address.
|
|
177
|
+
*/
|
|
178
|
+
const [publishedPath, setPublishedPath] = useState<string | null>(null)
|
|
179
|
+
const [directives, setDirectives] = useState<RobotsPermissions>(DEFAULT_DIRECTIVES)
|
|
180
|
+
/**
|
|
181
|
+
* Defaults ON. The editor changing a live page's slug almost always wants the
|
|
182
|
+
* redirect, and the cost of an unwanted one is a rule they can delete, while
|
|
183
|
+
* the cost of a missing one is 404s discovered by visitors.
|
|
184
|
+
*/
|
|
185
|
+
const [redirectOnPublish, setRedirectOnPublish] = useState(true)
|
|
186
|
+
|
|
187
|
+
// The reflow guard's inputs, gathered in one place. Memoised because a new
|
|
188
|
+
// object identity on every render would restart the debounce in every guarded
|
|
189
|
+
// field's effect — the measurement would then never settle while typing,
|
|
190
|
+
// which is precisely when it is wanted.
|
|
191
|
+
const reflowContext = useMemo(
|
|
192
|
+
() => ({
|
|
193
|
+
canvas: canvasEl,
|
|
194
|
+
sectionId: selectedId,
|
|
195
|
+
baseline,
|
|
196
|
+
device: viewport,
|
|
197
|
+
onProof: setViewport,
|
|
198
|
+
}),
|
|
199
|
+
[canvasEl, selectedId, baseline, viewport],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Take delivery of server state.
|
|
204
|
+
*
|
|
205
|
+
* Every path that receives a fresh page from the API goes through here so the
|
|
206
|
+
* reflow baseline is re-captured with it. Saving a PUBLISHED page writes
|
|
207
|
+
* straight through to the live content — there is no draft/published split —
|
|
208
|
+
* so a save moves the baseline exactly as a publish does. Missing one would
|
|
209
|
+
* leave the guard comparing against content that is no longer on the site.
|
|
210
|
+
*/
|
|
211
|
+
const acceptPage = useCallback((p: EditorPage) => {
|
|
212
|
+
setPage(p)
|
|
213
|
+
setBaseline(snapshotBaseline(p.sections, p.status))
|
|
214
|
+
setPublishedPath(p.status === 'PUBLISHED' ? p.path : null)
|
|
215
|
+
}, [])
|
|
112
216
|
const [dirty, setDirty] = useState(false)
|
|
113
217
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
114
218
|
const [publishing, setPublishing] = useState(false)
|
|
115
219
|
|
|
116
|
-
const [addOpen, setAddOpen] = useState(false)
|
|
117
220
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
118
|
-
|
|
221
|
+
/** Active rail tab. Replaces the old boolean `seoOpen` right-hand aside. */
|
|
222
|
+
const [railTab, setRailTab] = useState<RailTabId>('sections')
|
|
119
223
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
120
|
-
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
121
224
|
const [confirmPublish, setConfirmPublish] = useState(false)
|
|
122
225
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
123
226
|
const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
|
|
124
227
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
125
228
|
|
|
126
|
-
const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
|
|
229
|
+
const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session])
|
|
127
230
|
const customRenderers = useAdminSectionRenderers()
|
|
128
231
|
const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
|
|
129
232
|
|
|
@@ -148,18 +251,80 @@ export function PageSectionEditor({
|
|
|
148
251
|
)
|
|
149
252
|
const publishReady = Boolean(page) && publishCheck.errors.length === 0
|
|
150
253
|
|
|
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
254
|
const publishDisabledReason = publishCheck.errors[0]
|
|
162
255
|
|
|
256
|
+
// Saved-record audit, available from first paint. Both this and the pane's
|
|
257
|
+
// live audit go through `buildPageAuditInput`, so they can differ only in
|
|
258
|
+
// saved-versus-unsaved metadata — never in how a check is decided.
|
|
259
|
+
const {
|
|
260
|
+
audit: savedAudit,
|
|
261
|
+
record: seoRecord,
|
|
262
|
+
reload: reloadAudit,
|
|
263
|
+
} = usePageAudit({
|
|
264
|
+
entityType: 'page',
|
|
265
|
+
entityId: page?.id ?? null,
|
|
266
|
+
sections: page?.sections ?? [],
|
|
267
|
+
slug: page?.slug ?? '',
|
|
268
|
+
fallback: { title: page?.seoTitle, description: page?.seoDescription },
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// Seed directives from the saved record. Keyed on the record's identity, not
|
|
272
|
+
// on its fields — re-running on every field change would discard whatever the
|
|
273
|
+
// editor had just toggled and not yet saved.
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!seoRecord) return
|
|
276
|
+
setDirectives(
|
|
277
|
+
directivesFromData({
|
|
278
|
+
noindex: seoRecord.noindex,
|
|
279
|
+
nofollow: seoRecord.nofollow,
|
|
280
|
+
nosnippet: seoRecord.nosnippet,
|
|
281
|
+
noarchive: seoRecord.noarchive,
|
|
282
|
+
maxImagePreview: seoRecord.maxImagePreview,
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
}, [seoRecord?.id])
|
|
286
|
+
|
|
287
|
+
// Unsaved edits win: while the SEO pane is open its number is the truthful
|
|
288
|
+
// one, and every surface reads this single value.
|
|
289
|
+
const audit = liveAudit ?? savedAudit
|
|
290
|
+
|
|
291
|
+
// Content the SEO pane analyses. Hoisted out of the old aside wrapper so the
|
|
292
|
+
// rail can render the pane directly — the aside only ever existed to supply
|
|
293
|
+
// a resizable container, which the rail now is.
|
|
294
|
+
const seoContentHtml = useMemo(
|
|
295
|
+
() => buildEditorContentHtml(page?.sections ?? []),
|
|
296
|
+
[page?.sections],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const railTabs = useMemo<RailTab[]>(
|
|
300
|
+
() => [
|
|
301
|
+
{
|
|
302
|
+
id: 'sections',
|
|
303
|
+
label: 'Sections',
|
|
304
|
+
title: 'Page content',
|
|
305
|
+
count: page?.sections.length ?? 0,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'seo',
|
|
309
|
+
label: 'SEO & GEO',
|
|
310
|
+
title: 'Search & AI readiness',
|
|
311
|
+
// The weaker of the two: a page can be perfectly crawlable and still
|
|
312
|
+
// invisible to answer engines, and a dot that averaged them would hide
|
|
313
|
+
// whichever half is failing. `null` means a gate is off, which the tab
|
|
314
|
+
// reports as unrated rather than as a low score.
|
|
315
|
+
score: weakestAuditScore(audit),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'page',
|
|
319
|
+
label: 'Page',
|
|
320
|
+
title: 'Page settings',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
[page?.sections.length, audit],
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const activeRailTab = railTabs.find((tab) => tab.id === railTab) ?? railTabs[0]!
|
|
327
|
+
|
|
163
328
|
// ─── Load ──────────────────────────────────────────────────────────
|
|
164
329
|
useEffect(() => {
|
|
165
330
|
let cancelled = false
|
|
@@ -175,7 +340,7 @@ export function PageSectionEditor({
|
|
|
175
340
|
}
|
|
176
341
|
|
|
177
342
|
if (!documentId) {
|
|
178
|
-
|
|
343
|
+
acceptPage(emptyPage(initialParentId ?? null))
|
|
179
344
|
setLoading(false)
|
|
180
345
|
// Start a brand-new page the way WordPress does: name it first. The
|
|
181
346
|
// settings modal opens with the title focused and the slug/path
|
|
@@ -187,7 +352,7 @@ export function PageSectionEditor({
|
|
|
187
352
|
fetchPageForEditor(documentId)
|
|
188
353
|
.then((p) => {
|
|
189
354
|
if (cancelled) return
|
|
190
|
-
|
|
355
|
+
acceptPage(p)
|
|
191
356
|
// Default to a clean canvas (no selection / no inspector) to match
|
|
192
357
|
// the editor's default composition; the inspector opens only once
|
|
193
358
|
// the user picks a section to edit.
|
|
@@ -218,38 +383,11 @@ export function PageSectionEditor({
|
|
|
218
383
|
[],
|
|
219
384
|
)
|
|
220
385
|
|
|
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
386
|
const handleToggleVisibility = useCallback(
|
|
237
387
|
(id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
|
|
238
388
|
[mutateSections],
|
|
239
389
|
)
|
|
240
390
|
|
|
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
391
|
const handleContentChange = useCallback(
|
|
254
392
|
(patch: Record<string, unknown>) => {
|
|
255
393
|
if (!selectedId) return
|
|
@@ -282,6 +420,35 @@ export function PageSectionEditor({
|
|
|
282
420
|
}, [])
|
|
283
421
|
|
|
284
422
|
// ─── Save / publish ──────────────────────────────────────────────────
|
|
423
|
+
/**
|
|
424
|
+
* Persist robots directives. They live on the SEO record, not the document
|
|
425
|
+
* body, so they need their own write — and it has to happen on both save and
|
|
426
|
+
* publish, or a toggle would appear to stick until the next reload.
|
|
427
|
+
*
|
|
428
|
+
* Reported on failure rather than swallowed: a directive that silently failed
|
|
429
|
+
* to save is the same defect as one that never reached the head.
|
|
430
|
+
*/
|
|
431
|
+
const saveDirectives = useCallback(
|
|
432
|
+
async (pageId: string) => {
|
|
433
|
+
const { error } = await updateSeoFields('page', pageId, {
|
|
434
|
+
noindex: !directives.index,
|
|
435
|
+
nofollow: !directives.follow,
|
|
436
|
+
nosnippet: !directives.snippet,
|
|
437
|
+
noarchive: !directives.archive,
|
|
438
|
+
maxImagePreview: directives.maxImagePreview,
|
|
439
|
+
})
|
|
440
|
+
if (error) {
|
|
441
|
+
toast.error(`Search directives were not saved: ${error}`)
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
// The header chip and rail dot read the saved SEO record. Without a
|
|
445
|
+
// reload they keep the pre-save score after noindex/nosnippet flips.
|
|
446
|
+
await reloadAudit()
|
|
447
|
+
return true
|
|
448
|
+
},
|
|
449
|
+
[directives, reloadAudit],
|
|
450
|
+
)
|
|
451
|
+
|
|
285
452
|
const handleSaveDraft = useCallback(async (): Promise<boolean> => {
|
|
286
453
|
if (!page) return false
|
|
287
454
|
const result = validatePage(page, false)
|
|
@@ -294,7 +461,8 @@ export function PageSectionEditor({
|
|
|
294
461
|
setSaveState('saving')
|
|
295
462
|
try {
|
|
296
463
|
const saved = await savePageDraft(page)
|
|
297
|
-
|
|
464
|
+
if (saved.id) await saveDirectives(saved.id)
|
|
465
|
+
acceptPage(saved)
|
|
298
466
|
setDirty(false)
|
|
299
467
|
setSaveState('saved')
|
|
300
468
|
setValidation(null)
|
|
@@ -306,7 +474,7 @@ export function PageSectionEditor({
|
|
|
306
474
|
toast.error(err instanceof Error ? err.message : 'Failed to save draft')
|
|
307
475
|
return false
|
|
308
476
|
}
|
|
309
|
-
}, [page, documentId, onNavigate])
|
|
477
|
+
}, [page, documentId, onNavigate, saveDirectives, acceptPage])
|
|
310
478
|
|
|
311
479
|
// Inline branded preview — Next builder canvas (same origin as admin), not
|
|
312
480
|
// the Astro public apex. "View on site" still uses publicSiteUrl below.
|
|
@@ -375,8 +543,27 @@ export function PageSectionEditor({
|
|
|
375
543
|
if (!page) return
|
|
376
544
|
setPublishing(true)
|
|
377
545
|
try {
|
|
546
|
+
// The move has to be read BEFORE publishing — `acceptPage` moves the
|
|
547
|
+
// published path to the new one, after which there is nothing to detect.
|
|
548
|
+
const move =
|
|
549
|
+
redirectOnPublish && canCreateRedirect ? pendingSlugChange(publishedPath, page.path) : null
|
|
550
|
+
|
|
378
551
|
const published = await publishPage(page)
|
|
379
|
-
|
|
552
|
+
if (published.id) await saveDirectives(published.id)
|
|
553
|
+
|
|
554
|
+
if (move) {
|
|
555
|
+
const { error } = await createPublishRedirect(move)
|
|
556
|
+
if (error) {
|
|
557
|
+
// Loud, and naming both paths: the page has already moved, so a
|
|
558
|
+
// silently-failed redirect leaves exactly the broken links the switch
|
|
559
|
+
// promised to prevent.
|
|
560
|
+
toast.error(`Published, but the redirect ${move.from} → ${move.to} failed: ${error}`)
|
|
561
|
+
} else {
|
|
562
|
+
toast.success(`Redirect created: ${move.from} → ${move.to}`)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
acceptPage(published)
|
|
380
567
|
setDirty(false)
|
|
381
568
|
setSaveState('saved')
|
|
382
569
|
setValidation(null)
|
|
@@ -387,7 +574,41 @@ export function PageSectionEditor({
|
|
|
387
574
|
} finally {
|
|
388
575
|
setPublishing(false)
|
|
389
576
|
}
|
|
390
|
-
}, [
|
|
577
|
+
}, [
|
|
578
|
+
page,
|
|
579
|
+
documentId,
|
|
580
|
+
onNavigate,
|
|
581
|
+
saveDirectives,
|
|
582
|
+
redirectOnPublish,
|
|
583
|
+
canCreateRedirect,
|
|
584
|
+
publishedPath,
|
|
585
|
+
acceptPage,
|
|
586
|
+
])
|
|
587
|
+
|
|
588
|
+
const handleDuplicate = useCallback(async () => {
|
|
589
|
+
if (!page?.id) return
|
|
590
|
+
const { id, error } = await duplicatePage(page.id)
|
|
591
|
+
if (error || !id) {
|
|
592
|
+
toast.error(error ?? 'Could not duplicate the page')
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
toast.success('Page duplicated')
|
|
596
|
+
// Navigate to the copy rather than staying put. The duplicate is a draft
|
|
597
|
+
// named "(Copy)", and leaving the editor on the original makes it look as
|
|
598
|
+
// though nothing happened.
|
|
599
|
+
onNavigate(`/pages/${id}/edit`)
|
|
600
|
+
}, [page?.id, onNavigate])
|
|
601
|
+
|
|
602
|
+
const handleTrash = useCallback(async () => {
|
|
603
|
+
if (!page?.id) return
|
|
604
|
+
const { error } = await deletePage(page.id)
|
|
605
|
+
if (error) {
|
|
606
|
+
toast.error(error)
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
toast.success('Page moved to trash')
|
|
610
|
+
onNavigate('/pages')
|
|
611
|
+
}, [page?.id, onNavigate])
|
|
391
612
|
|
|
392
613
|
// After a version restore the server has already written the restored
|
|
393
614
|
// data — refetch so the editor reflects it instead of the stale state.
|
|
@@ -395,9 +616,9 @@ export function PageSectionEditor({
|
|
|
395
616
|
if (!page?.id) return
|
|
396
617
|
fetchPageForEditor(page.id)
|
|
397
618
|
.then((p) => {
|
|
398
|
-
|
|
619
|
+
acceptPage(p)
|
|
399
620
|
setSelectedId(null)
|
|
400
|
-
|
|
621
|
+
setRailTab('sections')
|
|
401
622
|
setDirty(false)
|
|
402
623
|
setSaveState('idle')
|
|
403
624
|
setValidation(null)
|
|
@@ -408,23 +629,29 @@ export function PageSectionEditor({
|
|
|
408
629
|
}, [page?.id])
|
|
409
630
|
|
|
410
631
|
const handleSeoSaved = useCallback(() => {
|
|
632
|
+
reloadAudit()
|
|
411
633
|
if (!page?.id) return
|
|
412
634
|
fetchPageForEditor(page.id)
|
|
413
|
-
.then((p) =>
|
|
635
|
+
.then((p) => acceptPage(p))
|
|
414
636
|
.catch(() => {
|
|
415
637
|
/* SEO API saved; editor meta will sync on next full load */
|
|
416
638
|
})
|
|
417
|
-
}, [page?.id])
|
|
639
|
+
}, [page?.id, reloadAudit])
|
|
418
640
|
|
|
641
|
+
// The header's SEO chip is a shortcut to the rail's SEO tab. Toggling back
|
|
642
|
+
// returns to Sections rather than leaving the rail on a tab the chip no
|
|
643
|
+
// longer appears to be showing.
|
|
419
644
|
const handleToggleSeo = useCallback(() => {
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
return
|
|
645
|
+
setRailTab((tab) => {
|
|
646
|
+
if (tab !== 'seo') setSelectedId(null)
|
|
647
|
+
return tab === 'seo' ? 'sections' : 'seo'
|
|
423
648
|
})
|
|
424
649
|
}, [])
|
|
425
650
|
|
|
426
651
|
const handleSelectSection = useCallback((id: string | null, fieldPath?: string) => {
|
|
427
|
-
|
|
652
|
+
// Selecting on the canvas is a request to edit that section — show the
|
|
653
|
+
// outline it belongs to, whichever tab was open.
|
|
654
|
+
setRailTab('sections')
|
|
428
655
|
setSelectedId(id)
|
|
429
656
|
setPendingFocusPath(fieldPath ?? null)
|
|
430
657
|
}, [])
|
|
@@ -620,6 +847,11 @@ export function PageSectionEditor({
|
|
|
620
847
|
publishDisabledReason={publishDisabledReason}
|
|
621
848
|
viewport={viewport}
|
|
622
849
|
onViewport={setViewport}
|
|
850
|
+
{...(sitePreview.active
|
|
851
|
+
? // The site preview is an iframe of the real front end, not a
|
|
852
|
+
// scaled canvas — a zoom percentage there would be a lie.
|
|
853
|
+
{}
|
|
854
|
+
: { zoom, canvasScale, onZoom: setZoom })}
|
|
623
855
|
onBack={() => guardedNavigate('/pages')}
|
|
624
856
|
onPreview={handlePreview}
|
|
625
857
|
onViewOnSite={page.id ? handleViewOnSite : undefined}
|
|
@@ -628,8 +860,14 @@ export function PageSectionEditor({
|
|
|
628
860
|
onHistory={page.id ? () => setHistoryOpen(true) : undefined}
|
|
629
861
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
630
862
|
onOpenSeo={handleToggleSeo}
|
|
631
|
-
seoOpen={
|
|
632
|
-
|
|
863
|
+
seoOpen={railTab === 'seo'}
|
|
864
|
+
// The audit's SEO score, banded by the audit's own scale. Previously
|
|
865
|
+
// this chip showed `computeLiveSeoScore` — a different model — so the
|
|
866
|
+
// header read 53 while the rings beside it read 0 and 57.
|
|
867
|
+
seoScore={audit?.seo.score ?? undefined}
|
|
868
|
+
seoScoreBand={
|
|
869
|
+
typeof audit?.seo.score === 'number' ? scoreBand(audit.seo.score) : undefined
|
|
870
|
+
}
|
|
633
871
|
onSaveDraft={handleSaveDraft}
|
|
634
872
|
onPublish={requestPublish}
|
|
635
873
|
onSchedule={page.id ? requestSchedule : undefined}
|
|
@@ -687,78 +925,121 @@ export function PageSectionEditor({
|
|
|
687
925
|
/>
|
|
688
926
|
)}
|
|
689
927
|
|
|
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
|
-
|
|
928
|
+
<ReflowContext.Provider value={reflowContext}>
|
|
929
|
+
<div className="relative flex flex-1 overflow-hidden">
|
|
930
|
+
<EditorRail collapsedLabel={activeRailTab.label}>
|
|
931
|
+
{({ width, dragging, collapse }) => (
|
|
932
|
+
<>
|
|
933
|
+
<EditorRailTabs tabs={railTabs} active={railTab} onChange={setRailTab} />
|
|
934
|
+
<RailHeader
|
|
935
|
+
title={activeRailTab.title}
|
|
936
|
+
width={width}
|
|
937
|
+
dragging={dragging}
|
|
938
|
+
onCollapse={collapse}
|
|
939
|
+
/>
|
|
940
|
+
|
|
941
|
+
<div
|
|
942
|
+
role="tabpanel"
|
|
943
|
+
id={`rail-panel-${railTab}`}
|
|
944
|
+
aria-labelledby={`rail-tab-${railTab}`}
|
|
945
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
946
|
+
>
|
|
947
|
+
{railTab === 'sections' ? (
|
|
948
|
+
<SectionsList
|
|
949
|
+
sections={page.sections}
|
|
950
|
+
selectedId={selectedId}
|
|
951
|
+
canEdit={effectiveCanEdit}
|
|
952
|
+
onSelect={handleSelectSection}
|
|
953
|
+
onToggleVisibility={handleToggleVisibility}
|
|
954
|
+
/>
|
|
955
|
+
) : railTab === 'page' ? (
|
|
956
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
957
|
+
<PageTab
|
|
958
|
+
page={page}
|
|
959
|
+
canEdit={effectiveCanEdit}
|
|
960
|
+
publishedPath={publishedPath}
|
|
961
|
+
siteUrl={publicSiteUrl}
|
|
962
|
+
directives={directives}
|
|
963
|
+
onDirectivesChange={(next) => {
|
|
964
|
+
setDirectives(next)
|
|
965
|
+
setDirty(true)
|
|
966
|
+
}}
|
|
967
|
+
redirectOnPublish={redirectOnPublish}
|
|
968
|
+
onRedirectOnPublishChange={setRedirectOnPublish}
|
|
969
|
+
canCreateRedirect={canCreateRedirect}
|
|
970
|
+
onPageChange={handlePageSettings}
|
|
971
|
+
onDuplicate={handleDuplicate}
|
|
972
|
+
onTrash={handleTrash}
|
|
973
|
+
schemaType={seoRecord?.structuredDataType ?? undefined}
|
|
974
|
+
brandName={config?.seo?.siteName ?? undefined}
|
|
975
|
+
/>
|
|
976
|
+
</div>
|
|
977
|
+
) : (
|
|
978
|
+
<SeoEditorPane
|
|
979
|
+
entityType="page"
|
|
980
|
+
entityId={page.id}
|
|
981
|
+
slug={page.slug}
|
|
982
|
+
auditSections={page.sections}
|
|
983
|
+
onAuditChange={setLiveAudit}
|
|
984
|
+
contentHtml={seoContentHtml}
|
|
985
|
+
draftMeta={{
|
|
986
|
+
seoTitle: page.seoTitle,
|
|
987
|
+
seoDescription: page.seoDescription,
|
|
988
|
+
}}
|
|
989
|
+
onDraftChange={handlePageSettings}
|
|
990
|
+
onClose={() => setRailTab('sections')}
|
|
991
|
+
onSaved={handleSeoSaved}
|
|
992
|
+
/>
|
|
993
|
+
)}
|
|
994
|
+
</div>
|
|
995
|
+
</>
|
|
996
|
+
)}
|
|
997
|
+
</EditorRail>
|
|
998
|
+
|
|
999
|
+
<div className="flex-1 overflow-hidden">
|
|
1000
|
+
{sitePreview.active ? (
|
|
1001
|
+
<SitePreviewFrame
|
|
1002
|
+
src={sitePreview.url}
|
|
1003
|
+
viewport={viewport}
|
|
1004
|
+
reloadKey={sitePreview.reloadKey}
|
|
1005
|
+
title={`Site preview: ${page.title || 'Untitled page'}`}
|
|
1006
|
+
selectedId={selectedId}
|
|
1007
|
+
sections={page.sections}
|
|
1008
|
+
suppressSectionsEcho={suppressSectionsEcho}
|
|
1009
|
+
onSelectSection={handleSelectSection}
|
|
1010
|
+
onCanvasEdit={handleCanvasEdit}
|
|
1011
|
+
onCanvasRequestMedia={handleCanvasRequestMedia}
|
|
1012
|
+
/>
|
|
1013
|
+
) : (
|
|
1014
|
+
<EditorCanvas
|
|
1015
|
+
sections={page.sections}
|
|
1016
|
+
selectedId={selectedId}
|
|
1017
|
+
breakpoint={viewport}
|
|
1018
|
+
zoom={zoom}
|
|
1019
|
+
onScaleChange={setCanvasScale}
|
|
1020
|
+
onCanvasElement={setCanvasEl}
|
|
1021
|
+
onSelect={handleSelectSection}
|
|
1022
|
+
structuralTypes={structuralTypes}
|
|
1023
|
+
sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
|
|
1024
|
+
/>
|
|
1025
|
+
)}
|
|
1026
|
+
</div>
|
|
1027
|
+
|
|
1028
|
+
{selected && railTab === 'sections' && (
|
|
1029
|
+
<SectionInspector
|
|
1030
|
+
section={selected}
|
|
1031
|
+
canEdit={effectiveCanEdit}
|
|
1032
|
+
onClose={() => setSelectedId(null)}
|
|
1033
|
+
onContentChange={handleContentChange}
|
|
1034
|
+
onSettingsChange={handleSettingsChange}
|
|
1035
|
+
onMetaChange={handleMetaChange}
|
|
1036
|
+
onToggleVisibility={() => handleToggleVisibility(selected.id)}
|
|
726
1037
|
/>
|
|
727
1038
|
)}
|
|
728
1039
|
</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>
|
|
1040
|
+
</ReflowContext.Provider>
|
|
758
1041
|
</div>
|
|
759
1042
|
|
|
760
|
-
<AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
|
|
761
|
-
|
|
762
1043
|
{page.id && (
|
|
763
1044
|
<VersionHistory
|
|
764
1045
|
collectionSlug="pages"
|
|
@@ -777,16 +1058,6 @@ export function PageSectionEditor({
|
|
|
777
1058
|
onSave={handlePageSettings}
|
|
778
1059
|
/>
|
|
779
1060
|
|
|
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
1061
|
<ConfirmDialog
|
|
791
1062
|
open={confirmPublish}
|
|
792
1063
|
onClose={() => setConfirmPublish(false)}
|