@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,219 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
RAIL_COLLAPSED_WIDTH,
|
|
7
|
+
RAIL_DEFAULT_WIDTH,
|
|
8
|
+
RAIL_MAX_WIDTH,
|
|
9
|
+
RAIL_MIN_WIDTH,
|
|
10
|
+
clampRailWidth,
|
|
11
|
+
readRailWidth,
|
|
12
|
+
writeRailWidth,
|
|
13
|
+
} from '../lib/rail-width.js'
|
|
14
|
+
|
|
15
|
+
/** What the rail hands its children so they can render a header. */
|
|
16
|
+
export interface RailState {
|
|
17
|
+
width: number
|
|
18
|
+
dragging: boolean
|
|
19
|
+
collapse: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EditorRailProps {
|
|
23
|
+
/** Short label for the collapsed strip — normally the active tab's name. */
|
|
24
|
+
collapsedLabel: string
|
|
25
|
+
children: (state: RailState) => React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The editor's left rail: resizable, collapsible, and width-persistent.
|
|
30
|
+
*
|
|
31
|
+
* Collapsing UNMOUNTS the rail and swaps in a narrow strip rather than
|
|
32
|
+
* animating its width to zero. A zero-width panel leaves a visible sliver of
|
|
33
|
+
* border and keeps its contents in the accessibility tree, so a screen-reader
|
|
34
|
+
* user lands inside a panel sighted users cannot see.
|
|
35
|
+
*
|
|
36
|
+
* Children are a render prop rather than plain nodes because the panel header
|
|
37
|
+
* needs the live width and drag state — the numbers the rail owns — and
|
|
38
|
+
* threading them through context for one consumer is more machinery than the
|
|
39
|
+
* problem deserves.
|
|
40
|
+
*/
|
|
41
|
+
export function EditorRail({ collapsedLabel, children }: EditorRailProps) {
|
|
42
|
+
const [width, setWidth] = useState(RAIL_DEFAULT_WIDTH)
|
|
43
|
+
const [open, setOpen] = useState(true)
|
|
44
|
+
const [dragging, setDragging] = useState(false)
|
|
45
|
+
const railRef = useRef<HTMLDivElement>(null)
|
|
46
|
+
|
|
47
|
+
// Read persisted width after mount, never during render: there is no
|
|
48
|
+
// localStorage on the server, and seeding initial state from it would
|
|
49
|
+
// hydrate-mismatch.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setWidth(readRailWidth())
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const applyWidth = useCallback((next: number) => {
|
|
55
|
+
const clamped = clampRailWidth(next)
|
|
56
|
+
setWidth(clamped)
|
|
57
|
+
writeRailWidth(clamped)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const collapse = useCallback(() => setOpen(false), [])
|
|
61
|
+
|
|
62
|
+
// Pointer listeners go on `window`, not the handle: the pointer routinely
|
|
63
|
+
// outruns a 7px target mid-drag, and a handle-local listener would drop the
|
|
64
|
+
// drag the moment it did.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!dragging) return
|
|
67
|
+
|
|
68
|
+
const onMove = (event: PointerEvent) => {
|
|
69
|
+
const left = railRef.current?.getBoundingClientRect().left ?? 0
|
|
70
|
+
applyWidth(event.clientX - left)
|
|
71
|
+
}
|
|
72
|
+
const stop = () => setDragging(false)
|
|
73
|
+
|
|
74
|
+
window.addEventListener('pointermove', onMove)
|
|
75
|
+
window.addEventListener('pointerup', stop)
|
|
76
|
+
window.addEventListener('pointercancel', stop)
|
|
77
|
+
|
|
78
|
+
// Lock cursor and selection for the duration. Without this the cursor
|
|
79
|
+
// flickers between col-resize and text as it crosses elements, and the
|
|
80
|
+
// drag selects the panel's copy.
|
|
81
|
+
const { body } = document
|
|
82
|
+
const prevCursor = body.style.cursor
|
|
83
|
+
const prevSelect = body.style.userSelect
|
|
84
|
+
body.style.cursor = 'col-resize'
|
|
85
|
+
body.style.userSelect = 'none'
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
window.removeEventListener('pointermove', onMove)
|
|
89
|
+
window.removeEventListener('pointerup', stop)
|
|
90
|
+
window.removeEventListener('pointercancel', stop)
|
|
91
|
+
body.style.cursor = prevCursor
|
|
92
|
+
body.style.userSelect = prevSelect
|
|
93
|
+
}
|
|
94
|
+
}, [dragging, applyWidth])
|
|
95
|
+
|
|
96
|
+
if (!open) {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className="bg-card border-border flex shrink-0 flex-col items-center gap-3 border-r py-3"
|
|
100
|
+
style={{ width: RAIL_COLLAPSED_WIDTH }}
|
|
101
|
+
>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => setOpen(true)}
|
|
105
|
+
aria-label={`Expand ${collapsedLabel} panel`}
|
|
106
|
+
title={`Expand ${collapsedLabel} panel`}
|
|
107
|
+
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
108
|
+
>
|
|
109
|
+
<PanelLeftOpen className="h-4 w-4" aria-hidden />
|
|
110
|
+
</button>
|
|
111
|
+
{/* Vertical label so the strip still says which panel is hidden. The
|
|
112
|
+
button's accessible name already carries it, hence aria-hidden. */}
|
|
113
|
+
<span
|
|
114
|
+
className="text-muted-foreground text-[10px] font-bold tracking-[0.8px] whitespace-nowrap uppercase [writing-mode:vertical-rl]"
|
|
115
|
+
aria-hidden
|
|
116
|
+
>
|
|
117
|
+
{collapsedLabel}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
ref={railRef}
|
|
126
|
+
className={`bg-card border-border relative flex shrink-0 flex-col border-r ${
|
|
127
|
+
// A width transition during a drag fights the pointer: the panel
|
|
128
|
+
// chases the cursor a frame behind and the resize feels broken.
|
|
129
|
+
dragging ? '' : 'motion-safe:transition-[width] motion-safe:duration-(--mo-slow)'
|
|
130
|
+
}`}
|
|
131
|
+
style={{ width }}
|
|
132
|
+
>
|
|
133
|
+
<div className="flex min-h-0 flex-1 flex-col">{children({ width, dragging, collapse })}</div>
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
// `separator` with an orientation and a value range is what makes this
|
|
137
|
+
// operable without a pointer. A bare div with a pointerdown handler is
|
|
138
|
+
// invisible to keyboard and assistive-tech users.
|
|
139
|
+
role="separator"
|
|
140
|
+
aria-orientation="vertical"
|
|
141
|
+
aria-label="Resize panel"
|
|
142
|
+
aria-valuenow={width}
|
|
143
|
+
aria-valuemin={RAIL_MIN_WIDTH}
|
|
144
|
+
aria-valuemax={RAIL_MAX_WIDTH}
|
|
145
|
+
tabIndex={0}
|
|
146
|
+
onPointerDown={(event) => {
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
setDragging(true)
|
|
149
|
+
}}
|
|
150
|
+
onDoubleClick={() => applyWidth(RAIL_DEFAULT_WIDTH)}
|
|
151
|
+
onKeyDown={(event) => {
|
|
152
|
+
const step = event.shiftKey ? 48 : 16
|
|
153
|
+
if (event.key === 'ArrowLeft') {
|
|
154
|
+
event.preventDefault()
|
|
155
|
+
applyWidth(width - step)
|
|
156
|
+
} else if (event.key === 'ArrowRight') {
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
applyWidth(width + step)
|
|
159
|
+
} else if (event.key === 'Home') {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
applyWidth(RAIL_DEFAULT_WIDTH)
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
className="group absolute inset-y-0 -right-[3px] z-30 w-[7px] cursor-col-resize focus-visible:outline-none"
|
|
165
|
+
>
|
|
166
|
+
{/* The 2px line it draws sits inside a 7px hit area on purpose — a 2px
|
|
167
|
+
pointer target is a coin flip. */}
|
|
168
|
+
<span
|
|
169
|
+
aria-hidden
|
|
170
|
+
className={`pointer-events-none absolute inset-y-0 left-[2.5px] w-0.5 transition-colors ${
|
|
171
|
+
dragging
|
|
172
|
+
? 'bg-brand'
|
|
173
|
+
: 'group-hover:bg-brand group-focus-visible:bg-brand bg-transparent'
|
|
174
|
+
}`}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Header strip under the rail's tabs: panel title, the live width while
|
|
183
|
+
* dragging, and the collapse control.
|
|
184
|
+
*/
|
|
185
|
+
export function RailHeader({
|
|
186
|
+
title,
|
|
187
|
+
width,
|
|
188
|
+
dragging,
|
|
189
|
+
onCollapse,
|
|
190
|
+
}: {
|
|
191
|
+
title: string
|
|
192
|
+
width: number
|
|
193
|
+
dragging: boolean
|
|
194
|
+
onCollapse: () => void
|
|
195
|
+
}) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="border-border flex items-center justify-between gap-2 border-b px-3.5 py-2.5">
|
|
198
|
+
<h2 className="text-foreground truncate text-[12.5px] font-semibold">{title}</h2>
|
|
199
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
200
|
+
{/* Only while dragging: a permanent pixel readout is noise, but during
|
|
201
|
+
a resize it is the one number the user is actually aiming at. */}
|
|
202
|
+
{dragging && (
|
|
203
|
+
<span className="text-muted-foreground text-[11px] tabular-nums" aria-hidden>
|
|
204
|
+
{width}px
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
onClick={onCollapse}
|
|
210
|
+
aria-label={`Collapse ${title} panel`}
|
|
211
|
+
title={`Collapse ${title} panel`}
|
|
212
|
+
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
213
|
+
>
|
|
214
|
+
<PanelLeftClose className="h-4 w-4" aria-hidden />
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { scoreBand } from '@prenta/core/seo/page-audit'
|
|
4
|
+
|
|
5
|
+
export type RailTabId = 'sections' | 'seo' | 'page'
|
|
6
|
+
|
|
7
|
+
export interface RailTab {
|
|
8
|
+
id: RailTabId
|
|
9
|
+
label: string
|
|
10
|
+
/** Panel title shown in the header below the strip. */
|
|
11
|
+
title: string
|
|
12
|
+
/** Count pill (Sections). */
|
|
13
|
+
count?: number
|
|
14
|
+
/**
|
|
15
|
+
* Score for the dot, banded by the page audit's own scale.
|
|
16
|
+
*
|
|
17
|
+
* `undefined` — not known yet; no dot is shown rather than a misleading one.
|
|
18
|
+
* `null` — deliberately not rated (a gate is off, e.g. the page is
|
|
19
|
+
* noindexed). Renders neutral, never as a low score.
|
|
20
|
+
*/
|
|
21
|
+
score?: number | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EditorRailTabsProps {
|
|
25
|
+
tabs: RailTab[]
|
|
26
|
+
active: RailTabId
|
|
27
|
+
onChange: (id: RailTabId) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Three equal tabs across the top of the rail.
|
|
32
|
+
*
|
|
33
|
+
* Each tab carries a status glyph so the rail reads at a glance without being
|
|
34
|
+
* opened: a count for Sections, a band-coloured dot for SEO. The dot is
|
|
35
|
+
* accompanied by text in the accessible name — colour alone is not a signal
|
|
36
|
+
* for anyone who cannot distinguish these hues.
|
|
37
|
+
*/
|
|
38
|
+
export function EditorRailTabs({ tabs, active, onChange }: EditorRailTabsProps) {
|
|
39
|
+
return (
|
|
40
|
+
<div role="tablist" aria-label="Editor panels" className="border-border flex border-b">
|
|
41
|
+
{tabs.map((tab) => {
|
|
42
|
+
const selected = tab.id === active
|
|
43
|
+
// Bands with `scoreBand` from the audit itself, NOT `gradeFromScore`.
|
|
44
|
+
// The two disagree between 55 and 59 — a page scoring 57 read "fair"
|
|
45
|
+
// in the rings and "poor" on the tab summarising them, which is the
|
|
46
|
+
// same class of disagreement this dot was rewired to eliminate.
|
|
47
|
+
const band = typeof tab.score === 'number' ? scoreBand(tab.score) : null
|
|
48
|
+
const unrated = tab.score === null
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
key={tab.id}
|
|
53
|
+
type="button"
|
|
54
|
+
role="tab"
|
|
55
|
+
id={`rail-tab-${tab.id}`}
|
|
56
|
+
aria-selected={selected}
|
|
57
|
+
aria-controls={`rail-panel-${tab.id}`}
|
|
58
|
+
tabIndex={selected ? 0 : -1}
|
|
59
|
+
onClick={() => onChange(tab.id)}
|
|
60
|
+
onKeyDown={(event) => {
|
|
61
|
+
// Arrow keys move between tabs — expected of a tablist, and the
|
|
62
|
+
// only way to reach a tab whose tabIndex is -1.
|
|
63
|
+
const index = tabs.findIndex((t) => t.id === active)
|
|
64
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
const delta = event.key === 'ArrowRight' ? 1 : -1
|
|
67
|
+
const next = tabs[(index + delta + tabs.length) % tabs.length]
|
|
68
|
+
if (next) {
|
|
69
|
+
onChange(next.id)
|
|
70
|
+
document.getElementById(`rail-tab-${next.id}`)?.focus()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
className={`flex flex-1 items-center justify-center gap-1.5 border-b-2 pt-2.5 pb-2 text-[12.5px] transition-colors ${
|
|
75
|
+
selected
|
|
76
|
+
? 'border-brand text-brand font-semibold'
|
|
77
|
+
: 'text-muted-foreground hover:text-foreground border-transparent font-medium'
|
|
78
|
+
}`}
|
|
79
|
+
>
|
|
80
|
+
{tab.label}
|
|
81
|
+
|
|
82
|
+
{tab.count !== undefined && (
|
|
83
|
+
<span
|
|
84
|
+
className={`rounded-full px-1.5 text-[10.5px] font-semibold tabular-nums ${
|
|
85
|
+
selected ? 'bg-accent text-accent-foreground' : 'bg-muted text-muted-foreground'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
{tab.count}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{(band || unrated) && (
|
|
93
|
+
<>
|
|
94
|
+
<span
|
|
95
|
+
className={`h-[7px] w-[7px] shrink-0 rounded-full ${
|
|
96
|
+
band === 'good'
|
|
97
|
+
? 'bg-success'
|
|
98
|
+
: band === 'fair'
|
|
99
|
+
? 'bg-warning'
|
|
100
|
+
: band === 'poor'
|
|
101
|
+
? 'bg-destructive'
|
|
102
|
+
: 'bg-muted-foreground/40'
|
|
103
|
+
}`}
|
|
104
|
+
aria-hidden
|
|
105
|
+
/>
|
|
106
|
+
{/* Colour alone cannot carry the score. */}
|
|
107
|
+
<span className="sr-only">
|
|
108
|
+
{unrated ? 'not rated' : `score ${tab.score} of 100`}
|
|
109
|
+
</span>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</button>
|
|
113
|
+
)
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from 'react'
|
|
4
|
+
import { Check, ImageIcon, Trash2 } from 'lucide-react'
|
|
5
|
+
import { useApiData } from '../lib/useApiData.js'
|
|
6
|
+
import {
|
|
7
|
+
CROP_RATIO,
|
|
8
|
+
assetWash,
|
|
9
|
+
displayName,
|
|
10
|
+
formatBadge,
|
|
11
|
+
ratioLabel,
|
|
12
|
+
type MediaCrop,
|
|
13
|
+
} from '../lib/media-format.js'
|
|
14
|
+
|
|
15
|
+
interface MediaItem {
|
|
16
|
+
id: string
|
|
17
|
+
filename: string
|
|
18
|
+
storageKey: string
|
|
19
|
+
url?: string
|
|
20
|
+
altTag?: string
|
|
21
|
+
mimeType: string
|
|
22
|
+
width?: number
|
|
23
|
+
height?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MediaBlockProps {
|
|
27
|
+
/** Current asset URL, or empty for the empty state. */
|
|
28
|
+
value: string
|
|
29
|
+
onChange: (url: string, item?: MediaItem) => void
|
|
30
|
+
/** The crop the template renders this image at. */
|
|
31
|
+
crop?: MediaCrop
|
|
32
|
+
/** Rows rendered between the frame and the toolbar (the social preview). */
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
disabled?: boolean
|
|
35
|
+
/** Accessible label, e.g. "Hero image". */
|
|
36
|
+
label?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One component for every image — section slots and the social/OG card alike.
|
|
41
|
+
*
|
|
42
|
+
* ── The frame shows the TEMPLATE's crop, not the asset's ─────────────────
|
|
43
|
+
*
|
|
44
|
+
* A section image renders at 16:10 and a social card at 1.91:1 regardless of
|
|
45
|
+
* what was uploaded. Framing the asset's own ratio would tell an editor their
|
|
46
|
+
* portrait photo looks fine and then crop its subject out on the page. What
|
|
47
|
+
* the asset actually is gets reported in the toolbar instead, as a ratio label
|
|
48
|
+
* beside its filename, so both facts are visible and neither is implied.
|
|
49
|
+
*
|
|
50
|
+
* ── The toolbar is always visible ────────────────────────────────────────
|
|
51
|
+
*
|
|
52
|
+
* Never hover-only. Replace and Remove are the two things an editor comes here
|
|
53
|
+
* to do, and hiding them behind a hover makes them undiscoverable on touch and
|
|
54
|
+
* invisible to anyone scanning the panel.
|
|
55
|
+
*
|
|
56
|
+
* ── The picker opens inside the shell ────────────────────────────────────
|
|
57
|
+
*
|
|
58
|
+
* Not a modal. Choosing an image is a comparison against the slot it is going
|
|
59
|
+
* into, and a dialog covering the slot removes the only context that matters.
|
|
60
|
+
*/
|
|
61
|
+
export function MediaBlock({
|
|
62
|
+
value,
|
|
63
|
+
onChange,
|
|
64
|
+
crop = 'section',
|
|
65
|
+
children,
|
|
66
|
+
disabled = false,
|
|
67
|
+
label = 'Image',
|
|
68
|
+
}: MediaBlockProps) {
|
|
69
|
+
const [picking, setPicking] = useState(false)
|
|
70
|
+
const { data } = useApiData<{ items?: MediaItem[] }>(picking ? '/media?pageSize=50' : null)
|
|
71
|
+
const items = (Array.isArray(data) ? data : (data?.items ?? [])).filter((i: MediaItem) =>
|
|
72
|
+
i.mimeType?.startsWith('image/'),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const ratio = CROP_RATIO[crop]
|
|
76
|
+
const name = value ? displayName(value) : ''
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="border-border bg-card overflow-hidden rounded-[10px] border">
|
|
80
|
+
{value ? (
|
|
81
|
+
<div
|
|
82
|
+
className="w-full bg-cover bg-center"
|
|
83
|
+
style={{ aspectRatio: ratio, backgroundImage: assetWash(name) }}
|
|
84
|
+
>
|
|
85
|
+
<img
|
|
86
|
+
src={value}
|
|
87
|
+
alt=""
|
|
88
|
+
className="h-full w-full object-cover"
|
|
89
|
+
style={{ aspectRatio: ratio }}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => setPicking(true)}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
aria-label={`Choose ${label.toLowerCase()}`}
|
|
98
|
+
className="bg-muted hover:bg-accent group flex w-full items-center justify-center p-3 transition-colors disabled:opacity-50"
|
|
99
|
+
style={{ aspectRatio: ratio }}
|
|
100
|
+
>
|
|
101
|
+
<span className="border-border group-hover:border-primary group-hover:text-primary text-muted-foreground flex h-full w-full flex-col items-center justify-center gap-1.5 rounded-lg border-[1.5px] border-dashed transition-colors">
|
|
102
|
+
<ImageIcon size={22} aria-hidden />
|
|
103
|
+
<span className="text-foreground text-[12.5px] font-semibold">No image selected</span>
|
|
104
|
+
<span className="text-[10.5px]">Choose one from the library</span>
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* The social block puts its preview rows here — between the frame and
|
|
110
|
+
the toolbar — so the share card and its upload target read as one
|
|
111
|
+
object rather than two stacked controls. */}
|
|
112
|
+
{children}
|
|
113
|
+
|
|
114
|
+
<div className="border-border flex items-center gap-2 border-t px-2.5 py-2">
|
|
115
|
+
{value ? (
|
|
116
|
+
<>
|
|
117
|
+
<span
|
|
118
|
+
className="text-foreground min-w-0 flex-1 truncate text-[11.5px] font-semibold"
|
|
119
|
+
title={name}
|
|
120
|
+
>
|
|
121
|
+
{name}
|
|
122
|
+
</span>
|
|
123
|
+
<span className="text-muted-foreground shrink-0 text-[10.5px] tabular-nums">
|
|
124
|
+
{formatBadge(value)}
|
|
125
|
+
</span>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={() => setPicking((v) => !v)}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
className="border-border hover:bg-accent shrink-0 rounded-md border px-2 py-1 text-[11px] font-medium disabled:opacity-50"
|
|
131
|
+
>
|
|
132
|
+
Replace
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={() => onChange('')}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
aria-label={`Remove ${label.toLowerCase()}`}
|
|
139
|
+
className="text-muted-foreground hover:text-destructive shrink-0 rounded p-1 disabled:opacity-50"
|
|
140
|
+
>
|
|
141
|
+
<Trash2 size={13} aria-hidden />
|
|
142
|
+
</button>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<span className="text-muted-foreground min-w-0 flex-1 text-[11.5px]">
|
|
147
|
+
No image selected
|
|
148
|
+
</span>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => setPicking((v) => !v)}
|
|
152
|
+
disabled={disabled}
|
|
153
|
+
className="border-border hover:bg-accent shrink-0 rounded-md border px-2 py-1 text-[11px] font-medium disabled:opacity-50"
|
|
154
|
+
>
|
|
155
|
+
Choose
|
|
156
|
+
</button>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{picking && (
|
|
162
|
+
<div className="border-border bg-muted max-h-[262px] overflow-y-auto border-t p-2">
|
|
163
|
+
{items.length === 0 ? (
|
|
164
|
+
<p className="text-muted-foreground px-1 py-3 text-center text-[11px]">
|
|
165
|
+
No images in the library yet.
|
|
166
|
+
</p>
|
|
167
|
+
) : (
|
|
168
|
+
<ul className="grid grid-cols-2 gap-2">
|
|
169
|
+
{items.map((item) => {
|
|
170
|
+
const url = item.url || item.storageKey
|
|
171
|
+
const selected = url === value
|
|
172
|
+
const itemRatio = ratioLabel(item.width, item.height)
|
|
173
|
+
return (
|
|
174
|
+
<li key={item.id}>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={() => {
|
|
178
|
+
onChange(url, item)
|
|
179
|
+
setPicking(false)
|
|
180
|
+
}}
|
|
181
|
+
aria-current={selected || undefined}
|
|
182
|
+
className={`border-border bg-card w-full overflow-hidden rounded-lg border text-left transition-colors ${
|
|
183
|
+
selected ? 'border-primary' : 'hover:border-ring'
|
|
184
|
+
}`}
|
|
185
|
+
>
|
|
186
|
+
<span
|
|
187
|
+
className="relative block w-full"
|
|
188
|
+
style={{
|
|
189
|
+
aspectRatio: '16 / 10',
|
|
190
|
+
backgroundImage: assetWash(item.filename),
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{url && <img src={url} alt="" className="h-full w-full object-cover" />}
|
|
194
|
+
<span className="absolute top-1 left-1 rounded-[5px] bg-white/80 px-1.5 py-px text-[9.5px] font-extrabold tracking-wide text-zinc-800">
|
|
195
|
+
{formatBadge(item.filename, item.mimeType)}
|
|
196
|
+
</span>
|
|
197
|
+
{selected && (
|
|
198
|
+
<span className="bg-primary text-primary-foreground absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full">
|
|
199
|
+
<Check size={10} aria-hidden />
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
</span>
|
|
203
|
+
<span className="block px-1.5 py-1">
|
|
204
|
+
<span className="text-foreground block truncate text-[10px] font-semibold">
|
|
205
|
+
{item.filename}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="text-muted-foreground block text-[9px] tabular-nums">
|
|
208
|
+
{item.width && item.height
|
|
209
|
+
? `${item.width} × ${item.height}${itemRatio ? ` · ${itemRatio}` : ''}`
|
|
210
|
+
: 'Dimensions unknown'}
|
|
211
|
+
</span>
|
|
212
|
+
</span>
|
|
213
|
+
</button>
|
|
214
|
+
</li>
|
|
215
|
+
)
|
|
216
|
+
})}
|
|
217
|
+
</ul>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from 'lucide-react'
|
|
4
|
+
import { BREAKPOINTS, getBreakpoint, type BreakpointId } from '../lib/breakpoints.js'
|
|
5
|
+
import type { ReflowGuardResult } from '../hooks/useReflowGuard.js'
|
|
6
|
+
|
|
7
|
+
export interface ReflowMeterProps {
|
|
8
|
+
guard: ReflowGuardResult
|
|
9
|
+
/** Character count of the current value. */
|
|
10
|
+
length: number
|
|
11
|
+
/** Breakpoint the canvas is currently showing — marked in the row of dots. */
|
|
12
|
+
device: BreakpointId
|
|
13
|
+
/** Switch the canvas to a breakpoint so the editor can look at the damage. */
|
|
14
|
+
onProof: (id: BreakpointId) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DOT_TONE: Record<string, string> = {
|
|
18
|
+
same: 'bg-success/12 text-success',
|
|
19
|
+
grew: 'bg-warning/15 text-warning',
|
|
20
|
+
'grew-badly': 'bg-destructive/12 text-destructive',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-field reflow readout: character count, line count where you're looking,
|
|
25
|
+
* one dot per breakpoint, and a proof prompt when wrapping changed.
|
|
26
|
+
*
|
|
27
|
+
* ── Why the dots are buttons ──────────────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* The guard's whole value is that it reports damage at a width you are NOT
|
|
30
|
+
* looking at. Telling an editor "this now runs three lines on mobile" while
|
|
31
|
+
* making them hunt for the breakpoint switcher wastes the finding. Each dot
|
|
32
|
+
* takes the canvas there.
|
|
33
|
+
*
|
|
34
|
+
* Colour is never the only signal: every dot carries its breakpoint's initials
|
|
35
|
+
* and an accessible name stating the line count and what it was published at,
|
|
36
|
+
* and the alert below repeats the finding in words.
|
|
37
|
+
*/
|
|
38
|
+
export function ReflowMeter({ guard, length, device, onProof }: ReflowMeterProps) {
|
|
39
|
+
const here = guard.rows.find((r) => r.id === device)
|
|
40
|
+
const worst = guard.worst ? guard.rows.find((r) => r.id === guard.worst) : undefined
|
|
41
|
+
|
|
42
|
+
// Named breakpoints while the list is short enough to read; initials once it
|
|
43
|
+
// isn't; "every breakpoint" when it's all of them, which is shorter and is
|
|
44
|
+
// also the more useful statement.
|
|
45
|
+
const grown = guard.grown
|
|
46
|
+
const scope =
|
|
47
|
+
grown.length === BREAKPOINTS.length
|
|
48
|
+
? 'every breakpoint'
|
|
49
|
+
: grown.length > 3
|
|
50
|
+
? grown.map((r) => getBreakpoint(r.id).abbr).join(', ')
|
|
51
|
+
: grown.map((r) => getBreakpoint(r.id).name).join(', ')
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
56
|
+
<span className="text-muted-foreground text-[11px] whitespace-nowrap tabular-nums">
|
|
57
|
+
<b className="text-foreground/80 font-semibold">{length}</b> chars
|
|
58
|
+
{guard.delta !== 0 && (
|
|
59
|
+
<span className={guard.delta > 0 ? 'text-warning font-bold' : ''}>
|
|
60
|
+
{' '}
|
|
61
|
+
({guard.delta > 0 ? '+' : ''}
|
|
62
|
+
{guard.delta})
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
{here && (
|
|
66
|
+
<>
|
|
67
|
+
{' · '}
|
|
68
|
+
<b className="text-foreground/80 font-semibold">{here.lines}</b> line
|
|
69
|
+
{here.lines === 1 ? '' : 's'} here
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</span>
|
|
73
|
+
|
|
74
|
+
{guard.rows.length > 0 && (
|
|
75
|
+
<div className="ml-auto flex gap-0.75">
|
|
76
|
+
{guard.rows.map((row) => {
|
|
77
|
+
const bp = getBreakpoint(row.id)
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
key={row.id}
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() => onProof(row.id)}
|
|
83
|
+
className={`flex h-4.25 w-4.25 items-center justify-center rounded-[5px] text-[8px] font-bold transition-transform hover:-translate-y-px ${
|
|
84
|
+
DOT_TONE[row.state] ?? 'bg-muted text-muted-foreground'
|
|
85
|
+
} ${device === row.id ? 'outline-primary outline-[1.5px] outline-offset-1' : ''}`}
|
|
86
|
+
title={`${bp.name} — ${row.lines} line${row.lines === 1 ? '' : 's'}${
|
|
87
|
+
row.diff > 0 ? ` (published: ${row.was}) — wrapping changed` : ''
|
|
88
|
+
}. Click to proof.`}
|
|
89
|
+
>
|
|
90
|
+
<span aria-hidden>{bp.abbr}</span>
|
|
91
|
+
{/* Leads with the breakpoint and its finding, not with the
|
|
92
|
+
action. The alert below carries a "Proof on ‹bp›" button,
|
|
93
|
+
and two controls with the same accessible name in one
|
|
94
|
+
region are indistinguishable to a screen reader. */}
|
|
95
|
+
<span className="sr-only">
|
|
96
|
+
{bp.name}: {row.lines} line{row.lines === 1 ? '' : 's'}
|
|
97
|
+
{row.diff > 0 ? `, was ${row.was} when published` : ''} — proof here
|
|
98
|
+
</span>
|
|
99
|
+
</button>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{worst && (
|
|
107
|
+
<div className="bg-warning/10 border-warning/30 mt-1.5 grid grid-cols-[auto_1fr] gap-x-2.5 gap-y-1 rounded-lg border p-2.5">
|
|
108
|
+
<AlertTriangle size={14} className="text-warning mt-px shrink-0" aria-hidden />
|
|
109
|
+
<p className="text-[11.5px] leading-normal">
|
|
110
|
+
Wrapping changed on <b className="text-warning font-bold">{scope}</b> —{' '}
|
|
111
|
+
{getBreakpoint(worst.id).name} now runs {worst.lines} lines instead of {worst.was}.
|
|
112
|
+
Proof it before publishing.
|
|
113
|
+
</p>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => onProof(worst.id)}
|
|
117
|
+
className="border-warning text-warning hover:bg-warning/15 col-start-2 mt-0.5 justify-self-start rounded-md border px-2.5 py-1 text-[11px] font-semibold whitespace-nowrap transition-colors"
|
|
118
|
+
>
|
|
119
|
+
Proof on {getBreakpoint(worst.id).name}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
)
|
|
125
|
+
}
|