@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,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/Header.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { Search,
|
|
3
|
+
import { useState, type ReactNode } from 'react'
|
|
4
|
+
import { Search, User, ChevronDown, Menu, Sun, Moon } from 'lucide-react'
|
|
5
5
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
6
6
|
import { CommandPalette } from '../components/CommandPalette.js'
|
|
7
|
+
import { NotificationBell } from '../components/NotificationBell.js'
|
|
7
8
|
import { useTheme } from '../components/ThemeProvider.js'
|
|
8
9
|
import { LocaleSwitcher } from '../components/LocaleSwitcher.js'
|
|
9
10
|
|
|
@@ -11,9 +12,21 @@ export interface HeaderProps {
|
|
|
11
12
|
onToggleSidebar: () => void
|
|
12
13
|
session?: any
|
|
13
14
|
onNavigate: (path: string) => void
|
|
15
|
+
/**
|
|
16
|
+
* Page-specific actions rendered on the far right of the top bar
|
|
17
|
+
* (after the search input + utility icons). Used to surface
|
|
18
|
+
* dashboard-level primary actions like "+ New Post" / "View Site"
|
|
19
|
+
* without forcing every view to render its own header.
|
|
20
|
+
*
|
|
21
|
+
* The Layout decides what to pass here based on `currentPath`.
|
|
22
|
+
* Keeping the slot on Header rather than per-view means the
|
|
23
|
+
* actions row stays consistent across navigation and never reflows
|
|
24
|
+
* on view-change.
|
|
25
|
+
*/
|
|
26
|
+
pageActions?: ReactNode
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
29
|
+
export function Header({ onToggleSidebar, session, onNavigate, pageActions }: HeaderProps) {
|
|
17
30
|
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
|
18
31
|
const [searchQuery, setSearchQuery] = useState('')
|
|
19
32
|
const { resolvedTheme, setTheme } = useTheme()
|
|
@@ -22,9 +35,14 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
22
35
|
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
const initial =
|
|
39
|
+
session?.user?.name?.charAt(0)?.toUpperCase() ??
|
|
40
|
+
session?.user?.email?.charAt(0)?.toUpperCase() ??
|
|
41
|
+
'A'
|
|
42
|
+
|
|
25
43
|
return (
|
|
26
44
|
<>
|
|
27
|
-
<header className="border-border bg-background flex h-14 items-center
|
|
45
|
+
<header className="border-border bg-background flex h-14 items-center gap-3 border-b px-4">
|
|
28
46
|
<button
|
|
29
47
|
onClick={onToggleSidebar}
|
|
30
48
|
className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
|
|
@@ -33,39 +51,50 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
33
51
|
<Menu className="text-foreground h-5 w-5" strokeWidth={2} />
|
|
34
52
|
</button>
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
{/* Search: takes available width on desktop so it visually anchors
|
|
55
|
+
the top bar like the reference design. */}
|
|
56
|
+
<div className="relative hidden flex-1 md:block">
|
|
57
|
+
<Search
|
|
58
|
+
className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
|
|
59
|
+
aria-hidden
|
|
60
|
+
/>
|
|
61
|
+
<input
|
|
62
|
+
type="text"
|
|
63
|
+
placeholder="Search..."
|
|
64
|
+
aria-label="Search content"
|
|
65
|
+
value={searchQuery}
|
|
66
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
67
|
+
onFocus={() => setShowCommandPalette(true)}
|
|
68
|
+
onKeyDown={(e) => {
|
|
69
|
+
if (e.key === 'Escape') (e.target as HTMLInputElement).blur()
|
|
70
|
+
}}
|
|
71
|
+
className="border-border bg-input-background text-foreground placeholder:text-muted-foreground focus:ring-ring h-9 w-full max-w-md rounded-lg border py-1.5 pr-14 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
|
|
72
|
+
/>
|
|
73
|
+
{/* Keyboard hint pill. Hidden on narrow widths to avoid overlap. */}
|
|
74
|
+
<span
|
|
75
|
+
className="border-border bg-background text-muted-foreground pointer-events-none absolute top-1/2 right-2 hidden -translate-y-1/2 rounded border px-1.5 py-0.5 font-mono text-[10px] sm:inline-flex"
|
|
76
|
+
aria-hidden
|
|
77
|
+
>
|
|
78
|
+
⌘K
|
|
79
|
+
</span>
|
|
38
80
|
</div>
|
|
39
81
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
value={searchQuery}
|
|
49
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
50
|
-
onFocus={() => setShowCommandPalette(true)}
|
|
51
|
-
className="border-border bg-input-background text-foreground focus:ring-ring w-64 rounded-lg border py-1.5 pr-3 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
|
|
52
|
-
/>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<button
|
|
56
|
-
onClick={() => setShowCommandPalette(true)}
|
|
57
|
-
className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
|
|
58
|
-
aria-label="Search"
|
|
59
|
-
>
|
|
60
|
-
<Search className="text-muted-foreground h-5 w-5" />
|
|
61
|
-
</button>
|
|
82
|
+
{/* Mobile search trigger (icon-only). */}
|
|
83
|
+
<button
|
|
84
|
+
onClick={() => setShowCommandPalette(true)}
|
|
85
|
+
className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
|
|
86
|
+
aria-label="Search"
|
|
87
|
+
>
|
|
88
|
+
<Search className="text-muted-foreground h-5 w-5" />
|
|
89
|
+
</button>
|
|
62
90
|
|
|
91
|
+
<div className="ml-auto flex items-center gap-1.5">
|
|
63
92
|
<LocaleSwitcher />
|
|
64
93
|
|
|
65
94
|
<button
|
|
66
95
|
onClick={toggleTheme}
|
|
67
96
|
className="hover:bg-accent rounded-lg p-2 transition-colors"
|
|
68
|
-
aria-label=
|
|
97
|
+
aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
69
98
|
>
|
|
70
99
|
{resolvedTheme === 'dark' ? (
|
|
71
100
|
<Sun className="text-muted-foreground h-5 w-5" />
|
|
@@ -74,23 +103,24 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
74
103
|
)}
|
|
75
104
|
</button>
|
|
76
105
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<Bell className="text-muted-foreground h-5 w-5" />
|
|
82
|
-
<span className="bg-destructive absolute top-1.5 right-1.5 h-2 w-2 rounded-full" />
|
|
83
|
-
</button>
|
|
106
|
+
{/* Real notification bell — replaces the previous inert decoration.
|
|
107
|
+
Renders its own dropdown panel, SSE-backed unread counter, and
|
|
108
|
+
click-to-mark-read flow. */}
|
|
109
|
+
<NotificationBell />
|
|
84
110
|
|
|
85
111
|
<DropdownMenu.Root>
|
|
86
112
|
<DropdownMenu.Trigger asChild>
|
|
87
|
-
<button
|
|
113
|
+
<button
|
|
114
|
+
className="hover:bg-accent flex items-center gap-1.5 rounded-lg p-1 transition-colors"
|
|
115
|
+
aria-label="Account menu"
|
|
116
|
+
>
|
|
88
117
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600">
|
|
89
|
-
<span className="text-sm font-medium text-white">
|
|
90
|
-
{session?.user?.name?.charAt(0)?.toUpperCase() ?? 'A'}
|
|
91
|
-
</span>
|
|
118
|
+
<span className="text-sm font-medium text-white">{initial}</span>
|
|
92
119
|
</div>
|
|
93
|
-
<ChevronDown
|
|
120
|
+
<ChevronDown
|
|
121
|
+
className="text-muted-foreground hidden h-4 w-4 sm:block"
|
|
122
|
+
aria-hidden
|
|
123
|
+
/>
|
|
94
124
|
</button>
|
|
95
125
|
</DropdownMenu.Trigger>
|
|
96
126
|
|
|
@@ -106,7 +136,10 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
106
136
|
{session?.user?.email ?? 'admin@example.com'}
|
|
107
137
|
</p>
|
|
108
138
|
</div>
|
|
109
|
-
<DropdownMenu.Item
|
|
139
|
+
<DropdownMenu.Item
|
|
140
|
+
className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
|
|
141
|
+
onSelect={() => onNavigate('/profile')}
|
|
142
|
+
>
|
|
110
143
|
<User className="h-4 w-4" />
|
|
111
144
|
Profile
|
|
112
145
|
</DropdownMenu.Item>
|
|
@@ -117,12 +150,26 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
117
150
|
Settings
|
|
118
151
|
</DropdownMenu.Item>
|
|
119
152
|
<DropdownMenu.Separator className="bg-border my-1 h-px" />
|
|
120
|
-
<DropdownMenu.Item
|
|
153
|
+
<DropdownMenu.Item
|
|
154
|
+
className="text-destructive hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
|
|
155
|
+
onSelect={() => onNavigate('/logout')}
|
|
156
|
+
>
|
|
121
157
|
Logout
|
|
122
158
|
</DropdownMenu.Item>
|
|
123
159
|
</DropdownMenu.Content>
|
|
124
160
|
</DropdownMenu.Portal>
|
|
125
161
|
</DropdownMenu.Root>
|
|
162
|
+
|
|
163
|
+
{/* Page actions slot — sits to the right of the utility icons so
|
|
164
|
+
the primary CTA is the rightmost element, mirroring the
|
|
165
|
+
reference. Hidden on mobile because narrow viewports lack the
|
|
166
|
+
room; pages can re-surface the same action in their hero
|
|
167
|
+
there. */}
|
|
168
|
+
{pageActions && (
|
|
169
|
+
<div className="border-border ml-2 hidden items-center gap-2 border-l pl-3 md:flex">
|
|
170
|
+
{pageActions}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
126
173
|
</div>
|
|
127
174
|
</header>
|
|
128
175
|
|
package/src/layout/Layout.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, type ReactNode } from 'react'
|
|
3
|
+
import { useMemo, useState, useEffect, type ReactNode } from 'react'
|
|
4
|
+
import { ExternalLink, Plus } from 'lucide-react'
|
|
4
5
|
import { Sidebar } from './Sidebar.js'
|
|
5
6
|
import { Header } from './Header.js'
|
|
6
7
|
import { Breadcrumbs } from '../components/Breadcrumbs.js'
|
|
@@ -37,6 +38,11 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
|
|
|
37
38
|
setMobileSidebarOpen(false)
|
|
38
39
|
}, [currentPath])
|
|
39
40
|
|
|
41
|
+
const pageActions = useMemo(
|
|
42
|
+
() => computePageActions({ currentPath, config, onNavigate }),
|
|
43
|
+
[currentPath, config, onNavigate],
|
|
44
|
+
)
|
|
45
|
+
|
|
40
46
|
return (
|
|
41
47
|
<>
|
|
42
48
|
<Toaster position="bottom-right" />
|
|
@@ -57,6 +63,7 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
|
|
|
57
63
|
onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
|
|
58
64
|
session={session}
|
|
59
65
|
onNavigate={onNavigate}
|
|
66
|
+
pageActions={pageActions}
|
|
60
67
|
/>
|
|
61
68
|
}
|
|
62
69
|
breadcrumbs={<Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />}
|
|
@@ -66,3 +73,161 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
|
|
|
66
73
|
</>
|
|
67
74
|
)
|
|
68
75
|
}
|
|
76
|
+
|
|
77
|
+
// ─── Page actions ────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
interface PageActionsContext {
|
|
80
|
+
currentPath: string
|
|
81
|
+
config: any
|
|
82
|
+
onNavigate: (path: string) => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Per-route primary actions that render on the right of the top bar.
|
|
87
|
+
*
|
|
88
|
+
* Centralised here so the top bar's actions row stays consistent
|
|
89
|
+
* across view changes — no flicker on navigation, no per-view header
|
|
90
|
+
* reimplementation. Views that need view-local actions (e.g. a
|
|
91
|
+
* "Save draft" button on the post editor) continue to render them
|
|
92
|
+
* inside their own hero; this slot is for the highest-level
|
|
93
|
+
* primary/secondary pair that mirrors the reference dashboard.
|
|
94
|
+
*
|
|
95
|
+
* Resolution rules:
|
|
96
|
+
* - Dashboard / home → "+ New Post" + "View Site"
|
|
97
|
+
* - List views → just "+ New <Type>"
|
|
98
|
+
* - Editor views → no top-bar action (editor renders its own toolbar)
|
|
99
|
+
*/
|
|
100
|
+
function computePageActions({
|
|
101
|
+
currentPath,
|
|
102
|
+
config,
|
|
103
|
+
onNavigate,
|
|
104
|
+
}: PageActionsContext): ReactNode | null {
|
|
105
|
+
const collections = collectionList(config)
|
|
106
|
+
const postCollections = collections.filter((c) => c.type === 'post')
|
|
107
|
+
const newPostPath = resolveNewPostPath(postCollections)
|
|
108
|
+
const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
109
|
+
|
|
110
|
+
const isDashboard = currentPath === '/' || currentPath === '/dashboard'
|
|
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.
|
|
115
|
+
if (isDashboard) {
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => onNavigate(newPostPath)}
|
|
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"
|
|
122
|
+
>
|
|
123
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
124
|
+
New Post
|
|
125
|
+
</button>
|
|
126
|
+
{siteUrl ? (
|
|
127
|
+
<a
|
|
128
|
+
href={siteUrl}
|
|
129
|
+
target="_blank"
|
|
130
|
+
rel="noopener noreferrer"
|
|
131
|
+
className="border-border bg-background text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
|
|
132
|
+
>
|
|
133
|
+
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
134
|
+
View Site
|
|
135
|
+
</a>
|
|
136
|
+
) : null}
|
|
137
|
+
</>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
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
|
+
|
|
187
|
+
// List views — surface "+ New X" so authors can keep creating without
|
|
188
|
+
// hunting for a per-list button. Skip on editor views; those have their
|
|
189
|
+
// own toolbar.
|
|
190
|
+
for (const c of collections) {
|
|
191
|
+
if (currentPath === `/${c.slug}`) {
|
|
192
|
+
return (
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={() => onNavigate(`/${c.slug}/new`)}
|
|
196
|
+
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"
|
|
197
|
+
>
|
|
198
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
199
|
+
New {c.labels?.singular ?? c.slug}
|
|
200
|
+
</button>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface CollectionMeta {
|
|
209
|
+
slug: string
|
|
210
|
+
type?: string
|
|
211
|
+
labels?: { singular?: string; plural?: string }
|
|
212
|
+
admin?: { hidden?: boolean }
|
|
213
|
+
}
|
|
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
|
+
|
|
228
|
+
function collectionList(config: any): CollectionMeta[] {
|
|
229
|
+
if (!config?.collections) return []
|
|
230
|
+
const raw = config.collections
|
|
231
|
+
const list: any[] = Array.isArray(raw) ? raw : Object.values(raw)
|
|
232
|
+
return list.filter((c) => !c?.admin?.hidden)
|
|
233
|
+
}
|