@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { BREAKPOINTS, type BreakpointId } from './breakpoints.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Measures how a field's copy wraps at every breakpoint, by laying it out for
|
|
5
|
+
* real rather than estimating.
|
|
6
|
+
*
|
|
7
|
+
* ── Why not a class chain ─────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* The design handoff specifies rebuilding each probe from a hardcoded list of
|
|
10
|
+
* wrapper classes per guarded field, and warns twice that a width cap outside
|
|
11
|
+
* that list makes the guard lie — at wide breakpoints only, so it looks correct
|
|
12
|
+
* on mobile. By the handoff's own account that list drifted twice.
|
|
13
|
+
*
|
|
14
|
+
* A chain is a second source of truth that has to track the renderers' markup
|
|
15
|
+
* by hand. Worse here: the prototype had semantic classes (`.pv-hero-h`), while
|
|
16
|
+
* these components are styled with Tailwind utility strings that Prettier's
|
|
17
|
+
* plugin reorders on save. And a chain written in the admin cannot describe a
|
|
18
|
+
* consumer's own registered renderer at all.
|
|
19
|
+
*
|
|
20
|
+
* So the probe measures the REAL element: the caller renders the actual section
|
|
21
|
+
* into a hidden host, and this module finds the field by its `data-cms-field`
|
|
22
|
+
* marker — the same attribute the click-to-edit bridge uses. There is nothing
|
|
23
|
+
* to keep in sync, and a renderer refactor cannot silently invalidate it.
|
|
24
|
+
*
|
|
25
|
+
* ── Why one host, resized ─────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Six widths are measured by resizing a single rendered copy, not by rendering
|
|
28
|
+
* six. Layout is recomputed either way; React reconciliation is not.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** A field's wrap count at one breakpoint. */
|
|
32
|
+
export interface FieldLineCount {
|
|
33
|
+
id: BreakpointId
|
|
34
|
+
lines: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Line count from an element's laid-out height.
|
|
39
|
+
*
|
|
40
|
+
* Falls back to `fontSize * 1.25` when `line-height` computes to `normal`,
|
|
41
|
+
* which it does for any element that never set one. Rounding rather than
|
|
42
|
+
* flooring: sub-pixel line heights accumulate, and 2.97 lines is three.
|
|
43
|
+
*/
|
|
44
|
+
export function linesFromMetrics(height: number, lineHeight: number, fontSize: number): number {
|
|
45
|
+
const effective = Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : fontSize * 1.25
|
|
46
|
+
if (!Number.isFinite(effective) || effective <= 0) return 1
|
|
47
|
+
// An empty field still occupies one line's worth of intent.
|
|
48
|
+
return Math.max(1, Math.round(height / effective))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How many lines an element's text actually occupies.
|
|
53
|
+
*
|
|
54
|
+
* Counts LINE BOXES via a Range over the element's contents, deduplicated by
|
|
55
|
+
* their top edge. Dividing the element's height by its line-height — the
|
|
56
|
+
* obvious approach, and what the prototype does — counts padding as text: a
|
|
57
|
+
* button reading "Get Started" measured 2 lines, because its vertical padding
|
|
58
|
+
* came to roughly another line's worth of height. Every padded element was
|
|
59
|
+
* over-reported, which is most CTAs and every boxed heading.
|
|
60
|
+
*
|
|
61
|
+
* Rects are deduplicated by rounded top edge because a range spanning nested
|
|
62
|
+
* inline elements (a `<strong>` mid-sentence, an icon in a button) yields one
|
|
63
|
+
* rect per fragment, not per line.
|
|
64
|
+
*
|
|
65
|
+
* Falls back to the height ratio when the range produces nothing — an element
|
|
66
|
+
* whose content is a single replaced node, or a layout engine that gives no
|
|
67
|
+
* rects for empty text.
|
|
68
|
+
*/
|
|
69
|
+
export function countLines(el: HTMLElement): number {
|
|
70
|
+
const doc = el.ownerDocument
|
|
71
|
+
try {
|
|
72
|
+
const range = doc.createRange()
|
|
73
|
+
range.selectNodeContents(el)
|
|
74
|
+
const rects = Array.from(range.getClientRects()).filter((r) => r.height > 0)
|
|
75
|
+
range.detach?.()
|
|
76
|
+
if (rects.length > 0) {
|
|
77
|
+
const tops = new Set(rects.map((r) => Math.round(r.top)))
|
|
78
|
+
return Math.max(1, tops.size)
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// No Range support (or a detached document) — fall through to metrics.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const cs = getComputedStyle(el)
|
|
85
|
+
return linesFromMetrics(
|
|
86
|
+
el.getBoundingClientRect().height,
|
|
87
|
+
Number.parseFloat(cs.lineHeight),
|
|
88
|
+
Number.parseFloat(cs.fontSize),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The off-screen host.
|
|
94
|
+
*
|
|
95
|
+
* `left: -99999px`, never `display: none` — a hidden element has no layout, so
|
|
96
|
+
* every measurement would be zero. `visibility: hidden` would keep layout but
|
|
97
|
+
* also keep the element in the accessibility tree; moving it off-screen and
|
|
98
|
+
* marking it `aria-hidden` keeps it out of both the viewport and the a11y tree
|
|
99
|
+
* while still being laid out.
|
|
100
|
+
*
|
|
101
|
+
* MOUNT IT INSIDE `.prenta-admin`, never on `document.body`. Type tokens and
|
|
102
|
+
* the font stack are scoped to that root, so a host outside it lays text out in
|
|
103
|
+
* the document's default font and every measurement is taken at the wrong
|
|
104
|
+
* metrics. Caught by `e2e/reflow-probe.spec.ts` on its first run: a button
|
|
105
|
+
* measured 127px on the canvas and 139px in a body-mounted probe.
|
|
106
|
+
*
|
|
107
|
+
* Use `mountProbeHost` rather than appending by hand.
|
|
108
|
+
*/
|
|
109
|
+
export function createProbeHost(doc: Document = document): HTMLElement {
|
|
110
|
+
const host = doc.createElement('div')
|
|
111
|
+
host.setAttribute('aria-hidden', 'true')
|
|
112
|
+
host.dataset.reflowProbe = 'true'
|
|
113
|
+
Object.assign(host.style, {
|
|
114
|
+
position: 'fixed',
|
|
115
|
+
left: '-99999px',
|
|
116
|
+
top: '0',
|
|
117
|
+
// Never inherit the rail's width — the probe sets its own per breakpoint.
|
|
118
|
+
width: 'auto',
|
|
119
|
+
pointerEvents: 'none',
|
|
120
|
+
})
|
|
121
|
+
return host
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Attach a probe host in the only place it measures correctly.
|
|
126
|
+
*
|
|
127
|
+
* Returns a detach function. Falls back to `document.body` only when no admin
|
|
128
|
+
* root exists (it always does in the app) — and callers should treat a
|
|
129
|
+
* fallback mount as untrustworthy rather than silently measuring.
|
|
130
|
+
*/
|
|
131
|
+
export function mountProbeHost(
|
|
132
|
+
host: HTMLElement,
|
|
133
|
+
doc: Document = document,
|
|
134
|
+
): { detach: () => void; scoped: boolean } {
|
|
135
|
+
const root = doc.querySelector('.prenta-admin')
|
|
136
|
+
const parent = root ?? doc.body
|
|
137
|
+
parent.appendChild(host)
|
|
138
|
+
return { detach: () => host.remove(), scoped: Boolean(root) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Measure one field across every breakpoint.
|
|
143
|
+
*
|
|
144
|
+
* `container` must carry the same class chain the canvas uses
|
|
145
|
+
* (`prenta-canvas`, which supplies the query container). Its width is set per
|
|
146
|
+
* breakpoint, so it must not be width-constrained by a parent.
|
|
147
|
+
*
|
|
148
|
+
* `scope` narrows to one section and is REQUIRED whenever the container holds
|
|
149
|
+
* more than one, because `data-cms-field` paths are unique only within a
|
|
150
|
+
* section — every section has a `heading` and a `body`. Without it a whole page
|
|
151
|
+
* silently measures the first section's field and reports it for all of them.
|
|
152
|
+
* (That mistake is what `e2e/reflow-probe.spec.ts` caught on its second run.)
|
|
153
|
+
*
|
|
154
|
+
* Returns `null` when the field cannot be found — the canvas is rendering a
|
|
155
|
+
* component without `data-cms-field` markers, so there is nothing to measure.
|
|
156
|
+
* Reporting a line count from the wrong element would be worse than reporting
|
|
157
|
+
* none: an editor reads a number as a fact.
|
|
158
|
+
*/
|
|
159
|
+
export function measureFieldAcrossBreakpoints(
|
|
160
|
+
container: HTMLElement,
|
|
161
|
+
fieldPath: string,
|
|
162
|
+
scope?: string,
|
|
163
|
+
): FieldLineCount[] | null {
|
|
164
|
+
const field = `[data-cms-field="${CSS.escape(fieldPath)}"]`
|
|
165
|
+
const selector = scope ? `${scope} ${field}` : field
|
|
166
|
+
if (!container.querySelector(selector)) return null
|
|
167
|
+
|
|
168
|
+
const previousWidth = container.style.width
|
|
169
|
+
const out: FieldLineCount[] = []
|
|
170
|
+
|
|
171
|
+
for (const bp of BREAKPOINTS) {
|
|
172
|
+
container.style.width = `${bp.width}px`
|
|
173
|
+
// Re-query inside the loop: a renderer may swap elements between layouts
|
|
174
|
+
// (a grid that becomes a stack can remount its children).
|
|
175
|
+
const el = container.querySelector(selector)
|
|
176
|
+
out.push({ id: bp.id, lines: el ? countLines(el as HTMLElement) : 1 })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
container.style.width = previousWidth
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What a probe pass produces for one field. */
|
|
184
|
+
export interface FieldReflow {
|
|
185
|
+
current: FieldLineCount[]
|
|
186
|
+
baseline: FieldLineCount[]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Measure a field's wrapping now and as published, from the live canvas.
|
|
191
|
+
*
|
|
192
|
+
* The whole `.prenta-canvas` is cloned, not just the target section. Cloning
|
|
193
|
+
* the section alone would require rebuilding whatever sits between the canvas
|
|
194
|
+
* root and the section — which is the class-chain problem again, in a smaller
|
|
195
|
+
* disguise. `e2e/reflow-probe.spec.ts` asserts the whole-canvas clone
|
|
196
|
+
* reproduces every field's rendered width exactly; nothing asserts that for a
|
|
197
|
+
* hand-rebuilt wrapper.
|
|
198
|
+
*
|
|
199
|
+
* The baseline is measured by writing the published string into the SAME cloned
|
|
200
|
+
* element and re-measuring, rather than by rendering the published section. The
|
|
201
|
+
* two differ in exactly one field, so everything else — width caps, siblings,
|
|
202
|
+
* grid tracks — is held identical by construction rather than by hoping a second
|
|
203
|
+
* render matches.
|
|
204
|
+
*
|
|
205
|
+
* `baselineText` of `undefined` means the field has no published version (a new
|
|
206
|
+
* section), so there is no change to report and current is echoed as baseline.
|
|
207
|
+
*
|
|
208
|
+
* Returns `null` when the section or field is absent from the canvas — a
|
|
209
|
+
* consumer's renderer may carry no `data-cms-field` markers, and a line count
|
|
210
|
+
* read off the wrong element is worse than none.
|
|
211
|
+
*/
|
|
212
|
+
export function probeFieldReflow(options: {
|
|
213
|
+
canvas: HTMLElement
|
|
214
|
+
sectionId: string
|
|
215
|
+
fieldPath: string
|
|
216
|
+
baselineText: string | undefined
|
|
217
|
+
doc?: Document
|
|
218
|
+
}): FieldReflow | null {
|
|
219
|
+
const { canvas, sectionId, fieldPath, baselineText, doc = document } = options
|
|
220
|
+
|
|
221
|
+
const sectionSel = `[data-section-id="${CSS.escape(sectionId)}"]`
|
|
222
|
+
const fieldSel = `${sectionSel} [data-cms-field="${CSS.escape(fieldPath)}"]`
|
|
223
|
+
if (!canvas.querySelector(fieldSel)) return null
|
|
224
|
+
|
|
225
|
+
const host = createProbeHost(doc)
|
|
226
|
+
const clone = canvas.cloneNode(true) as HTMLElement
|
|
227
|
+
// The canvas is scaled to fit its column. A transform does not change layout
|
|
228
|
+
// width, but it does scale the measured rects — and line height is read from
|
|
229
|
+
// a rect. Left on, every count would be off by the scale factor.
|
|
230
|
+
clone.style.transform = 'none'
|
|
231
|
+
clone.style.maxWidth = 'none'
|
|
232
|
+
host.appendChild(clone)
|
|
233
|
+
|
|
234
|
+
const { detach, scoped } = mountProbeHost(host, doc)
|
|
235
|
+
// Outside `.prenta-admin` the type tokens don't apply and every measurement
|
|
236
|
+
// is taken in the document's default font. Report nothing rather than that.
|
|
237
|
+
if (!scoped) {
|
|
238
|
+
detach()
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const current = measureFieldAcrossBreakpoints(clone, fieldPath, sectionSel)
|
|
244
|
+
if (!current) return null
|
|
245
|
+
if (baselineText === undefined) return { current, baseline: current }
|
|
246
|
+
|
|
247
|
+
const el = clone.querySelector(fieldSel)
|
|
248
|
+
if (!el) return { current, baseline: current }
|
|
249
|
+
el.textContent = baselineText
|
|
250
|
+
const baseline = measureFieldAcrossBreakpoints(clone, fieldPath, sectionSel)
|
|
251
|
+
return { current, baseline: baseline ?? current }
|
|
252
|
+
} finally {
|
|
253
|
+
detach()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type ReflowSeverity = 'same' | 'grew' | 'grew-badly'
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Compare a field's current wrapping against its published baseline.
|
|
261
|
+
*
|
|
262
|
+
* Only growth is flagged. Copy that got shorter cannot break a layout that
|
|
263
|
+
* already held the longer version, so reporting it would be noise on exactly
|
|
264
|
+
* the edit an editor was asked to make.
|
|
265
|
+
*/
|
|
266
|
+
export function reflowSeverity(current: number, baseline: number): ReflowSeverity {
|
|
267
|
+
const delta = current - baseline
|
|
268
|
+
if (delta <= 0) return 'same'
|
|
269
|
+
return delta === 1 ? 'grew' : 'grew-badly'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The breakpoint worth proofing on: the biggest jump, and on a tie the
|
|
274
|
+
* narrowest width. A tie means the same damage happens at several sizes, and
|
|
275
|
+
* the narrowest is where it is most likely to look broken.
|
|
276
|
+
*/
|
|
277
|
+
export function worstBreakpoint(
|
|
278
|
+
current: FieldLineCount[],
|
|
279
|
+
baseline: FieldLineCount[],
|
|
280
|
+
): BreakpointId | null {
|
|
281
|
+
const widthOf = (id: BreakpointId) => BREAKPOINTS.find((b) => b.id === id)?.width ?? 0
|
|
282
|
+
let worst: { id: BreakpointId; delta: number } | null = null
|
|
283
|
+
|
|
284
|
+
for (const c of current) {
|
|
285
|
+
const b = baseline.find((x) => x.id === c.id)
|
|
286
|
+
if (!b) continue
|
|
287
|
+
const delta = c.lines - b.lines
|
|
288
|
+
if (delta <= 0) continue
|
|
289
|
+
if (
|
|
290
|
+
!worst ||
|
|
291
|
+
delta > worst.delta ||
|
|
292
|
+
(delta === worst.delta && widthOf(c.id) < widthOf(worst.id))
|
|
293
|
+
) {
|
|
294
|
+
worst = { id: c.id, delta }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return worst?.id ?? null
|
|
299
|
+
}
|
package/src/lib/seo-service.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { BrandAlignmentScoreResult } from '@prenta/core/brand-voice'
|
|
12
12
|
import type { SEOAnalysisResult } from '@prenta/core/seo/analysis'
|
|
13
13
|
import { cmsApi } from './api.js'
|
|
14
|
+
import type { MaxImagePreview } from '@prenta/core/seo/robots-directives'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* `cmsApi` never throws — it returns `{ error, status }` on a network/HTTP
|
|
@@ -182,6 +183,12 @@ export interface SeoContentRecord {
|
|
|
182
183
|
twitterImageId: string | null
|
|
183
184
|
noindex: boolean
|
|
184
185
|
nofollow: boolean
|
|
186
|
+
/** Blocks text snippets, and with them AI Overview and chat citations. */
|
|
187
|
+
nosnippet: boolean
|
|
188
|
+
/** Blocks cached copies. */
|
|
189
|
+
noarchive: boolean
|
|
190
|
+
/** Largest image preview a result may show. */
|
|
191
|
+
maxImagePreview: MaxImagePreview
|
|
185
192
|
structuredDataType: string | null
|
|
186
193
|
structuredData: Record<string, unknown> | null
|
|
187
194
|
/** llms.txt curation: AUTO (heuristic), ALWAYS (pin), NEVER (exclude). */
|
|
@@ -232,6 +239,9 @@ export interface SeoFieldPayload {
|
|
|
232
239
|
twitterImage?: string
|
|
233
240
|
noindex?: boolean
|
|
234
241
|
nofollow?: boolean
|
|
242
|
+
nosnippet?: boolean
|
|
243
|
+
noarchive?: boolean
|
|
244
|
+
maxImagePreview?: MaxImagePreview
|
|
235
245
|
structuredDataType?: string
|
|
236
246
|
structuredData?: Record<string, unknown>
|
|
237
247
|
llmsInclude?: LlmsInclude
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level diff between two page snapshots.
|
|
3
|
+
*
|
|
4
|
+
* One of the three parts of the page-builder handoff specified as real logic
|
|
5
|
+
* rather than mockup ("the version diff — field-level, computed from full
|
|
6
|
+
* snapshots, and its output drives the changed-section outlines on the
|
|
7
|
+
* canvas"). Nothing here is display-only: the same result feeds the history
|
|
8
|
+
* panel's per-field list AND the set of sections the canvas outlines while
|
|
9
|
+
* reviewing, so the two can never disagree about what changed.
|
|
10
|
+
*
|
|
11
|
+
* Pure over `PageSection[]`, with no React and no fetching, because the diff
|
|
12
|
+
* is asked for in three different places (panel rows, canvas outlines, the
|
|
13
|
+
* review bar's count) and recomputing it from the same inputs has to be free
|
|
14
|
+
* of side effects.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
getSectionType,
|
|
18
|
+
resolveRepeaterField,
|
|
19
|
+
type SectionFieldDef,
|
|
20
|
+
} from '../views/page-editor/section-types.js'
|
|
21
|
+
import type { PageSection } from './page-editor-service.js'
|
|
22
|
+
|
|
23
|
+
/** One changed field on one section. */
|
|
24
|
+
export interface FieldChange {
|
|
25
|
+
sectionId: string
|
|
26
|
+
/** Editor-facing section name, captured so the panel needn't re-look it up. */
|
|
27
|
+
sectionName: string
|
|
28
|
+
/** Human label for the field ("Heading"), from the section registry. */
|
|
29
|
+
field: string
|
|
30
|
+
/** Formatted previous value; `—` when absent. */
|
|
31
|
+
from: string
|
|
32
|
+
/** Formatted new value; `—` when absent. */
|
|
33
|
+
to: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Changes for one section, in the order the sections appear on the page. */
|
|
37
|
+
export interface SectionChangeGroup {
|
|
38
|
+
sectionId: string
|
|
39
|
+
sectionName: string
|
|
40
|
+
changes: FieldChange[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Labels for the keys that are not content fields and so have no entry in the
|
|
45
|
+
* section registry. Visibility earns its place here: hiding a section is the
|
|
46
|
+
* one structural change a design-owned CMS lets an editor make, so a history
|
|
47
|
+
* that silently dropped it would be missing the only structural edit there is.
|
|
48
|
+
*/
|
|
49
|
+
const META_LABELS: Record<string, string> = {
|
|
50
|
+
visible: 'Visibility',
|
|
51
|
+
name: 'Section name',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SETTING_LABELS: Record<string, string> = {
|
|
55
|
+
backgroundType: 'Background type',
|
|
56
|
+
backgroundColor: 'Background colour',
|
|
57
|
+
paddingY: 'Vertical padding',
|
|
58
|
+
anchorId: 'Anchor id',
|
|
59
|
+
cssClass: 'CSS class',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const EMPTY = '—'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Label a content key through the section registry — the same definitions the
|
|
66
|
+
* inspector renders from, so the diff reads in the words the editor typed
|
|
67
|
+
* under. Falls back to the raw key for content on a custom or unmanaged type
|
|
68
|
+
* the registry does not describe: a key is uglier than a label but it is true,
|
|
69
|
+
* whereas omitting the row would hide a real change.
|
|
70
|
+
*/
|
|
71
|
+
function contentFieldLabel(sectionType: string, key: string): string {
|
|
72
|
+
const def = getSectionType(sectionType)
|
|
73
|
+
const field = def?.fields.find((f: SectionFieldDef) => f.key === key)
|
|
74
|
+
return field?.label ?? key
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The repeater shape behind a content key, or null when the field is not one.
|
|
79
|
+
*
|
|
80
|
+
* Goes through `resolveRepeaterField` rather than reading `field.fields`
|
|
81
|
+
* directly: `stats` and `gallery` are shorthand presets whose sub-fields are
|
|
82
|
+
* supplied by the registry, not spelled out on the definition, so reading the
|
|
83
|
+
* raw property finds nothing and the diff falls back to raw keys ("value"
|
|
84
|
+
* instead of "Value") on the two repeaters that ship by default.
|
|
85
|
+
*/
|
|
86
|
+
function repeaterFor(sectionType: string, key: string) {
|
|
87
|
+
const def = getSectionType(sectionType)
|
|
88
|
+
const field = def?.fields.find((f: SectionFieldDef) => f.key === key)
|
|
89
|
+
return field ? resolveRepeaterField(field) : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Values that all render as `—` and must therefore compare as equal.
|
|
98
|
+
*
|
|
99
|
+
* Without this, tightening the comparison from formatted strings to structure
|
|
100
|
+
* would start reporting `'' → undefined` as a change, emitting rows that read
|
|
101
|
+
* "— → —". Blank is blank however it is spelled.
|
|
102
|
+
*/
|
|
103
|
+
function isBlank(value: unknown): boolean {
|
|
104
|
+
if (value === null || value === undefined) return true
|
|
105
|
+
if (typeof value === 'string') return value.trim() === ''
|
|
106
|
+
if (Array.isArray(value)) return value.length === 0
|
|
107
|
+
if (isPlainObject(value)) return Object.keys(value).length === 0
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Structural equality, which is what decides whether a field changed.
|
|
113
|
+
*
|
|
114
|
+
* This deliberately does NOT go through {@link formatValue}. Comparing the
|
|
115
|
+
* display strings is the obvious shortcut and it silently loses real edits:
|
|
116
|
+
* `formatValue` collapses an array to "N items", so retyping a stats row while
|
|
117
|
+
* keeping the row count formats identically on both sides and the field reports
|
|
118
|
+
* no change — no row in the panel, and no outline on the canvas. That is
|
|
119
|
+
* precisely the edit the reviewer is looking for.
|
|
120
|
+
*/
|
|
121
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
122
|
+
if (isBlank(a) && isBlank(b)) return true
|
|
123
|
+
if (a === b) return true
|
|
124
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
125
|
+
return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]))
|
|
126
|
+
}
|
|
127
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
128
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)])
|
|
129
|
+
for (const key of keys) if (!deepEqual(a[key], b[key])) return false
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Render a value as the one line the diff shows.
|
|
137
|
+
*
|
|
138
|
+
* Lossy on purpose — a repeater row expanded in full would bury the change it
|
|
139
|
+
* is reporting. It is used for DISPLAY ONLY; {@link deepEqual} decides whether
|
|
140
|
+
* a field changed, and repeater rows are descended into by
|
|
141
|
+
* {@link compareRepeater} so a collapsed summary never has to stand in for a
|
|
142
|
+
* change it cannot show.
|
|
143
|
+
*/
|
|
144
|
+
export function formatValue(value: unknown): string {
|
|
145
|
+
if (value === null || value === undefined) return EMPTY
|
|
146
|
+
if (typeof value === 'string') return value.trim() === '' ? EMPTY : value
|
|
147
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
148
|
+
if (typeof value === 'number') return String(value)
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
if (value.length === 0) return EMPTY
|
|
151
|
+
return `${value.length} item${value.length === 1 ? '' : 's'}`
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === 'object') {
|
|
154
|
+
const record = value as Record<string, unknown>
|
|
155
|
+
for (const key of ['name', 'title', 'label', 'url', 'alt']) {
|
|
156
|
+
const candidate = record[key]
|
|
157
|
+
if (typeof candidate === 'string' && candidate.trim() !== '') return candidate
|
|
158
|
+
}
|
|
159
|
+
const count = Object.keys(record).length
|
|
160
|
+
return count === 0 ? EMPTY : `${count} field${count === 1 ? '' : 's'}`
|
|
161
|
+
}
|
|
162
|
+
return String(value)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Visibility reads as a state, not a boolean — "No" is not a useful diff. */
|
|
166
|
+
function formatVisible(value: unknown): string {
|
|
167
|
+
return value === false ? 'Hidden' : 'Shown'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function push(
|
|
171
|
+
out: FieldChange[],
|
|
172
|
+
section: Pick<PageSection, 'id' | 'name'>,
|
|
173
|
+
field: string,
|
|
174
|
+
from: string,
|
|
175
|
+
to: string,
|
|
176
|
+
): void {
|
|
177
|
+
out.push({ sectionId: section.id, sectionName: section.name, field, from, to })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function compare(
|
|
181
|
+
out: FieldChange[],
|
|
182
|
+
section: Pick<PageSection, 'id' | 'name'>,
|
|
183
|
+
field: string,
|
|
184
|
+
before: unknown,
|
|
185
|
+
after: unknown,
|
|
186
|
+
format: (v: unknown) => string = formatValue,
|
|
187
|
+
): void {
|
|
188
|
+
if (deepEqual(before, after)) return
|
|
189
|
+
push(out, section, field, format(before), format(after))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Diff a repeater (stats rows, gallery images, FAQ entries) row by row.
|
|
194
|
+
*
|
|
195
|
+
* A single "2 items → 2 items" row would be worse than useless: it is the one
|
|
196
|
+
* case where the summary cannot express the change, and the reviewer's next
|
|
197
|
+
* question — *which* row — has no answer. Descending gives
|
|
198
|
+
* "Stats · Row 2 · Value" and a real before/after.
|
|
199
|
+
*/
|
|
200
|
+
function compareRepeater(
|
|
201
|
+
out: FieldChange[],
|
|
202
|
+
section: Pick<PageSection, 'id' | 'name'>,
|
|
203
|
+
sectionType: string,
|
|
204
|
+
key: string,
|
|
205
|
+
label: string,
|
|
206
|
+
before: readonly unknown[],
|
|
207
|
+
after: readonly unknown[],
|
|
208
|
+
): void {
|
|
209
|
+
const repeater = repeaterFor(sectionType, key)
|
|
210
|
+
// The registry's own noun for a row ("Stat", "Image") when it has one.
|
|
211
|
+
const rowNoun = repeater?.rowLabel ?? 'Row'
|
|
212
|
+
const subLabel = (subKey: string) =>
|
|
213
|
+
repeater?.fields.find((f: SectionFieldDef) => f.key === subKey)?.label ?? subKey
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < Math.max(before.length, after.length); i++) {
|
|
216
|
+
const rowLabel = `${label} · ${rowNoun} ${i + 1}`
|
|
217
|
+
const prev = before[i]
|
|
218
|
+
const next = after[i]
|
|
219
|
+
|
|
220
|
+
if (prev === undefined) {
|
|
221
|
+
push(out, section, rowLabel, EMPTY, 'Added')
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (next === undefined) {
|
|
225
|
+
push(out, section, rowLabel, 'Present', 'Removed')
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (deepEqual(prev, next)) continue
|
|
229
|
+
|
|
230
|
+
// Rows are normally objects of sub-fields; anything else (a bare string in
|
|
231
|
+
// a legacy gallery, say) still gets a row rather than being skipped.
|
|
232
|
+
if (!isPlainObject(prev) || !isPlainObject(next)) {
|
|
233
|
+
push(out, section, rowLabel, formatValue(prev), formatValue(next))
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const subKey of new Set([...Object.keys(prev), ...Object.keys(next)])) {
|
|
238
|
+
compare(out, section, `${rowLabel} · ${subLabel(subKey)}`, prev[subKey], next[subKey])
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** One content field: descends into repeaters, otherwise a plain comparison. */
|
|
244
|
+
function compareContentField(
|
|
245
|
+
out: FieldChange[],
|
|
246
|
+
section: Pick<PageSection, 'id' | 'name'>,
|
|
247
|
+
sectionType: string,
|
|
248
|
+
key: string,
|
|
249
|
+
before: unknown,
|
|
250
|
+
after: unknown,
|
|
251
|
+
): void {
|
|
252
|
+
if (deepEqual(before, after)) return
|
|
253
|
+
|
|
254
|
+
const label = contentFieldLabel(sectionType, key)
|
|
255
|
+
const beforeRows = Array.isArray(before) ? before : null
|
|
256
|
+
const afterRows = Array.isArray(after) ? after : null
|
|
257
|
+
|
|
258
|
+
if (beforeRows && afterRows) {
|
|
259
|
+
compareRepeater(out, section, sectionType, key, label, beforeRows, afterRows)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
push(out, section, label, formatValue(before), formatValue(after))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compute what changed going FROM `older` TO `newer`.
|
|
268
|
+
*
|
|
269
|
+
* Diffing version *i* against version *i+1* therefore yields what changed *in*
|
|
270
|
+
* version *i* — the framing the history panel needs, where each row answers
|
|
271
|
+
* "what did this save do?" rather than "how does this differ from now?".
|
|
272
|
+
*
|
|
273
|
+
* `older` is optional because the earliest version in a list has nothing
|
|
274
|
+
* before it. That case is no changes, not "everything was added": the oldest
|
|
275
|
+
* snapshot we happen to hold is not the moment the page came into existence,
|
|
276
|
+
* and reporting every field as new would say it was.
|
|
277
|
+
*/
|
|
278
|
+
export function diffVersions(
|
|
279
|
+
newer: readonly PageSection[],
|
|
280
|
+
older: readonly PageSection[] | null | undefined,
|
|
281
|
+
): FieldChange[] {
|
|
282
|
+
if (!older) return []
|
|
283
|
+
|
|
284
|
+
const out: FieldChange[] = []
|
|
285
|
+
const olderById = new Map(older.map((s) => [s.id, s]))
|
|
286
|
+
|
|
287
|
+
for (const next of newer) {
|
|
288
|
+
const prev = olderById.get(next.id)
|
|
289
|
+
|
|
290
|
+
if (!prev) {
|
|
291
|
+
out.push({
|
|
292
|
+
sectionId: next.id,
|
|
293
|
+
sectionName: next.name,
|
|
294
|
+
field: 'Section',
|
|
295
|
+
from: EMPTY,
|
|
296
|
+
to: 'Added',
|
|
297
|
+
})
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (next.name !== prev.name) {
|
|
302
|
+
compare(out, next, META_LABELS.name!, prev.name, next.name)
|
|
303
|
+
}
|
|
304
|
+
compare(out, next, META_LABELS.visible!, prev.visible, next.visible, formatVisible)
|
|
305
|
+
|
|
306
|
+
// Union of both key sets: a field cleared to undefined only appears in the
|
|
307
|
+
// older snapshot, and clearing a heading is exactly the kind of edit a
|
|
308
|
+
// history is consulted about.
|
|
309
|
+
const contentKeys = new Set([
|
|
310
|
+
...Object.keys(prev.content ?? {}),
|
|
311
|
+
...Object.keys(next.content ?? {}),
|
|
312
|
+
])
|
|
313
|
+
for (const key of contentKeys) {
|
|
314
|
+
compareContentField(
|
|
315
|
+
out,
|
|
316
|
+
next,
|
|
317
|
+
next.sectionType,
|
|
318
|
+
key,
|
|
319
|
+
prev.content?.[key],
|
|
320
|
+
next.content?.[key],
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const settingKeys = new Set([
|
|
325
|
+
...Object.keys(prev.settings ?? {}),
|
|
326
|
+
...Object.keys(next.settings ?? {}),
|
|
327
|
+
])
|
|
328
|
+
for (const key of settingKeys) {
|
|
329
|
+
compare(
|
|
330
|
+
out,
|
|
331
|
+
next,
|
|
332
|
+
SETTING_LABELS[key] ?? key,
|
|
333
|
+
(prev.settings as Record<string, unknown>)?.[key],
|
|
334
|
+
(next.settings as Record<string, unknown>)?.[key],
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Sections present before and gone after. Reported against the OLD section's
|
|
340
|
+
// name because the new snapshot has nothing to name it with.
|
|
341
|
+
const newerIds = new Set(newer.map((s) => s.id))
|
|
342
|
+
for (const gone of older) {
|
|
343
|
+
if (newerIds.has(gone.id)) continue
|
|
344
|
+
out.push({
|
|
345
|
+
sectionId: gone.id,
|
|
346
|
+
sectionName: gone.name,
|
|
347
|
+
field: 'Section',
|
|
348
|
+
from: 'Present',
|
|
349
|
+
to: 'Removed',
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return out
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Group changes by section, preserving first-seen order so the panel lists
|
|
358
|
+
* them top-to-bottom in the order they appear on the page.
|
|
359
|
+
*/
|
|
360
|
+
export function groupChangesBySection(changes: readonly FieldChange[]): SectionChangeGroup[] {
|
|
361
|
+
const groups = new Map<string, SectionChangeGroup>()
|
|
362
|
+
for (const change of changes) {
|
|
363
|
+
let group = groups.get(change.sectionId)
|
|
364
|
+
if (!group) {
|
|
365
|
+
group = { sectionId: change.sectionId, sectionName: change.sectionName, changes: [] }
|
|
366
|
+
groups.set(change.sectionId, group)
|
|
367
|
+
}
|
|
368
|
+
group.changes.push(change)
|
|
369
|
+
}
|
|
370
|
+
return [...groups.values()]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The ids the canvas outlines while reviewing a version. */
|
|
374
|
+
export function changedSectionIds(changes: readonly FieldChange[]): Set<string> {
|
|
375
|
+
return new Set(changes.map((c) => c.sectionId))
|
|
376
|
+
}
|