@actuate-media/cms-admin 0.15.0 → 0.16.1
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/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +148 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
- package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
- package/dist/__tests__/components/posts-badges.test.js +68 -0
- package/dist/__tests__/components/posts-badges.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +137 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service.test.js +140 -0
- package/dist/__tests__/lib/pages-service.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +185 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
- package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/posts-service.test.js +244 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -0
- package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
- package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
- package/dist/__tests__/lib/useApiData.test.js +84 -0
- package/dist/__tests__/lib/useApiData.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.d.ts +2 -0
- package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +138 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.js +98 -0
- package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/pages/tag-badges.d.ts +21 -0
- package/dist/components/pages/tag-badges.d.ts.map +1 -0
- package/dist/components/pages/tag-badges.js +56 -0
- package/dist/components/pages/tag-badges.js.map +1 -0
- package/dist/components/posts/badges.d.ts +25 -0
- package/dist/components/posts/badges.d.ts.map +1 -0
- package/dist/components/posts/badges.js +123 -0
- package/dist/components/posts/badges.js.map +1 -0
- package/dist/components/posts/icons.d.ts +12 -0
- package/dist/components/posts/icons.d.ts.map +1 -0
- package/dist/components/posts/icons.js +26 -0
- package/dist/components/posts/icons.js.map +1 -0
- package/dist/layout/Layout.js +36 -8
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +9 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +100 -48
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +45 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -0
- package/dist/lib/page-editor-service.js +153 -0
- package/dist/lib/page-editor-service.js.map +1 -0
- package/dist/lib/pages-service.d.ts +143 -0
- package/dist/lib/pages-service.d.ts.map +1 -0
- package/dist/lib/pages-service.js +444 -0
- package/dist/lib/pages-service.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +57 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -0
- package/dist/lib/post-editor-service.js +234 -0
- package/dist/lib/post-editor-service.js.map +1 -0
- package/dist/lib/posts-service.d.ts +73 -0
- package/dist/lib/posts-service.d.ts.map +1 -0
- package/dist/lib/posts-service.js +245 -0
- package/dist/lib/posts-service.js.map +1 -0
- package/dist/lib/preview-link.d.ts +32 -0
- package/dist/lib/preview-link.d.ts.map +1 -0
- package/dist/lib/preview-link.js +40 -0
- package/dist/lib/preview-link.js.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +9 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +64 -20
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts +14 -0
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
- package/dist/views/Pages/PageTagManager.js +145 -0
- package/dist/views/Pages/PageTagManager.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts +18 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -0
- package/dist/views/Pages/PagesListView.js +287 -0
- package/dist/views/Pages/PagesListView.js.map +1 -0
- package/dist/views/Posts/NewPostPicker.d.ts +16 -0
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
- package/dist/views/Posts/NewPostPicker.js +59 -0
- package/dist/views/Posts/NewPostPicker.js.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +26 -0
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
- package/dist/views/Posts/PostTypesView.js +52 -0
- package/dist/views/Posts/PostTypesView.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts +25 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -0
- package/dist/views/Posts/PostsListView.js +239 -0
- package/dist/views/Posts/PostsListView.js.map +1 -0
- package/dist/views/Posts/post-type-presets.d.ts +32 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
- package/dist/views/Posts/post-type-presets.js +72 -0
- package/dist/views/Posts/post-type-presets.js.map +1 -0
- package/dist/views/page-builder/Widgets.d.ts +13 -0
- package/dist/views/page-builder/Widgets.d.ts.map +1 -0
- package/dist/views/page-builder/Widgets.js +15 -0
- package/dist/views/page-builder/Widgets.js.map +1 -0
- package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
- package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
- package/dist/views/page-editor/AddSectionModal.js +22 -0
- package/dist/views/page-editor/AddSectionModal.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
- package/dist/views/page-editor/EditorCanvas.js +64 -0
- package/dist/views/page-editor/EditorCanvas.js.map +1 -0
- package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
- package/dist/views/page-editor/EditorTopBar.js +42 -0
- package/dist/views/page-editor/EditorTopBar.js.map +1 -0
- package/dist/views/page-editor/PagePreview.d.ts +13 -0
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
- package/dist/views/page-editor/PagePreview.js +37 -0
- package/dist/views/page-editor/PagePreview.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.js +272 -0
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.js +52 -0
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts +17 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
- package/dist/views/page-editor/SectionInspector.js +98 -0
- package/dist/views/page-editor/SectionInspector.js.map +1 -0
- package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
- package/dist/views/page-editor/SectionsPanel.js +43 -0
- package/dist/views/page-editor/SectionsPanel.js.map +1 -0
- package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
- package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
- package/dist/views/page-editor/ValidationSummary.js +10 -0
- package/dist/views/page-editor/ValidationSummary.js.map +1 -0
- package/dist/views/page-editor/section-icons.d.ts +4 -0
- package/dist/views/page-editor/section-icons.d.ts.map +1 -0
- package/dist/views/page-editor/section-icons.js +20 -0
- package/dist/views/page-editor/section-icons.js.map +1 -0
- package/dist/views/page-editor/section-types.d.ts +11 -0
- package/dist/views/page-editor/section-types.d.ts.map +1 -0
- package/dist/views/page-editor/section-types.js +10 -0
- package/dist/views/page-editor/section-types.js.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.js +17 -0
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.js +12 -0
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +14 -0
- package/dist/views/page-editor/sections/index.d.ts.map +1 -0
- package/dist/views/page-editor/sections/index.js +12 -0
- package/dist/views/page-editor/sections/index.js.map +1 -0
- package/dist/views/page-editor/sections/parts.d.ts +55 -0
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
- package/dist/views/page-editor/sections/parts.js +60 -0
- package/dist/views/page-editor/sections/parts.js.map +1 -0
- package/dist/views/page-editor/viewports.d.ts +14 -0
- package/dist/views/page-editor/viewports.d.ts.map +1 -0
- package/dist/views/page-editor/viewports.js +12 -0
- package/dist/views/page-editor/viewports.js.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.js +58 -0
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.js +53 -0
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
- package/dist/views/post-editor/PostHeader.d.ts +15 -0
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeader.js +27 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.js +21 -0
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
- package/dist/views/post-editor/PostPreview.d.ts +14 -0
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
- package/dist/views/post-editor/PostPreview.js +53 -0
- package/dist/views/post-editor/PostPreview.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.js +290 -0
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.js +144 -0
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +214 -2
- package/src/__tests__/components/posts-badges.test.tsx +77 -0
- package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
- package/src/__tests__/lib/page-editor-service.test.ts +173 -0
- package/src/__tests__/lib/pages-service.test.ts +157 -0
- package/src/__tests__/lib/post-editor-service.test.ts +215 -0
- package/src/__tests__/lib/posts-service.test.ts +279 -0
- package/src/__tests__/lib/useApiData.test.ts +105 -0
- package/src/__tests__/views/dashboard.test.tsx +180 -0
- package/src/__tests__/views/posts-list-view.test.ts +110 -0
- package/src/components/pages/tag-badges.tsx +104 -0
- package/src/components/posts/badges.tsx +170 -0
- package/src/components/posts/icons.ts +40 -0
- package/src/layout/Layout.tsx +66 -10
- package/src/layout/Sidebar.tsx +116 -50
- package/src/lib/page-editor-service.ts +233 -0
- package/src/lib/pages-service.ts +599 -0
- package/src/lib/post-editor-service.ts +342 -0
- package/src/lib/posts-service.ts +366 -0
- package/src/lib/preview-link.ts +63 -0
- package/src/router/index.ts +10 -1
- package/src/views/Dashboard.tsx +142 -89
- package/src/views/Pages/PageTagManager.tsx +436 -0
- package/src/views/Pages/PagesListView.tsx +944 -0
- package/src/views/Posts/NewPostPicker.tsx +147 -0
- package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
- package/src/views/Posts/PostTypesView.tsx +194 -0
- package/src/views/Posts/PostsListView.tsx +869 -0
- package/src/views/Posts/post-type-presets.ts +105 -0
- package/src/views/page-builder/Widgets.tsx +44 -0
- package/src/views/page-editor/AddSectionModal.tsx +56 -0
- package/src/views/page-editor/EditorCanvas.tsx +127 -0
- package/src/views/page-editor/EditorTopBar.tsx +233 -0
- package/src/views/page-editor/PagePreview.tsx +96 -0
- package/src/views/page-editor/PageSectionEditor.tsx +476 -0
- package/src/views/page-editor/PageSettingsModal.tsx +154 -0
- package/src/views/page-editor/SectionInspector.tsx +599 -0
- package/src/views/page-editor/SectionsPanel.tsx +270 -0
- package/src/views/page-editor/ValidationSummary.tsx +53 -0
- package/src/views/page-editor/section-icons.ts +33 -0
- package/src/views/page-editor/section-types.ts +27 -0
- package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
- package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
- package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
- package/src/views/page-editor/sections/GallerySection.tsx +54 -0
- package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
- package/src/views/page-editor/sections/MissionSection.tsx +22 -0
- package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
- package/src/views/page-editor/sections/index.ts +13 -0
- package/src/views/page-editor/sections/parts.tsx +113 -0
- package/src/views/page-editor/viewports.ts +25 -0
- package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
- package/src/views/post-editor/PostFieldsModal.tsx +196 -0
- package/src/views/post-editor/PostHeader.tsx +87 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
- package/src/views/post-editor/PostPreview.tsx +114 -0
- package/src/views/post-editor/PostSectionEditor.tsx +506 -0
- package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PostTypeIconName } from '../../components/posts/icons.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Quick Post Type presets — curated starter templates surfaced on the
|
|
5
|
+
* New Post Type screen. Picking one pre-fills the form (slug, labels,
|
|
6
|
+
* icon, accent color, description) AND a tailored set of extra fields
|
|
7
|
+
* that get injected into the generated `actuate.config.ts` snippet
|
|
8
|
+
* after the shared base fields (title / slug / body / SEO / …).
|
|
9
|
+
*
|
|
10
|
+
* Presets are NOT a runtime data model — they only seed the snippet.
|
|
11
|
+
* Collections stay code-defined; this just saves the developer from
|
|
12
|
+
* hand-authoring common field sets.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type PresetColor = 'purple' | 'green' | 'orange' | 'blue' | 'red' | 'pink'
|
|
16
|
+
|
|
17
|
+
export interface QuickPreset {
|
|
18
|
+
key: string
|
|
19
|
+
/** Gallery card label, e.g. "Case Studies". */
|
|
20
|
+
label: string
|
|
21
|
+
singular: string
|
|
22
|
+
plural: string
|
|
23
|
+
slug: string
|
|
24
|
+
description: string
|
|
25
|
+
color: PresetColor
|
|
26
|
+
icon: PostTypeIconName
|
|
27
|
+
/**
|
|
28
|
+
* Field definition lines injected after the shared base fields.
|
|
29
|
+
* Each entry is a complete `key: { … },` line (no leading indent —
|
|
30
|
+
* the snippet generator indents them).
|
|
31
|
+
*/
|
|
32
|
+
extraFields: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const POST_TYPE_PRESETS: QuickPreset[] = [
|
|
36
|
+
{
|
|
37
|
+
key: 'case-studies',
|
|
38
|
+
label: 'Case Studies',
|
|
39
|
+
singular: 'Case Study',
|
|
40
|
+
plural: 'Case Studies',
|
|
41
|
+
slug: 'case-studies',
|
|
42
|
+
description: 'Client success stories and detailed project case studies.',
|
|
43
|
+
color: 'green',
|
|
44
|
+
icon: 'briefcase',
|
|
45
|
+
extraFields: [
|
|
46
|
+
"client: { type: 'text', label: 'Client Name' },",
|
|
47
|
+
"industry: { type: 'text', label: 'Industry' },",
|
|
48
|
+
"results: { type: 'richText', label: 'Results & Outcomes' },",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'news',
|
|
53
|
+
label: 'News',
|
|
54
|
+
singular: 'News Item',
|
|
55
|
+
plural: 'News',
|
|
56
|
+
slug: 'news',
|
|
57
|
+
description: 'Company announcements, press releases, and industry news.',
|
|
58
|
+
color: 'orange',
|
|
59
|
+
icon: 'newspaper',
|
|
60
|
+
extraFields: ["source: { type: 'text', label: 'Source' },"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: 'portfolio',
|
|
64
|
+
label: 'Portfolio',
|
|
65
|
+
singular: 'Portfolio Item',
|
|
66
|
+
plural: 'Portfolio',
|
|
67
|
+
slug: 'portfolio',
|
|
68
|
+
description: 'Showcase projects, work samples, and creative pieces.',
|
|
69
|
+
color: 'blue',
|
|
70
|
+
icon: 'image',
|
|
71
|
+
extraFields: [
|
|
72
|
+
"projectUrl: { type: 'url', label: 'Project URL' },",
|
|
73
|
+
"gallery: { type: 'media', label: 'Gallery' },",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'events',
|
|
78
|
+
label: 'Events',
|
|
79
|
+
singular: 'Event',
|
|
80
|
+
plural: 'Events',
|
|
81
|
+
slug: 'events',
|
|
82
|
+
description: 'Webinars, conferences, and scheduled happenings.',
|
|
83
|
+
color: 'pink',
|
|
84
|
+
icon: 'calendar',
|
|
85
|
+
extraFields: [
|
|
86
|
+
"startDate: { type: 'date', label: 'Start Date', includeTime: true },",
|
|
87
|
+
"endDate: { type: 'date', label: 'End Date', includeTime: true },",
|
|
88
|
+
"location: { type: 'text', label: 'Location' },",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'team',
|
|
93
|
+
label: 'Team Members',
|
|
94
|
+
singular: 'Team Member',
|
|
95
|
+
plural: 'Team',
|
|
96
|
+
slug: 'team',
|
|
97
|
+
description: 'Staff bios, headshots, and role information.',
|
|
98
|
+
color: 'purple',
|
|
99
|
+
icon: 'users',
|
|
100
|
+
extraFields: [
|
|
101
|
+
"role: { type: 'text', label: 'Role' },",
|
|
102
|
+
"headshot: { type: 'media', label: 'Headshot' },",
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { LayoutGrid } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
export interface WidgetsProps {
|
|
6
|
+
onNavigate?: (path: string) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* /widgets — placeholder for the embeddable Widgets library.
|
|
11
|
+
*
|
|
12
|
+
* Widgets are reusable, embeddable components (forms, CTAs, banners)
|
|
13
|
+
* that can be dropped into pages and saved sections. The management UI
|
|
14
|
+
* ships in a later release; this view establishes the route and nav
|
|
15
|
+
* entry with an honest empty state rather than a dead link.
|
|
16
|
+
*/
|
|
17
|
+
export function Widgets({ onNavigate }: WidgetsProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="p-4 pr-8">
|
|
20
|
+
<div className="mb-4">
|
|
21
|
+
<h1 className="text-foreground mb-1 text-2xl font-medium">Widgets</h1>
|
|
22
|
+
<p className="text-muted-foreground text-sm">
|
|
23
|
+
Reusable, embeddable components for pages and saved sections.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="border-border bg-card rounded-lg border p-8 text-center">
|
|
28
|
+
<LayoutGrid className="text-muted-foreground mx-auto mb-3 h-8 w-8" aria-hidden />
|
|
29
|
+
<h2 className="text-foreground mb-1 text-lg font-medium">Widgets are coming soon</h2>
|
|
30
|
+
<p className="text-muted-foreground mx-auto mb-4 max-w-md text-sm">
|
|
31
|
+
The widget library lets you build embeddable components once and reuse them across pages.
|
|
32
|
+
In the meantime, use Saved Sections for reusable content blocks.
|
|
33
|
+
</p>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => onNavigate?.('/saved-sections')}
|
|
37
|
+
className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm font-medium hover:opacity-90"
|
|
38
|
+
>
|
|
39
|
+
View Saved Sections
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Modal } from '../../components/ui/Modal.js'
|
|
4
|
+
import { getSectionTypesForScope, type SectionScope, type SectionTypeId } from './section-types.js'
|
|
5
|
+
import { sectionIconFor } from './section-icons.js'
|
|
6
|
+
|
|
7
|
+
interface AddSectionModalProps {
|
|
8
|
+
open: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
onAdd: (type: SectionTypeId) => void
|
|
11
|
+
/** Which editor surface this gallery serves. Defaults to the page editor. */
|
|
12
|
+
scope?: SectionScope
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Predefined section gallery. Only types that have a working renderer +
|
|
17
|
+
* inspector are listed (the registry is the source of truth), so every
|
|
18
|
+
* card here actually inserts a usable section.
|
|
19
|
+
*
|
|
20
|
+
* (Saved-section / template insertion is a separate flow — deferred until
|
|
21
|
+
* the saved-section storage model is reconciled with the flat section
|
|
22
|
+
* model; see the page-editor changeset.)
|
|
23
|
+
*/
|
|
24
|
+
export function AddSectionModal({ open, onClose, onAdd, scope = 'page' }: AddSectionModalProps) {
|
|
25
|
+
const types = getSectionTypesForScope(scope)
|
|
26
|
+
return (
|
|
27
|
+
<Modal open={open} onClose={onClose} title="Add a section">
|
|
28
|
+
<p className="text-muted-foreground mb-4 text-sm">
|
|
29
|
+
Choose a section type. It will be added to the bottom and selected for editing.
|
|
30
|
+
</p>
|
|
31
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
32
|
+
{types.map((type) => {
|
|
33
|
+
const Icon = sectionIconFor(type.id)
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={type.id}
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => onAdd(type.id)}
|
|
39
|
+
className="border-border hover:border-primary hover:bg-accent/50 focus-visible:ring-ring flex items-start gap-3 rounded-lg border p-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
40
|
+
>
|
|
41
|
+
<span className="bg-muted text-foreground flex h-9 w-9 shrink-0 items-center justify-center rounded-md">
|
|
42
|
+
<Icon className="h-5 w-5" aria-hidden />
|
|
43
|
+
</span>
|
|
44
|
+
<span className="min-w-0">
|
|
45
|
+
<span className="text-foreground block text-sm font-medium">{type.name}</span>
|
|
46
|
+
<span className="text-muted-foreground mt-0.5 block text-xs leading-snug">
|
|
47
|
+
{type.description}
|
|
48
|
+
</span>
|
|
49
|
+
</span>
|
|
50
|
+
</button>
|
|
51
|
+
)
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
</Modal>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
+
import { LayoutTemplate, Plus } from 'lucide-react'
|
|
5
|
+
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
6
|
+
import { SectionRenderer } from './sections/index.js'
|
|
7
|
+
import { getViewport, type ViewportId } from './viewports.js'
|
|
8
|
+
|
|
9
|
+
interface EditorCanvasProps {
|
|
10
|
+
sections: PageSection[]
|
|
11
|
+
selectedId: string | null
|
|
12
|
+
viewport: ViewportId
|
|
13
|
+
onSelect: (id: string) => void
|
|
14
|
+
onAddSection: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The preview canvas. Renders the page at the selected viewport's logical
|
|
19
|
+
* width and scales it down with a CSS transform so wide viewports (1440px,
|
|
20
|
+
* 1920px) fit inside the available canvas column. Scale is recomputed from
|
|
21
|
+
* a ResizeObserver so it stays correct as panels open/close.
|
|
22
|
+
*/
|
|
23
|
+
export function EditorCanvas({
|
|
24
|
+
sections,
|
|
25
|
+
selectedId,
|
|
26
|
+
viewport,
|
|
27
|
+
onSelect,
|
|
28
|
+
onAddSection,
|
|
29
|
+
}: EditorCanvasProps) {
|
|
30
|
+
const vp = getViewport(viewport)
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
32
|
+
const surfaceRef = useRef<HTMLDivElement>(null)
|
|
33
|
+
const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
|
|
34
|
+
const [scale, setScale] = useState(1)
|
|
35
|
+
const [surfaceHeight, setSurfaceHeight] = useState<number | null>(null)
|
|
36
|
+
|
|
37
|
+
const registerRef = useCallback((id: string, el: HTMLElement | null) => {
|
|
38
|
+
if (el) sectionRefs.current.set(id, el)
|
|
39
|
+
else sectionRefs.current.delete(id)
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
// Recompute scale + scaled height from the container width.
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
const container = containerRef.current
|
|
45
|
+
if (!container) return
|
|
46
|
+
|
|
47
|
+
const recompute = () => {
|
|
48
|
+
// 48px breathing room so the surface never kisses the column edges.
|
|
49
|
+
const available = container.clientWidth - 48
|
|
50
|
+
const next = Math.min(1, available / vp.width)
|
|
51
|
+
setScale(next > 0 ? next : 1)
|
|
52
|
+
if (surfaceRef.current) {
|
|
53
|
+
setSurfaceHeight(surfaceRef.current.offsetHeight * (next > 0 ? next : 1))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
recompute()
|
|
58
|
+
const ro = new ResizeObserver(recompute)
|
|
59
|
+
ro.observe(container)
|
|
60
|
+
if (surfaceRef.current) ro.observe(surfaceRef.current)
|
|
61
|
+
return () => ro.disconnect()
|
|
62
|
+
}, [vp.width, sections])
|
|
63
|
+
|
|
64
|
+
// Scroll the selected section into view within the canvas.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!selectedId) return
|
|
67
|
+
const el = sectionRefs.current.get(selectedId)
|
|
68
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
69
|
+
}, [selectedId])
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div ref={containerRef} className="bg-muted h-full overflow-auto p-6">
|
|
73
|
+
{sections.length === 0 ? (
|
|
74
|
+
<EmptyCanvas onAddSection={onAddSection} />
|
|
75
|
+
) : (
|
|
76
|
+
<div
|
|
77
|
+
className="mx-auto"
|
|
78
|
+
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
ref={surfaceRef}
|
|
82
|
+
className="border-border origin-top overflow-hidden rounded-xl border bg-white shadow-sm"
|
|
83
|
+
style={{
|
|
84
|
+
width: vp.width,
|
|
85
|
+
transform: `scale(${scale})`,
|
|
86
|
+
transformOrigin: 'top left',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{sections.map((section) => (
|
|
90
|
+
<SectionRenderer
|
|
91
|
+
key={section.id}
|
|
92
|
+
section={section}
|
|
93
|
+
editable
|
|
94
|
+
selected={section.id === selectedId}
|
|
95
|
+
onSelect={onSelect}
|
|
96
|
+
registerRef={registerRef}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function EmptyCanvas({ onAddSection }: { onAddSection: () => void }) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="mx-auto flex h-full max-w-md flex-col items-center justify-center text-center">
|
|
109
|
+
<div className="bg-card border-border mb-4 flex h-14 w-14 items-center justify-center rounded-full border">
|
|
110
|
+
<LayoutTemplate className="text-muted-foreground h-6 w-6" aria-hidden />
|
|
111
|
+
</div>
|
|
112
|
+
<h2 className="text-foreground text-lg font-medium">This page has no sections yet</h2>
|
|
113
|
+
<p className="text-muted-foreground mt-1 mb-5 text-sm">
|
|
114
|
+
Add your first section to start building the page. You can reorder, hide, and edit sections
|
|
115
|
+
at any time.
|
|
116
|
+
</p>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={onAddSection}
|
|
120
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
121
|
+
>
|
|
122
|
+
<Plus className="h-4 w-4" aria-hidden />
|
|
123
|
+
Add Section
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
ExternalLink,
|
|
6
|
+
Eye,
|
|
7
|
+
Laptop,
|
|
8
|
+
Loader2,
|
|
9
|
+
type LucideIcon,
|
|
10
|
+
Monitor,
|
|
11
|
+
Settings2,
|
|
12
|
+
Smartphone,
|
|
13
|
+
Tablet,
|
|
14
|
+
TriangleAlert,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import type { PageStatus } from '../../lib/page-editor-service.js'
|
|
17
|
+
import { VIEWPORTS, type ViewportId } from './viewports.js'
|
|
18
|
+
|
|
19
|
+
export type SaveState = 'idle' | 'saving' | 'saved' | 'error'
|
|
20
|
+
|
|
21
|
+
interface EditorTopBarProps {
|
|
22
|
+
title: string
|
|
23
|
+
status: PageStatus
|
|
24
|
+
dirty: boolean
|
|
25
|
+
saveState: SaveState
|
|
26
|
+
publishing: boolean
|
|
27
|
+
canEdit: boolean
|
|
28
|
+
canPublish: boolean
|
|
29
|
+
viewport: ViewportId
|
|
30
|
+
/** Breadcrumb root label + back-button text (e.g. "Pages" or "Posts"). */
|
|
31
|
+
backLabel?: string
|
|
32
|
+
onViewport: (v: ViewportId) => void
|
|
33
|
+
onBack: () => void
|
|
34
|
+
onPreview: () => void
|
|
35
|
+
/**
|
|
36
|
+
* Open the draft on the public site (real front-end) in a new tab. When
|
|
37
|
+
* omitted the "View on site" button is hidden — e.g. for unsaved documents
|
|
38
|
+
* or installs without a public URL.
|
|
39
|
+
*/
|
|
40
|
+
onViewOnSite?: () => void
|
|
41
|
+
onOpenSettings: () => void
|
|
42
|
+
onSaveDraft: () => void
|
|
43
|
+
onPublish: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VIEWPORT_ICONS: Record<string, LucideIcon> = {
|
|
47
|
+
monitor: Monitor,
|
|
48
|
+
laptop: Laptop,
|
|
49
|
+
tablet: Tablet,
|
|
50
|
+
smartphone: Smartphone,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STATUS_TEXT: Record<PageStatus, { label: string; className: string }> = {
|
|
54
|
+
DRAFT: { label: 'Draft', className: 'text-amber-600 dark:text-amber-400' },
|
|
55
|
+
PUBLISHED: { label: 'Published', className: 'text-emerald-600 dark:text-emerald-400' },
|
|
56
|
+
SCHEDULED: { label: 'Scheduled', className: 'text-amber-600 dark:text-amber-400' },
|
|
57
|
+
ARCHIVED: { label: 'Archived', className: 'text-muted-foreground' },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function EditorTopBar({
|
|
61
|
+
title,
|
|
62
|
+
status,
|
|
63
|
+
dirty,
|
|
64
|
+
saveState,
|
|
65
|
+
publishing,
|
|
66
|
+
canEdit,
|
|
67
|
+
canPublish,
|
|
68
|
+
viewport,
|
|
69
|
+
backLabel = 'Pages',
|
|
70
|
+
onViewport,
|
|
71
|
+
onBack,
|
|
72
|
+
onPreview,
|
|
73
|
+
onViewOnSite,
|
|
74
|
+
onOpenSettings,
|
|
75
|
+
onSaveDraft,
|
|
76
|
+
onPublish,
|
|
77
|
+
}: EditorTopBarProps) {
|
|
78
|
+
const statusInfo = STATUS_TEXT[status]
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<header className="bg-card border-border flex h-14 shrink-0 items-center justify-between gap-4 border-b px-4">
|
|
82
|
+
{/* Breadcrumb + status */}
|
|
83
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
84
|
+
<nav aria-label="Breadcrumb" className="flex min-w-0 items-center gap-1.5 text-sm">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={onBack}
|
|
88
|
+
className="text-muted-foreground hover:text-foreground shrink-0"
|
|
89
|
+
>
|
|
90
|
+
{backLabel}
|
|
91
|
+
</button>
|
|
92
|
+
<span className="text-muted-foreground" aria-hidden>
|
|
93
|
+
/
|
|
94
|
+
</span>
|
|
95
|
+
<span className="text-foreground truncate font-medium">{title || 'Untitled page'}</span>
|
|
96
|
+
</nav>
|
|
97
|
+
<span
|
|
98
|
+
className={`ml-2 shrink-0 text-xs font-medium ${statusInfo.className}`}
|
|
99
|
+
aria-label={`Status: ${statusInfo.label}`}
|
|
100
|
+
>
|
|
101
|
+
{statusInfo.label}
|
|
102
|
+
</span>
|
|
103
|
+
<SaveStatus state={saveState} dirty={dirty} />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Right controls */}
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<div
|
|
109
|
+
className="border-border bg-background hidden items-center gap-0.5 rounded-lg border p-0.5 md:flex"
|
|
110
|
+
role="group"
|
|
111
|
+
aria-label="Preview viewport"
|
|
112
|
+
>
|
|
113
|
+
{VIEWPORTS.map((v) => {
|
|
114
|
+
const Icon = VIEWPORT_ICONS[v.icon] ?? Monitor
|
|
115
|
+
const active = v.id === viewport
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
key={v.id}
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => onViewport(v.id)}
|
|
121
|
+
aria-pressed={active}
|
|
122
|
+
title={`${v.label} (${v.width}px)`}
|
|
123
|
+
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
|
|
124
|
+
active
|
|
125
|
+
? 'bg-primary text-primary-foreground'
|
|
126
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
127
|
+
}`}
|
|
128
|
+
>
|
|
129
|
+
<Icon className="h-3.5 w-3.5" aria-hidden />
|
|
130
|
+
<span className="hidden lg:inline">{v.label}</span>
|
|
131
|
+
</button>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onOpenSettings}
|
|
139
|
+
aria-label="Page settings"
|
|
140
|
+
title="Page settings"
|
|
141
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
142
|
+
>
|
|
143
|
+
<Settings2 className="h-4 w-4" aria-hidden />
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={onPreview}
|
|
149
|
+
aria-label="Preview page"
|
|
150
|
+
title="Preview page"
|
|
151
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
152
|
+
>
|
|
153
|
+
<Eye className="h-4 w-4" aria-hidden />
|
|
154
|
+
</button>
|
|
155
|
+
|
|
156
|
+
{onViewOnSite && (
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={onViewOnSite}
|
|
160
|
+
aria-label="View draft on site"
|
|
161
|
+
title="View draft on site"
|
|
162
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
|
|
163
|
+
>
|
|
164
|
+
<ExternalLink className="h-4 w-4" aria-hidden />
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={onSaveDraft}
|
|
171
|
+
disabled={!canEdit || saveState === 'saving' || (!dirty && saveState !== 'error')}
|
|
172
|
+
className="border-border text-foreground hover:bg-accent rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
173
|
+
>
|
|
174
|
+
{saveState === 'saving' ? 'Saving…' : 'Save Draft'}
|
|
175
|
+
</button>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={onPublish}
|
|
180
|
+
disabled={!canPublish || publishing || saveState === 'saving'}
|
|
181
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
182
|
+
>
|
|
183
|
+
{publishing && <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />}
|
|
184
|
+
Publish
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</header>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function SaveStatus({ state, dirty }: { state: SaveState; dirty: boolean }) {
|
|
192
|
+
if (state === 'saving') {
|
|
193
|
+
return (
|
|
194
|
+
<span
|
|
195
|
+
className="text-muted-foreground ml-2 inline-flex items-center gap-1 text-xs"
|
|
196
|
+
aria-live="polite"
|
|
197
|
+
>
|
|
198
|
+
<Loader2 className="h-3 w-3 animate-spin" aria-hidden />
|
|
199
|
+
Saving…
|
|
200
|
+
</span>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (state === 'error') {
|
|
204
|
+
return (
|
|
205
|
+
<span
|
|
206
|
+
className="text-destructive ml-2 inline-flex items-center gap-1 text-xs"
|
|
207
|
+
aria-live="polite"
|
|
208
|
+
>
|
|
209
|
+
<TriangleAlert className="h-3 w-3" aria-hidden />
|
|
210
|
+
Failed to save
|
|
211
|
+
</span>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (dirty) {
|
|
215
|
+
return (
|
|
216
|
+
<span className="text-muted-foreground ml-2 text-xs" aria-live="polite">
|
|
217
|
+
Unsaved changes
|
|
218
|
+
</span>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
if (state === 'saved') {
|
|
222
|
+
return (
|
|
223
|
+
<span
|
|
224
|
+
className="text-muted-foreground ml-2 inline-flex items-center gap-1 text-xs"
|
|
225
|
+
aria-live="polite"
|
|
226
|
+
>
|
|
227
|
+
<Check className="h-3 w-3" aria-hidden />
|
|
228
|
+
Saved
|
|
229
|
+
</span>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { AlertTriangle, Loader2, Pencil, X } from 'lucide-react'
|
|
5
|
+
import { fetchPageForEditor, type EditorPage } from '../../lib/page-editor-service.js'
|
|
6
|
+
import { SectionRenderer } from './sections/index.js'
|
|
7
|
+
|
|
8
|
+
interface PagePreviewProps {
|
|
9
|
+
documentId: string
|
|
10
|
+
onNavigate: (path: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Clean, chrome-free render of a page using the SAME section renderers as
|
|
15
|
+
* the editor canvas — so the preview reflects what will actually ship.
|
|
16
|
+
* Only visible sections are rendered (hidden sections are omitted, exactly
|
|
17
|
+
* as the public page would behave).
|
|
18
|
+
*/
|
|
19
|
+
export function PagePreview({ documentId, onNavigate }: PagePreviewProps) {
|
|
20
|
+
const [page, setPage] = useState<EditorPage | null>(null)
|
|
21
|
+
const [loading, setLoading] = useState(true)
|
|
22
|
+
const [error, setError] = useState<string | null>(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false
|
|
26
|
+
setLoading(true)
|
|
27
|
+
fetchPageForEditor(documentId)
|
|
28
|
+
.then((p) => !cancelled && setPage(p))
|
|
29
|
+
.catch((err) => !cancelled && setError(err instanceof Error ? err.message : 'Failed to load'))
|
|
30
|
+
.finally(() => !cancelled && setLoading(false))
|
|
31
|
+
return () => {
|
|
32
|
+
cancelled = true
|
|
33
|
+
}
|
|
34
|
+
}, [documentId])
|
|
35
|
+
|
|
36
|
+
if (loading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="bg-background flex h-full items-center justify-center">
|
|
39
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
|
|
40
|
+
<span className="sr-only">Loading preview…</span>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (error || !page) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="bg-background flex h-full items-center justify-center p-6">
|
|
48
|
+
<div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
|
|
49
|
+
<AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
|
|
50
|
+
<p className="text-foreground text-sm font-medium">{error ?? 'Page not found.'}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const visible = page.sections.filter((s) => s.visible)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex h-full flex-col">
|
|
60
|
+
{/* Slim preview bar — the only chrome; the page itself renders clean.
|
|
61
|
+
This view opens in its own browser tab, so actions are tab-scoped. */}
|
|
62
|
+
<div className="bg-card border-border flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={() => window.close()}
|
|
66
|
+
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm"
|
|
67
|
+
aria-label="Close preview tab"
|
|
68
|
+
>
|
|
69
|
+
<X className="h-4 w-4" aria-hidden />
|
|
70
|
+
Close
|
|
71
|
+
</button>
|
|
72
|
+
<span className="text-muted-foreground text-xs">
|
|
73
|
+
Preview · {page.title || 'Untitled'} {page.path}
|
|
74
|
+
</span>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => onNavigate(`/pages/${documentId}/edit`)}
|
|
78
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium"
|
|
79
|
+
>
|
|
80
|
+
<Pencil className="h-3.5 w-3.5" aria-hidden />
|
|
81
|
+
Open editor
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="flex-1 overflow-auto bg-white">
|
|
86
|
+
{visible.length === 0 ? (
|
|
87
|
+
<div className="text-muted-foreground flex h-full items-center justify-center text-sm">
|
|
88
|
+
This page has no visible sections yet.
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
visible.map((section) => <SectionRenderer key={section.id} section={section} />)
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|