@actuate-media/cms-admin 0.14.1 → 0.16.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/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__/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/Header.d.ts +14 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +10 -3
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +78 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +16 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +176 -49
- 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 +147 -62
- 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 +213 -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__/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/Header.tsx +90 -43
- package/src/layout/Layout.tsx +166 -1
- package/src/layout/Sidebar.tsx +287 -101
- 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 +270 -131
- 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,270 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Copy,
|
|
5
|
+
Eye,
|
|
6
|
+
EyeOff,
|
|
7
|
+
GripVertical,
|
|
8
|
+
type LucideIcon,
|
|
9
|
+
MoreVertical,
|
|
10
|
+
Plus,
|
|
11
|
+
Trash2,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import {
|
|
14
|
+
DndContext,
|
|
15
|
+
KeyboardSensor,
|
|
16
|
+
PointerSensor,
|
|
17
|
+
closestCenter,
|
|
18
|
+
useSensor,
|
|
19
|
+
useSensors,
|
|
20
|
+
type DragEndEvent,
|
|
21
|
+
} from '@dnd-kit/core'
|
|
22
|
+
import {
|
|
23
|
+
SortableContext,
|
|
24
|
+
sortableKeyboardCoordinates,
|
|
25
|
+
useSortable,
|
|
26
|
+
verticalListSortingStrategy,
|
|
27
|
+
} from '@dnd-kit/sortable'
|
|
28
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
29
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
30
|
+
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
31
|
+
import { sectionIconFor } from './section-icons.js'
|
|
32
|
+
|
|
33
|
+
interface SectionsPanelProps {
|
|
34
|
+
sections: PageSection[]
|
|
35
|
+
selectedId: string | null
|
|
36
|
+
canEdit: boolean
|
|
37
|
+
onSelect: (id: string) => void
|
|
38
|
+
onReorder: (orderedIds: string[]) => void
|
|
39
|
+
onToggleVisibility: (id: string) => void
|
|
40
|
+
onDuplicate: (id: string) => void
|
|
41
|
+
onRequestDelete: (id: string) => void
|
|
42
|
+
onAdd: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function SectionsPanel({
|
|
46
|
+
sections,
|
|
47
|
+
selectedId,
|
|
48
|
+
canEdit,
|
|
49
|
+
onSelect,
|
|
50
|
+
onReorder,
|
|
51
|
+
onToggleVisibility,
|
|
52
|
+
onDuplicate,
|
|
53
|
+
onRequestDelete,
|
|
54
|
+
onAdd,
|
|
55
|
+
}: SectionsPanelProps) {
|
|
56
|
+
const sensors = useSensors(
|
|
57
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
58
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
62
|
+
const { active, over } = event
|
|
63
|
+
if (!over || active.id === over.id) return
|
|
64
|
+
const ids = sections.map((s) => s.id)
|
|
65
|
+
const from = ids.indexOf(String(active.id))
|
|
66
|
+
const to = ids.indexOf(String(over.id))
|
|
67
|
+
if (from === -1 || to === -1) return
|
|
68
|
+
const next = ids.slice()
|
|
69
|
+
const [moved] = next.splice(from, 1)
|
|
70
|
+
next.splice(to, 0, moved!)
|
|
71
|
+
onReorder(next)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<aside
|
|
76
|
+
className="bg-card border-border flex h-full w-72 shrink-0 flex-col border-r"
|
|
77
|
+
aria-label="Page sections"
|
|
78
|
+
>
|
|
79
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<h2 className="text-foreground text-sm font-medium">Sections</h2>
|
|
82
|
+
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium tabular-nums">
|
|
83
|
+
{sections.length}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
89
|
+
{sections.length === 0 ? (
|
|
90
|
+
<p className="text-muted-foreground px-2 py-6 text-center text-sm">
|
|
91
|
+
No sections yet. Add one below to get started.
|
|
92
|
+
</p>
|
|
93
|
+
) : (
|
|
94
|
+
<DndContext
|
|
95
|
+
sensors={sensors}
|
|
96
|
+
collisionDetection={closestCenter}
|
|
97
|
+
onDragEnd={handleDragEnd}
|
|
98
|
+
>
|
|
99
|
+
<SortableContext
|
|
100
|
+
items={sections.map((s) => s.id)}
|
|
101
|
+
strategy={verticalListSortingStrategy}
|
|
102
|
+
>
|
|
103
|
+
<ul className="space-y-1">
|
|
104
|
+
{sections.map((section) => (
|
|
105
|
+
<SortableSectionItem
|
|
106
|
+
key={section.id}
|
|
107
|
+
section={section}
|
|
108
|
+
selected={section.id === selectedId}
|
|
109
|
+
canEdit={canEdit}
|
|
110
|
+
onSelect={onSelect}
|
|
111
|
+
onToggleVisibility={onToggleVisibility}
|
|
112
|
+
onDuplicate={onDuplicate}
|
|
113
|
+
onRequestDelete={onRequestDelete}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
</SortableContext>
|
|
118
|
+
</DndContext>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="border-border border-t p-3">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onAdd}
|
|
126
|
+
disabled={!canEdit}
|
|
127
|
+
className="border-border text-foreground hover:bg-accent flex w-full items-center justify-center gap-2 rounded-lg border border-dashed px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
128
|
+
>
|
|
129
|
+
<Plus className="h-4 w-4" aria-hidden />
|
|
130
|
+
Add Section
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</aside>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface SortableItemProps {
|
|
138
|
+
section: PageSection
|
|
139
|
+
selected: boolean
|
|
140
|
+
canEdit: boolean
|
|
141
|
+
onSelect: (id: string) => void
|
|
142
|
+
onToggleVisibility: (id: string) => void
|
|
143
|
+
onDuplicate: (id: string) => void
|
|
144
|
+
onRequestDelete: (id: string) => void
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SortableSectionItem({
|
|
148
|
+
section,
|
|
149
|
+
selected,
|
|
150
|
+
canEdit,
|
|
151
|
+
onSelect,
|
|
152
|
+
onToggleVisibility,
|
|
153
|
+
onDuplicate,
|
|
154
|
+
onRequestDelete,
|
|
155
|
+
}: SortableItemProps) {
|
|
156
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
157
|
+
id: section.id,
|
|
158
|
+
disabled: !canEdit,
|
|
159
|
+
})
|
|
160
|
+
const Icon = sectionIconFor(section.sectionType)
|
|
161
|
+
|
|
162
|
+
const style = {
|
|
163
|
+
transform: CSS.Transform.toString(transform),
|
|
164
|
+
transition,
|
|
165
|
+
zIndex: isDragging ? 10 : undefined,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<li ref={setNodeRef} style={style}>
|
|
170
|
+
<div
|
|
171
|
+
className={`group flex items-center gap-2 rounded-lg border px-2 py-2 ${
|
|
172
|
+
selected ? 'border-primary bg-accent' : 'hover:bg-accent/60 border-transparent'
|
|
173
|
+
} ${isDragging ? 'shadow-md' : ''}`}
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
className="text-muted-foreground hover:text-foreground shrink-0 cursor-grab touch-none disabled:cursor-not-allowed disabled:opacity-40"
|
|
178
|
+
aria-label={`Drag to reorder ${section.name}`}
|
|
179
|
+
disabled={!canEdit}
|
|
180
|
+
{...attributes}
|
|
181
|
+
{...listeners}
|
|
182
|
+
>
|
|
183
|
+
<GripVertical className="h-4 w-4" aria-hidden />
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={() => onSelect(section.id)}
|
|
189
|
+
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
|
190
|
+
>
|
|
191
|
+
<span className="bg-muted text-muted-foreground flex h-7 w-7 shrink-0 items-center justify-center rounded-md">
|
|
192
|
+
<Icon className="h-4 w-4" aria-hidden />
|
|
193
|
+
</span>
|
|
194
|
+
<span
|
|
195
|
+
className={`truncate text-sm ${section.visible ? 'text-foreground' : 'text-muted-foreground line-through'}`}
|
|
196
|
+
>
|
|
197
|
+
{section.name}
|
|
198
|
+
</span>
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => onToggleVisibility(section.id)}
|
|
204
|
+
disabled={!canEdit}
|
|
205
|
+
aria-pressed={!section.visible}
|
|
206
|
+
aria-label={section.visible ? `Hide ${section.name}` : `Show ${section.name}`}
|
|
207
|
+
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
|
|
208
|
+
>
|
|
209
|
+
{section.visible ? (
|
|
210
|
+
<Eye className="h-4 w-4" aria-hidden />
|
|
211
|
+
) : (
|
|
212
|
+
<EyeOff className="h-4 w-4" aria-hidden />
|
|
213
|
+
)}
|
|
214
|
+
</button>
|
|
215
|
+
|
|
216
|
+
<DropdownMenu.Root>
|
|
217
|
+
<DropdownMenu.Trigger asChild>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
disabled={!canEdit}
|
|
221
|
+
aria-label={`More actions for ${section.name}`}
|
|
222
|
+
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
|
|
223
|
+
>
|
|
224
|
+
<MoreVertical className="h-4 w-4" aria-hidden />
|
|
225
|
+
</button>
|
|
226
|
+
</DropdownMenu.Trigger>
|
|
227
|
+
<DropdownMenu.Portal>
|
|
228
|
+
<DropdownMenu.Content
|
|
229
|
+
align="end"
|
|
230
|
+
sideOffset={4}
|
|
231
|
+
className="border-border bg-popover z-[70] min-w-44 rounded-lg border p-1 shadow-md"
|
|
232
|
+
>
|
|
233
|
+
<MenuItem onSelect={() => onDuplicate(section.id)} icon={Copy}>
|
|
234
|
+
Duplicate
|
|
235
|
+
</MenuItem>
|
|
236
|
+
<DropdownMenu.Separator className="bg-border my-1 h-px" />
|
|
237
|
+
<MenuItem onSelect={() => onRequestDelete(section.id)} icon={Trash2} destructive>
|
|
238
|
+
Delete
|
|
239
|
+
</MenuItem>
|
|
240
|
+
</DropdownMenu.Content>
|
|
241
|
+
</DropdownMenu.Portal>
|
|
242
|
+
</DropdownMenu.Root>
|
|
243
|
+
</div>
|
|
244
|
+
</li>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function MenuItem({
|
|
249
|
+
children,
|
|
250
|
+
icon: Icon,
|
|
251
|
+
onSelect,
|
|
252
|
+
destructive,
|
|
253
|
+
}: {
|
|
254
|
+
children: React.ReactNode
|
|
255
|
+
icon: LucideIcon
|
|
256
|
+
onSelect: () => void
|
|
257
|
+
destructive?: boolean
|
|
258
|
+
}) {
|
|
259
|
+
return (
|
|
260
|
+
<DropdownMenu.Item
|
|
261
|
+
onSelect={onSelect}
|
|
262
|
+
className={`flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none ${
|
|
263
|
+
destructive ? 'text-destructive focus:bg-destructive/10' : 'text-foreground focus:bg-accent'
|
|
264
|
+
}`}
|
|
265
|
+
>
|
|
266
|
+
<Icon className="h-4 w-4" aria-hidden />
|
|
267
|
+
{children}
|
|
268
|
+
</DropdownMenu.Item>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { TriangleAlert, X } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
interface ValidationSummaryProps {
|
|
6
|
+
errors: string[]
|
|
7
|
+
warnings: string[]
|
|
8
|
+
onDismiss: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Inline banner listing blocking errors (and non-blocking warnings). */
|
|
12
|
+
export function ValidationSummary({ errors, warnings, onDismiss }: ValidationSummaryProps) {
|
|
13
|
+
if (errors.length === 0 && warnings.length === 0) return null
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
role="alert"
|
|
17
|
+
className="border-destructive/30 bg-destructive/5 mx-4 mt-3 rounded-lg border p-3"
|
|
18
|
+
>
|
|
19
|
+
<div className="flex items-start gap-2">
|
|
20
|
+
<TriangleAlert className="text-destructive mt-0.5 h-4 w-4 shrink-0" aria-hidden />
|
|
21
|
+
<div className="min-w-0 flex-1">
|
|
22
|
+
{errors.length > 0 && (
|
|
23
|
+
<>
|
|
24
|
+
<p className="text-foreground text-sm font-medium">
|
|
25
|
+
Resolve {errors.length} issue{errors.length === 1 ? '' : 's'} before publishing
|
|
26
|
+
</p>
|
|
27
|
+
<ul className="text-muted-foreground mt-1 list-disc space-y-0.5 pl-4 text-xs">
|
|
28
|
+
{errors.map((e, i) => (
|
|
29
|
+
<li key={`e-${i}`}>{e}</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
34
|
+
{warnings.length > 0 && (
|
|
35
|
+
<ul className="text-muted-foreground mt-2 list-disc space-y-0.5 pl-4 text-xs">
|
|
36
|
+
{warnings.map((w, i) => (
|
|
37
|
+
<li key={`w-${i}`}>{w}</li>
|
|
38
|
+
))}
|
|
39
|
+
</ul>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onDismiss}
|
|
45
|
+
aria-label="Dismiss"
|
|
46
|
+
className="text-muted-foreground hover:text-foreground rounded p-0.5"
|
|
47
|
+
>
|
|
48
|
+
<X className="h-4 w-4" aria-hidden />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlignLeft,
|
|
3
|
+
BarChart3,
|
|
4
|
+
Images,
|
|
5
|
+
LayoutGrid,
|
|
6
|
+
LayoutTemplate,
|
|
7
|
+
type LucideIcon,
|
|
8
|
+
Megaphone,
|
|
9
|
+
PanelsTopLeft,
|
|
10
|
+
Quote,
|
|
11
|
+
Type,
|
|
12
|
+
User,
|
|
13
|
+
} from 'lucide-react'
|
|
14
|
+
import { getSectionType } from './section-types.js'
|
|
15
|
+
|
|
16
|
+
const SECTION_ICONS: Record<string, LucideIcon> = {
|
|
17
|
+
'layout-template': LayoutTemplate,
|
|
18
|
+
'align-left': AlignLeft,
|
|
19
|
+
'panels-top-left': PanelsTopLeft,
|
|
20
|
+
'bar-chart-3': BarChart3,
|
|
21
|
+
megaphone: Megaphone,
|
|
22
|
+
text: Type,
|
|
23
|
+
quote: Quote,
|
|
24
|
+
user: User,
|
|
25
|
+
'layout-grid': LayoutGrid,
|
|
26
|
+
images: Images,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve a lucide icon for a section type id (with a safe fallback). */
|
|
30
|
+
export function sectionIconFor(sectionType: string): LucideIcon {
|
|
31
|
+
const name = getSectionType(sectionType)?.icon
|
|
32
|
+
return (name && SECTION_ICONS[name]) || LayoutTemplate
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section-type registry for the flat, section-based Page Editor.
|
|
3
|
+
*
|
|
4
|
+
* The definitions now live in `@actuate-media/cms-core/page-sections` so the
|
|
5
|
+
* admin editor, the REST API, and the MCP server validate against ONE source
|
|
6
|
+
* of truth. This module re-exports them to preserve the historical import
|
|
7
|
+
* path (`../views/page-editor/section-types.js`) used across the editor.
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
SECTION_TYPES,
|
|
11
|
+
SECTION_TYPE_LIST,
|
|
12
|
+
getSectionType,
|
|
13
|
+
getSectionTypesForScope,
|
|
14
|
+
sectionTypeLabel,
|
|
15
|
+
isKnownSectionType,
|
|
16
|
+
} from '@actuate-media/cms-core/page-sections'
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
SectionTypeId,
|
|
20
|
+
SectionScope,
|
|
21
|
+
SectionFieldType,
|
|
22
|
+
SectionFieldDef,
|
|
23
|
+
SectionSettings,
|
|
24
|
+
SectionTypeDef,
|
|
25
|
+
PostHeaderConfig,
|
|
26
|
+
PostTemplate,
|
|
27
|
+
} from '@actuate-media/cms-core/page-sections'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { bool, str, type PostRenderContext } from './parts.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders the post's main body. When `source === 'field'` it shows the
|
|
6
|
+
* post's own Body field (passed via context); otherwise it renders the
|
|
7
|
+
* section's own `body` copy. Body text is treated as plain paragraphs split
|
|
8
|
+
* on blank lines — the renderer never injects raw HTML.
|
|
9
|
+
*/
|
|
10
|
+
export function ArticleBodySection({
|
|
11
|
+
section,
|
|
12
|
+
context,
|
|
13
|
+
}: {
|
|
14
|
+
section: PageSection
|
|
15
|
+
context?: PostRenderContext
|
|
16
|
+
}) {
|
|
17
|
+
const c = section.content
|
|
18
|
+
const source = str(c, 'source', 'field')
|
|
19
|
+
const raw = source === 'field' ? (context?.body ?? '') : str(c, 'body')
|
|
20
|
+
const wide = str(c, 'width', 'normal') === 'wide'
|
|
21
|
+
const showToc = bool(c, 'showToc')
|
|
22
|
+
|
|
23
|
+
const paragraphs = raw
|
|
24
|
+
.split(/\n{2,}/)
|
|
25
|
+
.map((p) => p.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={`mx-auto px-6 ${wide ? 'max-w-4xl' : 'max-w-2xl'}`}>
|
|
30
|
+
{showToc && (
|
|
31
|
+
<div className="mb-8 rounded-lg border border-zinc-200 bg-zinc-50 p-4">
|
|
32
|
+
<p className="text-xs font-semibold tracking-wider text-zinc-500 uppercase">
|
|
33
|
+
On this page
|
|
34
|
+
</p>
|
|
35
|
+
<p className="mt-1 text-sm text-zinc-400">
|
|
36
|
+
A table of contents will be generated from the body headings.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
{paragraphs.length > 0 ? (
|
|
41
|
+
<div className="space-y-4">
|
|
42
|
+
{paragraphs.map((p, i) => (
|
|
43
|
+
<p key={i} className="text-base leading-relaxed text-zinc-700">
|
|
44
|
+
{p}
|
|
45
|
+
</p>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<p className="text-base leading-relaxed text-zinc-400 italic">
|
|
50
|
+
{source === 'field'
|
|
51
|
+
? "This will show the post's Body content."
|
|
52
|
+
: 'Add body copy in the inspector.'}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { bool, str, type PostRenderContext } from './parts.js'
|
|
3
|
+
|
|
4
|
+
/** A short author biography block, driven by the post's author context. */
|
|
5
|
+
export function AuthorBioSection({
|
|
6
|
+
section,
|
|
7
|
+
context,
|
|
8
|
+
}: {
|
|
9
|
+
section: PageSection
|
|
10
|
+
context?: PostRenderContext
|
|
11
|
+
}) {
|
|
12
|
+
const c = section.content
|
|
13
|
+
const heading = str(c, 'heading', 'About the author')
|
|
14
|
+
const showAvatar = bool(c, 'showAvatar', true)
|
|
15
|
+
const showSocial = bool(c, 'showSocial', true)
|
|
16
|
+
|
|
17
|
+
const name = context?.author?.name || 'Author name'
|
|
18
|
+
const bio =
|
|
19
|
+
context?.author?.bio || "The author's bio will appear here once it is set on their profile."
|
|
20
|
+
const avatarUrl = context?.author?.avatarUrl
|
|
21
|
+
const initials = name
|
|
22
|
+
.split(/\s+/)
|
|
23
|
+
.map((p) => p[0] ?? '')
|
|
24
|
+
.join('')
|
|
25
|
+
.slice(0, 2)
|
|
26
|
+
.toUpperCase()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="mx-auto max-w-3xl px-6">
|
|
30
|
+
<div className="flex items-start gap-4 rounded-xl border border-zinc-200 bg-zinc-50 p-5">
|
|
31
|
+
{showAvatar &&
|
|
32
|
+
(avatarUrl ? (
|
|
33
|
+
<img
|
|
34
|
+
src={avatarUrl}
|
|
35
|
+
alt={name}
|
|
36
|
+
className="h-14 w-14 shrink-0 rounded-full object-cover"
|
|
37
|
+
/>
|
|
38
|
+
) : (
|
|
39
|
+
<span className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-violet-100 text-sm font-medium text-violet-700">
|
|
40
|
+
{initials || '?'}
|
|
41
|
+
</span>
|
|
42
|
+
))}
|
|
43
|
+
<div className="min-w-0">
|
|
44
|
+
{heading && (
|
|
45
|
+
<p className="text-xs font-semibold tracking-wider text-zinc-500 uppercase">
|
|
46
|
+
{heading}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
<p className="mt-0.5 text-base font-medium text-zinc-900">{name}</p>
|
|
50
|
+
<p className="mt-1 text-sm leading-relaxed text-zinc-600">{bio}</p>
|
|
51
|
+
{showSocial && (
|
|
52
|
+
<div className="mt-2 flex gap-3 text-sm text-violet-600">
|
|
53
|
+
<span>Twitter</span>
|
|
54
|
+
<span>LinkedIn</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { str } from './parts.js'
|
|
3
|
+
|
|
4
|
+
interface Stat {
|
|
5
|
+
value?: unknown
|
|
6
|
+
label?: unknown
|
|
7
|
+
description?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ByTheNumbersSection({ section }: { section: PageSection }) {
|
|
11
|
+
const c = section.content
|
|
12
|
+
const heading = str(c, 'heading')
|
|
13
|
+
const body = str(c, 'body')
|
|
14
|
+
const stats = Array.isArray(c.stats) ? (c.stats as Stat[]) : []
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="mx-auto max-w-5xl px-6 text-center">
|
|
18
|
+
{heading && (
|
|
19
|
+
<h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
|
20
|
+
{heading}
|
|
21
|
+
</h2>
|
|
22
|
+
)}
|
|
23
|
+
{body && <p className="mx-auto mt-4 max-w-2xl text-base text-zinc-600">{body}</p>}
|
|
24
|
+
{stats.length > 0 && (
|
|
25
|
+
<dl className="mt-10 grid grid-cols-1 gap-8 sm:grid-cols-3">
|
|
26
|
+
{stats.map((s, i) => (
|
|
27
|
+
<div key={i}>
|
|
28
|
+
<dt className="text-4xl font-semibold tracking-tight text-violet-600">
|
|
29
|
+
{String(s.value ?? '')}
|
|
30
|
+
</dt>
|
|
31
|
+
<dd className="mt-2 text-sm font-medium text-zinc-700">{String(s.label ?? '')}</dd>
|
|
32
|
+
{s.description ? (
|
|
33
|
+
<dd className="mt-1 text-xs text-zinc-500">{String(s.description)}</dd>
|
|
34
|
+
) : null}
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</dl>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { PreviewButton, isDarkBackground, str } from './parts.js'
|
|
3
|
+
|
|
4
|
+
export function CtaBannerSection({ section }: { section: PageSection }) {
|
|
5
|
+
const c = section.content
|
|
6
|
+
const dark = isDarkBackground(section.settings)
|
|
7
|
+
const heading = str(c, 'heading')
|
|
8
|
+
const body = str(c, 'body')
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="mx-auto max-w-3xl px-6 text-center">
|
|
12
|
+
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">{heading}</h2>
|
|
13
|
+
{body && (
|
|
14
|
+
<p
|
|
15
|
+
className={`mx-auto mt-3 max-w-xl text-base ${dark ? 'text-violet-100' : 'text-zinc-600'}`}
|
|
16
|
+
>
|
|
17
|
+
{body}
|
|
18
|
+
</p>
|
|
19
|
+
)}
|
|
20
|
+
<div className="mt-6 flex justify-center">
|
|
21
|
+
<PreviewButton
|
|
22
|
+
label={str(c, 'buttonLabel')}
|
|
23
|
+
href={str(c, 'buttonUrl')}
|
|
24
|
+
tone={dark ? 'onDark' : 'primary'}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ImageIcon } from 'lucide-react'
|
|
2
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
3
|
+
import { PreviewButton, str } from './parts.js'
|
|
4
|
+
|
|
5
|
+
export function FeatureHighlightSection({ section }: { section: PageSection }) {
|
|
6
|
+
const c = section.content
|
|
7
|
+
const label = str(c, 'label')
|
|
8
|
+
const heading = str(c, 'heading')
|
|
9
|
+
const body = str(c, 'body')
|
|
10
|
+
const imageUrl = str(c, 'imageUrl')
|
|
11
|
+
const imageAlt = str(c, 'imageAlt')
|
|
12
|
+
const imageLeft = str(c, 'imagePosition', 'right') === 'left'
|
|
13
|
+
|
|
14
|
+
const text = (
|
|
15
|
+
<div className="flex-1">
|
|
16
|
+
{label && (
|
|
17
|
+
<span className="mb-3 inline-block rounded bg-violet-600 px-2 py-0.5 text-xs font-medium text-white">
|
|
18
|
+
{label}
|
|
19
|
+
</span>
|
|
20
|
+
)}
|
|
21
|
+
<h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">{heading}</h2>
|
|
22
|
+
{body && <p className="mt-4 text-base leading-relaxed text-zinc-600">{body}</p>}
|
|
23
|
+
<div className="mt-6">
|
|
24
|
+
<PreviewButton label={str(c, 'buttonLabel')} href={str(c, 'buttonUrl')} tone="primary" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const media = (
|
|
30
|
+
<div className="flex-1">
|
|
31
|
+
{imageUrl ? (
|
|
32
|
+
// Admin-only preview; next/image isn't in scope here.
|
|
33
|
+
<img
|
|
34
|
+
src={imageUrl}
|
|
35
|
+
alt={imageAlt}
|
|
36
|
+
className="aspect-[4/3] w-full rounded-xl object-cover"
|
|
37
|
+
/>
|
|
38
|
+
) : (
|
|
39
|
+
<div className="flex aspect-[4/3] w-full items-center justify-center rounded-xl bg-violet-50">
|
|
40
|
+
<ImageIcon className="h-10 w-10 text-violet-300" aria-hidden />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="mx-auto max-w-5xl px-6">
|
|
48
|
+
<div className="flex flex-col items-center gap-10 md:flex-row">
|
|
49
|
+
{imageLeft ? (
|
|
50
|
+
<>
|
|
51
|
+
{media}
|
|
52
|
+
{text}
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
<>
|
|
56
|
+
{text}
|
|
57
|
+
{media}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
2
|
+
import { str } from './parts.js'
|
|
3
|
+
|
|
4
|
+
interface GalleryImage {
|
|
5
|
+
url?: string
|
|
6
|
+
alt?: string
|
|
7
|
+
caption?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A responsive image grid with captions. */
|
|
11
|
+
export function GallerySection({ section }: { section: PageSection }) {
|
|
12
|
+
const c = section.content
|
|
13
|
+
const heading = str(c, 'heading')
|
|
14
|
+
const columns = Number.parseInt(str(c, 'columns', '3'), 10) || 3
|
|
15
|
+
const images: GalleryImage[] = Array.isArray(c.images) ? (c.images as GalleryImage[]) : []
|
|
16
|
+
const withUrl = images.filter((img) => img && typeof img.url === 'string' && img.url)
|
|
17
|
+
|
|
18
|
+
const cols =
|
|
19
|
+
columns === 2
|
|
20
|
+
? 'sm:grid-cols-2'
|
|
21
|
+
: columns === 4
|
|
22
|
+
? 'sm:grid-cols-2 lg:grid-cols-4'
|
|
23
|
+
: 'sm:grid-cols-3'
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="mx-auto max-w-5xl px-6">
|
|
27
|
+
{heading && (
|
|
28
|
+
<h2 className="mb-6 text-2xl font-semibold tracking-tight text-zinc-900">{heading}</h2>
|
|
29
|
+
)}
|
|
30
|
+
{withUrl.length > 0 ? (
|
|
31
|
+
<div className={`grid grid-cols-1 gap-3 ${cols}`}>
|
|
32
|
+
{withUrl.map((img, i) => (
|
|
33
|
+
<figure key={i} className="overflow-hidden rounded-lg border border-zinc-200">
|
|
34
|
+
<img
|
|
35
|
+
src={img.url}
|
|
36
|
+
alt={img.alt ?? ''}
|
|
37
|
+
className="aspect-square w-full object-cover"
|
|
38
|
+
/>
|
|
39
|
+
{img.caption && (
|
|
40
|
+
<figcaption className="px-2 py-1.5 text-xs text-zinc-500">{img.caption}</figcaption>
|
|
41
|
+
)}
|
|
42
|
+
</figure>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
) : (
|
|
46
|
+
<div className={`grid grid-cols-1 gap-3 ${cols}`}>
|
|
47
|
+
{Array.from({ length: columns }, (_, i) => (
|
|
48
|
+
<div key={i} className="aspect-square rounded-lg bg-zinc-100" aria-hidden />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|