@actuate-media/cms-admin 0.64.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/__tests__/components/ui-primitives.test.d.ts +2 -0
- package/dist/__tests__/components/ui-primitives.test.d.ts.map +1 -0
- package/dist/__tests__/components/ui-primitives.test.js +45 -0
- package/dist/__tests__/components/ui-primitives.test.js.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +1 -1
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/SEOPanel.d.ts.map +1 -1
- package/dist/components/SEOPanel.js +2 -1
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/forms/primitives.d.ts.map +1 -1
- package/dist/components/forms/primitives.js +5 -8
- package/dist/components/forms/primitives.js.map +1 -1
- package/dist/components/posts/badges.d.ts.map +1 -1
- package/dist/components/posts/badges.js +7 -13
- package/dist/components/posts/badges.js.map +1 -1
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +1 -1
- package/dist/components/seo/RedirectEditorDrawer.js +2 -1
- package/dist/components/seo/RedirectEditorDrawer.js.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorDrawer.js +3 -2
- package/dist/components/seo/SeoEditorDrawer.js.map +1 -1
- package/dist/components/seo/primitives.d.ts.map +1 -1
- package/dist/components/seo/primitives.js +9 -14
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/FilterPill.d.ts +14 -0
- package/dist/components/ui/FilterPill.d.ts.map +1 -0
- package/dist/components/ui/FilterPill.js +8 -0
- package/dist/components/ui/FilterPill.js.map +1 -0
- package/dist/components/ui/RowActions.d.ts +19 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -0
- package/dist/components/ui/RowActions.js +16 -0
- package/dist/components/ui/RowActions.js.map +1 -0
- package/dist/components/ui/StatPill.d.ts +18 -0
- package/dist/components/ui/StatPill.d.ts.map +1 -0
- package/dist/components/ui/StatPill.js +5 -0
- package/dist/components/ui/StatPill.js.map +1 -0
- package/dist/components/ui/StatusBadge.d.ts +25 -0
- package/dist/components/ui/StatusBadge.d.ts.map +1 -0
- package/dist/components/ui/StatusBadge.js +41 -0
- package/dist/components/ui/StatusBadge.js.map +1 -0
- package/dist/components/ui/index.d.ts +8 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +4 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/PropInput.d.ts.map +1 -1
- package/dist/fields/PropInput.js +2 -1
- package/dist/fields/PropInput.js.map +1 -1
- package/dist/fields/ToggleField.d.ts.map +1 -1
- package/dist/fields/ToggleField.js +2 -1
- package/dist/fields/ToggleField.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.js +8 -38
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -1
- package/dist/layout/primitives/PageContainer.js +1 -1
- package/dist/layout/primitives/PageContainer.js.map +1 -1
- package/dist/lib/pages-service.d.ts +9 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +14 -0
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/posts-service.d.ts +11 -0
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +13 -0
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +3 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +9 -1
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/FormEditor.d.ts.map +1 -1
- package/dist/views/FormEditor.js +2 -1
- package/dist/views/FormEditor.js.map +1 -1
- package/dist/views/FormEntries.js +1 -1
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.js +1 -1
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +1 -1
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +30 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -1
- package/dist/views/Pages/PageTagManager.js +1 -1
- package/dist/views/Pages/PageTagManager.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +59 -10
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages.js +1 -1
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -1
- package/dist/views/Posts/NewPostPicker.js +1 -1
- package/dist/views/Posts/NewPostPicker.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +66 -9
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Redirects.js +1 -1
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/SEO.js +1 -1
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTagEditor.d.ts.map +1 -1
- package/dist/views/ScriptTagEditor.js +1 -1
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +2 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.js +1 -1
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +2 -6
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +2 -2
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/NodeSettings.js +3 -3
- package/dist/views/page-builder/NodeSettings.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +1 -1
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-builder/SavedSections.js +2 -2
- package/dist/views/page-builder/SavedSections.js.map +1 -1
- package/dist/views/page-builder/Widgets.js +1 -1
- package/dist/views/page-builder/Widgets.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +2 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +2 -1
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
- package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +15 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +2 -1
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/components/ui-primitives.test.tsx +58 -0
- package/src/__tests__/views/updates-tab.render.test.tsx +2 -1
- package/src/components/SEOPanel.tsx +2 -11
- package/src/components/forms/primitives.tsx +6 -16
- package/src/components/posts/badges.tsx +8 -21
- package/src/components/seo/RedirectEditorDrawer.tsx +12 -16
- package/src/components/seo/SeoEditorDrawer.tsx +16 -24
- package/src/components/seo/primitives.tsx +10 -27
- package/src/components/ui/FilterPill.tsx +43 -0
- package/src/components/ui/RowActions.tsx +50 -0
- package/src/components/ui/StatPill.tsx +37 -0
- package/src/components/ui/StatusBadge.tsx +66 -0
- package/src/components/ui/index.ts +8 -0
- package/src/fields/PropInput.tsx +6 -15
- package/src/fields/ToggleField.tsx +3 -15
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/layout/primitives/PageContainer.tsx +1 -5
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +48 -18
- package/src/views/FormEditor.tsx +6 -9
- package/src/views/FormEntries.tsx +1 -1
- package/src/views/FormSubmissions.tsx +1 -1
- package/src/views/Forms.tsx +1 -1
- package/src/views/MediaBrowser.tsx +36 -8
- package/src/views/Pages/PageTagManager.tsx +1 -3
- package/src/views/Pages/PagesListView.tsx +115 -10
- package/src/views/Pages.tsx +1 -1
- package/src/views/Posts/NewPostPicker.tsx +1 -3
- package/src/views/Posts/NewPostTypeInstructions.tsx +1 -3
- package/src/views/Posts/PostTypesView.tsx +1 -3
- package/src/views/Posts/PostsListView.tsx +135 -10
- package/src/views/Profile.tsx +1 -1
- package/src/views/Redirects.tsx +1 -1
- package/src/views/SEO.tsx +1 -1
- package/src/views/ScriptTagEditor.tsx +1 -3
- package/src/views/ScriptTags.tsx +17 -1
- package/src/views/Settings.tsx +1 -1
- package/src/views/Users.tsx +6 -18
- package/src/views/page-builder/BlockEditor.tsx +3 -6
- package/src/views/page-builder/NodeSettings.tsx +7 -16
- package/src/views/page-builder/PageTemplates.tsx +1 -1
- package/src/views/page-builder/SavedSections.tsx +2 -2
- package/src/views/page-builder/Widgets.tsx +1 -1
- package/src/views/page-editor/SectionInspector.tsx +9 -21
- package/src/views/post-editor/PostHeaderPanel.tsx +10 -20
- package/src/views/seo/TechnicalTab.tsx +15 -0
- package/src/views/settings/components.tsx +8 -16
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import { cv, type VariantProps } from '../../lib/cv.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical status chip from the design system (`.status` + `.status-dot`):
|
|
6
|
+
* a rectangular 5px-radius chip with a 5px dot, 11.5px medium text, and
|
|
7
|
+
* tint backgrounds per tone. This is the single source of truth for
|
|
8
|
+
* document / entity statuses across Posts, Pages, Forms, SEO, and tables —
|
|
9
|
+
* do not roll per-view status pills.
|
|
10
|
+
*/
|
|
11
|
+
const statusBadge = cv(
|
|
12
|
+
'inline-flex items-center gap-[5px] rounded-[5px] px-[9px] py-[3px] text-[length:var(--fs-hint)] font-medium whitespace-nowrap',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
tone: {
|
|
16
|
+
/** Published / Active — success tint (`.s-pub`). */
|
|
17
|
+
success: 'bg-[var(--suc-l)] text-[var(--suc)]',
|
|
18
|
+
/** Scheduled / Pending — warning tint (`.s-sch`). */
|
|
19
|
+
warning: 'bg-[var(--warn-l)] text-[var(--warn)]',
|
|
20
|
+
/** Errors / failing states. */
|
|
21
|
+
danger: 'bg-[var(--err-l)] text-[var(--err)]',
|
|
22
|
+
/** Informational states. */
|
|
23
|
+
info: 'bg-[var(--info-l)] text-[var(--info)]',
|
|
24
|
+
/** Draft — page background + border (`.s-dft`). */
|
|
25
|
+
neutral: 'border border-[var(--bdr)] bg-[var(--bg)] text-[var(--sub)]',
|
|
26
|
+
/** Archived — dimmest treatment (`.s-arc`). */
|
|
27
|
+
muted: 'border border-[var(--bdr)] bg-[var(--bg)] text-[var(--muted)]',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { tone: 'neutral' },
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const DOT_CLASS: Record<NonNullable<StatusBadgeVariants['tone']>, string> = {
|
|
35
|
+
success: 'bg-[var(--suc)]',
|
|
36
|
+
warning: 'bg-[var(--warn)]',
|
|
37
|
+
danger: 'bg-[var(--err)]',
|
|
38
|
+
info: 'bg-[var(--info)]',
|
|
39
|
+
neutral: 'bg-[var(--muted)]',
|
|
40
|
+
muted: 'bg-[var(--muted)]',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StatusBadgeVariants = VariantProps<typeof statusBadge>
|
|
44
|
+
export type StatusBadgeTone = NonNullable<StatusBadgeVariants['tone']>
|
|
45
|
+
|
|
46
|
+
export interface StatusBadgeProps extends HTMLAttributes<HTMLSpanElement>, StatusBadgeVariants {
|
|
47
|
+
/** Hide the leading dot (rare — e.g. numeric chips). */
|
|
48
|
+
dot?: boolean
|
|
49
|
+
children: ReactNode
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function StatusBadge({ tone, dot = true, className, children, ...rest }: StatusBadgeProps) {
|
|
53
|
+
return (
|
|
54
|
+
<span className={statusBadge({ tone, class: className })} {...rest}>
|
|
55
|
+
{dot && (
|
|
56
|
+
<span
|
|
57
|
+
aria-hidden
|
|
58
|
+
className={`h-[5px] w-[5px] shrink-0 rounded-full ${DOT_CLASS[tone ?? 'neutral']}`}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
{children}
|
|
62
|
+
</span>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { statusBadge as statusBadgeVariants }
|
|
@@ -36,6 +36,14 @@ export { SearchInput } from './SearchInput.js'
|
|
|
36
36
|
export type { SearchInputProps } from './SearchInput.js'
|
|
37
37
|
export { Toggle, toggleTrackVariants, toggleThumbVariants } from './Toggle.js'
|
|
38
38
|
export type { ToggleProps, ToggleVariants } from './Toggle.js'
|
|
39
|
+
export { StatusBadge, statusBadgeVariants } from './StatusBadge.js'
|
|
40
|
+
export type { StatusBadgeProps, StatusBadgeVariants, StatusBadgeTone } from './StatusBadge.js'
|
|
41
|
+
export { FilterPill } from './FilterPill.js'
|
|
42
|
+
export type { FilterPillProps } from './FilterPill.js'
|
|
43
|
+
export { StatPill } from './StatPill.js'
|
|
44
|
+
export type { StatPillProps } from './StatPill.js'
|
|
45
|
+
export { RowActions, RowActionButton } from './RowActions.js'
|
|
46
|
+
export type { RowActionButtonProps } from './RowActions.js'
|
|
39
47
|
export { formControlBase, formControlState } from './form-control.js'
|
|
40
48
|
export { CommandPalette } from './CommandPalette.js'
|
|
41
49
|
export type { CommandPaletteProps } from './CommandPalette.js'
|
package/src/fields/PropInput.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
safeJsonStringify,
|
|
39
39
|
switchUnionVariant,
|
|
40
40
|
} from './component-block-helpers.js'
|
|
41
|
+
import { Toggle } from '../components/ui/Toggle.js'
|
|
41
42
|
|
|
42
43
|
export { defaultForType }
|
|
43
44
|
|
|
@@ -249,21 +250,11 @@ function BooleanInput({
|
|
|
249
250
|
return (
|
|
250
251
|
<div className="flex items-center justify-between text-sm">
|
|
251
252
|
<span>{label}</span>
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
aria-
|
|
256
|
-
|
|
257
|
-
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
|
|
258
|
-
boolVal ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'
|
|
259
|
-
}`}
|
|
260
|
-
>
|
|
261
|
-
<span
|
|
262
|
-
className={`bg-card pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm ring-0 transition-transform ${
|
|
263
|
-
boolVal ? 'translate-x-5' : 'translate-x-0'
|
|
264
|
-
}`}
|
|
265
|
-
/>
|
|
266
|
-
</button>
|
|
253
|
+
<Toggle
|
|
254
|
+
checked={boolVal}
|
|
255
|
+
onCheckedChange={(v) => onChange(v)}
|
|
256
|
+
aria-label={typeof label === 'string' ? label : undefined}
|
|
257
|
+
/>
|
|
267
258
|
</div>
|
|
268
259
|
)
|
|
269
260
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Toggle } from '../components/ui/Toggle.js'
|
|
4
|
+
|
|
3
5
|
export interface ToggleFieldProps {
|
|
4
6
|
label: string
|
|
5
7
|
value?: boolean
|
|
@@ -14,21 +16,7 @@ export function ToggleField({ label, value = false, onChange, helpText }: Toggle
|
|
|
14
16
|
<span className="text-sm font-medium">{label}</span>
|
|
15
17
|
{helpText && <p className="text-xs text-[var(--muted-foreground)]">{helpText}</p>}
|
|
16
18
|
</div>
|
|
17
|
-
<
|
|
18
|
-
type="button"
|
|
19
|
-
role="switch"
|
|
20
|
-
aria-checked={value}
|
|
21
|
-
onClick={() => onChange(!value)}
|
|
22
|
-
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
|
|
23
|
-
value ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'
|
|
24
|
-
}`}
|
|
25
|
-
>
|
|
26
|
-
<span
|
|
27
|
-
className={`bg-card pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm ring-0 transition-transform ${
|
|
28
|
-
value ? 'translate-x-5' : 'translate-x-0'
|
|
29
|
-
}`}
|
|
30
|
-
/>
|
|
31
|
-
</button>
|
|
19
|
+
<Toggle checked={value} onCheckedChange={onChange} aria-label={label} />
|
|
32
20
|
</div>
|
|
33
21
|
)
|
|
34
22
|
}
|
package/src/layout/Header.tsx
CHANGED
|
@@ -127,8 +127,8 @@ export function Header({
|
|
|
127
127
|
className="hover:bg-accent flex items-center gap-1.5 rounded-lg p-1 transition-colors"
|
|
128
128
|
aria-label="Account menu"
|
|
129
129
|
>
|
|
130
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-
|
|
131
|
-
<span className="text-
|
|
130
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--acc)]">
|
|
131
|
+
<span className="text-xs font-semibold text-white">{initial}</span>
|
|
132
132
|
</div>
|
|
133
133
|
<ChevronDown
|
|
134
134
|
className="text-muted-foreground hidden h-4 w-4 sm:block"
|
package/src/layout/Layout.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState, useEffect, type ReactNode } from 'react'
|
|
4
|
-
import {
|
|
4
|
+
import { Plus } from 'lucide-react'
|
|
5
5
|
import { Sidebar } from './Sidebar.js'
|
|
6
6
|
import { Header } from './Header.js'
|
|
7
7
|
import { Breadcrumbs } from '../components/Breadcrumbs.js'
|
|
@@ -102,8 +102,9 @@ interface PageActionsContext {
|
|
|
102
102
|
* primary/secondary pair that mirrors the reference dashboard.
|
|
103
103
|
*
|
|
104
104
|
* Resolution rules:
|
|
105
|
-
* - Dashboard /
|
|
106
|
-
*
|
|
105
|
+
* - Dashboard / Posts / Pages → nothing here; those views render their
|
|
106
|
+
* hero CTAs inside the page header per the design
|
|
107
|
+
* - Other list views → just "+ New <Type>"
|
|
107
108
|
* - Editor views → no top-bar action (editor renders its own toolbar)
|
|
108
109
|
*/
|
|
109
110
|
function computePageActions({
|
|
@@ -112,56 +113,12 @@ function computePageActions({
|
|
|
112
113
|
onNavigate,
|
|
113
114
|
}: PageActionsContext): ReactNode | null {
|
|
114
115
|
const collections = collectionList(config)
|
|
115
|
-
const postCollections = collections.filter((c) => c.type === 'post')
|
|
116
|
-
const newPostPath = resolveNewPostPath(postCollections)
|
|
117
|
-
const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
118
116
|
|
|
117
|
+
// Dashboard, /posts, and /pages render their own hero CTAs in the page
|
|
118
|
+
// header (design `.page-hdr` → `.hdr-r`), so the top bar stays empty there.
|
|
119
119
|
const isDashboard = currentPath === '/' || currentPath === '/dashboard'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// a single Post Type we route straight to its editor; with several we
|
|
123
|
-
// route to the picker so the user can choose the type first.
|
|
124
|
-
if (isDashboard) {
|
|
125
|
-
return (
|
|
126
|
-
<>
|
|
127
|
-
<button
|
|
128
|
-
type="button"
|
|
129
|
-
onClick={() => onNavigate(newPostPath)}
|
|
130
|
-
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"
|
|
131
|
-
>
|
|
132
|
-
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
133
|
-
New Post
|
|
134
|
-
</button>
|
|
135
|
-
{siteUrl ? (
|
|
136
|
-
<a
|
|
137
|
-
href={siteUrl}
|
|
138
|
-
target="_blank"
|
|
139
|
-
rel="noopener noreferrer"
|
|
140
|
-
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"
|
|
141
|
-
>
|
|
142
|
-
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
143
|
-
View Site
|
|
144
|
-
</a>
|
|
145
|
-
) : null}
|
|
146
|
-
</>
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Posts area — "+ New Post" + "Manage types" on the list page;
|
|
151
|
-
// "+ New Post Type" on the types page. The buttons live in the top
|
|
152
|
-
// bar so the page header stays clean. See the dashboard rule for
|
|
153
|
-
// why these come from a single config-aware helper.
|
|
154
|
-
if (currentPath === '/posts') {
|
|
155
|
-
return (
|
|
156
|
-
<button
|
|
157
|
-
type="button"
|
|
158
|
-
onClick={() => onNavigate(newPostPath)}
|
|
159
|
-
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"
|
|
160
|
-
>
|
|
161
|
-
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
162
|
-
New Post
|
|
163
|
-
</button>
|
|
164
|
-
)
|
|
120
|
+
if (isDashboard || currentPath === '/posts' || currentPath === '/pages') {
|
|
121
|
+
return null
|
|
165
122
|
}
|
|
166
123
|
|
|
167
124
|
if (currentPath === '/posts/types') {
|
|
@@ -177,22 +134,6 @@ function computePageActions({
|
|
|
177
134
|
)
|
|
178
135
|
}
|
|
179
136
|
|
|
180
|
-
// Pages list — "+ New page" routes to the page creation flow. Handled
|
|
181
|
-
// explicitly (not via the collection loop) so the label/route stay
|
|
182
|
-
// exact even though `pages` is a reserved route.
|
|
183
|
-
if (currentPath === '/pages') {
|
|
184
|
-
return (
|
|
185
|
-
<button
|
|
186
|
-
type="button"
|
|
187
|
-
onClick={() => onNavigate('/pages/new')}
|
|
188
|
-
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"
|
|
189
|
-
>
|
|
190
|
-
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
191
|
-
New page
|
|
192
|
-
</button>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
137
|
// List views — surface "+ New X" so authors can keep creating without
|
|
197
138
|
// hunting for a per-list button. Skip on editor views; those have their
|
|
198
139
|
// own toolbar.
|
|
@@ -221,19 +162,6 @@ interface CollectionMeta {
|
|
|
221
162
|
admin?: { hidden?: boolean }
|
|
222
163
|
}
|
|
223
164
|
|
|
224
|
-
/**
|
|
225
|
-
* Where "+ New Post" should land, given the configured post-type
|
|
226
|
-
* collections:
|
|
227
|
-
* - exactly one → straight to that type's editor (`/<slug>/new`)
|
|
228
|
-
* - more than one → the type picker (`/posts/new`)
|
|
229
|
-
* - none → the Post Type setup instructions
|
|
230
|
-
*/
|
|
231
|
-
function resolveNewPostPath(postCollections: CollectionMeta[]): string {
|
|
232
|
-
if (postCollections.length === 1) return `/${postCollections[0]!.slug}/new`
|
|
233
|
-
if (postCollections.length > 1) return '/posts/new'
|
|
234
|
-
return '/posts/types/new'
|
|
235
|
-
}
|
|
236
|
-
|
|
237
165
|
function collectionList(config: any): CollectionMeta[] {
|
|
238
166
|
if (!config?.collections) return []
|
|
239
167
|
const raw = config.collections
|
|
@@ -78,11 +78,7 @@ export function PageContainer({
|
|
|
78
78
|
<header className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
79
79
|
{(title || description) && (
|
|
80
80
|
<div className="min-w-0">
|
|
81
|
-
{title &&
|
|
82
|
-
<h1 className="text-foreground truncate text-2xl font-medium tracking-tight">
|
|
83
|
-
{title}
|
|
84
|
-
</h1>
|
|
85
|
-
)}
|
|
81
|
+
{title && <h1 className="text-foreground truncate">{title}</h1>}
|
|
86
82
|
{description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
|
|
87
83
|
</div>
|
|
88
84
|
)}
|
package/src/lib/pages-service.ts
CHANGED
|
@@ -510,6 +510,24 @@ export async function bulkUpdatePages(
|
|
|
510
510
|
})
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
/** Duplicate a single page. Server resets status to DRAFT and appends "(Copy)". */
|
|
514
|
+
export async function duplicatePage(id: string): Promise<{ id?: string; error?: string }> {
|
|
515
|
+
const res = await cmsApi<DocumentApiRow>(
|
|
516
|
+
`/collections/${PAGES_COLLECTION}/${encodeURIComponent(id)}/duplicate`,
|
|
517
|
+
{ method: 'POST' },
|
|
518
|
+
)
|
|
519
|
+
if (res.error) return { error: res.error }
|
|
520
|
+
return { id: res.data?.id }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Soft-delete a single page. */
|
|
524
|
+
export async function deletePage(id: string): Promise<{ error?: string }> {
|
|
525
|
+
const res = await cmsApi(`/collections/${PAGES_COLLECTION}/${encodeURIComponent(id)}`, {
|
|
526
|
+
method: 'DELETE',
|
|
527
|
+
})
|
|
528
|
+
return res.error ? { error: res.error } : {}
|
|
529
|
+
}
|
|
530
|
+
|
|
513
531
|
export async function bulkDeletePages(ids: string[]): Promise<BulkResult> {
|
|
514
532
|
return mapWithConcurrency(ids, 5, async (id) => {
|
|
515
533
|
const res = await cmsApi(`/collections/${PAGES_COLLECTION}/${encodeURIComponent(id)}`, {
|
package/src/lib/posts-service.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface Post {
|
|
|
50
50
|
publishDate: string | null
|
|
51
51
|
updatedDate: string
|
|
52
52
|
seoScore: number | null
|
|
53
|
+
/** Featured-image URL for the list thumbnail, when the document has one. */
|
|
54
|
+
imageUrl: string | null
|
|
53
55
|
createdAt: string
|
|
54
56
|
updatedAt: string
|
|
55
57
|
}
|
|
@@ -151,6 +153,7 @@ interface PostsApiPost {
|
|
|
151
153
|
createdAt: string
|
|
152
154
|
updatedAt: string
|
|
153
155
|
seoScore: number | null
|
|
156
|
+
image?: string | null
|
|
154
157
|
author: { id: string; name: string } | null
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -240,6 +243,7 @@ export async function fetchPosts(query: PostsQuery = {}): Promise<PostsResult> {
|
|
|
240
243
|
publishDate: d.publishedAt ?? d.scheduledAt ?? null,
|
|
241
244
|
updatedDate: pickDisplayDate(d),
|
|
242
245
|
seoScore: d.seoScore,
|
|
246
|
+
imageUrl: d.image ?? null,
|
|
243
247
|
createdAt: d.createdAt,
|
|
244
248
|
updatedAt: d.updatedAt,
|
|
245
249
|
}
|
|
@@ -282,6 +286,28 @@ export async function bulkUpdatePosts(
|
|
|
282
286
|
return { ok, failed }
|
|
283
287
|
}
|
|
284
288
|
|
|
289
|
+
/** Duplicate a single post. Server resets status to DRAFT and appends "(Copy)". */
|
|
290
|
+
export async function duplicatePost(
|
|
291
|
+
postTypeId: string,
|
|
292
|
+
id: string,
|
|
293
|
+
): Promise<{ id?: string; error?: string }> {
|
|
294
|
+
const res = await cmsApi<{ id?: string }>(
|
|
295
|
+
`/collections/${encodeURIComponent(postTypeId)}/${encodeURIComponent(id)}/duplicate`,
|
|
296
|
+
{ method: 'POST' },
|
|
297
|
+
)
|
|
298
|
+
if (res.error) return { error: res.error }
|
|
299
|
+
return { id: res.data?.id }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Soft-delete a single post. */
|
|
303
|
+
export async function deletePost(postTypeId: string, id: string): Promise<{ error?: string }> {
|
|
304
|
+
const res = await cmsApi(
|
|
305
|
+
`/collections/${encodeURIComponent(postTypeId)}/${encodeURIComponent(id)}`,
|
|
306
|
+
{ method: 'DELETE' },
|
|
307
|
+
)
|
|
308
|
+
return res.error ? { error: res.error } : {}
|
|
309
|
+
}
|
|
310
|
+
|
|
285
311
|
export async function bulkDeletePosts(
|
|
286
312
|
ids: Array<{ postTypeId: string; id: string }>,
|
|
287
313
|
): Promise<{ ok: number; failed: number }> {
|
package/src/lib/seo-service.ts
CHANGED
|
@@ -635,6 +635,9 @@ export interface CrawlSettings {
|
|
|
635
635
|
indexEntireSite: boolean
|
|
636
636
|
includeImagesInSitemap: boolean
|
|
637
637
|
includePostsInSitemap: boolean
|
|
638
|
+
sitemapEnabled: boolean
|
|
639
|
+
autoRegenerateSitemapOnPublish: boolean
|
|
640
|
+
pingSearchEnginesOnPublish: boolean
|
|
638
641
|
}
|
|
639
642
|
|
|
640
643
|
export async function fetchSitemapStatus(): Promise<SitemapStatus | null> {
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -287,6 +287,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
287
287
|
|
|
288
288
|
const collections = useMemo(() => resolveCollections(config), [config])
|
|
289
289
|
|
|
290
|
+
// Hero CTA targets (mirrors the quick-action "New Post" resolution).
|
|
291
|
+
const heroNewPostPath = useMemo(() => {
|
|
292
|
+
const postTypes = collections.filter((c) => c.type === 'post')
|
|
293
|
+
if (postTypes.length === 1) return `/${postTypes[0]!.slug}/new`
|
|
294
|
+
return postTypes.length > 1 ? '/posts/new' : '/posts/types/new'
|
|
295
|
+
}, [collections])
|
|
296
|
+
const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
|
|
297
|
+
|
|
290
298
|
// ── Stat cards ──────────────────────────────────────────────────────────
|
|
291
299
|
const statCards: StatCardData[] = useMemo(() => {
|
|
292
300
|
const counts = stats?.collectionCounts ?? {}
|
|
@@ -561,24 +569,46 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
561
569
|
)}
|
|
562
570
|
|
|
563
571
|
{/* Hero greeting ─────────────────────────────────────────────────
|
|
564
|
-
|
|
565
|
-
(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
572
|
+
Per the design, the primary "+ New Post" and "View Site" actions
|
|
573
|
+
sit on the right of the hero (`.dash-hdr` → `.dh-r`), not in the
|
|
574
|
+
top bar. The quick-action row below keeps the wider authoring
|
|
575
|
+
shortcuts. */}
|
|
576
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
577
|
+
<div className="flex flex-col gap-1 sm:gap-0.5">
|
|
578
|
+
<h1 className="text-foreground">
|
|
579
|
+
{greeting}, {userName} <span aria-hidden>👋</span>
|
|
580
|
+
</h1>
|
|
581
|
+
<p className="text-muted-foreground text-sm">
|
|
582
|
+
{dateStr}
|
|
583
|
+
{totalIssues > 0 && (
|
|
584
|
+
<span className="hidden sm:inline">
|
|
585
|
+
{' · '}
|
|
586
|
+
{totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
|
|
587
|
+
</span>
|
|
588
|
+
)}
|
|
589
|
+
</p>
|
|
590
|
+
</div>
|
|
591
|
+
<div className="flex items-center gap-2">
|
|
592
|
+
<button
|
|
593
|
+
type="button"
|
|
594
|
+
onClick={() => nav(heroNewPostPath)}
|
|
595
|
+
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"
|
|
596
|
+
>
|
|
597
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
598
|
+
New Post
|
|
599
|
+
</button>
|
|
600
|
+
{siteUrl ? (
|
|
601
|
+
<a
|
|
602
|
+
href={siteUrl}
|
|
603
|
+
target="_blank"
|
|
604
|
+
rel="noopener noreferrer"
|
|
605
|
+
className="border-border bg-card 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"
|
|
606
|
+
>
|
|
607
|
+
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
608
|
+
View Site
|
|
609
|
+
</a>
|
|
610
|
+
) : null}
|
|
611
|
+
</div>
|
|
582
612
|
</div>
|
|
583
613
|
|
|
584
614
|
{/* Quick actions ──────────────────────────────────────────────── */}
|
package/src/views/FormEditor.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from 'lucide-react'
|
|
17
17
|
import { useApiData } from '../lib/useApiData.js'
|
|
18
18
|
import { cmsApi } from '../lib/api.js'
|
|
19
|
+
import { Toggle } from '../components/ui/Toggle.js'
|
|
19
20
|
|
|
20
21
|
interface FormField {
|
|
21
22
|
id: string
|
|
@@ -463,15 +464,11 @@ export function FormEditor({ formId, onNavigate }: FormEditorProps) {
|
|
|
463
464
|
{expandedSection === 'analytics' && (
|
|
464
465
|
<div className="border-border border-t p-4">
|
|
465
466
|
<div className="mb-4 flex items-center gap-3">
|
|
466
|
-
<
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
className="peer sr-only"
|
|
472
|
-
/>
|
|
473
|
-
<div className="bg-muted peer peer-checked:bg-primary peer-focus:ring-ring/50 after:border-border after:bg-card h-5 w-9 rounded-full peer-focus:ring-2 peer-focus:outline-none after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:border after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white" />
|
|
474
|
-
</label>
|
|
467
|
+
<Toggle
|
|
468
|
+
checked={analytics.enabled}
|
|
469
|
+
onCheckedChange={(v) => setAnalytics((a) => ({ ...a, enabled: v }))}
|
|
470
|
+
aria-label="Push events to Google Analytics 4"
|
|
471
|
+
/>
|
|
475
472
|
<span className="text-foreground text-sm font-medium">
|
|
476
473
|
Push events to Google Analytics 4
|
|
477
474
|
</span>
|
|
@@ -234,7 +234,7 @@ export function FormEntries({ onNavigate }: FormEntriesProps) {
|
|
|
234
234
|
<div className="space-y-5 p-6">
|
|
235
235
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
236
236
|
<div>
|
|
237
|
-
<h1 className="text-foreground
|
|
237
|
+
<h1 className="text-foreground">All Entries</h1>
|
|
238
238
|
<p className="text-muted-foreground mt-1 text-sm">
|
|
239
239
|
{total} {total === 1 ? 'submission' : 'submissions'} across your forms
|
|
240
240
|
</p>
|
|
@@ -254,7 +254,7 @@ export function FormSubmissions({ formId, onNavigate }: FormSubmissionsProps) {
|
|
|
254
254
|
</button>
|
|
255
255
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
256
256
|
<div>
|
|
257
|
-
<h1 className="text-foreground
|
|
257
|
+
<h1 className="text-foreground">
|
|
258
258
|
{formName ? `${formName} — Submissions` : 'Submissions'}
|
|
259
259
|
</h1>
|
|
260
260
|
<p className="text-muted-foreground mt-1 text-sm">
|
package/src/views/Forms.tsx
CHANGED
|
@@ -158,7 +158,7 @@ export function Forms({ onNavigate }: FormsProps) {
|
|
|
158
158
|
<div className="space-y-5 p-6">
|
|
159
159
|
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
160
160
|
<div>
|
|
161
|
-
<h1 className="text-foreground
|
|
161
|
+
<h1 className="text-foreground">Forms</h1>
|
|
162
162
|
<p className="text-muted-foreground mt-1 text-sm">
|
|
163
163
|
Manage forms and view incoming submissions
|
|
164
164
|
</p>
|
|
@@ -30,6 +30,8 @@ import { sortByRelevance, type SortConfig, toggleSort } from '../lib/search.js'
|
|
|
30
30
|
import { useApiData } from '../lib/useApiData.js'
|
|
31
31
|
import { cmsApi } from '../lib/api.js'
|
|
32
32
|
import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
33
|
+
import { StatusBadge } from '../components/ui/StatusBadge.js'
|
|
34
|
+
import { StatPill } from '../components/ui/StatPill.js'
|
|
33
35
|
|
|
34
36
|
interface MediaItem {
|
|
35
37
|
id: number | string
|
|
@@ -62,6 +64,14 @@ function matchesMediaType(item: MediaItem, filterType: string): boolean {
|
|
|
62
64
|
return item.type === filterType
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function formatStorage(bytes: number): string {
|
|
68
|
+
if (bytes <= 0) return '0 B'
|
|
69
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
70
|
+
const i = Math.min(units.length - 1, Math.floor(Math.log2(bytes) / 10))
|
|
71
|
+
const value = bytes / 1024 ** i
|
|
72
|
+
return `${value >= 100 || i === 0 ? Math.round(value) : value.toFixed(1)} ${units[i]}`
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
function MediaPreview({ item }: { item: MediaItem }) {
|
|
66
76
|
if (isImageMedia(item)) {
|
|
67
77
|
return (
|
|
@@ -149,6 +159,22 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
149
159
|
return results
|
|
150
160
|
}, [mediaItems, searchQuery, filterType, sortConfig])
|
|
151
161
|
|
|
162
|
+
// Header stat pills (design `.mc-stats`): counts over the current folder
|
|
163
|
+
// scope, before search/type filters, so the numbers describe the library.
|
|
164
|
+
const mediaStats = useMemo(() => {
|
|
165
|
+
let images = 0
|
|
166
|
+
let videos = 0
|
|
167
|
+
let documents = 0
|
|
168
|
+
let bytes = 0
|
|
169
|
+
for (const item of mediaItems) {
|
|
170
|
+
if (item.type?.startsWith('image/') || isImageMedia(item)) images++
|
|
171
|
+
else if (item.type?.startsWith('video/')) videos++
|
|
172
|
+
else documents++
|
|
173
|
+
bytes += item.sizeBytes || 0
|
|
174
|
+
}
|
|
175
|
+
return { total: mediaItems.length, images, videos, documents, bytes }
|
|
176
|
+
}, [mediaItems])
|
|
177
|
+
|
|
152
178
|
const openDetail = (item: MediaItem) => {
|
|
153
179
|
setActiveItem(item)
|
|
154
180
|
setEditAlt(item.altTag ?? '')
|
|
@@ -411,8 +437,14 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
411
437
|
<FolderInput className="text-muted-foreground h-5 w-5" />
|
|
412
438
|
</button>
|
|
413
439
|
<div>
|
|
414
|
-
<h1 className="text-foreground mb-1
|
|
415
|
-
<
|
|
440
|
+
<h1 className="text-foreground mb-1">Media Library</h1>
|
|
441
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
442
|
+
<StatPill value={mediaStats.total} label="Total Files" />
|
|
443
|
+
<StatPill value={mediaStats.images} label="Images" />
|
|
444
|
+
<StatPill value={mediaStats.videos} label="Videos" />
|
|
445
|
+
<StatPill value={mediaStats.documents} label="Documents" />
|
|
446
|
+
<StatPill value={formatStorage(mediaStats.bytes)} label="Storage used" />
|
|
447
|
+
</div>
|
|
416
448
|
</div>
|
|
417
449
|
</div>
|
|
418
450
|
<div>
|
|
@@ -722,13 +754,9 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
722
754
|
</td>
|
|
723
755
|
<td className="px-3 py-2">
|
|
724
756
|
{hasIssues ? (
|
|
725
|
-
<
|
|
726
|
-
<AlertTriangle className="h-3 w-3" /> Needs attention
|
|
727
|
-
</span>
|
|
757
|
+
<StatusBadge tone="warning">Needs attention</StatusBadge>
|
|
728
758
|
) : (
|
|
729
|
-
<
|
|
730
|
-
Complete
|
|
731
|
-
</span>
|
|
759
|
+
<StatusBadge tone="success">Complete</StatusBadge>
|
|
732
760
|
)}
|
|
733
761
|
</td>
|
|
734
762
|
</tr>
|
|
@@ -85,9 +85,7 @@ export function PageTagManager({ onNavigate, startCreating = false }: PageTagMan
|
|
|
85
85
|
|
|
86
86
|
<div className="mb-5 flex items-center justify-between">
|
|
87
87
|
<div>
|
|
88
|
-
<h1 className="text-foreground mb-1
|
|
89
|
-
Page Tags
|
|
90
|
-
</h1>
|
|
88
|
+
<h1 className="text-foreground mb-1">Page Tags</h1>
|
|
91
89
|
<p className="text-muted-foreground text-sm">
|
|
92
90
|
Organize pages with reusable, color-coded labels.
|
|
93
91
|
</p>
|