@actuate-media/cms-admin 0.64.0 → 0.65.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 +10 -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/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 +6 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -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/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/views/Dashboard.js +1 -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 +3 -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.js +1 -1
- 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.js +1 -1
- 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/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 +3 -3
- 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/StatPill.tsx +37 -0
- package/src/components/ui/StatusBadge.tsx +66 -0
- package/src/components/ui/index.ts +6 -0
- package/src/fields/PropInput.tsx +6 -15
- package/src/fields/ToggleField.tsx +3 -15
- package/src/layout/primitives/PageContainer.tsx +1 -5
- package/src/views/Dashboard.tsx +1 -1
- 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 +4 -7
- package/src/views/Pages/PageTagManager.tsx +1 -3
- package/src/views/Pages/PagesListView.tsx +1 -1
- 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 +1 -1
- 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/settings/components.tsx +8 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
"tailwindcss": "^4.0.0",
|
|
96
96
|
"typescript": "^5.7.0",
|
|
97
97
|
"vitest": "^4.1.0",
|
|
98
|
-
"@actuate-media/
|
|
99
|
-
"@actuate-media/
|
|
98
|
+
"@actuate-media/component-blocks": "0.2.1",
|
|
99
|
+
"@actuate-media/cms-core": "0.85.3"
|
|
100
100
|
},
|
|
101
101
|
"scripts": {
|
|
102
102
|
"build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/actuate-admin.css --minify",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { render, screen } from '@testing-library/react'
|
|
4
|
+
import { StatusBadge } from '../../components/ui/StatusBadge.js'
|
|
5
|
+
import { FilterPill } from '../../components/ui/FilterPill.js'
|
|
6
|
+
import { StatPill } from '../../components/ui/StatPill.js'
|
|
7
|
+
|
|
8
|
+
describe('StatusBadge', () => {
|
|
9
|
+
it('renders the label with a leading dot by default', () => {
|
|
10
|
+
const { container } = render(<StatusBadge tone="success">Published</StatusBadge>)
|
|
11
|
+
expect(screen.getByText('Published')).toBeTruthy()
|
|
12
|
+
expect(container.querySelectorAll('[aria-hidden]')).toHaveLength(1)
|
|
13
|
+
})
|
|
14
|
+
it('omits the dot when dot={false}', () => {
|
|
15
|
+
const { container } = render(
|
|
16
|
+
<StatusBadge tone="neutral" dot={false}>
|
|
17
|
+
Draft
|
|
18
|
+
</StatusBadge>,
|
|
19
|
+
)
|
|
20
|
+
expect(container.querySelectorAll('[aria-hidden]')).toHaveLength(0)
|
|
21
|
+
})
|
|
22
|
+
it('forwards aria-label for screen readers', () => {
|
|
23
|
+
const { container } = render(
|
|
24
|
+
<StatusBadge tone="warning" aria-label="Status: Scheduled">
|
|
25
|
+
Scheduled
|
|
26
|
+
</StatusBadge>,
|
|
27
|
+
)
|
|
28
|
+
expect(container.querySelector('[aria-label="Status: Scheduled"]')).toBeTruthy()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('FilterPill', () => {
|
|
33
|
+
it('exposes active state via aria-pressed and fires onClick', () => {
|
|
34
|
+
const onClick = vi.fn()
|
|
35
|
+
render(
|
|
36
|
+
<FilterPill active onClick={onClick}>
|
|
37
|
+
All
|
|
38
|
+
</FilterPill>,
|
|
39
|
+
)
|
|
40
|
+
const btn = screen.getByRole('button', { name: 'All' })
|
|
41
|
+
expect(btn.getAttribute('aria-pressed')).toBe('true')
|
|
42
|
+
btn.click()
|
|
43
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
44
|
+
})
|
|
45
|
+
it('renders an optional count', () => {
|
|
46
|
+
render(<FilterPill count={12}>Admin</FilterPill>)
|
|
47
|
+
expect(screen.getByText('12')).toBeTruthy()
|
|
48
|
+
expect(screen.getByRole('button', { name: /Admin/ }).getAttribute('aria-pressed')).toBe('false')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('StatPill', () => {
|
|
53
|
+
it('renders value and label', () => {
|
|
54
|
+
render(<StatPill value={8} label="Active" />)
|
|
55
|
+
expect(screen.getByText('8')).toBeTruthy()
|
|
56
|
+
expect(screen.getByText('Active')).toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -54,7 +54,8 @@ function updateAvailable(overrides: Record<string, unknown> = {}) {
|
|
|
54
54
|
pluginCompatibility: [
|
|
55
55
|
{ name: '@actuate-media/plugin-seo', compatible: true, minVersion: '0.46.0' },
|
|
56
56
|
],
|
|
57
|
-
updateCommand:
|
|
57
|
+
updateCommand:
|
|
58
|
+
'npx actuate upgrade && npm install && npx actuate db:sync && npx prisma migrate deploy',
|
|
58
59
|
canManage: true,
|
|
59
60
|
hasGithubToken: false,
|
|
60
61
|
githubRepo: '',
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Star,
|
|
23
23
|
} from 'lucide-react'
|
|
24
24
|
import { SEO_SCHEMA_TYPES } from '../lib/seo-service.js'
|
|
25
|
+
import { Toggle } from './ui/Toggle.js'
|
|
25
26
|
|
|
26
27
|
export interface SEOData {
|
|
27
28
|
metaTitle?: string
|
|
@@ -469,17 +470,7 @@ function ToggleSwitch({
|
|
|
469
470
|
<label className="text-sm font-medium text-[var(--foreground)]">{label}</label>
|
|
470
471
|
<p className="mt-0.5 text-xs text-[var(--muted-foreground)]">{description}</p>
|
|
471
472
|
</div>
|
|
472
|
-
<
|
|
473
|
-
type="button"
|
|
474
|
-
role="switch"
|
|
475
|
-
aria-checked={checked}
|
|
476
|
-
onClick={() => onChange(!checked)}
|
|
477
|
-
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'}`}
|
|
478
|
-
>
|
|
479
|
-
<span
|
|
480
|
-
className={`bg-card pointer-events-none mt-0.5 block h-5 w-5 rounded-full shadow-sm transition-transform ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
|
|
481
|
-
/>
|
|
482
|
-
</button>
|
|
473
|
+
<Toggle checked={checked} onCheckedChange={onChange} aria-label={label} />
|
|
483
474
|
</div>
|
|
484
475
|
)
|
|
485
476
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ReactNode } from 'react'
|
|
11
11
|
import { AlertTriangle, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
|
|
12
|
+
import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
|
|
12
13
|
import type { FormFieldType, FormStatus } from '../../lib/forms-service.js'
|
|
13
14
|
|
|
14
15
|
// ─── Field-type badges ─────────────────────────────────────────────────────
|
|
@@ -70,26 +71,15 @@ export function FieldTypeBadge({ type }: { type: FormFieldType }) {
|
|
|
70
71
|
|
|
71
72
|
// ─── Status badges ─────────────────────────────────────────────────────────
|
|
72
73
|
|
|
73
|
-
const STATUS_META: Record<FormStatus, { label: string;
|
|
74
|
-
active: { label: 'Active',
|
|
75
|
-
draft: { label: 'Draft',
|
|
76
|
-
archived: {
|
|
77
|
-
label: 'Archived',
|
|
78
|
-
cls: 'bg-muted text-muted-foreground',
|
|
79
|
-
dot: 'bg-muted-foreground',
|
|
80
|
-
},
|
|
74
|
+
const STATUS_META: Record<FormStatus, { label: string; tone: StatusBadgeTone }> = {
|
|
75
|
+
active: { label: 'Active', tone: 'success' },
|
|
76
|
+
draft: { label: 'Draft', tone: 'warning' },
|
|
77
|
+
archived: { label: 'Archived', tone: 'muted' },
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
export function FormStatusBadge({ status }: { status: FormStatus }) {
|
|
84
81
|
const meta = STATUS_META[status] ?? STATUS_META.draft
|
|
85
|
-
return
|
|
86
|
-
<span
|
|
87
|
-
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${meta.cls}`}
|
|
88
|
-
>
|
|
89
|
-
<span className={`h-1.5 w-1.5 rounded-full ${meta.dot}`} aria-hidden />
|
|
90
|
-
{meta.label}
|
|
91
|
-
</span>
|
|
92
|
-
)
|
|
82
|
+
return <StatusBadge tone={meta.tone}>{meta.label}</StatusBadge>
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
/** Small red count badge for unread submissions. Hidden when count is 0. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
|
|
4
5
|
import type { PostStatus } from '../../lib/posts-service.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -65,32 +66,18 @@ const STATUS_LABEL: Record<PostStatus, string> = {
|
|
|
65
66
|
ARCHIVED: 'Archived',
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
-
PUBLISHED: '
|
|
70
|
-
DRAFT: '
|
|
71
|
-
SCHEDULED: '
|
|
72
|
-
ARCHIVED: '
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const STATUS_GLYPH: Record<PostStatus, string> = {
|
|
76
|
-
PUBLISHED: '●',
|
|
77
|
-
DRAFT: '○',
|
|
78
|
-
SCHEDULED: '◐',
|
|
79
|
-
ARCHIVED: '◌',
|
|
69
|
+
const STATUS_TONE: Record<PostStatus, StatusBadgeTone> = {
|
|
70
|
+
PUBLISHED: 'success',
|
|
71
|
+
DRAFT: 'neutral',
|
|
72
|
+
SCHEDULED: 'warning',
|
|
73
|
+
ARCHIVED: 'muted',
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
export function PostStatusBadge({ status }: { status: PostStatus }) {
|
|
83
|
-
const p = paletteFor(STATUS_PALETTE[status])
|
|
84
77
|
return (
|
|
85
|
-
<
|
|
86
|
-
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${p.bg} ${p.text}`}
|
|
87
|
-
aria-label={`Status: ${STATUS_LABEL[status]}`}
|
|
88
|
-
>
|
|
89
|
-
<span aria-hidden className="text-[10px] leading-none">
|
|
90
|
-
{STATUS_GLYPH[status]}
|
|
91
|
-
</span>
|
|
78
|
+
<StatusBadge tone={STATUS_TONE[status]} aria-label={`Status: ${STATUS_LABEL[status]}`}>
|
|
92
79
|
{STATUS_LABEL[status]}
|
|
93
|
-
</
|
|
80
|
+
</StatusBadge>
|
|
94
81
|
)
|
|
95
82
|
}
|
|
96
83
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../lib/seo-service.js'
|
|
12
12
|
import { Drawer } from './Drawer.js'
|
|
13
13
|
import { btnPrimary, btnSecondary } from './primitives.js'
|
|
14
|
+
import { Toggle } from '../ui/Toggle.js'
|
|
14
15
|
|
|
15
16
|
const STATUS_CODES: { value: RedirectStatusCode; label: string }[] = [
|
|
16
17
|
{ value: 301, label: '301 — Permanent' },
|
|
@@ -197,19 +198,17 @@ export function RedirectEditorDrawer({
|
|
|
197
198
|
</div>
|
|
198
199
|
|
|
199
200
|
<div className="border-border space-y-3 rounded-lg border p-4">
|
|
200
|
-
<
|
|
201
|
+
<div className="flex items-center justify-between gap-3">
|
|
201
202
|
<span className="text-foreground text-sm font-medium">Enabled</span>
|
|
202
|
-
<
|
|
203
|
-
type="checkbox"
|
|
204
|
-
role="switch"
|
|
205
|
-
aria-checked={!!form.enabled}
|
|
203
|
+
<Toggle
|
|
206
204
|
checked={!!form.enabled}
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
onCheckedChange={(v) => update({ enabled: v })}
|
|
206
|
+
variant="enabled"
|
|
207
|
+
aria-label="Enabled"
|
|
209
208
|
/>
|
|
210
|
-
</
|
|
209
|
+
</div>
|
|
211
210
|
{!isGone && (
|
|
212
|
-
<
|
|
211
|
+
<div className="flex items-center justify-between gap-3">
|
|
213
212
|
<span className="min-w-0">
|
|
214
213
|
<span className="text-foreground block text-sm font-medium">
|
|
215
214
|
Preserve query string
|
|
@@ -218,15 +217,12 @@ export function RedirectEditorDrawer({
|
|
|
218
217
|
Append the original query params to the destination.
|
|
219
218
|
</span>
|
|
220
219
|
</span>
|
|
221
|
-
<
|
|
222
|
-
type="checkbox"
|
|
223
|
-
role="switch"
|
|
224
|
-
aria-checked={!!form.preserveQuery}
|
|
220
|
+
<Toggle
|
|
225
221
|
checked={!!form.preserveQuery}
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
onCheckedChange={(v) => update({ preserveQuery: v })}
|
|
223
|
+
aria-label="Preserve query string"
|
|
228
224
|
/>
|
|
229
|
-
</
|
|
225
|
+
</div>
|
|
230
226
|
)}
|
|
231
227
|
</div>
|
|
232
228
|
</div>
|
|
@@ -17,6 +17,7 @@ import { fixBrandVoiceContent } from '../../lib/brand-voice-client.js'
|
|
|
17
17
|
import { BrandVoiceAlignmentBadge } from '../BrandVoiceAlignmentBadge.js'
|
|
18
18
|
import { Drawer } from './Drawer.js'
|
|
19
19
|
import { SeoLoading, SeoErrorState, btnPrimary, btnSecondary, gradeMeta } from './primitives.js'
|
|
20
|
+
import { Toggle } from '../ui/Toggle.js'
|
|
20
21
|
|
|
21
22
|
const META_TITLE_MAX = 60
|
|
22
23
|
const META_DESC_MAX = 160
|
|
@@ -422,38 +423,32 @@ export function SeoEditorDrawer({
|
|
|
422
423
|
|
|
423
424
|
{/* Indexing toggles */}
|
|
424
425
|
<div className="border-border space-y-3 rounded-lg border p-4">
|
|
425
|
-
<
|
|
426
|
+
<div className="flex items-center justify-between gap-3">
|
|
426
427
|
<span className="min-w-0">
|
|
427
428
|
<span className="text-foreground block text-sm font-medium">No-index</span>
|
|
428
429
|
<span className="text-muted-foreground block text-sm">
|
|
429
430
|
Hide this page from search engines.
|
|
430
431
|
</span>
|
|
431
432
|
</span>
|
|
432
|
-
<
|
|
433
|
-
type="checkbox"
|
|
434
|
-
role="switch"
|
|
435
|
-
aria-checked={!!form.noindex}
|
|
433
|
+
<Toggle
|
|
436
434
|
checked={!!form.noindex}
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
onCheckedChange={(v) => update({ noindex: v })}
|
|
436
|
+
aria-label="No-index"
|
|
439
437
|
/>
|
|
440
|
-
</
|
|
441
|
-
<
|
|
438
|
+
</div>
|
|
439
|
+
<div className="flex items-center justify-between gap-3">
|
|
442
440
|
<span className="min-w-0">
|
|
443
441
|
<span className="text-foreground block text-sm font-medium">No-follow</span>
|
|
444
442
|
<span className="text-muted-foreground block text-sm">
|
|
445
443
|
Tell crawlers not to follow links on this page.
|
|
446
444
|
</span>
|
|
447
445
|
</span>
|
|
448
|
-
<
|
|
449
|
-
type="checkbox"
|
|
450
|
-
role="switch"
|
|
451
|
-
aria-checked={!!form.nofollow}
|
|
446
|
+
<Toggle
|
|
452
447
|
checked={!!form.nofollow}
|
|
453
|
-
|
|
454
|
-
|
|
448
|
+
onCheckedChange={(v) => update({ nofollow: v })}
|
|
449
|
+
aria-label="No-follow"
|
|
455
450
|
/>
|
|
456
|
-
</
|
|
451
|
+
</div>
|
|
457
452
|
</div>
|
|
458
453
|
|
|
459
454
|
{/* llms.txt curation */}
|
|
@@ -499,22 +494,19 @@ export function SeoEditorDrawer({
|
|
|
499
494
|
placeholder="Defaults to the content type"
|
|
500
495
|
/>
|
|
501
496
|
</Field>
|
|
502
|
-
<
|
|
497
|
+
<div className="flex items-center justify-between gap-3">
|
|
503
498
|
<span className="min-w-0">
|
|
504
499
|
<span className="text-foreground block text-sm font-medium">Optional</span>
|
|
505
500
|
<span className="text-muted-foreground block text-sm">
|
|
506
501
|
Group under “Optional” — agents skip these first when context is tight.
|
|
507
502
|
</span>
|
|
508
503
|
</span>
|
|
509
|
-
<
|
|
510
|
-
type="checkbox"
|
|
511
|
-
role="switch"
|
|
512
|
-
aria-checked={!!form.llmsOptional}
|
|
504
|
+
<Toggle
|
|
513
505
|
checked={!!form.llmsOptional}
|
|
514
|
-
|
|
515
|
-
|
|
506
|
+
onCheckedChange={(v) => update({ llmsOptional: v })}
|
|
507
|
+
aria-label="Optional in llms.txt"
|
|
516
508
|
/>
|
|
517
|
-
</
|
|
509
|
+
</div>
|
|
518
510
|
</div>
|
|
519
511
|
|
|
520
512
|
{/* Score summary */}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
9
9
|
import { AlertTriangle, Loader2 } from 'lucide-react'
|
|
10
10
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/Card.js'
|
|
11
|
+
import { StatusBadge, type StatusBadgeTone } from '../ui/StatusBadge.js'
|
|
11
12
|
import type { SeoGrade, SeoSeverity } from '../../lib/seo-service.js'
|
|
12
13
|
|
|
13
14
|
// ─── Grade + severity styling ────────────────────────────────────────
|
|
@@ -73,6 +74,14 @@ export function severityMeta(severity: SeoSeverity): GradeMeta {
|
|
|
73
74
|
|
|
74
75
|
// ─── Badges + bars ───────────────────────────────────────────────────
|
|
75
76
|
|
|
77
|
+
const SEO_TONE: Record<'good' | 'fair' | 'poor' | 'critical' | 'neutral', StatusBadgeTone> = {
|
|
78
|
+
good: 'success',
|
|
79
|
+
fair: 'warning',
|
|
80
|
+
poor: 'danger',
|
|
81
|
+
critical: 'danger',
|
|
82
|
+
neutral: 'neutral',
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
export function SeoStatusBadge({
|
|
77
86
|
label,
|
|
78
87
|
tone,
|
|
@@ -80,33 +89,7 @@ export function SeoStatusBadge({
|
|
|
80
89
|
label: string
|
|
81
90
|
tone: 'good' | 'fair' | 'poor' | 'critical' | 'neutral'
|
|
82
91
|
}) {
|
|
83
|
-
|
|
84
|
-
tone === 'good'
|
|
85
|
-
? 'bg-success/10 text-success'
|
|
86
|
-
: tone === 'fair'
|
|
87
|
-
? 'bg-warning/10 text-warning'
|
|
88
|
-
: tone === 'poor' || tone === 'critical'
|
|
89
|
-
? 'bg-destructive/10 text-destructive'
|
|
90
|
-
: 'bg-muted text-muted-foreground'
|
|
91
|
-
return (
|
|
92
|
-
<span
|
|
93
|
-
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${cls}`}
|
|
94
|
-
>
|
|
95
|
-
<span
|
|
96
|
-
className={`h-1.5 w-1.5 rounded-full ${
|
|
97
|
-
tone === 'good'
|
|
98
|
-
? 'bg-success'
|
|
99
|
-
: tone === 'fair'
|
|
100
|
-
? 'bg-warning'
|
|
101
|
-
: tone === 'neutral'
|
|
102
|
-
? 'bg-muted-foreground'
|
|
103
|
-
: 'bg-destructive'
|
|
104
|
-
}`}
|
|
105
|
-
aria-hidden
|
|
106
|
-
/>
|
|
107
|
-
{label}
|
|
108
|
-
</span>
|
|
109
|
-
)
|
|
92
|
+
return <StatusBadge tone={SEO_TONE[tone]}>{label}</StatusBadge>
|
|
110
93
|
}
|
|
111
94
|
|
|
112
95
|
/** A horizontal score bar (0-100), coloured by grade. */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Design-system filter pill (`.pill` / `.pill.act`): bordered card-surface
|
|
7
|
+
* chip that fills with the accent color when active. Used for status / role /
|
|
8
|
+
* category filter rows above list views.
|
|
9
|
+
*/
|
|
10
|
+
export interface FilterPillProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
|
|
11
|
+
active?: boolean
|
|
12
|
+
/** Optional trailing count rendered in tabular numerals. */
|
|
13
|
+
count?: number
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FilterPill({
|
|
18
|
+
active = false,
|
|
19
|
+
count,
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...rest
|
|
23
|
+
}: FilterPillProps) {
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
aria-pressed={active}
|
|
28
|
+
className={`inline-flex items-center gap-1.5 rounded-[7px] border px-3 py-1.5 text-[12.5px] font-medium transition-colors duration-[var(--motion-fast)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none ${
|
|
29
|
+
active
|
|
30
|
+
? 'border-[var(--acc)] bg-[var(--acc)] text-white'
|
|
31
|
+
: 'border-[var(--bdr)] bg-[var(--card)] text-[var(--sub)] hover:border-[var(--acc-m)] hover:text-[var(--acc)]'
|
|
32
|
+
} ${className ?? ''}`}
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
{count !== undefined && (
|
|
37
|
+
<span className={`tabular-nums ${active ? 'text-white/80' : 'text-[var(--muted)]'}`}>
|
|
38
|
+
{count}
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</button>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Design-system stat pill (`.stat-pill`): a compact card-surface chip with a
|
|
5
|
+
* bold count and a muted label, used in page headers to summarize list
|
|
6
|
+
* contents (e.g. "12 Total · 8 Active · 4 Inactive").
|
|
7
|
+
*/
|
|
8
|
+
export interface StatPillProps extends HTMLAttributes<HTMLSpanElement> {
|
|
9
|
+
/** The count/value, rendered bold in `--txt` (override color via valueClassName). */
|
|
10
|
+
value: ReactNode
|
|
11
|
+
/** Muted label shown after the value. */
|
|
12
|
+
label: ReactNode
|
|
13
|
+
/** Optional leading icon. */
|
|
14
|
+
icon?: ReactNode
|
|
15
|
+
/** Extra classes for the value (e.g. `text-[var(--suc)]` for an Active count). */
|
|
16
|
+
valueClassName?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function StatPill({
|
|
20
|
+
value,
|
|
21
|
+
label,
|
|
22
|
+
icon,
|
|
23
|
+
valueClassName,
|
|
24
|
+
className,
|
|
25
|
+
...rest
|
|
26
|
+
}: StatPillProps) {
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
className={`inline-flex items-center gap-[5px] rounded-[6px] border border-[var(--bdr)] bg-[var(--card)] px-2.5 py-1 text-[length:var(--fs-sub)] text-[var(--sub)] ${className ?? ''}`}
|
|
30
|
+
{...rest}
|
|
31
|
+
>
|
|
32
|
+
{icon}
|
|
33
|
+
<b className={`font-semibold text-[var(--txt)] ${valueClassName ?? ''}`}>{value}</b>
|
|
34
|
+
{label}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -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,12 @@ 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'
|
|
39
45
|
export { formControlBase, formControlState } from './form-control.js'
|
|
40
46
|
export { CommandPalette } from './CommandPalette.js'
|
|
41
47
|
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
|
}
|
|
@@ -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/views/Dashboard.tsx
CHANGED
|
@@ -567,7 +567,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
567
567
|
consistent across navigation. The on-page secondary action row
|
|
568
568
|
below covers the same creation paths plus authoring shortcuts. */}
|
|
569
569
|
<div className="flex flex-col gap-1 sm:gap-0.5">
|
|
570
|
-
<h1 className="text-foreground
|
|
570
|
+
<h1 className="text-foreground">
|
|
571
571
|
{greeting}, {userName} <span aria-hidden>👋</span>
|
|
572
572
|
</h1>
|
|
573
573
|
<p className="text-muted-foreground text-sm">
|