@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,104 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { paletteFor } from '../posts/badges.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tag UI for the Pages area. Two variants:
|
|
7
|
+
* - PageTagChip — interactive filter chip (filter bar). Toggles a tag
|
|
8
|
+
* on/off; announces pressed state for screen readers.
|
|
9
|
+
* - PageTagPill — static inline pill (Tags column). Compact, with a
|
|
10
|
+
* colored dot so tags don't rely on color alone.
|
|
11
|
+
*
|
|
12
|
+
* Both resolve color through the shared `paletteFor` palette so they
|
|
13
|
+
* stay consistent with Post Type badges and work in light/dark mode.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DOT_COLOR: Record<string, string> = {
|
|
17
|
+
purple: 'bg-violet-500',
|
|
18
|
+
green: 'bg-emerald-500',
|
|
19
|
+
orange: 'bg-amber-500',
|
|
20
|
+
red: 'bg-red-500',
|
|
21
|
+
blue: 'bg-blue-500',
|
|
22
|
+
pink: 'bg-pink-500',
|
|
23
|
+
gray: 'bg-muted-foreground',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tagDotColor(color: string | null | undefined): string {
|
|
27
|
+
if (!color) return DOT_COLOR.gray!
|
|
28
|
+
return DOT_COLOR[color] ?? DOT_COLOR.gray!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PageTagChip({
|
|
32
|
+
name,
|
|
33
|
+
color,
|
|
34
|
+
count,
|
|
35
|
+
selected,
|
|
36
|
+
onToggle,
|
|
37
|
+
}: {
|
|
38
|
+
name: string
|
|
39
|
+
color: string | null
|
|
40
|
+
count?: number
|
|
41
|
+
selected: boolean
|
|
42
|
+
onToggle: () => void
|
|
43
|
+
}) {
|
|
44
|
+
const p = paletteFor(color)
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onToggle}
|
|
49
|
+
aria-pressed={selected}
|
|
50
|
+
className={`focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 ${
|
|
51
|
+
selected
|
|
52
|
+
? `${p.bg} ${p.text} border-transparent ring-1 ${p.ring}`
|
|
53
|
+
: 'border-border bg-background text-foreground hover:bg-accent'
|
|
54
|
+
}`}
|
|
55
|
+
>
|
|
56
|
+
<span aria-hidden className={`h-2 w-2 rounded-full ${tagDotColor(color)}`} />
|
|
57
|
+
{name}
|
|
58
|
+
{typeof count === 'number' && (
|
|
59
|
+
<span className="text-muted-foreground tabular-nums">{count}</span>
|
|
60
|
+
)}
|
|
61
|
+
</button>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function PageTagPill({ name, color }: { name: string; color: string | null }) {
|
|
66
|
+
const p = paletteFor(color)
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
|
|
70
|
+
>
|
|
71
|
+
<span aria-hidden className={`h-1.5 w-1.5 rounded-full ${tagDotColor(color)}`} />
|
|
72
|
+
{name}
|
|
73
|
+
</span>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function PageTagsCell({
|
|
78
|
+
tagSlugs,
|
|
79
|
+
primaryTagId,
|
|
80
|
+
resolve,
|
|
81
|
+
}: {
|
|
82
|
+
tagSlugs: string[]
|
|
83
|
+
primaryTagId: string | null
|
|
84
|
+
resolve: (slug: string) => { name: string; color: string | null } | undefined
|
|
85
|
+
}) {
|
|
86
|
+
if (tagSlugs.length === 0) {
|
|
87
|
+
return <span className="text-muted-foreground text-xs">—</span>
|
|
88
|
+
}
|
|
89
|
+
// Show the primary tag first, then the rest, so grouping context is
|
|
90
|
+
// visible at a glance.
|
|
91
|
+
const ordered = [...tagSlugs].sort((a, b) => {
|
|
92
|
+
if (a === primaryTagId) return -1
|
|
93
|
+
if (b === primaryTagId) return 1
|
|
94
|
+
return 0
|
|
95
|
+
})
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
98
|
+
{ordered.map((slug) => {
|
|
99
|
+
const tag = resolve(slug)
|
|
100
|
+
return <PageTagPill key={slug} name={tag?.name ?? slug} color={tag?.color ?? null} />
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import type { PostStatus } from '../../lib/posts-service.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared status / type / SEO indicators for the Posts admin area.
|
|
8
|
+
*
|
|
9
|
+
* Colors are restricted to the theme tokens already used elsewhere in
|
|
10
|
+
* the admin (`success`, `warning`, `destructive`, `primary`, `muted`)
|
|
11
|
+
* plus a small named-color palette (`purple`, `green`, `orange`, ...)
|
|
12
|
+
* that maps to fixed Tailwind utilities. This keeps the badges working
|
|
13
|
+
* in both light and dark mode and avoids leaking `bg-[#...]` arbitrary
|
|
14
|
+
* values into components.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ─── Named color palette ─────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const PALETTE: Record<string, { bg: string; text: string; ring: string; border: string }> = {
|
|
20
|
+
purple: {
|
|
21
|
+
bg: 'bg-violet-50 dark:bg-violet-950/40',
|
|
22
|
+
text: 'text-violet-700 dark:text-violet-300',
|
|
23
|
+
ring: 'ring-violet-200 dark:ring-violet-900',
|
|
24
|
+
border: 'border-violet-500',
|
|
25
|
+
},
|
|
26
|
+
green: {
|
|
27
|
+
bg: 'bg-emerald-50 dark:bg-emerald-950/40',
|
|
28
|
+
text: 'text-emerald-700 dark:text-emerald-300',
|
|
29
|
+
ring: 'ring-emerald-200 dark:ring-emerald-900',
|
|
30
|
+
border: 'border-emerald-500',
|
|
31
|
+
},
|
|
32
|
+
orange: {
|
|
33
|
+
bg: 'bg-amber-50 dark:bg-amber-950/40',
|
|
34
|
+
text: 'text-amber-700 dark:text-amber-300',
|
|
35
|
+
ring: 'ring-amber-200 dark:ring-amber-900',
|
|
36
|
+
border: 'border-amber-500',
|
|
37
|
+
},
|
|
38
|
+
red: {
|
|
39
|
+
bg: 'bg-red-50 dark:bg-red-950/40',
|
|
40
|
+
text: 'text-red-700 dark:text-red-300',
|
|
41
|
+
ring: 'ring-red-200 dark:ring-red-900',
|
|
42
|
+
border: 'border-red-500',
|
|
43
|
+
},
|
|
44
|
+
blue: {
|
|
45
|
+
bg: 'bg-blue-50 dark:bg-blue-950/40',
|
|
46
|
+
text: 'text-blue-700 dark:text-blue-300',
|
|
47
|
+
ring: 'ring-blue-200 dark:ring-blue-900',
|
|
48
|
+
border: 'border-blue-500',
|
|
49
|
+
},
|
|
50
|
+
pink: {
|
|
51
|
+
bg: 'bg-pink-50 dark:bg-pink-950/40',
|
|
52
|
+
text: 'text-pink-700 dark:text-pink-300',
|
|
53
|
+
ring: 'ring-pink-200 dark:ring-pink-900',
|
|
54
|
+
border: 'border-pink-500',
|
|
55
|
+
},
|
|
56
|
+
gray: {
|
|
57
|
+
bg: 'bg-muted',
|
|
58
|
+
text: 'text-muted-foreground',
|
|
59
|
+
ring: 'ring-border',
|
|
60
|
+
border: 'border-border',
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function paletteFor(color: string | null | undefined) {
|
|
65
|
+
if (!color) return PALETTE.gray!
|
|
66
|
+
return PALETTE[color] ?? PALETTE.gray!
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── PostStatusBadge ─────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const STATUS_LABEL: Record<PostStatus, string> = {
|
|
72
|
+
PUBLISHED: 'Published',
|
|
73
|
+
DRAFT: 'Draft',
|
|
74
|
+
SCHEDULED: 'Scheduled',
|
|
75
|
+
ARCHIVED: 'Archived',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const STATUS_PALETTE: Record<PostStatus, string> = {
|
|
79
|
+
PUBLISHED: 'green',
|
|
80
|
+
DRAFT: 'gray',
|
|
81
|
+
SCHEDULED: 'orange',
|
|
82
|
+
ARCHIVED: 'gray',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const STATUS_GLYPH: Record<PostStatus, string> = {
|
|
86
|
+
PUBLISHED: '●',
|
|
87
|
+
DRAFT: '○',
|
|
88
|
+
SCHEDULED: '◐',
|
|
89
|
+
ARCHIVED: '◌',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function PostStatusBadge({ status }: { status: PostStatus }) {
|
|
93
|
+
const p = paletteFor(STATUS_PALETTE[status])
|
|
94
|
+
return (
|
|
95
|
+
<span
|
|
96
|
+
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
|
|
97
|
+
aria-label={`Status: ${STATUS_LABEL[status]}`}
|
|
98
|
+
>
|
|
99
|
+
<span aria-hidden className="text-[10px] leading-none">
|
|
100
|
+
{STATUS_GLYPH[status]}
|
|
101
|
+
</span>
|
|
102
|
+
{STATUS_LABEL[status]}
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── PostTypeBadge ───────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export function PostTypeBadge({
|
|
110
|
+
label,
|
|
111
|
+
color,
|
|
112
|
+
onClick,
|
|
113
|
+
}: {
|
|
114
|
+
label: string
|
|
115
|
+
color: string | null
|
|
116
|
+
onClick?: () => void
|
|
117
|
+
}): ReactNode {
|
|
118
|
+
const p = paletteFor(color)
|
|
119
|
+
const className = `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`
|
|
120
|
+
if (onClick) {
|
|
121
|
+
return (
|
|
122
|
+
<button type="button" onClick={onClick} className={`${className} hover:opacity-80`}>
|
|
123
|
+
{label}
|
|
124
|
+
</button>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
return <span className={className}>{label}</span>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── SeoScoreIndicator ───────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the palette key for an SEO score so callers can match the
|
|
134
|
+
* colored dot. 80+ green, 60–79 orange, 1–59 red, missing = gray.
|
|
135
|
+
*/
|
|
136
|
+
export function seoScorePalette(score: number | null): string {
|
|
137
|
+
if (score === null || Number.isNaN(score)) return 'gray'
|
|
138
|
+
if (score >= 80) return 'green'
|
|
139
|
+
if (score >= 60) return 'orange'
|
|
140
|
+
if (score >= 1) return 'red'
|
|
141
|
+
return 'gray'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function SeoScoreIndicator({ score }: { score: number | null }) {
|
|
145
|
+
const colorKey = seoScorePalette(score)
|
|
146
|
+
const p = paletteFor(colorKey)
|
|
147
|
+
if (score === null) {
|
|
148
|
+
return (
|
|
149
|
+
<span className="text-muted-foreground inline-flex items-center gap-1.5 text-xs">
|
|
150
|
+
<span aria-hidden className={`h-2 w-2 rounded-full ${p.bg.split(' ')[0]}`} />
|
|
151
|
+
<span className="tabular-nums">—</span>
|
|
152
|
+
</span>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
return (
|
|
156
|
+
<span className="text-foreground inline-flex items-center gap-1.5 text-xs">
|
|
157
|
+
<span
|
|
158
|
+
aria-hidden
|
|
159
|
+
className={`h-2 w-2 rounded-full ${
|
|
160
|
+
colorKey === 'green'
|
|
161
|
+
? 'bg-emerald-500'
|
|
162
|
+
: colorKey === 'orange'
|
|
163
|
+
? 'bg-amber-500'
|
|
164
|
+
: 'bg-red-500'
|
|
165
|
+
}`}
|
|
166
|
+
/>
|
|
167
|
+
<span className="tabular-nums">{score}</span>
|
|
168
|
+
</span>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileText,
|
|
3
|
+
Briefcase,
|
|
4
|
+
Newspaper,
|
|
5
|
+
BookOpen,
|
|
6
|
+
Folder,
|
|
7
|
+
Layers,
|
|
8
|
+
Image as ImageIcon,
|
|
9
|
+
Calendar,
|
|
10
|
+
Users,
|
|
11
|
+
HelpCircle,
|
|
12
|
+
type LucideIcon,
|
|
13
|
+
} from 'lucide-react'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Icon registry for Post Types. Keyed by the string value stored in a
|
|
17
|
+
* collection's `admin.icon`. Rendering surfaces (Post Types cards, the
|
|
18
|
+
* New Post picker, the Quick Post Type gallery) all resolve through
|
|
19
|
+
* `resolvePostTypeIcon` so a single map keeps them in sync — adding an
|
|
20
|
+
* icon here makes it available everywhere at once.
|
|
21
|
+
*/
|
|
22
|
+
export const POST_TYPE_ICONS: Record<string, LucideIcon> = {
|
|
23
|
+
'file-text': FileText,
|
|
24
|
+
briefcase: Briefcase,
|
|
25
|
+
newspaper: Newspaper,
|
|
26
|
+
book: BookOpen,
|
|
27
|
+
folder: Folder,
|
|
28
|
+
layers: Layers,
|
|
29
|
+
image: ImageIcon,
|
|
30
|
+
calendar: Calendar,
|
|
31
|
+
users: Users,
|
|
32
|
+
help: HelpCircle,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PostTypeIconName = keyof typeof POST_TYPE_ICONS
|
|
36
|
+
|
|
37
|
+
export function resolvePostTypeIcon(name: string | null | undefined): LucideIcon {
|
|
38
|
+
if (!name) return FileText
|
|
39
|
+
return POST_TYPE_ICONS[name.toLowerCase()] ?? FileText
|
|
40
|
+
}
|
package/src/layout/Layout.tsx
CHANGED
|
@@ -102,29 +102,26 @@ function computePageActions({
|
|
|
102
102
|
config,
|
|
103
103
|
onNavigate,
|
|
104
104
|
}: PageActionsContext): ReactNode | null {
|
|
105
|
-
// Resolve the "primary" content collection (used for both the dashboard
|
|
106
|
-
// and any explicit /posts route). Prefer a real `posts` or `post`-type
|
|
107
|
-
// collection over the hard-coded slug so admin-managed CMS configs work.
|
|
108
105
|
const collections = collectionList(config)
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
collections.find((c) => c.type === 'post')
|
|
112
|
-
const postsSlug = postsCol?.slug ?? 'posts'
|
|
113
|
-
const postsLabel = postsCol?.labels?.singular ?? 'Post'
|
|
106
|
+
const postCollections = collections.filter((c) => c.type === 'post')
|
|
107
|
+
const newPostPath = resolveNewPostPath(postCollections)
|
|
114
108
|
const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
115
109
|
|
|
116
110
|
const isDashboard = currentPath === '/' || currentPath === '/dashboard'
|
|
117
111
|
|
|
112
|
+
// Dashboard: keep the existing "+ New Post" / "View Site" pair. With
|
|
113
|
+
// a single Post Type we route straight to its editor; with several we
|
|
114
|
+
// route to the picker so the user can choose the type first.
|
|
118
115
|
if (isDashboard) {
|
|
119
116
|
return (
|
|
120
117
|
<>
|
|
121
118
|
<button
|
|
122
119
|
type="button"
|
|
123
|
-
onClick={() => onNavigate(
|
|
120
|
+
onClick={() => onNavigate(newPostPath)}
|
|
124
121
|
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
125
122
|
>
|
|
126
123
|
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
127
|
-
New
|
|
124
|
+
New Post
|
|
128
125
|
</button>
|
|
129
126
|
{siteUrl ? (
|
|
130
127
|
<a
|
|
@@ -141,6 +138,52 @@ function computePageActions({
|
|
|
141
138
|
)
|
|
142
139
|
}
|
|
143
140
|
|
|
141
|
+
// Posts area — "+ New Post" + "Manage types" on the list page;
|
|
142
|
+
// "+ New Post Type" on the types page. The buttons live in the top
|
|
143
|
+
// bar so the page header stays clean. See the dashboard rule for
|
|
144
|
+
// why these come from a single config-aware helper.
|
|
145
|
+
if (currentPath === '/posts') {
|
|
146
|
+
return (
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => onNavigate(newPostPath)}
|
|
150
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
151
|
+
>
|
|
152
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
153
|
+
New Post
|
|
154
|
+
</button>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (currentPath === '/posts/types') {
|
|
159
|
+
return (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => onNavigate('/posts/types/new')}
|
|
163
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
164
|
+
>
|
|
165
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
166
|
+
New Post Type
|
|
167
|
+
</button>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pages list — "+ New page" routes to the page creation flow. Handled
|
|
172
|
+
// explicitly (not via the collection loop) so the label/route stay
|
|
173
|
+
// exact even though `pages` is a reserved route.
|
|
174
|
+
if (currentPath === '/pages') {
|
|
175
|
+
return (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={() => onNavigate('/pages/new')}
|
|
179
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
|
|
180
|
+
>
|
|
181
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
182
|
+
New page
|
|
183
|
+
</button>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
144
187
|
// List views — surface "+ New X" so authors can keep creating without
|
|
145
188
|
// hunting for a per-list button. Skip on editor views; those have their
|
|
146
189
|
// own toolbar.
|
|
@@ -169,6 +212,19 @@ interface CollectionMeta {
|
|
|
169
212
|
admin?: { hidden?: boolean }
|
|
170
213
|
}
|
|
171
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Where "+ New Post" should land, given the configured post-type
|
|
217
|
+
* collections:
|
|
218
|
+
* - exactly one → straight to that type's editor (`/<slug>/new`)
|
|
219
|
+
* - more than one → the type picker (`/posts/new`)
|
|
220
|
+
* - none → the Post Type setup instructions
|
|
221
|
+
*/
|
|
222
|
+
function resolveNewPostPath(postCollections: CollectionMeta[]): string {
|
|
223
|
+
if (postCollections.length === 1) return `/${postCollections[0]!.slug}/new`
|
|
224
|
+
if (postCollections.length > 1) return '/posts/new'
|
|
225
|
+
return '/posts/types/new'
|
|
226
|
+
}
|
|
227
|
+
|
|
172
228
|
function collectionList(config: any): CollectionMeta[] {
|
|
173
229
|
if (!config?.collections) return []
|
|
174
230
|
const raw = config.collections
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
PanelTop,
|
|
20
20
|
PanelBottom,
|
|
21
21
|
Layers,
|
|
22
|
+
LayoutGrid,
|
|
22
23
|
Code2,
|
|
23
24
|
Library,
|
|
24
25
|
KeyRound,
|
|
26
|
+
ListTree,
|
|
27
|
+
Menu as MenuIcon,
|
|
25
28
|
PanelLeftClose,
|
|
26
29
|
PanelLeftOpen,
|
|
27
30
|
} from 'lucide-react'
|
|
@@ -61,6 +64,9 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
61
64
|
book: BookOpen,
|
|
62
65
|
help: HelpCircle,
|
|
63
66
|
newspaper: Newspaper,
|
|
67
|
+
navigation: ListTree,
|
|
68
|
+
menu: MenuIcon,
|
|
69
|
+
'list-tree': ListTree,
|
|
64
70
|
PanelTop: PanelTop,
|
|
65
71
|
PanelBottom: PanelBottom,
|
|
66
72
|
Layers: Layers,
|
|
@@ -106,24 +112,45 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
* Children of the Posts parent. Same shape regardless of how many
|
|
116
|
+
* post-type collections are registered — Post Types are accessed via
|
|
117
|
+
* the `/posts/types` cards page, not as individual sidebar entries.
|
|
118
|
+
*/
|
|
119
|
+
const POSTS_CHILDREN: NavItem[] = [
|
|
120
|
+
{ path: '/posts', label: 'All Posts', icon: FileText },
|
|
121
|
+
{ path: '/posts/types', label: 'Post Types', icon: Layers },
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Children of the Templates parent. Templates is its own top-level item
|
|
126
|
+
* (page building tools) — Saved Sections are reusable content blocks and
|
|
127
|
+
* Widgets are embeddable components. These are deliberately NOT children
|
|
128
|
+
* of Pages: Pages is content, Templates/Sections/Widgets are structure.
|
|
129
|
+
*/
|
|
130
|
+
const TEMPLATES_CHILDREN: NavItem[] = [
|
|
131
|
+
{ path: '/saved-sections', label: 'Saved Sections', icon: Library },
|
|
132
|
+
{ path: '/widgets', label: 'Widgets', icon: LayoutGrid },
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Nav tree for the default (config-less) install. Dashboard sits on its
|
|
137
|
+
* own at the top, then a CONTENT group holding Pages (no children) above
|
|
138
|
+
* Posts (with All Posts + Post Types) and Templates (with Saved Sections
|
|
139
|
+
* + Widgets), followed by Media / Forms / SEO / Script Tags, then a
|
|
140
|
+
* SETTINGS group (Users / API Keys / Settings). The group ordering is
|
|
141
|
+
* meaningful — the renderer emits a section header the first time it
|
|
142
|
+
* sees a new group, so "CONTENT" renders once, above Pages.
|
|
115
143
|
*/
|
|
116
144
|
const defaultNavItems: NavItem[] = [
|
|
117
145
|
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
|
|
118
|
-
{ path: '/
|
|
146
|
+
{ path: '/pages', label: 'Pages', icon: File, group: 'Content' },
|
|
147
|
+
{ path: '/posts', label: 'Posts', icon: FileText, group: 'Content', children: POSTS_CHILDREN },
|
|
119
148
|
{
|
|
120
|
-
path: '/
|
|
121
|
-
label: '
|
|
122
|
-
icon:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{ path: '/page-templates', label: 'Templates', icon: Layers },
|
|
126
|
-
],
|
|
149
|
+
path: '/page-templates',
|
|
150
|
+
label: 'Templates',
|
|
151
|
+
icon: Layers,
|
|
152
|
+
group: 'Content',
|
|
153
|
+
children: TEMPLATES_CHILDREN,
|
|
127
154
|
},
|
|
128
155
|
{ path: '/media', label: 'Media', icon: Image, group: 'Content' },
|
|
129
156
|
{ path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
|
|
@@ -207,6 +234,15 @@ interface NavRenderContext {
|
|
|
207
234
|
|
|
208
235
|
function isPathActive(currentPath: string, itemPath: string): boolean {
|
|
209
236
|
if (itemPath === '/') return currentPath === '/'
|
|
237
|
+
|
|
238
|
+
// Special case: /posts is a prefix of /posts/types — without this
|
|
239
|
+
// guard, "All Posts" (/posts) would stay highlighted while the user
|
|
240
|
+
// is on /posts/types or /posts/types/new. Treat /posts as exact-only
|
|
241
|
+
// when a sibling /posts/* route exists.
|
|
242
|
+
if (itemPath === '/posts') {
|
|
243
|
+
return currentPath === '/posts' || currentPath === '/posts/new'
|
|
244
|
+
}
|
|
245
|
+
|
|
210
246
|
// Use boundary match so /pages doesn't match /pages-something. Exact, or
|
|
211
247
|
// followed by a `/` separator.
|
|
212
248
|
return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
|
|
@@ -236,10 +272,16 @@ function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
|
|
|
236
272
|
lastGroup = undefined
|
|
237
273
|
}
|
|
238
274
|
|
|
239
|
-
|
|
275
|
+
// Keys are namespaced because a parent and its first child can share
|
|
276
|
+
// the same path (e.g. the Posts parent and its "All Posts" child are
|
|
277
|
+
// both `/posts`). Using the raw path as the key collided and made
|
|
278
|
+
// React warn about — and risk dropping — duplicate siblings.
|
|
279
|
+
nodes.push(<NavLink key={`item:${item.path}`} item={item} ctx={ctx} />)
|
|
240
280
|
if (item.children && item.children.length > 0 && !ctx.collapsed) {
|
|
241
281
|
for (const child of item.children) {
|
|
242
|
-
nodes.push(
|
|
282
|
+
nodes.push(
|
|
283
|
+
<NavLink key={`child:${item.path}:${child.path}`} item={child} ctx={ctx} nested />,
|
|
284
|
+
)
|
|
243
285
|
}
|
|
244
286
|
}
|
|
245
287
|
}
|
|
@@ -315,24 +357,32 @@ export interface NavItem {
|
|
|
315
357
|
* destination (Media, Forms, SEO, Script Tags, Users, API Keys,
|
|
316
358
|
* Settings) is anchored under the right Content / Settings group.
|
|
317
359
|
*/
|
|
318
|
-
function buildNavItems(config: any): NavItem[] {
|
|
360
|
+
export function buildNavItems(config: any): NavItem[] {
|
|
319
361
|
if (!config?.collections) return defaultNavItems
|
|
320
362
|
|
|
321
363
|
const raw = config.collections
|
|
322
364
|
const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
|
|
323
365
|
const visible = collectionsList.filter((c) => !c.admin?.hidden)
|
|
324
366
|
|
|
367
|
+
// Treat `admin.group: 'Posts'` (case-insensitive) as a synonym for
|
|
368
|
+
// `type: 'post'` so consumers can tag arbitrary collections into
|
|
369
|
+
// the Posts area without changing the discriminator they already
|
|
370
|
+
// use elsewhere (sitemaps, schema.org, etc.).
|
|
371
|
+
const isPostsGroup = (c: any) =>
|
|
372
|
+
typeof c.admin?.group === 'string' && c.admin.group.toLowerCase() === 'posts'
|
|
325
373
|
const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
|
|
326
|
-
const posts = visible.filter((c) => c.type === 'post'
|
|
374
|
+
const posts = visible.filter((c) => c.type === 'post' || isPostsGroup(c))
|
|
327
375
|
const otherUngrouped = visible.filter(
|
|
328
376
|
(c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
|
|
329
377
|
)
|
|
330
|
-
|
|
378
|
+
// Anything explicitly grouped that isn't a posts collection.
|
|
379
|
+
const grouped = visible.filter((c) => c.admin?.group && !isPostsGroup(c) && c.type !== 'post')
|
|
331
380
|
|
|
332
381
|
const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]
|
|
333
382
|
|
|
334
|
-
//
|
|
335
|
-
|
|
383
|
+
// Ungrouped collections that are neither page- nor post-type stay at
|
|
384
|
+
// the top level, directly under Dashboard, before the CONTENT group.
|
|
385
|
+
for (const collection of otherUngrouped) {
|
|
336
386
|
items.push({
|
|
337
387
|
label: collection.labels?.plural ?? collection.slug,
|
|
338
388
|
path: `/${collection.slug}`,
|
|
@@ -340,37 +390,59 @@ function buildNavItems(config: any): NavItem[] {
|
|
|
340
390
|
})
|
|
341
391
|
}
|
|
342
392
|
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
393
|
+
// CONTENT group. Everything below shares `group: 'Content'`, so the
|
|
394
|
+
// renderer emits the "CONTENT" header once — above Pages. Order:
|
|
395
|
+
// Pages → Posts → static content destinations (Media / Forms / SEO /
|
|
396
|
+
// Script Tags).
|
|
397
|
+
|
|
398
|
+
// Pages-style collections render as plain Content entries (no nested
|
|
399
|
+
// children). Saved Sections / Widgets now live under the dedicated
|
|
400
|
+
// Templates parent below, not under Pages.
|
|
401
|
+
pages.forEach((collection) => {
|
|
402
|
+
items.push({
|
|
351
403
|
label: collection.labels?.plural ?? collection.slug,
|
|
352
404
|
path: `/${collection.slug}`,
|
|
353
405
|
icon: resolveIcon(collection),
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
item.children = [
|
|
357
|
-
{ path: '/saved-sections', label: 'Saved Sections', icon: Library },
|
|
358
|
-
{ path: '/page-templates', label: 'Templates', icon: Layers },
|
|
359
|
-
]
|
|
360
|
-
}
|
|
361
|
-
items.push(item)
|
|
406
|
+
group: 'Content',
|
|
407
|
+
})
|
|
362
408
|
})
|
|
363
409
|
|
|
364
|
-
|
|
410
|
+
// Posts area — collapses every post-type collection under a single
|
|
411
|
+
// "Posts" parent. The aggregated table at /posts and the type cards
|
|
412
|
+
// at /posts/types are the entry points; individual post-type
|
|
413
|
+
// collections are not enumerated in the sidebar to avoid the
|
|
414
|
+
// duplicate-nav problem when a project has many types.
|
|
415
|
+
if (posts.length > 0) {
|
|
365
416
|
items.push({
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
icon:
|
|
417
|
+
path: '/posts',
|
|
418
|
+
label: 'Posts',
|
|
419
|
+
icon: FileText,
|
|
420
|
+
group: 'Content',
|
|
421
|
+
children: POSTS_CHILDREN,
|
|
369
422
|
})
|
|
370
423
|
}
|
|
371
424
|
|
|
372
|
-
//
|
|
373
|
-
//
|
|
425
|
+
// Templates — its own parent holding Saved Sections + Widgets (page
|
|
426
|
+
// building structure, distinct from Pages content).
|
|
427
|
+
items.push({
|
|
428
|
+
path: '/page-templates',
|
|
429
|
+
label: 'Templates',
|
|
430
|
+
icon: Layers,
|
|
431
|
+
group: 'Content',
|
|
432
|
+
children: TEMPLATES_CHILDREN,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
// Standard Content destinations (always present — they map to CMS
|
|
436
|
+
// features, not collections).
|
|
437
|
+
items.push(
|
|
438
|
+
{ path: '/media', label: 'Media', icon: Image, group: 'Content' },
|
|
439
|
+
{ path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
|
|
440
|
+
{ path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
|
|
441
|
+
{ path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// Config-supplied custom groups land after the standard Content block
|
|
445
|
+
// and before Settings. Each renders under its own header.
|
|
374
446
|
const groupBuckets = new Map<string, NavItem[]>()
|
|
375
447
|
for (const col of grouped) {
|
|
376
448
|
const group = col.admin.group as string
|
|
@@ -385,14 +457,8 @@ function buildNavItems(config: any): NavItem[] {
|
|
|
385
457
|
}
|
|
386
458
|
for (const bucket of groupBuckets.values()) items.push(...bucket)
|
|
387
459
|
|
|
388
|
-
//
|
|
389
|
-
// present, regardless of which collections the admin configures,
|
|
390
|
-
// because they map to CMS features (not collections).
|
|
460
|
+
// Settings destinations — always last.
|
|
391
461
|
items.push(
|
|
392
|
-
{ path: '/media', label: 'Media', icon: Image, group: 'Content' },
|
|
393
|
-
{ path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
|
|
394
|
-
{ path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
|
|
395
|
-
{ path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
|
|
396
462
|
{ path: '/users', label: 'Users', icon: Users, group: 'Settings' },
|
|
397
463
|
{ path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
|
|
398
464
|
{ path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
|