@prenta/admin 0.92.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +552 -0
- package/design-system/tokens.css +173 -71
- package/design-system/tokens.json +244 -63
- package/design-system/tokens.scss +203 -98
- package/design-system/tokens.tailwind.cjs +93 -38
- package/dist/AdminRoot.d.ts +27 -6
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.js +138 -0
- package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
- package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
- package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/media-block.render.test.js +108 -0
- package/dist/__tests__/components/media-block.render.test.js.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
- package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
- package/dist/__tests__/components/seo-char-count.test.js +24 -6
- package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
- package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
- package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/serp-preview.render.test.js +25 -0
- package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.js +67 -0
- package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
- package/dist/__tests__/lib/media-format.test.d.ts +2 -0
- package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
- package/dist/__tests__/lib/media-format.test.js +70 -0
- package/dist/__tests__/lib/media-format.test.js.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.js +122 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +2 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
- package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-redirect.test.js +26 -0
- package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
- package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
- package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
- package/dist/__tests__/lib/rail-width.test.js +69 -0
- package/dist/__tests__/lib/rail-width.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
- package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.js +73 -0
- package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
- package/dist/__tests__/styles/packaging.test.d.ts +2 -0
- package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
- package/dist/__tests__/styles/packaging.test.js +31 -0
- package/dist/__tests__/styles/packaging.test.js.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.js +92 -0
- package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
- package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
- package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js +1 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-tab.render.test.js +151 -0
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.js +81 -0
- package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
- package/dist/components/BreakpointPicker.d.ts +18 -0
- package/dist/components/BreakpointPicker.d.ts.map +1 -0
- package/dist/components/BreakpointPicker.js +31 -0
- package/dist/components/BreakpointPicker.js.map +1 -0
- package/dist/components/CanvasEditTip.d.ts +6 -0
- package/dist/components/CanvasEditTip.d.ts.map +1 -0
- package/dist/components/CanvasEditTip.js +22 -0
- package/dist/components/CanvasEditTip.js.map +1 -0
- package/dist/components/EditorRail.d.ts +36 -0
- package/dist/components/EditorRail.d.ts.map +1 -0
- package/dist/components/EditorRail.js +104 -0
- package/dist/components/EditorRail.js.map +1 -0
- package/dist/components/EditorRailTabs.d.ts +32 -0
- package/dist/components/EditorRailTabs.d.ts.map +1 -0
- package/dist/components/EditorRailTabs.js +45 -0
- package/dist/components/EditorRailTabs.js.map +1 -0
- package/dist/components/MediaBlock.d.ts +49 -0
- package/dist/components/MediaBlock.d.ts.map +1 -0
- package/dist/components/MediaBlock.js +50 -0
- package/dist/components/MediaBlock.js.map +1 -0
- package/dist/components/ReflowMeter.d.ts +28 -0
- package/dist/components/ReflowMeter.d.ts.map +1 -0
- package/dist/components/ReflowMeter.js +42 -0
- package/dist/components/ReflowMeter.js.map +1 -0
- package/dist/components/ZoomChip.d.ts +17 -0
- package/dist/components/ZoomChip.d.ts.map +1 -0
- package/dist/components/ZoomChip.js +17 -0
- package/dist/components/ZoomChip.js.map +1 -0
- package/dist/components/breakpoint-icons.d.ts +3 -0
- package/dist/components/breakpoint-icons.d.ts.map +1 -0
- package/dist/components/breakpoint-icons.js +16 -0
- package/dist/components/breakpoint-icons.js.map +1 -0
- package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
- package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
- package/dist/components/page-tab/AuthorPicker.js +95 -0
- package/dist/components/page-tab/AuthorPicker.js.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.js +29 -0
- package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
- package/dist/components/seo/PageAuditPanel.d.ts +14 -0
- package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
- package/dist/components/seo/PageAuditPanel.js +59 -0
- package/dist/components/seo/PageAuditPanel.js.map +1 -0
- package/dist/components/seo/SeoCharCount.d.ts +19 -7
- package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
- package/dist/components/seo/SeoCharCount.js +30 -16
- package/dist/components/seo/SeoCharCount.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts +15 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +118 -21
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SerpPreview.d.ts +20 -0
- package/dist/components/seo/SerpPreview.d.ts.map +1 -0
- package/dist/components/seo/SerpPreview.js +6 -0
- package/dist/components/seo/SerpPreview.js.map +1 -0
- package/dist/hooks/usePageAudit.d.ts +41 -0
- package/dist/hooks/usePageAudit.d.ts.map +1 -0
- package/dist/hooks/usePageAudit.js +68 -0
- package/dist/hooks/usePageAudit.js.map +1 -0
- package/dist/hooks/useReflowGuard.d.ts +47 -0
- package/dist/hooks/useReflowGuard.d.ts.map +1 -0
- package/dist/hooks/useReflowGuard.js +52 -0
- package/dist/hooks/useReflowGuard.js.map +1 -0
- package/dist/lib/breakpoints.d.ts +51 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +84 -0
- package/dist/lib/breakpoints.js.map +1 -0
- package/dist/lib/canvas-edit-chrome.d.ts +22 -0
- package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
- package/dist/lib/canvas-edit-chrome.js +81 -0
- package/dist/lib/canvas-edit-chrome.js.map +1 -0
- package/dist/lib/media-format.d.ts +55 -0
- package/dist/lib/media-format.d.ts.map +1 -0
- package/dist/lib/media-format.js +93 -0
- package/dist/lib/media-format.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +45 -0
- package/dist/lib/page-audit-input.d.ts.map +1 -0
- package/dist/lib/page-audit-input.js +53 -0
- package/dist/lib/page-audit-input.js.map +1 -0
- package/dist/lib/page-editor-service.d.ts +12 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +6 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/page-redirect.d.ts +39 -0
- package/dist/lib/page-redirect.d.ts.map +1 -0
- package/dist/lib/page-redirect.js +38 -0
- package/dist/lib/page-redirect.js.map +1 -0
- package/dist/lib/rail-width.d.ts +23 -0
- package/dist/lib/rail-width.d.ts.map +1 -0
- package/dist/lib/rail-width.js +59 -0
- package/dist/lib/rail-width.js.map +1 -0
- package/dist/lib/reflow-baseline.d.ts +45 -0
- package/dist/lib/reflow-baseline.d.ts.map +1 -0
- package/dist/lib/reflow-baseline.js +38 -0
- package/dist/lib/reflow-baseline.js.map +1 -0
- package/dist/lib/reflow-probe.d.ts +159 -0
- package/dist/lib/reflow-probe.d.ts.map +1 -0
- package/dist/lib/reflow-probe.js +228 -0
- package/dist/lib/reflow-probe.js.map +1 -0
- package/dist/lib/seo-service.d.ts +10 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
- package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
- package/dist/views/page-editor/CanvasMetaLine.js +23 -0
- package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +34 -8
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +68 -34
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +14 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +8 -17
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +275 -53
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageTab.d.ts +36 -0
- package/dist/views/page-editor/PageTab.d.ts.map +1 -0
- package/dist/views/page-editor/PageTab.js +65 -0
- package/dist/views/page-editor/PageTab.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +63 -19
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +38 -7
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.js +3 -0
- package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
- package/dist/views/page-editor/reflow-context.d.ts +28 -0
- package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
- package/dist/views/page-editor/reflow-context.js +14 -0
- package/dist/views/page-editor/reflow-context.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.js +7 -3
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.js +1 -1
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +49 -24
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.js +27 -15
- package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
- package/dist/views/page-editor/sections/parts.d.ts +0 -1
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
- package/dist/views/page-editor/sections/parts.js +41 -16
- package/dist/views/page-editor/sections/parts.js.map +1 -1
- package/dist/views/page-editor/viewports.d.ts +7 -14
- package/dist/views/page-editor/viewports.d.ts.map +1 -1
- package/dist/views/page-editor/viewports.js +7 -12
- package/dist/views/page-editor/viewports.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +5 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +2 -20
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +4 -1
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +5 -3
- package/src/AdminRoot.tsx +27 -6
- package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
- package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
- package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
- package/src/__tests__/components/media-block.render.test.tsx +126 -0
- package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
- package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
- package/src/__tests__/components/seo-char-count.test.tsx +26 -6
- package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
- package/src/__tests__/lib/breakpoints.test.ts +84 -0
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
- package/src/__tests__/lib/media-format.test.ts +89 -0
- package/src/__tests__/lib/page-audit-input.test.ts +144 -0
- package/src/__tests__/lib/page-editor-service.test.ts +2 -0
- package/src/__tests__/lib/page-redirect.test.ts +29 -0
- package/src/__tests__/lib/rail-width.test.ts +86 -0
- package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
- package/src/__tests__/lib/reflow-probe.test.ts +95 -0
- package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
- package/src/__tests__/styles/packaging.test.ts +38 -0
- package/src/__tests__/styles/tokens-drift.test.ts +110 -0
- package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
- package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
- package/src/__tests__/views/editor-top-bar.render.test.tsx +1 -1
- package/src/__tests__/views/page-tab.render.test.tsx +226 -0
- package/src/__tests__/views/section-class-parity.test.ts +98 -0
- package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
- package/src/components/BreakpointPicker.tsx +114 -0
- package/src/components/CanvasEditTip.tsx +46 -0
- package/src/components/EditorRail.tsx +219 -0
- package/src/components/EditorRailTabs.tsx +117 -0
- package/src/components/MediaBlock.tsx +223 -0
- package/src/components/ReflowMeter.tsx +125 -0
- package/src/components/ZoomChip.tsx +39 -0
- package/src/components/breakpoint-icons.ts +17 -0
- package/src/components/page-tab/AuthorPicker.tsx +240 -0
- package/src/components/page-tab/DirectiveSwitch.tsx +86 -0
- package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
- package/src/components/seo/PageAuditPanel.tsx +192 -0
- package/src/components/seo/SeoCharCount.tsx +55 -24
- package/src/components/seo/SeoEditorPane.tsx +213 -31
- package/src/components/seo/SerpPreview.tsx +41 -0
- package/src/hooks/usePageAudit.ts +98 -0
- package/src/hooks/useReflowGuard.ts +96 -0
- package/src/lib/breakpoints.ts +107 -0
- package/src/lib/canvas-edit-chrome.ts +82 -0
- package/src/lib/media-format.ts +99 -0
- package/src/lib/page-audit-input.ts +104 -0
- package/src/lib/page-editor-service.ts +18 -0
- package/src/lib/page-redirect.ts +60 -0
- package/src/lib/rail-width.ts +58 -0
- package/src/lib/reflow-baseline.ts +70 -0
- package/src/lib/reflow-probe.ts +299 -0
- package/src/lib/seo-service.ts +10 -0
- package/src/styles/prenta-tokens.css +120 -42
- package/src/styles/theme.css +130 -0
- package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
- package/src/views/page-editor/EditorCanvas.tsx +109 -50
- package/src/views/page-editor/EditorTopBar.tsx +35 -43
- package/src/views/page-editor/PageSectionEditor.tsx +419 -148
- package/src/views/page-editor/PageTab.tsx +433 -0
- package/src/views/page-editor/SectionInspector.tsx +82 -41
- package/src/views/page-editor/SectionsPanel.tsx +281 -140
- package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
- package/src/views/page-editor/reflow-context.ts +43 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
- package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
- package/src/views/page-editor/sections/GallerySection.tsx +7 -3
- package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
- package/src/views/page-editor/sections/MissionSection.tsx +4 -2
- package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
- package/src/views/page-editor/sections/SectionRenderer.tsx +69 -29
- package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
- package/src/views/page-editor/sections/parts.tsx +46 -18
- package/src/views/page-editor/viewports.ts +13 -26
- package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
- package/src/views/post-editor/PostSectionEditor.tsx +1 -56
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { AlertTriangle, Check, Copy, ExternalLink, Layers, Lock, Trash2 } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import {
|
|
7
|
+
restrictedCount,
|
|
8
|
+
robotsMetaLine,
|
|
9
|
+
type MaxImagePreview,
|
|
10
|
+
type RobotsPermissions,
|
|
11
|
+
} from '@prenta/core/seo/robots-directives'
|
|
12
|
+
import {
|
|
13
|
+
SeoAccordion,
|
|
14
|
+
SeoAccordionBadge,
|
|
15
|
+
SeoAccordionItem,
|
|
16
|
+
} from '../../components/seo/SeoAccordion.js'
|
|
17
|
+
import { AuthorPicker } from '../../components/page-tab/AuthorPicker.js'
|
|
18
|
+
import { DirectiveSwitch } from '../../components/page-tab/DirectiveSwitch.js'
|
|
19
|
+
import { pendingSlugChange } from '../../lib/page-redirect.js'
|
|
20
|
+
import type { EditorPage } from '../../lib/page-editor-service.js'
|
|
21
|
+
|
|
22
|
+
const INPUT =
|
|
23
|
+
'border-border bg-input-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 w-full rounded-lg border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none'
|
|
24
|
+
const LABEL = 'text-foreground mb-1 block text-xs font-medium'
|
|
25
|
+
|
|
26
|
+
export interface PageTabProps {
|
|
27
|
+
page: EditorPage
|
|
28
|
+
canEdit: boolean
|
|
29
|
+
/** Path as it is on the live site, or null when never published. */
|
|
30
|
+
publishedPath: string | null
|
|
31
|
+
/** Site origin, for resolving the full URL. */
|
|
32
|
+
siteUrl: string
|
|
33
|
+
directives: RobotsPermissions
|
|
34
|
+
onDirectivesChange: (next: RobotsPermissions) => void
|
|
35
|
+
/** Whether a 301 should be created when the moved page is published. */
|
|
36
|
+
redirectOnPublish: boolean
|
|
37
|
+
onRedirectOnPublishChange: (next: boolean) => void
|
|
38
|
+
/**
|
|
39
|
+
* Whether this user can create redirects. `POST /redirects` is limited to
|
|
40
|
+
* publishers who may also publish the page; when false the URL-changed
|
|
41
|
+
* warning still shows, but the checkbox that would promise a 301 does not.
|
|
42
|
+
*/
|
|
43
|
+
canCreateRedirect?: boolean
|
|
44
|
+
onPageChange: (patch: Partial<EditorPage>) => void
|
|
45
|
+
onDuplicate: () => void
|
|
46
|
+
onTrash: () => void
|
|
47
|
+
/** Template/schema binding, shown read-only. */
|
|
48
|
+
templateName?: string
|
|
49
|
+
schemaType?: string
|
|
50
|
+
/** Offered as the brand byline in the author picker. */
|
|
51
|
+
brandName?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rail tab 3 — everything about the page that is not its content or its search
|
|
56
|
+
* copy: where it lives, when it goes live, what crawlers may do with it, what
|
|
57
|
+
* owns its layout, and how to get rid of it.
|
|
58
|
+
*/
|
|
59
|
+
export function PageTab({
|
|
60
|
+
page,
|
|
61
|
+
canEdit,
|
|
62
|
+
publishedPath,
|
|
63
|
+
siteUrl,
|
|
64
|
+
directives,
|
|
65
|
+
onDirectivesChange,
|
|
66
|
+
redirectOnPublish,
|
|
67
|
+
onRedirectOnPublishChange,
|
|
68
|
+
canCreateRedirect = true,
|
|
69
|
+
onPageChange,
|
|
70
|
+
onDuplicate,
|
|
71
|
+
onTrash,
|
|
72
|
+
templateName,
|
|
73
|
+
schemaType,
|
|
74
|
+
brandName,
|
|
75
|
+
}: PageTabProps) {
|
|
76
|
+
const [copied, setCopied] = useState(false)
|
|
77
|
+
const [confirmTrash, setConfirmTrash] = useState(false)
|
|
78
|
+
|
|
79
|
+
const fullUrl = useMemo(() => {
|
|
80
|
+
const origin = siteUrl.replace(/\/+$/, '')
|
|
81
|
+
const path = page.path.startsWith('/') ? page.path : `/${page.path}`
|
|
82
|
+
return `${origin}${path}`
|
|
83
|
+
}, [siteUrl, page.path])
|
|
84
|
+
|
|
85
|
+
const move = pendingSlugChange(publishedPath, page.path)
|
|
86
|
+
const restricted = restrictedCount(directives)
|
|
87
|
+
const set = (patch: Partial<RobotsPermissions>) => onDirectivesChange({ ...directives, ...patch })
|
|
88
|
+
|
|
89
|
+
async function copyUrl() {
|
|
90
|
+
try {
|
|
91
|
+
await navigator.clipboard.writeText(fullUrl)
|
|
92
|
+
setCopied(true)
|
|
93
|
+
setTimeout(() => setCopied(false), 1600)
|
|
94
|
+
} catch {
|
|
95
|
+
toast.error('Could not copy — your browser blocked clipboard access.')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<SeoAccordion defaultOpen={['address', 'publishing']}>
|
|
101
|
+
{/* ── Address ────────────────────────────────────────────────────── */}
|
|
102
|
+
<SeoAccordionItem
|
|
103
|
+
value="address"
|
|
104
|
+
title="Address"
|
|
105
|
+
badge={move ? <SeoAccordionBadge tone="warn">URL CHANGED</SeoAccordionBadge> : undefined}
|
|
106
|
+
>
|
|
107
|
+
<div>
|
|
108
|
+
<label className={LABEL} htmlFor="pt-slug">
|
|
109
|
+
URL slug
|
|
110
|
+
</label>
|
|
111
|
+
<input
|
|
112
|
+
id="pt-slug"
|
|
113
|
+
className={INPUT}
|
|
114
|
+
value={page.slug}
|
|
115
|
+
disabled={!canEdit}
|
|
116
|
+
onChange={(e) => {
|
|
117
|
+
// Path is the public URL the redirect detector and the displayed
|
|
118
|
+
// full link both read. Keep it in lockstep when it was derived
|
|
119
|
+
// from the previous slug — the same rule as PageSettingsModal.
|
|
120
|
+
// A custom path (e.g. `/team/about` while slug is `about`) is
|
|
121
|
+
// left alone; the editor must change that elsewhere on purpose.
|
|
122
|
+
const slug = e.target.value
|
|
123
|
+
const patch: Partial<EditorPage> = { slug }
|
|
124
|
+
if (!page.path || page.path === `/${page.slug}`) {
|
|
125
|
+
patch.path = slug ? `/${slug}` : '/'
|
|
126
|
+
}
|
|
127
|
+
onPageChange(patch)
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
<div className="mt-1.5 flex items-center gap-1.5">
|
|
131
|
+
{/* `data-volatile` so visual baselines mask it: the origin comes
|
|
132
|
+
from `NEXT_PUBLIC_SITE_URL`, which is set in some environments
|
|
133
|
+
and not others, and a screenshot that bakes one in fails in the
|
|
134
|
+
other. */}
|
|
135
|
+
<code
|
|
136
|
+
data-volatile
|
|
137
|
+
className="text-muted-foreground min-w-0 flex-1 truncate font-mono text-[11px]"
|
|
138
|
+
>
|
|
139
|
+
{fullUrl}
|
|
140
|
+
</code>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={copyUrl}
|
|
144
|
+
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1"
|
|
145
|
+
aria-label={copied ? 'Full URL copied' : 'Copy full URL'}
|
|
146
|
+
>
|
|
147
|
+
{copied ? (
|
|
148
|
+
<Check size={13} className="text-success" aria-hidden />
|
|
149
|
+
) : (
|
|
150
|
+
<Copy size={13} aria-hidden />
|
|
151
|
+
)}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Only for a page that is actually live. A slug change on a draft
|
|
157
|
+
breaks nothing, and warning about it would train editors to dismiss
|
|
158
|
+
the warning that matters. */}
|
|
159
|
+
{move && (
|
|
160
|
+
<div className="bg-warning/10 border-warning/30 space-y-2 rounded-lg border p-2.5">
|
|
161
|
+
<div className="flex gap-2">
|
|
162
|
+
<AlertTriangle size={14} className="text-warning mt-px shrink-0" aria-hidden />
|
|
163
|
+
<p className="text-[11.5px] leading-normal">
|
|
164
|
+
This page is live at <b className="font-medium">{move.from}</b>. Publishing moves
|
|
165
|
+
it, and every existing link to the old address will 404.
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
{canCreateRedirect ? (
|
|
169
|
+
<label className="flex cursor-pointer items-start gap-2 text-[11.5px]">
|
|
170
|
+
<input
|
|
171
|
+
type="checkbox"
|
|
172
|
+
checked={redirectOnPublish}
|
|
173
|
+
disabled={!canEdit}
|
|
174
|
+
onChange={(e) => onRedirectOnPublishChange(e.target.checked)}
|
|
175
|
+
className="accent-primary mt-0.5"
|
|
176
|
+
/>
|
|
177
|
+
<span>
|
|
178
|
+
Create a 301 redirect on publish
|
|
179
|
+
<span className="text-muted-foreground mt-0.5 block font-mono text-[10.5px]">
|
|
180
|
+
{move.from} → {move.to}
|
|
181
|
+
</span>
|
|
182
|
+
</span>
|
|
183
|
+
</label>
|
|
184
|
+
) : (
|
|
185
|
+
<p className="text-muted-foreground text-[11.5px] leading-normal">
|
|
186
|
+
Ask an admin to create a 301 from{' '}
|
|
187
|
+
<span className="font-mono text-[10.5px]">
|
|
188
|
+
{move.from} → {move.to}
|
|
189
|
+
</span>{' '}
|
|
190
|
+
so existing links keep working.
|
|
191
|
+
</p>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{page.status === 'PUBLISHED' && (
|
|
197
|
+
<a
|
|
198
|
+
href={fullUrl}
|
|
199
|
+
target="_blank"
|
|
200
|
+
rel="noreferrer"
|
|
201
|
+
className="border-border hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium"
|
|
202
|
+
>
|
|
203
|
+
<ExternalLink size={12} aria-hidden />
|
|
204
|
+
Open live page
|
|
205
|
+
</a>
|
|
206
|
+
)}
|
|
207
|
+
</SeoAccordionItem>
|
|
208
|
+
|
|
209
|
+
{/* ── Publishing ─────────────────────────────────────────────────── */}
|
|
210
|
+
<SeoAccordionItem
|
|
211
|
+
value="publishing"
|
|
212
|
+
title="Publishing"
|
|
213
|
+
badge={
|
|
214
|
+
<SeoAccordionBadge tone={page.status === 'PUBLISHED' ? 'good' : 'neutral'}>
|
|
215
|
+
{page.status}
|
|
216
|
+
</SeoAccordionBadge>
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-[11.5px]">
|
|
220
|
+
<dt className="text-muted-foreground">Last edited</dt>
|
|
221
|
+
<dd data-volatile>{formatStamp(page.updatedAt) ?? '—'}</dd>
|
|
222
|
+
<dt className="text-muted-foreground">Published</dt>
|
|
223
|
+
<dd data-volatile>{formatStamp(page.publishedAt) ?? 'Not yet published'}</dd>
|
|
224
|
+
{page.scheduledAt && (
|
|
225
|
+
<>
|
|
226
|
+
<dt className="text-muted-foreground">Goes live</dt>
|
|
227
|
+
<dd data-volatile>{formatStamp(page.scheduledAt)}</dd>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
{page.scheduledUnpublishAt && (
|
|
231
|
+
<>
|
|
232
|
+
<dt className="text-muted-foreground">Comes down</dt>
|
|
233
|
+
<dd data-volatile>{formatStamp(page.scheduledUnpublishAt)}</dd>
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</dl>
|
|
237
|
+
<div>
|
|
238
|
+
<p className={LABEL}>Byline</p>
|
|
239
|
+
<AuthorPicker
|
|
240
|
+
value={{ name: page.authorName, id: page.authorId }}
|
|
241
|
+
onChange={(next) => onPageChange({ authorName: next.name, authorId: next.id })}
|
|
242
|
+
brandName={brandName}
|
|
243
|
+
disabled={!canEdit}
|
|
244
|
+
/>
|
|
245
|
+
<p className="text-muted-foreground mt-1 text-[11px] leading-snug">
|
|
246
|
+
Shown on the page and in its structured data. Separate from who created the record,
|
|
247
|
+
which is not reassigned.
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<p className="text-muted-foreground text-[11px] leading-snug">
|
|
252
|
+
Status is changed with Publish and Schedule in the toolbar, so there is one way to do it
|
|
253
|
+
rather than two that can disagree.
|
|
254
|
+
</p>
|
|
255
|
+
</SeoAccordionItem>
|
|
256
|
+
|
|
257
|
+
{/* ── Search directives ──────────────────────────────────────────── */}
|
|
258
|
+
<SeoAccordionItem
|
|
259
|
+
value="directives"
|
|
260
|
+
title="Search directives"
|
|
261
|
+
badge={
|
|
262
|
+
restricted === 0 ? (
|
|
263
|
+
<SeoAccordionBadge tone="good">OPEN</SeoAccordionBadge>
|
|
264
|
+
) : (
|
|
265
|
+
<SeoAccordionBadge tone="warn">{restricted} RESTRICTED</SeoAccordionBadge>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
>
|
|
269
|
+
<div className="divide-border divide-y">
|
|
270
|
+
<DirectiveSwitch
|
|
271
|
+
id="pt-index"
|
|
272
|
+
label="Show in search results"
|
|
273
|
+
hint="Off removes the page from Google and every other index."
|
|
274
|
+
checked={directives.index}
|
|
275
|
+
onChange={(v) => set({ index: v })}
|
|
276
|
+
permissiveToken="index"
|
|
277
|
+
restrictiveToken="noindex"
|
|
278
|
+
disabled={!canEdit}
|
|
279
|
+
/>
|
|
280
|
+
<DirectiveSwitch
|
|
281
|
+
id="pt-follow"
|
|
282
|
+
label="Follow links on this page"
|
|
283
|
+
hint="Off stops crawlers passing authority to anything this page links to."
|
|
284
|
+
checked={directives.follow}
|
|
285
|
+
onChange={(v) => set({ follow: v })}
|
|
286
|
+
permissiveToken="follow"
|
|
287
|
+
restrictiveToken="nofollow"
|
|
288
|
+
disabled={!canEdit}
|
|
289
|
+
/>
|
|
290
|
+
<DirectiveSwitch
|
|
291
|
+
id="pt-snippet"
|
|
292
|
+
label="Allow text snippets"
|
|
293
|
+
hint="Off blocks AI Overviews and chat citations entirely."
|
|
294
|
+
checked={directives.snippet}
|
|
295
|
+
onChange={(v) => set({ snippet: v })}
|
|
296
|
+
permissiveToken="snippet"
|
|
297
|
+
restrictiveToken="nosnippet"
|
|
298
|
+
disabled={!canEdit || !directives.index}
|
|
299
|
+
/>
|
|
300
|
+
<DirectiveSwitch
|
|
301
|
+
id="pt-archive"
|
|
302
|
+
label="Allow cached copies"
|
|
303
|
+
checked={directives.archive}
|
|
304
|
+
onChange={(v) => set({ archive: v })}
|
|
305
|
+
permissiveToken="archive"
|
|
306
|
+
restrictiveToken="noarchive"
|
|
307
|
+
disabled={!canEdit || !directives.index}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div>
|
|
312
|
+
<label className={LABEL} htmlFor="pt-img-preview">
|
|
313
|
+
Image preview size
|
|
314
|
+
</label>
|
|
315
|
+
<select
|
|
316
|
+
id="pt-img-preview"
|
|
317
|
+
className={INPUT}
|
|
318
|
+
value={directives.maxImagePreview}
|
|
319
|
+
disabled={!canEdit || !directives.index || !directives.snippet}
|
|
320
|
+
onChange={(e) => set({ maxImagePreview: e.target.value as MaxImagePreview })}
|
|
321
|
+
>
|
|
322
|
+
<option value="large">Large — full-width image in results</option>
|
|
323
|
+
<option value="standard">Standard — thumbnail</option>
|
|
324
|
+
<option value="none">None</option>
|
|
325
|
+
</select>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div>
|
|
329
|
+
<p className={LABEL}>What ships in the page head</p>
|
|
330
|
+
{/* The literal line, composed by the same function the renderer uses.
|
|
331
|
+
An editor should never have to trust that this UI and the output
|
|
332
|
+
agree — they are the same code. */}
|
|
333
|
+
<code className="bg-muted text-muted-foreground block overflow-x-auto rounded-lg px-2.5 py-2 font-mono text-[10.5px] whitespace-pre">
|
|
334
|
+
{robotsMetaLine(directives)}
|
|
335
|
+
</code>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{!directives.index && (
|
|
339
|
+
<div className="bg-warning/10 border-warning/30 flex gap-2 rounded-lg border p-2.5">
|
|
340
|
+
<AlertTriangle size={14} className="text-warning mt-px shrink-0" aria-hidden />
|
|
341
|
+
<p className="text-[11.5px] leading-normal">
|
|
342
|
+
<b className="font-semibold">This page is hidden from search.</b> Publishing will not
|
|
343
|
+
make it findable.
|
|
344
|
+
</p>
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</SeoAccordionItem>
|
|
348
|
+
|
|
349
|
+
{/* ── Layout ─────────────────────────────────────────────────────── */}
|
|
350
|
+
<SeoAccordionItem
|
|
351
|
+
value="layout"
|
|
352
|
+
title="Layout"
|
|
353
|
+
badge={<SeoAccordionBadge tone="neutral">LOCKED</SeoAccordionBadge>}
|
|
354
|
+
>
|
|
355
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-[11.5px]">
|
|
356
|
+
<dt className="text-muted-foreground">Template</dt>
|
|
357
|
+
<dd className="flex items-center gap-1.5">
|
|
358
|
+
<Layers size={12} className="text-muted-foreground" aria-hidden />
|
|
359
|
+
{templateName ?? 'Default'}
|
|
360
|
+
</dd>
|
|
361
|
+
<dt className="text-muted-foreground">Schema</dt>
|
|
362
|
+
<dd>{schemaType ?? 'WebPage'}</dd>
|
|
363
|
+
</dl>
|
|
364
|
+
<div className="text-muted-foreground flex gap-2 text-[11px] leading-snug">
|
|
365
|
+
<Lock size={13} className="mt-px shrink-0" aria-hidden />
|
|
366
|
+
<p>
|
|
367
|
+
Layout and styling are design-owned — built in code, not configured here. This tab edits
|
|
368
|
+
where the page lives and how it is indexed.
|
|
369
|
+
</p>
|
|
370
|
+
</div>
|
|
371
|
+
</SeoAccordionItem>
|
|
372
|
+
|
|
373
|
+
{/* ── Danger zone ────────────────────────────────────────────────── */}
|
|
374
|
+
<SeoAccordionItem value="danger" title="Danger zone">
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={onDuplicate}
|
|
378
|
+
disabled={!canEdit || !page.id}
|
|
379
|
+
className="border-border hover:border-destructive hover:bg-destructive/10 hover:text-destructive flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-colors disabled:opacity-50"
|
|
380
|
+
>
|
|
381
|
+
<Copy size={13} aria-hidden />
|
|
382
|
+
Duplicate page
|
|
383
|
+
</button>
|
|
384
|
+
|
|
385
|
+
{confirmTrash ? (
|
|
386
|
+
<div className="border-destructive/40 bg-destructive/10 space-y-2 rounded-lg border p-2.5">
|
|
387
|
+
<p className="text-[11.5px] leading-normal">
|
|
388
|
+
Move <b className="font-semibold">{page.title || 'this page'}</b> to trash?
|
|
389
|
+
{page.status === 'PUBLISHED' && ' It is live, and will stop being served.'}
|
|
390
|
+
</p>
|
|
391
|
+
<div className="flex gap-2">
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
onClick={onTrash}
|
|
395
|
+
className="bg-destructive text-destructive-foreground rounded-md px-2.5 py-1 text-[11px] font-semibold"
|
|
396
|
+
>
|
|
397
|
+
Move to trash
|
|
398
|
+
</button>
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
onClick={() => setConfirmTrash(false)}
|
|
402
|
+
className="border-border rounded-md border px-2.5 py-1 text-[11px] font-medium"
|
|
403
|
+
>
|
|
404
|
+
Cancel
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
) : (
|
|
409
|
+
<button
|
|
410
|
+
type="button"
|
|
411
|
+
onClick={() => setConfirmTrash(true)}
|
|
412
|
+
disabled={!canEdit || !page.id}
|
|
413
|
+
className="border-border hover:border-destructive hover:bg-destructive/10 hover:text-destructive flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-colors disabled:opacity-50"
|
|
414
|
+
>
|
|
415
|
+
<Trash2 size={13} aria-hidden />
|
|
416
|
+
Move to trash
|
|
417
|
+
</button>
|
|
418
|
+
)}
|
|
419
|
+
</SeoAccordionItem>
|
|
420
|
+
</SeoAccordion>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** `null` rather than a placeholder string, so callers choose their own dash. */
|
|
425
|
+
function formatStamp(value: string | null): string | null {
|
|
426
|
+
if (!value) return null
|
|
427
|
+
const d = new Date(value)
|
|
428
|
+
if (Number.isNaN(d.getTime())) return null
|
|
429
|
+
return d.toLocaleString(undefined, {
|
|
430
|
+
dateStyle: 'medium',
|
|
431
|
+
timeStyle: 'short',
|
|
432
|
+
})
|
|
433
|
+
}
|
|
@@ -18,11 +18,15 @@ import {
|
|
|
18
18
|
verticalListSortingStrategy,
|
|
19
19
|
} from '@dnd-kit/sortable'
|
|
20
20
|
import { CSS } from '@dnd-kit/utilities'
|
|
21
|
-
import {
|
|
21
|
+
import { MediaBlock } from '../../components/MediaBlock.js'
|
|
22
|
+
import { ReflowMeter } from '../../components/ReflowMeter.js'
|
|
22
23
|
import { InspectorExpandButton, ResizableAside } from '../../components/ui/ResizableAside.js'
|
|
23
24
|
import { Toggle } from '../../components/ui/Toggle.js'
|
|
24
25
|
import { RichTextField } from '../../fields/RichTextField.js'
|
|
26
|
+
import { useReflowGuard } from '../../hooks/useReflowGuard.js'
|
|
27
|
+
import { baselineFieldText } from '../../lib/reflow-baseline.js'
|
|
25
28
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
29
|
+
import { useReflowContext } from './reflow-context.js'
|
|
26
30
|
import type { ResolvedRepeater, SectionSettings } from './section-types.js'
|
|
27
31
|
import {
|
|
28
32
|
blankRepeaterRow,
|
|
@@ -328,12 +332,52 @@ export function SectionInspector({
|
|
|
328
332
|
|
|
329
333
|
// ─── Field renderers ─────────────────────────────────────────────────
|
|
330
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Field types the reflow guard measures.
|
|
337
|
+
*
|
|
338
|
+
* Text that wraps, and nothing else. A `select` has a fixed set of values the
|
|
339
|
+
* design already accounts for; a `boolean` has no text; `media` wraps nothing.
|
|
340
|
+
*
|
|
341
|
+
* `richText` is excluded deliberately. The probe establishes a baseline by
|
|
342
|
+
* writing the published string into the cloned element, which for rich text
|
|
343
|
+
* would mean injecting stored markup into the DOM — and the whole body is one
|
|
344
|
+
* marker, so a line count across several paragraphs answers no question an
|
|
345
|
+
* editor is asking.
|
|
346
|
+
*/
|
|
347
|
+
const GUARDED_FIELD_TYPES = new Set(['text', 'textarea', 'url'])
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Attaches the reflow readout under a field.
|
|
351
|
+
*
|
|
352
|
+
* Renders nothing at all when the guard has no rows — the section may be drawn
|
|
353
|
+
* by a consumer's renderer that carries no `data-cms-field` markers, and an
|
|
354
|
+
* empty row of dots would read as "measured, all fine" rather than "not
|
|
355
|
+
* measured".
|
|
356
|
+
*/
|
|
357
|
+
function FieldReflow({ fieldPath, value }: { fieldPath: string; value: string }) {
|
|
358
|
+
const ctx = useReflowContext()
|
|
359
|
+
const guard = useReflowGuard({
|
|
360
|
+
canvas: ctx.canvas,
|
|
361
|
+
sectionId: ctx.sectionId ?? '',
|
|
362
|
+
fieldPath,
|
|
363
|
+
value,
|
|
364
|
+
baseline: baselineFieldText(ctx.baseline, ctx.sectionId ?? '', fieldPath),
|
|
365
|
+
enabled: Boolean(ctx.canvas && ctx.sectionId),
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (guard.rows.length === 0) return null
|
|
369
|
+
return (
|
|
370
|
+
<ReflowMeter guard={guard} length={value.length} device={ctx.device} onProof={ctx.onProof} />
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
331
374
|
function ContentField({
|
|
332
375
|
field,
|
|
333
376
|
value,
|
|
334
377
|
onChange,
|
|
335
378
|
onPatch,
|
|
336
379
|
id: idProp,
|
|
380
|
+
fieldPath,
|
|
337
381
|
}: {
|
|
338
382
|
field: SectionFieldDef
|
|
339
383
|
value: unknown
|
|
@@ -345,9 +389,17 @@ function ContentField({
|
|
|
345
389
|
onPatch?: (patch: Record<string, unknown>) => void
|
|
346
390
|
/** Explicit input id (repeater rows pass a row-scoped id to stay unique). */
|
|
347
391
|
id?: string
|
|
392
|
+
/**
|
|
393
|
+
* Dotted `data-cms-field` path for the reflow guard. Differs from `field.key`
|
|
394
|
+
* inside a repeater, where the marker is `stats.0.label`. Defaults to the
|
|
395
|
+
* key, which is right for every top-level field.
|
|
396
|
+
*/
|
|
397
|
+
fieldPath?: string
|
|
348
398
|
}) {
|
|
349
399
|
const id = idProp ?? `insp-field-${field.key}`
|
|
400
|
+
const path = fieldPath ?? field.key
|
|
350
401
|
const text = typeof value === 'string' ? value : ''
|
|
402
|
+
const guarded = GUARDED_FIELD_TYPES.has(field.type)
|
|
351
403
|
|
|
352
404
|
const repeater = resolveRepeaterField(field)
|
|
353
405
|
if (repeater) {
|
|
@@ -405,6 +457,7 @@ function ContentField({
|
|
|
405
457
|
placeholder={field.placeholder}
|
|
406
458
|
onChange={(e) => onChange(e.target.value)}
|
|
407
459
|
/>
|
|
460
|
+
{guarded && <FieldReflow fieldPath={path} value={text} />}
|
|
408
461
|
{field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
|
|
409
462
|
</div>
|
|
410
463
|
)
|
|
@@ -424,6 +477,7 @@ function ContentField({
|
|
|
424
477
|
placeholder={field.placeholder}
|
|
425
478
|
onChange={(e) => onChange(e.target.value)}
|
|
426
479
|
/>
|
|
480
|
+
{guarded && <FieldReflow fieldPath={path} value={text} />}
|
|
427
481
|
{field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
|
|
428
482
|
</div>
|
|
429
483
|
)
|
|
@@ -459,56 +513,42 @@ function MediaField({
|
|
|
459
513
|
/**
|
|
460
514
|
* When provided, picking from the library applies a multi-key patch (URL +
|
|
461
515
|
* intrinsic width/height) in one update. Falls back to `onChange` (URL only)
|
|
462
|
-
*
|
|
516
|
+
* when no patch sink is wired.
|
|
463
517
|
*/
|
|
464
518
|
onPatch?: (patch: Record<string, unknown>) => void
|
|
465
519
|
}) {
|
|
466
|
-
const [open, setOpen] = useState(false)
|
|
467
520
|
return (
|
|
468
521
|
<div>
|
|
469
|
-
|
|
522
|
+
{/* Not `htmlFor` — the block is a composite of buttons, not a single
|
|
523
|
+
labellable control. The label names the group; each control inside
|
|
524
|
+
carries its own accessible name. */}
|
|
525
|
+
<span className={LABEL} id={`${id}-label`}>
|
|
470
526
|
{field.label}
|
|
471
|
-
</
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
onClick={() => setOpen(true)}
|
|
483
|
-
className="bg-accent text-foreground border-border hover:bg-accent/80 rounded-md border px-3 py-1.5 text-xs font-medium"
|
|
484
|
-
>
|
|
485
|
-
Browse
|
|
486
|
-
</button>
|
|
487
|
-
</div>
|
|
488
|
-
{value && (
|
|
489
|
-
<div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
|
|
490
|
-
<img src={value} alt="" className="h-28 w-full object-contain" />
|
|
491
|
-
</div>
|
|
492
|
-
)}
|
|
493
|
-
<MediaPickerModal
|
|
494
|
-
open={open}
|
|
495
|
-
onClose={() => setOpen(false)}
|
|
496
|
-
onSelectItem={(item) => {
|
|
497
|
-
if (!onPatch) return
|
|
527
|
+
</span>
|
|
528
|
+
<MediaBlock
|
|
529
|
+
value={value}
|
|
530
|
+
label={field.label}
|
|
531
|
+
onChange={(url, item) => {
|
|
532
|
+
if (!onPatch) {
|
|
533
|
+
onChange(url)
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
// Persist the intrinsic dimensions alongside the URL so the public
|
|
537
|
+
// renderer can use the CLS-safe path without a read-time lookup.
|
|
498
538
|
const { width: wKey, height: hKey } = companionDimensionKeys(field.key)
|
|
499
|
-
const patch: Record<string, unknown> = { [field.key]:
|
|
500
|
-
if (typeof item
|
|
501
|
-
if (typeof item
|
|
539
|
+
const patch: Record<string, unknown> = { [field.key]: url }
|
|
540
|
+
if (typeof item?.width === 'number') patch[wKey] = item.width
|
|
541
|
+
if (typeof item?.height === 'number') patch[hKey] = item.height
|
|
542
|
+
// Clearing the slot clears the dimensions with it — leaving them
|
|
543
|
+
// behind would size the next image by the previous one's box.
|
|
544
|
+
if (!url) {
|
|
545
|
+
patch[wKey] = undefined
|
|
546
|
+
patch[hKey] = undefined
|
|
547
|
+
}
|
|
502
548
|
onPatch(patch)
|
|
503
549
|
}}
|
|
504
|
-
onSelect={(url) => {
|
|
505
|
-
// onSelectItem handles persistence when a patch sink is wired; only
|
|
506
|
-
// fall back to a URL-only update when it isn't.
|
|
507
|
-
if (!onPatch) onChange(url)
|
|
508
|
-
setOpen(false)
|
|
509
|
-
}}
|
|
510
|
-
accept="image/*"
|
|
511
550
|
/>
|
|
551
|
+
{field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
|
|
512
552
|
</div>
|
|
513
553
|
)
|
|
514
554
|
}
|
|
@@ -796,6 +836,7 @@ function SortableRepeaterRow({
|
|
|
796
836
|
<ContentField
|
|
797
837
|
key={sub.key}
|
|
798
838
|
id={`insp-field-${fieldKey}-${index}-${sub.key}`}
|
|
839
|
+
fieldPath={`${fieldKey}.${index}.${sub.key}`}
|
|
799
840
|
field={sub}
|
|
800
841
|
value={row[sub.key]}
|
|
801
842
|
onChange={(v) => onPatch({ [sub.key]: v })}
|