@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
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type ReactElement,
|
|
16
16
|
type ReactNode,
|
|
17
17
|
} from 'react'
|
|
18
|
-
import { Loader2, Monitor, Smartphone, Sparkles, Wand2 } from 'lucide-react'
|
|
18
|
+
import { ImagePlus, Loader2, Monitor, Smartphone, Sparkles, Trash2, Wand2 } from 'lucide-react'
|
|
19
19
|
import { toast } from 'sonner'
|
|
20
20
|
import type { BrandAlignmentScoreResult } from '@prenta/core/brand-voice'
|
|
21
21
|
import { resolveRenderedMetaTitle } from '@prenta/core/client'
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
stripHtmlTags,
|
|
26
26
|
type SEOAnalysisResult,
|
|
27
27
|
} from '@prenta/core/seo/analysis'
|
|
28
|
+
import { auditPage, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
29
|
+
import { buildPageAuditInput } from '../../lib/page-audit-input.js'
|
|
30
|
+
import type { PageSection } from '@prenta/core/page-sections'
|
|
28
31
|
import {
|
|
29
32
|
fetchSeoContentRecord,
|
|
30
33
|
updateSeoFields,
|
|
@@ -39,13 +42,21 @@ import { fixBrandVoiceContent } from '../../lib/brand-voice-client.js'
|
|
|
39
42
|
import { BrandVoiceAlignmentBadge } from '../BrandVoiceAlignmentBadge.js'
|
|
40
43
|
import { InspectorPaneBody, InspectorPaneFooter, InspectorPaneHeader } from '../ui/InspectorPane.js'
|
|
41
44
|
import { Toggle } from '../ui/Toggle.js'
|
|
45
|
+
import { MediaPickerModal } from '../MediaPickerModal.js'
|
|
46
|
+
import { PageAuditPanel } from './PageAuditPanel.js'
|
|
47
|
+
import { SeoCharCount } from './SeoCharCount.js'
|
|
48
|
+
import { SerpPreview } from './SerpPreview.js'
|
|
42
49
|
import { SeoLoading, SeoErrorState, gradeMeta } from './primitives.js'
|
|
43
50
|
import { SeoAccordion, SeoAccordionBadge, SeoAccordionItem } from './SeoAccordion.js'
|
|
44
51
|
import { SeoAnalysisList } from './SeoAnalysisList.js'
|
|
45
52
|
import { SeoInsightsMetrics } from './SeoInsightsMetrics.js'
|
|
46
53
|
import { SeoLinkingSection } from './SeoLinkingSection.js'
|
|
47
54
|
|
|
55
|
+
// Ranges the audit's title/description checks score against — one definition,
|
|
56
|
+
// so the meter cannot tell an editor they are in range while the check fails.
|
|
57
|
+
const META_TITLE_MIN = 30
|
|
48
58
|
const META_TITLE_MAX = 60
|
|
59
|
+
const META_DESC_MIN = 70
|
|
49
60
|
const META_DESC_MAX = 160
|
|
50
61
|
const KEY_TAKEAWAY_MAX = 240
|
|
51
62
|
const LLMS_DESC_MAX = 160
|
|
@@ -66,15 +77,6 @@ function seoBrandChannel(field: AiField): string | undefined {
|
|
|
66
77
|
const inputCls =
|
|
67
78
|
'w-full rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-1.5 text-[12.5px] text-[var(--txt)] placeholder:text-[var(--muted)] focus:border-[var(--acc)] focus:outline-none'
|
|
68
79
|
|
|
69
|
-
function CharCount({ value, max }: { value: string; max: number }) {
|
|
70
|
-
const over = value.length > max
|
|
71
|
-
return (
|
|
72
|
-
<span className={`text-[11px] ${over ? 'text-[var(--err)]' : 'text-[var(--muted)]'}`}>
|
|
73
|
-
{value.length}/{max}
|
|
74
|
-
</span>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
80
|
function PaneField({
|
|
79
81
|
label,
|
|
80
82
|
children,
|
|
@@ -131,6 +133,18 @@ function PaneToggleRow({
|
|
|
131
133
|
|
|
132
134
|
export interface SeoEditorPaneProps {
|
|
133
135
|
entityType: 'page' | 'post'
|
|
136
|
+
/**
|
|
137
|
+
* Sections for the page-level SEO/GEO audit. Omit to hide the twin rings —
|
|
138
|
+
* the audit reads structure (stats, quotes, image slots), which a post body
|
|
139
|
+
* alone cannot supply.
|
|
140
|
+
*/
|
|
141
|
+
auditSections?: PageSection[]
|
|
142
|
+
/**
|
|
143
|
+
* Reports the audit upward so the rail's tab dot can show the same number
|
|
144
|
+
* this panel shows. Without it the dot and the panel compute independently
|
|
145
|
+
* and disagree — which is exactly what they did before the audit existed.
|
|
146
|
+
*/
|
|
147
|
+
onAuditChange?: (audit: PageAuditResult | null) => void
|
|
134
148
|
/** When absent, draft-only mode — meta fields sync via `onDraftChange`. */
|
|
135
149
|
entityId: string | null
|
|
136
150
|
onClose: () => void
|
|
@@ -145,6 +159,8 @@ export interface SeoEditorPaneProps {
|
|
|
145
159
|
export function SeoEditorPane({
|
|
146
160
|
entityType,
|
|
147
161
|
entityId,
|
|
162
|
+
auditSections,
|
|
163
|
+
onAuditChange,
|
|
148
164
|
onClose,
|
|
149
165
|
onSaved,
|
|
150
166
|
contentHtml = '',
|
|
@@ -165,6 +181,7 @@ export function SeoEditorPane({
|
|
|
165
181
|
const [form, setForm] = useState<SeoFieldPayload>({})
|
|
166
182
|
const [previewDevice, setPreviewDevice] = useState<'mobile' | 'desktop'>('mobile')
|
|
167
183
|
const [socialPlatform, setSocialPlatform] = useState<'facebook' | 'twitter'>('facebook')
|
|
184
|
+
const [imagePickerOpen, setImagePickerOpen] = useState(false)
|
|
168
185
|
const [remoteAnalysis, setRemoteAnalysis] = useState<SEOAnalysisResult | null>(null)
|
|
169
186
|
|
|
170
187
|
useEffect(() => {
|
|
@@ -198,7 +215,24 @@ export function SeoEditorPane({
|
|
|
198
215
|
.then((rec) => {
|
|
199
216
|
if (!alive) return
|
|
200
217
|
if (!rec) {
|
|
201
|
-
|
|
218
|
+
// Same situation as the catch below, reached without a throw.
|
|
219
|
+
setRecord(null)
|
|
220
|
+
setForm({
|
|
221
|
+
metaTitle: draftMeta?.seoTitle ?? '',
|
|
222
|
+
metaDescription: draftMeta?.seoDescription ?? '',
|
|
223
|
+
focusKeyword: '',
|
|
224
|
+
keyTakeaway: '',
|
|
225
|
+
ogTitle: '',
|
|
226
|
+
ogDescription: '',
|
|
227
|
+
canonicalUrl: '',
|
|
228
|
+
structuredDataType: 'WebPage',
|
|
229
|
+
noindex: false,
|
|
230
|
+
nofollow: false,
|
|
231
|
+
llmsInclude: 'AUTO',
|
|
232
|
+
llmsDescription: '',
|
|
233
|
+
llmsSection: '',
|
|
234
|
+
llmsOptional: false,
|
|
235
|
+
})
|
|
202
236
|
return
|
|
203
237
|
}
|
|
204
238
|
setRecord(rec)
|
|
@@ -210,6 +244,7 @@ export function SeoEditorPane({
|
|
|
210
244
|
keyTakeaway: rec.keyTakeaway ?? '',
|
|
211
245
|
ogTitle: rec.ogTitle ?? '',
|
|
212
246
|
ogDescription: rec.ogDescription ?? '',
|
|
247
|
+
ogImage: rec.ogImage ?? '',
|
|
213
248
|
twitterTitle: rec.twitterTitle ?? '',
|
|
214
249
|
twitterDescription: rec.twitterDescription ?? '',
|
|
215
250
|
structuredDataType: rec.structuredDataType ?? 'WebPage',
|
|
@@ -221,12 +256,36 @@ export function SeoEditorPane({
|
|
|
221
256
|
llmsOptional: rec.llmsOptional ?? false,
|
|
222
257
|
})
|
|
223
258
|
})
|
|
224
|
-
.catch((
|
|
259
|
+
.catch(() => {
|
|
260
|
+
if (!alive) return
|
|
261
|
+
// No record is not a failure. Older cms-core resolves this endpoint
|
|
262
|
+
// through a PUBLISHED-only pipeline, so a saved draft 404s here even
|
|
263
|
+
// though the matching PUT accepts one — showing an error where the form
|
|
264
|
+
// belongs made a draft's SEO uneditable. Seed from the document's own
|
|
265
|
+
// meta instead; saving creates the record.
|
|
266
|
+
setRecord(null)
|
|
267
|
+
setForm({
|
|
268
|
+
metaTitle: draftMeta?.seoTitle ?? '',
|
|
269
|
+
metaDescription: draftMeta?.seoDescription ?? '',
|
|
270
|
+
focusKeyword: '',
|
|
271
|
+
keyTakeaway: '',
|
|
272
|
+
ogTitle: '',
|
|
273
|
+
ogDescription: '',
|
|
274
|
+
canonicalUrl: '',
|
|
275
|
+
structuredDataType: 'WebPage',
|
|
276
|
+
noindex: false,
|
|
277
|
+
nofollow: false,
|
|
278
|
+
llmsInclude: 'AUTO',
|
|
279
|
+
llmsDescription: '',
|
|
280
|
+
llmsSection: '',
|
|
281
|
+
llmsOptional: false,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
225
284
|
.finally(() => alive && setLoading(false))
|
|
226
285
|
return () => {
|
|
227
286
|
alive = false
|
|
228
287
|
}
|
|
229
|
-
}, [entityType, entityId, isDraft])
|
|
288
|
+
}, [entityType, entityId, isDraft, draftMeta?.seoTitle, draftMeta?.seoDescription])
|
|
230
289
|
|
|
231
290
|
useEffect(() => {
|
|
232
291
|
if (isDraft || contentHtml.trim() || !entityId) return
|
|
@@ -374,6 +433,52 @@ export function SeoEditorPane({
|
|
|
374
433
|
const url = record?.url ?? (slug ? `/${slug.replace(/^\//, '')}` : '')
|
|
375
434
|
const displayUrl = record?.url ?? url
|
|
376
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Page-level audit, distinct from `analyzeContent` below: binary checks over
|
|
438
|
+
* page structure and metadata, spanning a true 0-100. `analyzeContent` stays
|
|
439
|
+
* as the prose analysis (readability, density) it is good at.
|
|
440
|
+
*
|
|
441
|
+
* Built from LIVE form state so the score moves as the editor types. See
|
|
442
|
+
* `buildPageAuditInput` for which fields feed which check, and for why the
|
|
443
|
+
* OG image and author are reported as unknown rather than missing.
|
|
444
|
+
*/
|
|
445
|
+
const audit = useMemo(() => {
|
|
446
|
+
if (!auditSections) return null
|
|
447
|
+
return auditPage(
|
|
448
|
+
buildPageAuditInput({
|
|
449
|
+
sections: auditSections,
|
|
450
|
+
// LIVE form state, not the saved record — the score has to move as the
|
|
451
|
+
// editor types. The page editor builds the same input from the saved
|
|
452
|
+
// record for its first-paint score; routing both through one builder is
|
|
453
|
+
// what keeps the two numbers from drifting apart.
|
|
454
|
+
meta: {
|
|
455
|
+
metaTitle: form.metaTitle ?? '',
|
|
456
|
+
metaDescription: form.metaDescription ?? '',
|
|
457
|
+
focusKeyword: form.focusKeyword ?? '',
|
|
458
|
+
structuredDataType: form.structuredDataType ?? '',
|
|
459
|
+
noindex: Boolean(form.noindex),
|
|
460
|
+
// Directives are edited in the Page tab, so this pane has no live
|
|
461
|
+
// value to offer — it reads the saved one. Defaulting to `false`
|
|
462
|
+
// instead would tell the GEO gate that snippets are allowed on a page
|
|
463
|
+
// that has turned them off.
|
|
464
|
+
nosnippet: Boolean(record?.nosnippet),
|
|
465
|
+
llmsInclude: form.llmsInclude ?? 'AUTO',
|
|
466
|
+
// Live value, so the check flips the moment an image is chosen or
|
|
467
|
+
// cleared rather than waiting for a save round-trip. `??` and not
|
|
468
|
+
// `||`: an empty string is a deliberate removal, not "unset".
|
|
469
|
+
hasOgImage: Boolean(form.ogImage ?? record?.ogImage),
|
|
470
|
+
},
|
|
471
|
+
slug,
|
|
472
|
+
record,
|
|
473
|
+
fallbackTitle: draftMeta?.seoTitle,
|
|
474
|
+
}),
|
|
475
|
+
)
|
|
476
|
+
}, [auditSections, form, slug, record, draftMeta?.seoTitle])
|
|
477
|
+
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
onAuditChange?.(audit)
|
|
480
|
+
}, [audit, onAuditChange])
|
|
481
|
+
|
|
377
482
|
const analysisInput = useMemo(
|
|
378
483
|
() => ({
|
|
379
484
|
title: record?.title ?? draftMeta?.seoTitle ?? '',
|
|
@@ -389,7 +494,7 @@ export function SeoEditorPane({
|
|
|
389
494
|
canonical: form.canonicalUrl,
|
|
390
495
|
ogTitle: form.ogTitle,
|
|
391
496
|
ogDescription: form.ogDescription,
|
|
392
|
-
ogImage: record?.ogImage
|
|
497
|
+
ogImage: (form.ogImage ?? record?.ogImage) || undefined,
|
|
393
498
|
}),
|
|
394
499
|
[record, draftMeta, slug, contentHtml, form, renderedTitle],
|
|
395
500
|
)
|
|
@@ -426,6 +531,8 @@ export function SeoEditorPane({
|
|
|
426
531
|
)
|
|
427
532
|
}
|
|
428
533
|
|
|
534
|
+
const shareImage = form.ogImage ?? record?.ogImage ?? ''
|
|
535
|
+
|
|
429
536
|
const socialTitle =
|
|
430
537
|
socialPlatform === 'twitter'
|
|
431
538
|
? form.twitterTitle || form.ogTitle || renderedTitle
|
|
@@ -447,6 +554,7 @@ export function SeoEditorPane({
|
|
|
447
554
|
</InspectorPaneHeader>
|
|
448
555
|
|
|
449
556
|
<InspectorPaneBody>
|
|
557
|
+
{audit && !loading && <PageAuditPanel audit={audit} />}
|
|
450
558
|
{loading ? (
|
|
451
559
|
<div className="px-5 py-4">
|
|
452
560
|
<SeoLoading />
|
|
@@ -520,7 +628,11 @@ export function SeoEditorPane({
|
|
|
520
628
|
label="Key takeaway"
|
|
521
629
|
hint={
|
|
522
630
|
<span className="flex items-center gap-2">
|
|
523
|
-
<
|
|
631
|
+
<SeoCharCount
|
|
632
|
+
value={form.keyTakeaway ?? ''}
|
|
633
|
+
max={KEY_TAKEAWAY_MAX}
|
|
634
|
+
bar={false}
|
|
635
|
+
/>
|
|
524
636
|
{aiButton('keyTakeaway')}
|
|
525
637
|
</span>
|
|
526
638
|
}
|
|
@@ -564,18 +676,21 @@ export function SeoEditorPane({
|
|
|
564
676
|
</button>
|
|
565
677
|
</div>
|
|
566
678
|
</div>
|
|
567
|
-
<div
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<
|
|
572
|
-
<p className="line-clamp-2 text-[12px] text-[var(--sub)]">{desc}</p>
|
|
679
|
+
<div className={previewDevice === 'mobile' ? 'max-w-[320px]' : ''}>
|
|
680
|
+
{/* The title previously rendered in the brand accent, which
|
|
681
|
+
made the card a preview of the admin rather than of a
|
|
682
|
+
search result. */}
|
|
683
|
+
<SerpPreview title={renderedTitle} url={displayUrl} description={desc} />
|
|
573
684
|
</div>
|
|
574
685
|
<PaneField
|
|
575
686
|
label="SEO title"
|
|
576
687
|
hint={
|
|
577
688
|
<span className="flex items-center gap-2">
|
|
578
|
-
<
|
|
689
|
+
<SeoCharCount
|
|
690
|
+
value={form.metaTitle ?? ''}
|
|
691
|
+
min={META_TITLE_MIN}
|
|
692
|
+
max={META_TITLE_MAX}
|
|
693
|
+
/>
|
|
579
694
|
{aiButton('metaTitle')}
|
|
580
695
|
</span>
|
|
581
696
|
}
|
|
@@ -598,7 +713,11 @@ export function SeoEditorPane({
|
|
|
598
713
|
label="Meta description"
|
|
599
714
|
hint={
|
|
600
715
|
<span className="flex items-center gap-2">
|
|
601
|
-
<
|
|
716
|
+
<SeoCharCount
|
|
717
|
+
value={form.metaDescription ?? ''}
|
|
718
|
+
min={META_DESC_MIN}
|
|
719
|
+
max={META_DESC_MAX}
|
|
720
|
+
/>
|
|
602
721
|
{aiButton('metaDescription')}
|
|
603
722
|
</span>
|
|
604
723
|
}
|
|
@@ -613,7 +732,19 @@ export function SeoEditorPane({
|
|
|
613
732
|
{alignmentRow('metaDescription')}
|
|
614
733
|
</SeoAccordionItem>
|
|
615
734
|
|
|
616
|
-
<SeoAccordionItem
|
|
735
|
+
<SeoAccordionItem
|
|
736
|
+
value="social-appearance"
|
|
737
|
+
title="Social appearance"
|
|
738
|
+
badge={
|
|
739
|
+
shareImage ? (
|
|
740
|
+
<SeoAccordionBadge tone="good">Image set</SeoAccordionBadge>
|
|
741
|
+
) : (
|
|
742
|
+
// A shared link with no image is the single biggest drop in
|
|
743
|
+
// engagement, so it is worth stating while collapsed.
|
|
744
|
+
<SeoAccordionBadge tone="warn">No image</SeoAccordionBadge>
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
>
|
|
617
748
|
<p className="text-[11.5px] text-[var(--sub)]">
|
|
618
749
|
Customize title, description, and image used when sharing on social platforms.
|
|
619
750
|
</p>
|
|
@@ -630,19 +761,34 @@ export function SeoEditorPane({
|
|
|
630
761
|
</button>
|
|
631
762
|
))}
|
|
632
763
|
</div>
|
|
764
|
+
{/*
|
|
765
|
+
The share preview and the upload target are one object: the
|
|
766
|
+
card an editor is judging is the control they change it with.
|
|
767
|
+
Previously the image was display-only — there was no way to
|
|
768
|
+
set an OG image from the SEO editor at all.
|
|
769
|
+
*/}
|
|
633
770
|
<div className="overflow-hidden rounded-[7px] border border-[var(--bdr)]">
|
|
634
|
-
{
|
|
771
|
+
{shareImage ? (
|
|
635
772
|
// eslint-disable-next-line @next/next/no-img-element
|
|
636
773
|
<img
|
|
637
|
-
src={
|
|
774
|
+
src={shareImage}
|
|
638
775
|
alt=""
|
|
639
|
-
className="aspect-
|
|
776
|
+
className="aspect-1200/630 w-full bg-[var(--bg)] object-cover"
|
|
640
777
|
/>
|
|
641
778
|
) : (
|
|
642
|
-
<
|
|
643
|
-
|
|
644
|
-
|
|
779
|
+
<button
|
|
780
|
+
type="button"
|
|
781
|
+
onClick={() => setImagePickerOpen(true)}
|
|
782
|
+
className="flex aspect-1200/630 w-full flex-col items-center justify-center gap-1 bg-[var(--bg)] text-[12px] text-[var(--muted)] transition-colors hover:bg-[var(--acc-l)] hover:text-[var(--acc)]"
|
|
783
|
+
>
|
|
784
|
+
<ImagePlus className="h-5 w-5" aria-hidden />
|
|
785
|
+
Choose a share image
|
|
786
|
+
<span className="text-[10.5px]">1200×630 or wider, about 1.91:1</span>
|
|
787
|
+
</button>
|
|
645
788
|
)}
|
|
789
|
+
{/* Domain / title / description sit between the frame and the
|
|
790
|
+
toolbar so the card reads as the shared post, not as a
|
|
791
|
+
form with a thumbnail on top. */}
|
|
646
792
|
<div className="p-2.5">
|
|
647
793
|
<p className="truncate text-[10.5px] text-[var(--muted)] uppercase">
|
|
648
794
|
{displayUrl.replace(/^https?:\/\//, '').split('/')[0] || 'yoursite.com'}
|
|
@@ -652,6 +798,30 @@ export function SeoEditorPane({
|
|
|
652
798
|
</p>
|
|
653
799
|
<p className="line-clamp-2 text-[12px] text-[var(--sub)]">{socialDesc}</p>
|
|
654
800
|
</div>
|
|
801
|
+
{/* Always visible, never hover-only: a control an editor has
|
|
802
|
+
to discover by hovering is a control most never find. */}
|
|
803
|
+
<div className="flex items-center gap-2 border-t border-[var(--bdr)] bg-[var(--bg)] px-2.5 py-2">
|
|
804
|
+
<span className="min-w-0 flex-1 truncate text-[11.5px] text-[var(--sub)]">
|
|
805
|
+
{shareImage ? shareImage.split('/').pop() : 'No image selected'}
|
|
806
|
+
</span>
|
|
807
|
+
<button
|
|
808
|
+
type="button"
|
|
809
|
+
onClick={() => setImagePickerOpen(true)}
|
|
810
|
+
className="shrink-0 rounded-[5px] px-2 py-1 text-[11.5px] font-medium text-[var(--acc)] transition-colors hover:bg-[var(--acc-l)]"
|
|
811
|
+
>
|
|
812
|
+
{shareImage ? 'Replace' : 'Choose'}
|
|
813
|
+
</button>
|
|
814
|
+
{shareImage && (
|
|
815
|
+
<button
|
|
816
|
+
type="button"
|
|
817
|
+
onClick={() => update({ ogImage: '' })}
|
|
818
|
+
aria-label="Remove share image"
|
|
819
|
+
className="shrink-0 rounded-[5px] p-1 text-[var(--muted)] transition-colors hover:bg-[var(--err-l)] hover:text-[var(--err)]"
|
|
820
|
+
>
|
|
821
|
+
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
|
822
|
+
</button>
|
|
823
|
+
)}
|
|
824
|
+
</div>
|
|
655
825
|
</div>
|
|
656
826
|
{socialPlatform === 'facebook' ? (
|
|
657
827
|
<>
|
|
@@ -775,7 +945,13 @@ export function SeoEditorPane({
|
|
|
775
945
|
</PaneField>
|
|
776
946
|
<PaneField
|
|
777
947
|
label="Agent description"
|
|
778
|
-
hint={
|
|
948
|
+
hint={
|
|
949
|
+
<SeoCharCount
|
|
950
|
+
value={form.llmsDescription ?? ''}
|
|
951
|
+
max={LLMS_DESC_MAX}
|
|
952
|
+
bar={false}
|
|
953
|
+
/>
|
|
954
|
+
}
|
|
779
955
|
>
|
|
780
956
|
<textarea
|
|
781
957
|
className={`${inputCls} min-h-16 resize-y`}
|
|
@@ -808,6 +984,12 @@ export function SeoEditorPane({
|
|
|
808
984
|
</SeoAccordion>
|
|
809
985
|
</>
|
|
810
986
|
)}
|
|
987
|
+
<MediaPickerModal
|
|
988
|
+
open={imagePickerOpen}
|
|
989
|
+
onClose={() => setImagePickerOpen(false)}
|
|
990
|
+
accept="image/*"
|
|
991
|
+
onSelect={(url) => update({ ogImage: url })}
|
|
992
|
+
/>
|
|
811
993
|
</InspectorPaneBody>
|
|
812
994
|
|
|
813
995
|
<InspectorPaneFooter>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A search-result preview for the SEO editor.
|
|
5
|
+
*
|
|
6
|
+
* Renders the three things a searcher actually sees, in the order and weight
|
|
7
|
+
* they see them, so an editor can judge a title and description as a result
|
|
8
|
+
* rather than as two form fields.
|
|
9
|
+
*
|
|
10
|
+
* The link blue is a literal, not a token: it is Google's result colour, and
|
|
11
|
+
* the point of the card is to look like the thing being previewed. Tokenising
|
|
12
|
+
* it would let a brand accent repaint it and quietly stop it being a preview.
|
|
13
|
+
* `#8ab4f8` is the dark-scheme equivalent.
|
|
14
|
+
*/
|
|
15
|
+
export interface SerpPreviewProps {
|
|
16
|
+
title: string
|
|
17
|
+
/** Full resolved URL, shown above the title as search engines display it. */
|
|
18
|
+
url: string
|
|
19
|
+
description: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SerpPreview({ title, url, description }: SerpPreviewProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className="border-border bg-card rounded-lg border px-3 py-[11px]"
|
|
26
|
+
role="group"
|
|
27
|
+
aria-label="Search result preview"
|
|
28
|
+
>
|
|
29
|
+
<p className="truncate text-[11px] text-emerald-700 dark:text-emerald-400">
|
|
30
|
+
{url || 'https://example.com/page'}
|
|
31
|
+
</p>
|
|
32
|
+
<p className="truncate text-[14px] leading-snug text-[#1a0dab] dark:text-[#8ab4f8]">
|
|
33
|
+
{title || 'Untitled page'}
|
|
34
|
+
</p>
|
|
35
|
+
{/* Two lines is what a result gets; more written here simply is not read. */}
|
|
36
|
+
<p className="text-muted-foreground line-clamp-2 text-[11.5px] leading-snug">
|
|
37
|
+
{description || 'No meta description set. Search engines will improvise one.'}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { PageSection } from '@prenta/core/page-sections'
|
|
5
|
+
import { auditPage, type PageAuditResult } from '@prenta/core/seo/page-audit'
|
|
6
|
+
import { fetchSeoContentRecord, type SeoContentRecord } from '../lib/seo-service.js'
|
|
7
|
+
import { buildPageAuditInput, metaFromRecord } from '../lib/page-audit-input.js'
|
|
8
|
+
|
|
9
|
+
export interface UsePageAuditArgs {
|
|
10
|
+
entityType: 'page' | 'post'
|
|
11
|
+
/** Null for an unsaved document — there is no record to audit against yet. */
|
|
12
|
+
entityId: string | null
|
|
13
|
+
sections: PageSection[]
|
|
14
|
+
slug: string
|
|
15
|
+
/**
|
|
16
|
+
* The document's own meta, used when no SEO record exists yet. A page that
|
|
17
|
+
* has never had SEO edited still has a title, a description and sections —
|
|
18
|
+
* it is scoreable, and refusing to score it leaves the header chip blank on
|
|
19
|
+
* exactly the pages that most need the nudge.
|
|
20
|
+
*/
|
|
21
|
+
fallback?: { title?: string; description?: string }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The page's audit, from its saved SEO record — or from the document's own
|
|
26
|
+
* fields when no record exists yet, so every page has a number.
|
|
27
|
+
*
|
|
28
|
+
* This exists so the header chip and the rail's tab dot have a score from first
|
|
29
|
+
* paint. Previously the audit was computed inside the SEO pane, which mounts
|
|
30
|
+
* only when its tab is opened — so before that the chip showed a number from a
|
|
31
|
+
* different scorer with different bands, and the header read 53 beside rings
|
|
32
|
+
* reading 0 and 57.
|
|
33
|
+
*
|
|
34
|
+
* While the SEO pane is open it reports a LIVE audit built from unsaved form
|
|
35
|
+
* state, which supersedes this one. Both go through `buildPageAuditInput`, so
|
|
36
|
+
* they differ only in saved-versus-unsaved metadata — the difference an editor
|
|
37
|
+
* expects to see — and never in how a check is decided.
|
|
38
|
+
*/
|
|
39
|
+
export function usePageAudit({
|
|
40
|
+
entityType,
|
|
41
|
+
entityId,
|
|
42
|
+
sections,
|
|
43
|
+
slug,
|
|
44
|
+
fallback,
|
|
45
|
+
}: UsePageAuditArgs): {
|
|
46
|
+
audit: PageAuditResult
|
|
47
|
+
record: SeoContentRecord | null
|
|
48
|
+
reload: () => void
|
|
49
|
+
} {
|
|
50
|
+
const [record, setRecord] = useState<SeoContentRecord | null>(null)
|
|
51
|
+
const [nonce, setNonce] = useState(0)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!entityId) {
|
|
55
|
+
setRecord(null)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
let alive = true
|
|
59
|
+
fetchSeoContentRecord(entityType, entityId)
|
|
60
|
+
.then((rec) => {
|
|
61
|
+
if (alive) setRecord(rec)
|
|
62
|
+
})
|
|
63
|
+
.catch(() => {
|
|
64
|
+
// A page that has never had SEO saved has no record. That is not an
|
|
65
|
+
// error worth surfacing here — the audit simply reports what it can,
|
|
66
|
+
// and the SEO pane shows its own error state if the user opens it.
|
|
67
|
+
if (alive) setRecord(null)
|
|
68
|
+
})
|
|
69
|
+
return () => {
|
|
70
|
+
alive = false
|
|
71
|
+
}
|
|
72
|
+
}, [entityType, entityId, nonce])
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* With a record, audit the saved SEO. Without one, audit the document's own
|
|
76
|
+
* fields and treat the record-only inputs as unset — which is honest: no
|
|
77
|
+
* focus keyword has been chosen, no schema has been bound, and nothing has
|
|
78
|
+
* set noindex. The score is real, and it improves the moment SEO is filled in.
|
|
79
|
+
*/
|
|
80
|
+
const meta = record
|
|
81
|
+
? metaFromRecord(record)
|
|
82
|
+
: {
|
|
83
|
+
metaTitle: fallback?.title ?? '',
|
|
84
|
+
metaDescription: fallback?.description ?? '',
|
|
85
|
+
focusKeyword: '',
|
|
86
|
+
structuredDataType: '',
|
|
87
|
+
noindex: false,
|
|
88
|
+
nosnippet: false,
|
|
89
|
+
llmsInclude: 'AUTO',
|
|
90
|
+
hasOgImage: false,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const audit = auditPage(buildPageAuditInput({ sections, meta, slug, record }))
|
|
94
|
+
|
|
95
|
+
const reload = useCallback(() => setNonce((n) => n + 1), [])
|
|
96
|
+
|
|
97
|
+
return { audit, record, reload }
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { BreakpointId } from '../lib/breakpoints.js'
|
|
5
|
+
import {
|
|
6
|
+
probeFieldReflow,
|
|
7
|
+
reflowSeverity,
|
|
8
|
+
worstBreakpoint,
|
|
9
|
+
type FieldLineCount,
|
|
10
|
+
type ReflowSeverity,
|
|
11
|
+
} from '../lib/reflow-probe.js'
|
|
12
|
+
|
|
13
|
+
export interface ReflowRow extends FieldLineCount {
|
|
14
|
+
/** Lines this field ran to in the published version. */
|
|
15
|
+
was: number
|
|
16
|
+
/** `lines - was`. Only growth is treated as a problem. */
|
|
17
|
+
diff: number
|
|
18
|
+
state: ReflowSeverity
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ReflowGuardResult {
|
|
22
|
+
rows: ReflowRow[]
|
|
23
|
+
/** Breakpoints where the field now wraps to more lines than published. */
|
|
24
|
+
grown: ReflowRow[]
|
|
25
|
+
/** The one worth proofing on, or `null` when nothing grew. */
|
|
26
|
+
worst: BreakpointId | null
|
|
27
|
+
/** Character delta against the published string; 0 when there is no baseline. */
|
|
28
|
+
delta: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EMPTY: ReflowGuardResult = { rows: [], grown: [], worst: null, delta: 0 }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Measures how one field wraps at every breakpoint, and how that compares to
|
|
35
|
+
* what is published.
|
|
36
|
+
*
|
|
37
|
+
* ── Why it runs on a delay ────────────────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* A pass clones the canvas and lays it out twelve times (six widths, current
|
|
40
|
+
* and published). That is far too much to do per keystroke, and it is also
|
|
41
|
+
* pointless — mid-word text tells an editor nothing. So it settles first.
|
|
42
|
+
*
|
|
43
|
+
* `null` from the probe means the field is not measurable: the section may be
|
|
44
|
+
* rendered by a consumer's own component that carries no `data-cms-field`
|
|
45
|
+
* markers. The guard then reports nothing rather than a number read off the
|
|
46
|
+
* wrong element — an editor treats a line count as a fact, and a confident
|
|
47
|
+
* wrong one is worse than a blank.
|
|
48
|
+
*/
|
|
49
|
+
export function useReflowGuard(options: {
|
|
50
|
+
canvas: HTMLElement | null
|
|
51
|
+
sectionId: string
|
|
52
|
+
fieldPath: string
|
|
53
|
+
/** Current field text. Only used to retrigger — the probe reads the DOM. */
|
|
54
|
+
value: string
|
|
55
|
+
/** Published text, or `undefined` when the field has never been published. */
|
|
56
|
+
baseline: string | undefined
|
|
57
|
+
/** Skip measuring entirely (field type isn't guarded, panel is collapsed). */
|
|
58
|
+
enabled?: boolean
|
|
59
|
+
delayMs?: number
|
|
60
|
+
}): ReflowGuardResult {
|
|
61
|
+
const { canvas, sectionId, fieldPath, value, baseline, enabled = true, delayMs = 200 } = options
|
|
62
|
+
const [result, setResult] = useState<ReflowGuardResult>(EMPTY)
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!enabled || !canvas) {
|
|
66
|
+
setResult(EMPTY)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
const probed = probeFieldReflow({ canvas, sectionId, fieldPath, baselineText: baseline })
|
|
72
|
+
if (!probed) {
|
|
73
|
+
setResult(EMPTY)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rows: ReflowRow[] = probed.current.map((row) => {
|
|
78
|
+
const was = probed.baseline.find((b) => b.id === row.id)?.lines ?? row.lines
|
|
79
|
+
return { ...row, was, diff: row.lines - was, state: reflowSeverity(row.lines, was) }
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
setResult({
|
|
83
|
+
rows,
|
|
84
|
+
grown: rows.filter((r) => r.diff > 0),
|
|
85
|
+
worst: worstBreakpoint(probed.current, probed.baseline),
|
|
86
|
+
delta: baseline === undefined ? 0 : value.length - baseline.length,
|
|
87
|
+
})
|
|
88
|
+
}, delayMs)
|
|
89
|
+
|
|
90
|
+
return () => clearTimeout(timer)
|
|
91
|
+
// `value` is a dependency because the canvas re-renders as it changes —
|
|
92
|
+
// the probe reads the resulting DOM rather than the string itself.
|
|
93
|
+
}, [canvas, sectionId, fieldPath, value, baseline, enabled, delayMs])
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
@@ -71,6 +71,16 @@ export function AdminShell({
|
|
|
71
71
|
: { display: 'block', position: 'relative' }
|
|
72
72
|
}
|
|
73
73
|
>
|
|
74
|
+
{/*
|
|
75
|
+
* First focusable thing in the admin, so a keyboard user's very first
|
|
76
|
+
* Tab offers a jump past the sidebar's ~20 nav items. Off-screen until
|
|
77
|
+
* focused; styling lives in `responsive.css` because it has to survive
|
|
78
|
+
* a consumer whose Tailwind build does not scan this file.
|
|
79
|
+
*/}
|
|
80
|
+
<a className="prenta-skip-link" href="#prenta-main">
|
|
81
|
+
Skip to content
|
|
82
|
+
</a>
|
|
83
|
+
|
|
74
84
|
{!isDesktop && mobileSidebarOpen && (
|
|
75
85
|
<div
|
|
76
86
|
aria-hidden
|
|
@@ -111,7 +121,9 @@ export function AdminShell({
|
|
|
111
121
|
>
|
|
112
122
|
{header}
|
|
113
123
|
{breadcrumbs}
|
|
114
|
-
<main className="min-h-0 flex-1 overflow-y-auto">
|
|
124
|
+
<main id="prenta-main" className="min-h-0 flex-1 overflow-y-auto">
|
|
125
|
+
{children}
|
|
126
|
+
</main>
|
|
115
127
|
</div>
|
|
116
128
|
</div>
|
|
117
129
|
)
|