@prenta/admin 0.92.0 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +663 -0
- package/design-system/tokens.css +173 -71
- package/design-system/tokens.json +244 -63
- package/design-system/tokens.scss +203 -98
- package/design-system/tokens.tailwind.cjs +93 -38
- package/dist/AdminRoot.d.ts +27 -6
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts +2 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js +51 -0
- package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts +2 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js +37 -0
- package/dist/__tests__/components/canvas-edit-tip.render.test.js.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts +2 -0
- package/dist/__tests__/components/editor-rail.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/editor-rail.render.test.js +138 -0
- package/dist/__tests__/components/editor-rail.render.test.js.map +1 -0
- package/dist/__tests__/components/media-block.render.test.d.ts +2 -0
- package/dist/__tests__/components/media-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/media-block.render.test.js +108 -0
- package/dist/__tests__/components/media-block.render.test.js.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts +2 -0
- package/dist/__tests__/components/page-audit-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js +59 -0
- package/dist/__tests__/components/page-audit-panel.render.test.js.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts +2 -0
- package/dist/__tests__/components/reflow-meter.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/reflow-meter.render.test.js +94 -0
- package/dist/__tests__/components/reflow-meter.render.test.js.map +1 -0
- package/dist/__tests__/components/seo-char-count.test.js +24 -6
- package/dist/__tests__/components/seo-char-count.test.js.map +1 -1
- package/dist/__tests__/components/serp-preview.render.test.d.ts +2 -0
- package/dist/__tests__/components/serp-preview.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/serp-preview.render.test.js +25 -0
- package/dist/__tests__/components/serp-preview.render.test.js.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts +2 -0
- package/dist/__tests__/lib/breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/lib/breakpoints.test.js +67 -0
- package/dist/__tests__/lib/breakpoints.test.js.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts +2 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.d.ts.map +1 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js +44 -0
- package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -0
- package/dist/__tests__/lib/media-format.test.d.ts +2 -0
- package/dist/__tests__/lib/media-format.test.d.ts.map +1 -0
- package/dist/__tests__/lib/media-format.test.js +70 -0
- package/dist/__tests__/lib/media-format.test.js.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts +2 -0
- package/dist/__tests__/lib/page-audit-input.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-audit-input.test.js +122 -0
- package/dist/__tests__/lib/page-audit-input.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +2 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -1
- package/dist/__tests__/lib/page-redirect.test.d.ts +2 -0
- package/dist/__tests__/lib/page-redirect.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-redirect.test.js +26 -0
- package/dist/__tests__/lib/page-redirect.test.js.map +1 -0
- package/dist/__tests__/lib/rail-width.test.d.ts +2 -0
- package/dist/__tests__/lib/rail-width.test.d.ts.map +1 -0
- package/dist/__tests__/lib/rail-width.test.js +69 -0
- package/dist/__tests__/lib/rail-width.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-baseline.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-baseline.test.js +56 -0
- package/dist/__tests__/lib/reflow-baseline.test.js.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts +2 -0
- package/dist/__tests__/lib/reflow-probe.test.d.ts.map +1 -0
- package/dist/__tests__/lib/reflow-probe.test.js +73 -0
- package/dist/__tests__/lib/reflow-probe.test.js.map +1 -0
- package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
- package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-diff.test.js +230 -0
- package/dist/__tests__/lib/version-diff.test.js.map +1 -0
- package/dist/__tests__/lib/version-history.test.d.ts +2 -0
- package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-history.test.js +125 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts +2 -0
- package/dist/__tests__/styles/canvas-container-queries.test.d.ts.map +1 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js +87 -0
- package/dist/__tests__/styles/canvas-container-queries.test.js.map +1 -0
- package/dist/__tests__/styles/packaging.test.d.ts +2 -0
- package/dist/__tests__/styles/packaging.test.d.ts.map +1 -0
- package/dist/__tests__/styles/packaging.test.js +31 -0
- package/dist/__tests__/styles/packaging.test.js.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.js +127 -0
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts +2 -0
- package/dist/__tests__/styles/tokens-drift.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.js +92 -0
- package/dist/__tests__/styles/tokens-drift.test.js.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts +2 -0
- package/dist/__tests__/views/admin-renderer-compat.test.d.ts.map +1 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js +43 -0
- package/dist/__tests__/views/admin-renderer-compat.test.js.map +1 -0
- package/dist/__tests__/views/admin-section-renderers.test.js +52 -4
- package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +30 -1
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/page-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-tab.render.test.js +151 -0
- package/dist/__tests__/views/page-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts +2 -0
- package/dist/__tests__/views/section-class-parity.test.d.ts.map +1 -0
- package/dist/__tests__/views/section-class-parity.test.js +81 -0
- package/dist/__tests__/views/section-class-parity.test.js.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts +2 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js +86 -0
- package/dist/__tests__/views/sections-panel-design-owned.render.test.js.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
- package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
- package/dist/components/BreakpointPicker.d.ts +18 -0
- package/dist/components/BreakpointPicker.d.ts.map +1 -0
- package/dist/components/BreakpointPicker.js +31 -0
- package/dist/components/BreakpointPicker.js.map +1 -0
- package/dist/components/CanvasEditTip.d.ts +6 -0
- package/dist/components/CanvasEditTip.d.ts.map +1 -0
- package/dist/components/CanvasEditTip.js +22 -0
- package/dist/components/CanvasEditTip.js.map +1 -0
- package/dist/components/EditorRail.d.ts +36 -0
- package/dist/components/EditorRail.d.ts.map +1 -0
- package/dist/components/EditorRail.js +104 -0
- package/dist/components/EditorRail.js.map +1 -0
- package/dist/components/EditorRailTabs.d.ts +32 -0
- package/dist/components/EditorRailTabs.d.ts.map +1 -0
- package/dist/components/EditorRailTabs.js +45 -0
- package/dist/components/EditorRailTabs.js.map +1 -0
- package/dist/components/MediaBlock.d.ts +49 -0
- package/dist/components/MediaBlock.d.ts.map +1 -0
- package/dist/components/MediaBlock.js +50 -0
- package/dist/components/MediaBlock.js.map +1 -0
- package/dist/components/ReflowMeter.d.ts +28 -0
- package/dist/components/ReflowMeter.d.ts.map +1 -0
- package/dist/components/ReflowMeter.js +42 -0
- package/dist/components/ReflowMeter.js.map +1 -0
- package/dist/components/ZoomChip.d.ts +17 -0
- package/dist/components/ZoomChip.d.ts.map +1 -0
- package/dist/components/ZoomChip.js +17 -0
- package/dist/components/ZoomChip.js.map +1 -0
- package/dist/components/breakpoint-icons.d.ts +3 -0
- package/dist/components/breakpoint-icons.d.ts.map +1 -0
- package/dist/components/breakpoint-icons.js +16 -0
- package/dist/components/breakpoint-icons.js.map +1 -0
- package/dist/components/page-tab/AuthorPicker.d.ts +31 -0
- package/dist/components/page-tab/AuthorPicker.d.ts.map +1 -0
- package/dist/components/page-tab/AuthorPicker.js +95 -0
- package/dist/components/page-tab/AuthorPicker.js.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts +38 -0
- package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -0
- package/dist/components/page-tab/DirectiveSwitch.js +33 -0
- package/dist/components/page-tab/DirectiveSwitch.js.map +1 -0
- package/dist/components/seo/EditorSeoScoreBadge.d.ts +8 -1
- package/dist/components/seo/EditorSeoScoreBadge.d.ts.map +1 -1
- package/dist/components/seo/EditorSeoScoreBadge.js +2 -2
- package/dist/components/seo/EditorSeoScoreBadge.js.map +1 -1
- package/dist/components/seo/PageAuditPanel.d.ts +14 -0
- package/dist/components/seo/PageAuditPanel.d.ts.map +1 -0
- package/dist/components/seo/PageAuditPanel.js +59 -0
- package/dist/components/seo/PageAuditPanel.js.map +1 -0
- package/dist/components/seo/SeoCharCount.d.ts +19 -7
- package/dist/components/seo/SeoCharCount.d.ts.map +1 -1
- package/dist/components/seo/SeoCharCount.js +30 -16
- package/dist/components/seo/SeoCharCount.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts +15 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +118 -21
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SerpPreview.d.ts +20 -0
- package/dist/components/seo/SerpPreview.d.ts.map +1 -0
- package/dist/components/seo/SerpPreview.js +6 -0
- package/dist/components/seo/SerpPreview.js.map +1 -0
- package/dist/hooks/usePageAudit.d.ts +41 -0
- package/dist/hooks/usePageAudit.d.ts.map +1 -0
- package/dist/hooks/usePageAudit.js +68 -0
- package/dist/hooks/usePageAudit.js.map +1 -0
- package/dist/hooks/useReflowGuard.d.ts +47 -0
- package/dist/hooks/useReflowGuard.d.ts.map +1 -0
- package/dist/hooks/useReflowGuard.js +52 -0
- package/dist/hooks/useReflowGuard.js.map +1 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
- package/dist/layout/primitives/AdminShell.js +2 -2
- package/dist/layout/primitives/AdminShell.js.map +1 -1
- package/dist/lib/breakpoints.d.ts +51 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +84 -0
- package/dist/lib/breakpoints.js.map +1 -0
- package/dist/lib/canvas-edit-chrome.d.ts +22 -0
- package/dist/lib/canvas-edit-chrome.d.ts.map +1 -0
- package/dist/lib/canvas-edit-chrome.js +81 -0
- package/dist/lib/canvas-edit-chrome.js.map +1 -0
- package/dist/lib/media-format.d.ts +55 -0
- package/dist/lib/media-format.d.ts.map +1 -0
- package/dist/lib/media-format.js +93 -0
- package/dist/lib/media-format.js.map +1 -0
- package/dist/lib/page-audit-input.d.ts +45 -0
- package/dist/lib/page-audit-input.d.ts.map +1 -0
- package/dist/lib/page-audit-input.js +53 -0
- package/dist/lib/page-audit-input.js.map +1 -0
- package/dist/lib/page-editor-service.d.ts +12 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +6 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/page-redirect.d.ts +39 -0
- package/dist/lib/page-redirect.d.ts.map +1 -0
- package/dist/lib/page-redirect.js +38 -0
- package/dist/lib/page-redirect.js.map +1 -0
- package/dist/lib/rail-width.d.ts +23 -0
- package/dist/lib/rail-width.d.ts.map +1 -0
- package/dist/lib/rail-width.js +59 -0
- package/dist/lib/rail-width.js.map +1 -0
- package/dist/lib/reflow-baseline.d.ts +45 -0
- package/dist/lib/reflow-baseline.d.ts.map +1 -0
- package/dist/lib/reflow-baseline.js +38 -0
- package/dist/lib/reflow-baseline.js.map +1 -0
- package/dist/lib/reflow-probe.d.ts +159 -0
- package/dist/lib/reflow-probe.d.ts.map +1 -0
- package/dist/lib/reflow-probe.js +228 -0
- package/dist/lib/reflow-probe.js.map +1 -0
- package/dist/lib/seo-service.d.ts +10 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/lib/version-diff.d.ts +50 -0
- package/dist/lib/version-diff.d.ts.map +1 -0
- package/dist/lib/version-diff.js +294 -0
- package/dist/lib/version-diff.js.map +1 -0
- package/dist/lib/version-history.d.ts +73 -0
- package/dist/lib/version-history.d.ts.map +1 -0
- package/dist/lib/version-history.js +126 -0
- package/dist/lib/version-history.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/CanvasMetaLine.d.ts +19 -0
- package/dist/views/page-editor/CanvasMetaLine.d.ts.map +1 -0
- package/dist/views/page-editor/CanvasMetaLine.js +23 -0
- package/dist/views/page-editor/CanvasMetaLine.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +47 -8
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +68 -34
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +21 -2
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +13 -20
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +383 -78
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageTab.d.ts +36 -0
- package/dist/views/page-editor/PageTab.d.ts.map +1 -0
- package/dist/views/page-editor/PageTab.js +65 -0
- package/dist/views/page-editor/PageTab.js.map +1 -0
- package/dist/views/page-editor/ReviewBar.d.ts +21 -0
- package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
- package/dist/views/page-editor/ReviewBar.js +22 -0
- package/dist/views/page-editor/ReviewBar.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +64 -20
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.d.ts +30 -5
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +38 -7
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.d.ts.map +1 -1
- package/dist/views/page-editor/SitePreviewFrame.js +3 -0
- package/dist/views/page-editor/SitePreviewFrame.js.map +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
- package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
- package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
- package/dist/views/page-editor/reflow-context.d.ts +28 -0
- package/dist/views/page-editor/reflow-context.d.ts.map +1 -0
- package/dist/views/page-editor/reflow-context.js +14 -0
- package/dist/views/page-editor/reflow-context.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/GallerySection.js +7 -3
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/MissionSection.js +1 -1
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js +1 -1
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js +1 -1
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +66 -26
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +47 -15
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.js +27 -15
- package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
- package/dist/views/page-editor/sections/parts.d.ts +0 -1
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
- package/dist/views/page-editor/sections/parts.js +41 -16
- package/dist/views/page-editor/sections/parts.js.map +1 -1
- package/dist/views/page-editor/viewports.d.ts +7 -14
- package/dist/views/page-editor/viewports.d.ts.map +1 -1
- package/dist/views/page-editor/viewports.js +7 -12
- package/dist/views/page-editor/viewports.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -1
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +5 -2
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +2 -20
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +4 -1
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +5 -3
- package/src/AdminRoot.tsx +27 -6
- package/src/__tests__/components/breakpoint-controls.render.test.tsx +57 -0
- package/src/__tests__/components/canvas-edit-tip.render.test.tsx +41 -0
- package/src/__tests__/components/editor-rail.render.test.tsx +192 -0
- package/src/__tests__/components/media-block.render.test.tsx +126 -0
- package/src/__tests__/components/page-audit-panel.render.test.tsx +64 -0
- package/src/__tests__/components/reflow-meter.render.test.tsx +144 -0
- package/src/__tests__/components/seo-char-count.test.tsx +26 -6
- package/src/__tests__/components/serp-preview.render.test.tsx +32 -0
- package/src/__tests__/lib/breakpoints.test.ts +84 -0
- package/src/__tests__/lib/canvas-edit-chrome.test.ts +56 -0
- package/src/__tests__/lib/media-format.test.ts +89 -0
- package/src/__tests__/lib/page-audit-input.test.ts +144 -0
- package/src/__tests__/lib/page-editor-service.test.ts +2 -0
- package/src/__tests__/lib/page-redirect.test.ts +29 -0
- package/src/__tests__/lib/rail-width.test.ts +86 -0
- package/src/__tests__/lib/reflow-baseline.test.ts +64 -0
- package/src/__tests__/lib/reflow-probe.test.ts +95 -0
- package/src/__tests__/lib/version-diff.test.ts +278 -0
- package/src/__tests__/lib/version-history.test.ts +166 -0
- package/src/__tests__/styles/canvas-container-queries.test.ts +103 -0
- package/src/__tests__/styles/packaging.test.ts +38 -0
- package/src/__tests__/styles/responsive-layer.test.ts +152 -0
- package/src/__tests__/styles/tokens-drift.test.ts +110 -0
- package/src/__tests__/views/admin-renderer-compat.test.ts +48 -0
- package/src/__tests__/views/admin-section-renderers.test.tsx +70 -4
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +43 -1
- package/src/__tests__/views/page-tab.render.test.tsx +226 -0
- package/src/__tests__/views/section-class-parity.test.ts +98 -0
- package/src/__tests__/views/sections-panel-design-owned.render.test.tsx +104 -0
- package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
- package/src/components/BreakpointPicker.tsx +114 -0
- package/src/components/CanvasEditTip.tsx +46 -0
- package/src/components/EditorRail.tsx +219 -0
- package/src/components/EditorRailTabs.tsx +117 -0
- package/src/components/MediaBlock.tsx +223 -0
- package/src/components/ReflowMeter.tsx +125 -0
- package/src/components/ZoomChip.tsx +39 -0
- package/src/components/breakpoint-icons.ts +17 -0
- package/src/components/page-tab/AuthorPicker.tsx +240 -0
- package/src/components/page-tab/DirectiveSwitch.tsx +89 -0
- package/src/components/seo/EditorSeoScoreBadge.tsx +9 -1
- package/src/components/seo/PageAuditPanel.tsx +192 -0
- package/src/components/seo/SeoCharCount.tsx +55 -24
- package/src/components/seo/SeoEditorPane.tsx +213 -31
- package/src/components/seo/SerpPreview.tsx +41 -0
- package/src/hooks/usePageAudit.ts +98 -0
- package/src/hooks/useReflowGuard.ts +96 -0
- package/src/layout/primitives/AdminShell.tsx +13 -1
- package/src/lib/breakpoints.ts +107 -0
- package/src/lib/canvas-edit-chrome.ts +82 -0
- package/src/lib/media-format.ts +99 -0
- package/src/lib/page-audit-input.ts +104 -0
- package/src/lib/page-editor-service.ts +18 -0
- package/src/lib/page-redirect.ts +60 -0
- package/src/lib/rail-width.ts +58 -0
- package/src/lib/reflow-baseline.ts +70 -0
- package/src/lib/reflow-probe.ts +299 -0
- package/src/lib/seo-service.ts +10 -0
- package/src/lib/version-diff.ts +376 -0
- package/src/lib/version-history.ts +192 -0
- package/src/styles/prenta-tokens.css +120 -42
- package/src/styles/responsive.css +281 -0
- package/src/styles/theme.css +138 -0
- package/src/views/page-editor/CanvasMetaLine.tsx +50 -0
- package/src/views/page-editor/EditorCanvas.tsx +140 -54
- package/src/views/page-editor/EditorTopBar.tsx +55 -51
- package/src/views/page-editor/PageSectionEditor.tsx +575 -185
- package/src/views/page-editor/PageTab.tsx +433 -0
- package/src/views/page-editor/ReviewBar.tsx +76 -0
- package/src/views/page-editor/SectionInspector.tsx +84 -42
- package/src/views/page-editor/SectionsPanel.tsx +282 -141
- package/src/views/page-editor/SitePreviewFrame.tsx +3 -0
- package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
- package/src/views/page-editor/reflow-context.ts +43 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +3 -2
- package/src/views/page-editor/sections/CtaBannerSection.tsx +3 -1
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -2
- package/src/views/page-editor/sections/GallerySection.tsx +7 -3
- package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
- package/src/views/page-editor/sections/MissionSection.tsx +4 -2
- package/src/views/page-editor/sections/PullQuoteSection.tsx +3 -1
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +2 -1
- package/src/views/page-editor/sections/SectionRenderer.tsx +115 -34
- package/src/views/page-editor/sections/admin-renderers.tsx +54 -15
- package/src/views/page-editor/sections/parts.tsx +46 -18
- package/src/views/page-editor/viewports.ts +13 -26
- package/src/views/post-editor/PostEditorCanvas.tsx +18 -11
- package/src/views/post-editor/PostSectionEditor.tsx +1 -56
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Zoom } from '../lib/breakpoints.js'
|
|
4
|
+
|
|
5
|
+
export interface ZoomChipProps {
|
|
6
|
+
/** Current zoom mode. */
|
|
7
|
+
zoom: Zoom
|
|
8
|
+
/** Actual rendered scale, 0–1 — what the canvas measured, not the mode. */
|
|
9
|
+
scale: number
|
|
10
|
+
onChange: (zoom: Zoom) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads out the canvas scale and toggles fit ↔ 100%.
|
|
15
|
+
*
|
|
16
|
+
* It shows the *measured* scale rather than the mode, because "fit" is not a
|
|
17
|
+
* number an editor can reason about — at 1920px in a narrow rail it might be
|
|
18
|
+
* 38%, and that is the fact that explains why the preview looks small.
|
|
19
|
+
*/
|
|
20
|
+
export function ZoomChip({ zoom, scale, onChange }: ZoomChipProps) {
|
|
21
|
+
const percent = Math.round(scale * 100)
|
|
22
|
+
const next: Zoom = zoom === 'fit' ? 1 : 'fit'
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => onChange(next)}
|
|
28
|
+
aria-label={
|
|
29
|
+
zoom === 'fit'
|
|
30
|
+
? `Canvas scaled to ${percent} percent to fit. Switch to actual size.`
|
|
31
|
+
: 'Canvas at actual size. Switch to fit.'
|
|
32
|
+
}
|
|
33
|
+
title={zoom === 'fit' ? 'Fit to window — click for 100%' : 'Actual size — click to fit'}
|
|
34
|
+
className="border-border bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground h-[34px] min-w-[54px] rounded-lg border px-2 text-[11.5px] font-semibold tabular-nums transition-colors"
|
|
35
|
+
>
|
|
36
|
+
{percent}%
|
|
37
|
+
</button>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Laptop, Monitor, Smartphone, Tablet, type LucideIcon } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps a breakpoint's `icon` name to its component. Kept out of
|
|
5
|
+
* `lib/breakpoints.ts` so that module stays free of React imports — it is pure
|
|
6
|
+
* data and arithmetic, and its tests run without a DOM.
|
|
7
|
+
*/
|
|
8
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
9
|
+
monitor: Monitor,
|
|
10
|
+
laptop: Laptop,
|
|
11
|
+
tablet: Tablet,
|
|
12
|
+
smartphone: Smartphone,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function breakpointIcon(name: string): LucideIcon {
|
|
16
|
+
return ICONS[name] ?? Monitor
|
|
17
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { ChevronDown, Globe } from 'lucide-react'
|
|
5
|
+
import { cmsApi } from '../../lib/api.js'
|
|
6
|
+
|
|
7
|
+
interface CmsUser {
|
|
8
|
+
id: string
|
|
9
|
+
name?: string | null
|
|
10
|
+
email?: string | null
|
|
11
|
+
role?: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AuthorValue {
|
|
15
|
+
/** Displayed byline. Empty means no byline is set. */
|
|
16
|
+
name: string
|
|
17
|
+
/** CMS user id, when the byline is one. Empty for generic and guest. */
|
|
18
|
+
id: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthorPickerProps {
|
|
22
|
+
value: AuthorValue
|
|
23
|
+
onChange: (next: AuthorValue) => void
|
|
24
|
+
/** Brand byline offered under GENERIC — usually the site or org name. */
|
|
25
|
+
brandName?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Byline picker: a CMS user, the brand, or a one-off guest name.
|
|
31
|
+
*
|
|
32
|
+
* ── What this does and does not change ────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* It sets the page's DISPLAYED author, which reaches the page's structured
|
|
35
|
+
* data. It does not reassign the document's `createdById` — that records who
|
|
36
|
+
* created the record, and trading an audit trail for a label is not a swap
|
|
37
|
+
* worth making silently. A page written by a staff editor and bylined to the
|
|
38
|
+
* brand is a normal thing; a page whose history says someone else made it is
|
|
39
|
+
* not.
|
|
40
|
+
*
|
|
41
|
+
* The menu renders inline below the trigger rather than floating. The rail is
|
|
42
|
+
* narrow and scrollable, and a floating menu inside a scroll container either
|
|
43
|
+
* clips or detaches from its trigger on scroll.
|
|
44
|
+
*/
|
|
45
|
+
export function AuthorPicker({ value, onChange, brandName, disabled = false }: AuthorPickerProps) {
|
|
46
|
+
const [open, setOpen] = useState(false)
|
|
47
|
+
const [users, setUsers] = useState<CmsUser[]>([])
|
|
48
|
+
const [guest, setGuest] = useState('')
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!open || users.length > 0) return
|
|
52
|
+
let cancelled = false
|
|
53
|
+
cmsApi<CmsUser[] | { users?: CmsUser[] }>('/users').then((res) => {
|
|
54
|
+
if (cancelled || !res.data) return
|
|
55
|
+
const list = Array.isArray(res.data) ? res.data : (res.data.users ?? [])
|
|
56
|
+
setUsers(list)
|
|
57
|
+
})
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true
|
|
60
|
+
}
|
|
61
|
+
}, [open, users.length])
|
|
62
|
+
|
|
63
|
+
const isGeneric = Boolean(brandName) && value.name === brandName && !value.id
|
|
64
|
+
const label = value.name || 'No byline'
|
|
65
|
+
const sub = value.id
|
|
66
|
+
? (users.find((u) => u.id === value.id)?.role ?? 'CMS user')
|
|
67
|
+
: isGeneric
|
|
68
|
+
? 'Brand byline'
|
|
69
|
+
: value.name
|
|
70
|
+
? 'Guest byline'
|
|
71
|
+
: 'None set'
|
|
72
|
+
|
|
73
|
+
function pick(next: AuthorValue) {
|
|
74
|
+
onChange(next)
|
|
75
|
+
setOpen(false)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() => setOpen((v) => !v)}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
aria-expanded={open}
|
|
85
|
+
className="border-border bg-card hover:border-ring flex w-full items-center gap-2.5 rounded-[7px] border-[1.5px] px-2.5 py-[7px] text-left transition-colors disabled:opacity-50"
|
|
86
|
+
>
|
|
87
|
+
<Avatar name={value.name} generic={isGeneric} />
|
|
88
|
+
<span className="min-w-0 flex-1">
|
|
89
|
+
<span className="text-foreground block truncate text-[12.5px] font-semibold">
|
|
90
|
+
{label}
|
|
91
|
+
</span>
|
|
92
|
+
<span className="text-muted-foreground block text-[10.5px]">{sub}</span>
|
|
93
|
+
</span>
|
|
94
|
+
<ChevronDown
|
|
95
|
+
size={14}
|
|
96
|
+
aria-hidden
|
|
97
|
+
className={`text-muted-foreground shrink-0 motion-safe:transition-transform ${open ? 'rotate-180' : ''}`}
|
|
98
|
+
/>
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
{open && (
|
|
102
|
+
<div className="border-border mt-1.5 space-y-2 rounded-[9px] border p-2">
|
|
103
|
+
<Group label="CMS users">
|
|
104
|
+
{users.length === 0 ? (
|
|
105
|
+
<p className="text-muted-foreground px-1 py-1.5 text-[11px]">Loading…</p>
|
|
106
|
+
) : (
|
|
107
|
+
users.map((u) => {
|
|
108
|
+
const name = u.name || u.email || 'Unnamed user'
|
|
109
|
+
return (
|
|
110
|
+
<Row
|
|
111
|
+
key={u.id}
|
|
112
|
+
selected={value.id === u.id}
|
|
113
|
+
onClick={() => pick({ name, id: u.id })}
|
|
114
|
+
>
|
|
115
|
+
<Avatar name={name} />
|
|
116
|
+
<span className="min-w-0">
|
|
117
|
+
<span className="block truncate text-[12px] font-medium">{name}</span>
|
|
118
|
+
<span className="text-muted-foreground block text-[10px]">
|
|
119
|
+
{u.role ?? 'Author'}
|
|
120
|
+
</span>
|
|
121
|
+
</span>
|
|
122
|
+
</Row>
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
)}
|
|
126
|
+
</Group>
|
|
127
|
+
|
|
128
|
+
{brandName && (
|
|
129
|
+
<Group label="Generic">
|
|
130
|
+
<Row selected={isGeneric} onClick={() => pick({ name: brandName, id: '' })}>
|
|
131
|
+
<Avatar name={brandName} generic />
|
|
132
|
+
<span className="text-[12px] font-medium">{brandName}</span>
|
|
133
|
+
</Row>
|
|
134
|
+
</Group>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
<Group label="Guest byline">
|
|
138
|
+
<div className="flex gap-1.5 px-1">
|
|
139
|
+
<input
|
|
140
|
+
value={guest}
|
|
141
|
+
onChange={(e) => setGuest(e.target.value)}
|
|
142
|
+
placeholder="Name to display"
|
|
143
|
+
aria-label="Guest byline name"
|
|
144
|
+
className="border-border bg-input-background focus-visible:border-ring min-w-0 flex-1 rounded-md border px-2 py-1 text-[11.5px] focus-visible:outline-none"
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
// Disabled until non-empty: an empty guest byline would clear
|
|
149
|
+
// the author while looking like it set one.
|
|
150
|
+
disabled={guest.trim().length === 0}
|
|
151
|
+
onClick={() => {
|
|
152
|
+
pick({ name: guest.trim(), id: '' })
|
|
153
|
+
setGuest('')
|
|
154
|
+
}}
|
|
155
|
+
className="bg-primary text-primary-foreground rounded-md px-2 py-1 text-[11px] font-semibold disabled:opacity-40"
|
|
156
|
+
>
|
|
157
|
+
Add
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</Group>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Group({ label, children }: { label: string; children: React.ReactNode }) {
|
|
168
|
+
return (
|
|
169
|
+
<div>
|
|
170
|
+
<p className="text-muted-foreground px-1 pb-1 text-[9.5px] font-bold tracking-wide uppercase">
|
|
171
|
+
{label}
|
|
172
|
+
</p>
|
|
173
|
+
<div className="space-y-0.5">{children}</div>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function Row({
|
|
179
|
+
selected,
|
|
180
|
+
onClick,
|
|
181
|
+
children,
|
|
182
|
+
}: {
|
|
183
|
+
selected: boolean
|
|
184
|
+
onClick: () => void
|
|
185
|
+
children: React.ReactNode
|
|
186
|
+
}) {
|
|
187
|
+
return (
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={onClick}
|
|
191
|
+
aria-current={selected || undefined}
|
|
192
|
+
className={`flex w-full items-center gap-2 rounded-md px-1.5 py-1.5 text-left transition-colors ${
|
|
193
|
+
selected ? 'bg-accent' : 'hover:bg-accent/60'
|
|
194
|
+
}`}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</button>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Initials on a tint derived from the name, so the same person is the same
|
|
203
|
+
* colour everywhere without storing one. A globe replaces initials for the
|
|
204
|
+
* brand byline — "Ex" for "Example Inc" reads as a person, which is the one
|
|
205
|
+
* thing the generic option is not.
|
|
206
|
+
*/
|
|
207
|
+
function Avatar({ name, generic = false }: { name: string; generic?: boolean }) {
|
|
208
|
+
if (generic) {
|
|
209
|
+
return (
|
|
210
|
+
<span className="bg-muted text-muted-foreground flex h-[27px] w-[27px] shrink-0 items-center justify-center rounded-full">
|
|
211
|
+
<Globe size={14} aria-hidden />
|
|
212
|
+
</span>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const initials =
|
|
217
|
+
name
|
|
218
|
+
.trim()
|
|
219
|
+
.split(/\s+/)
|
|
220
|
+
.slice(0, 2)
|
|
221
|
+
.map((w) => w[0] ?? '')
|
|
222
|
+
.join('')
|
|
223
|
+
.toUpperCase() || '—'
|
|
224
|
+
|
|
225
|
+
let hash = 0
|
|
226
|
+
for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) % 360
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<span
|
|
230
|
+
aria-hidden
|
|
231
|
+
className="flex h-[27px] w-[27px] shrink-0 items-center justify-center rounded-full text-[10px] font-bold"
|
|
232
|
+
style={{
|
|
233
|
+
backgroundColor: `oklch(0.92 0.05 ${hash})`,
|
|
234
|
+
color: `oklch(0.42 0.12 ${hash})`,
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
{initials}
|
|
238
|
+
</span>
|
|
239
|
+
)
|
|
240
|
+
}
|