@actuate-media/cms-admin 0.72.1 → 0.72.3
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 +53 -0
- package/dist/__tests__/components/toggle.test.d.ts +2 -0
- package/dist/__tests__/components/toggle.test.d.ts.map +1 -0
- package/dist/__tests__/components/toggle.test.js +17 -0
- package/dist/__tests__/components/toggle.test.js.map +1 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js +38 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +1 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/TagInput.js +1 -1
- package/dist/components/TagInput.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +17 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +29 -3
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/Toggle.d.ts.map +1 -1
- package/dist/components/ui/Toggle.js +4 -3
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/fields/RichTextField.d.ts +3 -1
- package/dist/fields/RichTextField.d.ts.map +1 -1
- package/dist/fields/RichTextField.js +2 -2
- package/dist/fields/RichTextField.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +1 -1
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +1 -1
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/PagePreview.js +1 -1
- package/dist/views/page-editor/PagePreview.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts +4 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +8 -2
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js +1 -1
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
- package/dist/views/post-editor/PostFieldsModal.js +152 -13
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
- package/dist/views/post-editor/PostPreview.js +1 -1
- package/dist/views/post-editor/PostPreview.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +6 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/toggle.test.tsx +17 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +54 -0
- package/src/components/MediaPickerModal.tsx +6 -2
- package/src/components/TagInput.tsx +1 -1
- package/src/components/ui/Modal.tsx +46 -3
- package/src/components/ui/Toggle.tsx +4 -3
- package/src/fields/RichTextField.tsx +4 -1
- package/src/styles/actuate-tokens.css +7 -2
- package/src/styles/theme.css +34 -8
- package/src/views/Dashboard.tsx +2 -1
- package/src/views/page-editor/EditorCanvas.tsx +4 -1
- package/src/views/page-editor/PagePreview.tsx +1 -1
- package/src/views/page-editor/SectionInspector.tsx +23 -1
- package/src/views/post-editor/PostEditorCanvas.tsx +1 -1
- package/src/views/post-editor/PostFieldsModal.tsx +412 -174
- package/src/views/post-editor/PostPreview.tsx +1 -1
- package/src/views/post-editor/PostSectionEditor.tsx +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
4
|
+
import { Toggle } from '../../components/ui/Toggle.js'
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup)
|
|
7
|
+
|
|
8
|
+
describe('Toggle', () => {
|
|
9
|
+
it('positions the thumb with design-system absolute offsets', () => {
|
|
10
|
+
render(<Toggle checked={false} onCheckedChange={() => {}} aria-label="Test toggle" />)
|
|
11
|
+
const thumb = screen.getByRole('switch').querySelector('span')
|
|
12
|
+
expect(thumb?.className).toContain('absolute')
|
|
13
|
+
expect(thumb?.className).toContain('top-[2.5px]')
|
|
14
|
+
expect(thumb?.className).toContain('left-[2.5px]')
|
|
15
|
+
expect(thumb?.className).toContain('data-[state=checked]:left-[19.5px]')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -21,6 +21,17 @@ vi.mock('../../fields/RichTextField.js', () => ({
|
|
|
21
21
|
RichTextField: () => <div data-testid="rich-text" />,
|
|
22
22
|
}))
|
|
23
23
|
|
|
24
|
+
vi.mock('../../lib/coauthor-client.js', () => ({
|
|
25
|
+
coauthorText: vi.fn(async () => ({
|
|
26
|
+
ok: true,
|
|
27
|
+
result: { action: 'compress', text: 'AI excerpt' },
|
|
28
|
+
})),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
vi.mock('../../lib/seo-service.js', () => ({
|
|
32
|
+
generateSeoField: vi.fn(async () => ({ text: 'AI SEO title' })),
|
|
33
|
+
}))
|
|
34
|
+
|
|
24
35
|
const { PostFieldsModal } = await import('../../views/post-editor/PostFieldsModal.js')
|
|
25
36
|
type ModalProps = Parameters<typeof PostFieldsModal>[0]
|
|
26
37
|
|
|
@@ -123,4 +134,47 @@ describe('PostFieldsModal', () => {
|
|
|
123
134
|
expect.objectContaining({ category: 'tutorials', tags: ['AI', 'CMS'] }),
|
|
124
135
|
)
|
|
125
136
|
})
|
|
137
|
+
|
|
138
|
+
it('derives slug from title until the slug field is edited', () => {
|
|
139
|
+
render(
|
|
140
|
+
<PostFieldsModal
|
|
141
|
+
open
|
|
142
|
+
post={basePost({ title: '', slug: '' })}
|
|
143
|
+
canEdit
|
|
144
|
+
onClose={() => {}}
|
|
145
|
+
onSave={() => {}}
|
|
146
|
+
/>,
|
|
147
|
+
)
|
|
148
|
+
fireEvent.change(screen.getByLabelText(/^Title/), { target: { value: 'Hello World' } })
|
|
149
|
+
expect((screen.getByLabelText(/^Slug/) as HTMLInputElement).value).toBe('hello-world')
|
|
150
|
+
fireEvent.change(screen.getByLabelText(/^Slug/), { target: { value: 'custom-slug' } })
|
|
151
|
+
fireEvent.change(screen.getByLabelText(/^Title/), { target: { value: 'Changed Title' } })
|
|
152
|
+
expect((screen.getByLabelText(/^Slug/) as HTMLInputElement).value).toBe('custom-slug')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('exposes AI generate controls for excerpt and SEO fields', () => {
|
|
156
|
+
render(<PostFieldsModal open post={basePost()} canEdit onClose={() => {}} onSave={() => {}} />)
|
|
157
|
+
expect(screen.getByRole('button', { name: 'Generate excerpt from content' })).toBeTruthy()
|
|
158
|
+
expect(screen.getByRole('button', { name: 'Generate SEO title from content' })).toBeTruthy()
|
|
159
|
+
expect(
|
|
160
|
+
screen.getByRole('button', { name: 'Generate SEO description from content' }),
|
|
161
|
+
).toBeTruthy()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('prompts before discarding when Cancel is clicked with unsaved edits', () => {
|
|
165
|
+
const onClose = vi.fn()
|
|
166
|
+
render(<PostFieldsModal open post={basePost()} canEdit onClose={onClose} onSave={() => {}} />)
|
|
167
|
+
fireEvent.change(screen.getByLabelText(/^Title/), { target: { value: 'Changed title' } })
|
|
168
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
|
169
|
+
expect(onClose).not.toHaveBeenCalled()
|
|
170
|
+
expect(screen.getByRole('dialog', { name: 'Discard unsaved changes?' })).toBeTruthy()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('closes without prompting when nothing changed', () => {
|
|
174
|
+
const onClose = vi.fn()
|
|
175
|
+
render(<PostFieldsModal open post={basePost()} canEdit onClose={onClose} onSave={() => {}} />)
|
|
176
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
|
177
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
178
|
+
expect(screen.queryByRole('dialog', { name: 'Discard unsaved changes?' })).toBeNull()
|
|
179
|
+
})
|
|
126
180
|
})
|
|
@@ -129,10 +129,14 @@ export function MediaPickerModal({
|
|
|
129
129
|
return (
|
|
130
130
|
<Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
|
|
131
131
|
<Dialog.Portal container={adminPortalContainer()}>
|
|
132
|
-
|
|
132
|
+
{/*
|
|
133
|
+
z-70 so this picker stacks above the z-60 form modals that embed it
|
|
134
|
+
(PostFieldsModal featured image, TipTap insert image, etc.).
|
|
135
|
+
*/}
|
|
136
|
+
<Dialog.Overlay className="fixed inset-0 z-70 bg-black/40" />
|
|
133
137
|
<Dialog.Content
|
|
134
138
|
aria-describedby={undefined}
|
|
135
|
-
className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-
|
|
139
|
+
className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-70 mx-4 flex max-h-[80vh] w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col rounded-xl border shadow-2xl focus:outline-none"
|
|
136
140
|
>
|
|
137
141
|
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
138
142
|
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
@@ -35,7 +35,7 @@ interface FieldValuesResponse {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const INPUT =
|
|
38
|
-
'border-
|
|
38
|
+
'border-border bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
|
|
39
39
|
|
|
40
40
|
export function TagInput({
|
|
41
41
|
id,
|
|
@@ -5,12 +5,23 @@ import { X } from 'lucide-react'
|
|
|
5
5
|
import { type ReactNode, type RefObject } from 'react'
|
|
6
6
|
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
7
7
|
|
|
8
|
+
const MODAL_SIZE_CLASS = {
|
|
9
|
+
md: 'max-w-lg',
|
|
10
|
+
lg: 'max-w-3xl',
|
|
11
|
+
/** Wide form dialogs (post/page field editors) — body scrolls inside the shell. */
|
|
12
|
+
xl: 'flex max-h-[min(92vh,880px)] max-w-5xl flex-col',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export type ModalSize = keyof typeof MODAL_SIZE_CLASS
|
|
16
|
+
|
|
8
17
|
export interface ModalProps {
|
|
9
18
|
open: boolean
|
|
10
19
|
onClose: () => void
|
|
11
20
|
title: string
|
|
12
21
|
children: ReactNode
|
|
13
22
|
actions?: ReactNode
|
|
23
|
+
/** Dialog width preset. Defaults to `md` (512px). */
|
|
24
|
+
size?: ModalSize
|
|
14
25
|
/**
|
|
15
26
|
* Accessible description for the dialog, announced after the title. Optional —
|
|
16
27
|
* omit when the body content is self-explanatory.
|
|
@@ -22,6 +33,12 @@ export interface ModalProps {
|
|
|
22
33
|
* form dialogs where the user should land in the first input.
|
|
23
34
|
*/
|
|
24
35
|
initialFocusRef?: RefObject<HTMLElement | null>
|
|
36
|
+
/**
|
|
37
|
+
* When set, overlay click, Escape, and the header X invoke this instead of
|
|
38
|
+
* closing immediately — use to confirm before discarding unsaved edits.
|
|
39
|
+
* The dialog stays open until the parent calls `onClose()`.
|
|
40
|
+
*/
|
|
41
|
+
onDismiss?: () => void
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
/**
|
|
@@ -41,15 +58,39 @@ export function Modal({
|
|
|
41
58
|
title,
|
|
42
59
|
children,
|
|
43
60
|
actions,
|
|
61
|
+
size = 'md',
|
|
44
62
|
description,
|
|
45
63
|
initialFocusRef,
|
|
64
|
+
onDismiss,
|
|
46
65
|
}: ModalProps) {
|
|
66
|
+
const scrollBody = size === 'xl'
|
|
67
|
+
|
|
68
|
+
function handleDismissAttempt() {
|
|
69
|
+
if (onDismiss) onDismiss()
|
|
70
|
+
else onClose()
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
return (
|
|
48
|
-
<Dialog.Root
|
|
74
|
+
<Dialog.Root
|
|
75
|
+
open={open}
|
|
76
|
+
onOpenChange={(next) => {
|
|
77
|
+
if (!next) handleDismissAttempt()
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
49
80
|
<Dialog.Portal container={adminPortalContainer()}>
|
|
50
81
|
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/50" />
|
|
51
82
|
<Dialog.Content
|
|
52
|
-
className=
|
|
83
|
+
className={`bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none ${MODAL_SIZE_CLASS[size]}`}
|
|
84
|
+
onInteractOutside={(event) => {
|
|
85
|
+
if (!onDismiss) return
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
handleDismissAttempt()
|
|
88
|
+
}}
|
|
89
|
+
onEscapeKeyDown={(event) => {
|
|
90
|
+
if (!onDismiss) return
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
handleDismissAttempt()
|
|
93
|
+
}}
|
|
53
94
|
onOpenAutoFocus={
|
|
54
95
|
initialFocusRef
|
|
55
96
|
? (event) => {
|
|
@@ -78,7 +119,9 @@ export function Modal({
|
|
|
78
119
|
{description}
|
|
79
120
|
</Dialog.Description>
|
|
80
121
|
)}
|
|
81
|
-
<div className=
|
|
122
|
+
<div className={scrollBody ? 'min-h-0 flex-1 overflow-y-auto px-6 py-4' : 'px-6 py-4'}>
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
82
125
|
{actions && (
|
|
83
126
|
<div className="border-border flex justify-end gap-2 border-t px-6 py-4">{actions}</div>
|
|
84
127
|
)}
|
|
@@ -4,7 +4,7 @@ import * as Switch from '@radix-ui/react-switch'
|
|
|
4
4
|
import { cv, type VariantProps } from '../../lib/cv.js'
|
|
5
5
|
|
|
6
6
|
const toggleTrack = cv(
|
|
7
|
-
'relative inline-
|
|
7
|
+
'relative inline-block shrink-0 cursor-pointer rounded-[var(--r-pill)] border-0 p-0 transition-colors duration-[var(--motion-base)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
@@ -26,11 +26,12 @@ const toggleTrack = cv(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
const toggleThumb = cv(
|
|
29
|
-
'pointer-events-none block rounded-full bg-[var(--toggle-thumb)] shadow-[0_1px_3px_rgba(0,0,0,0.2)] transition-
|
|
29
|
+
'pointer-events-none absolute top-[2.5px] left-[2.5px] block rounded-full bg-[var(--toggle-thumb)] shadow-[0_1px_3px_rgba(0,0,0,0.2)] transition-[left] duration-[var(--motion-base)] data-[state=checked]:left-[19.5px]',
|
|
30
30
|
{
|
|
31
31
|
variants: {
|
|
32
32
|
size: {
|
|
33
|
-
|
|
33
|
+
/** Matches `.tog-thumb` in the design system handoff (16×16). */
|
|
34
|
+
md: 'h-4 w-4',
|
|
34
35
|
},
|
|
35
36
|
},
|
|
36
37
|
defaultVariants: {
|
|
@@ -8,6 +8,8 @@ export interface RichTextFieldProps {
|
|
|
8
8
|
onChange: (value: string) => void
|
|
9
9
|
required?: boolean
|
|
10
10
|
helpText?: string
|
|
11
|
+
/** Passed through to the underlying editor shell (e.g. max-height). */
|
|
12
|
+
className?: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function RichTextField({
|
|
@@ -16,6 +18,7 @@ export function RichTextField({
|
|
|
16
18
|
onChange,
|
|
17
19
|
required,
|
|
18
20
|
helpText,
|
|
21
|
+
className,
|
|
19
22
|
}: RichTextFieldProps) {
|
|
20
23
|
return (
|
|
21
24
|
<div>
|
|
@@ -23,7 +26,7 @@ export function RichTextField({
|
|
|
23
26
|
{label}
|
|
24
27
|
{required && <span className="ml-0.5 text-[var(--destructive)]">*</span>}
|
|
25
28
|
</label>
|
|
26
|
-
<TipTapEditor content={value} onChange={onChange} />
|
|
29
|
+
<TipTapEditor content={value} onChange={onChange} className={className} />
|
|
27
30
|
{helpText && <p className="mt-1 text-xs text-[var(--muted-foreground)]">{helpText}</p>}
|
|
28
31
|
</div>
|
|
29
32
|
)
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
/* Text */
|
|
15
15
|
--txt: #1a1827;
|
|
16
16
|
--sub: #6b6878;
|
|
17
|
-
|
|
17
|
+
/* Deviates from tokens.json's #B0ADBE (2.2:1 on white — illegible). The
|
|
18
|
+
admin uses --muted as real text (section labels, hints, icons), so it
|
|
19
|
+
must meet WCAG AA (>=4.5:1) on --card and --bg while staying one step
|
|
20
|
+
lighter than --sub to preserve the text hierarchy. */
|
|
21
|
+
--muted: #716e83;
|
|
18
22
|
|
|
19
23
|
/* Accent */
|
|
20
24
|
--acc: #7c3aed;
|
|
@@ -79,7 +83,8 @@
|
|
|
79
83
|
|
|
80
84
|
--txt: #eae8f5;
|
|
81
85
|
--sub: #8e8ba8;
|
|
82
|
-
|
|
86
|
+
/* Same AA deviation as light mode (tokens.json #504E68 is 2.1:1 on card). */
|
|
87
|
+
--muted: #8784a2;
|
|
83
88
|
|
|
84
89
|
--acc: #9d71f0;
|
|
85
90
|
--acc-l: #231950;
|
package/src/styles/theme.css
CHANGED
|
@@ -137,7 +137,19 @@
|
|
|
137
137
|
@apply border-border outline-ring/50;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
/*
|
|
141
|
+
* Base typography is written with doubled `.actuate-admin.actuate-admin`
|
|
142
|
+
* specificity on purpose. Consumer apps commonly import their site
|
|
143
|
+
* stylesheet into the admin route (for the page-editor canvas preview) and
|
|
144
|
+
* those sheets sometimes style `.actuate-admin` or plain `h1`/`body` in
|
|
145
|
+
* their own `@layer base` — with equal specificity, whichever file loads
|
|
146
|
+
* last wins, and the admin shell inherits site fonts/colors ("washed out"
|
|
147
|
+
* admin). Doubling the class wins those same-layer ties without `!important`
|
|
148
|
+
* while Tailwind utilities (a later layer) still override as usual.
|
|
149
|
+
* Consumer styles that should reach the canvas preview belong under
|
|
150
|
+
* `.actuate-admin .actuate-canvas` — see EditorCanvas.
|
|
151
|
+
*/
|
|
152
|
+
.actuate-admin.actuate-admin {
|
|
141
153
|
@apply bg-background text-foreground;
|
|
142
154
|
font-family: var(--actuate-admin-font, var(--font-sans)) !important;
|
|
143
155
|
font-size: var(--font-size);
|
|
@@ -148,44 +160,44 @@
|
|
|
148
160
|
-moz-osx-font-smoothing: grayscale;
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
.actuate-admin h1 {
|
|
163
|
+
.actuate-admin.actuate-admin h1 {
|
|
152
164
|
font-size: var(--fs-page);
|
|
153
165
|
font-weight: 700;
|
|
154
166
|
letter-spacing: -0.5px;
|
|
155
167
|
line-height: 1.5;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
.actuate-admin h2 {
|
|
170
|
+
.actuate-admin.actuate-admin h2 {
|
|
159
171
|
font-size: var(--fs-card);
|
|
160
172
|
font-weight: 600;
|
|
161
173
|
line-height: 1.5;
|
|
162
174
|
}
|
|
163
175
|
|
|
164
|
-
.actuate-admin h3 {
|
|
176
|
+
.actuate-admin.actuate-admin h3 {
|
|
165
177
|
font-size: var(--fs-card);
|
|
166
178
|
font-weight: 600;
|
|
167
179
|
line-height: 1.5;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
.actuate-admin h4 {
|
|
182
|
+
.actuate-admin.actuate-admin h4 {
|
|
171
183
|
font-size: var(--fs-body);
|
|
172
184
|
font-weight: 600;
|
|
173
185
|
line-height: 1.5;
|
|
174
186
|
}
|
|
175
187
|
|
|
176
|
-
.actuate-admin label {
|
|
188
|
+
.actuate-admin.actuate-admin label {
|
|
177
189
|
font-size: var(--fs-body);
|
|
178
190
|
font-weight: var(--font-weight-medium);
|
|
179
191
|
line-height: 1.5;
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
.actuate-admin button {
|
|
194
|
+
.actuate-admin.actuate-admin button {
|
|
183
195
|
font-size: var(--fs-body);
|
|
184
196
|
font-weight: var(--font-weight-medium);
|
|
185
197
|
line-height: 1.5;
|
|
186
198
|
}
|
|
187
199
|
|
|
188
|
-
.actuate-admin input {
|
|
200
|
+
.actuate-admin.actuate-admin input {
|
|
189
201
|
font-size: var(--fs-body);
|
|
190
202
|
font-weight: var(--font-weight-normal);
|
|
191
203
|
line-height: 1.5;
|
|
@@ -198,6 +210,20 @@
|
|
|
198
210
|
text-transform: uppercase;
|
|
199
211
|
color: var(--sub);
|
|
200
212
|
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* The sanctioned hook for consumer brand styles inside the editor canvas
|
|
216
|
+
* preview. Anything the site stylesheet must render in the preview should
|
|
217
|
+
* be scoped `.actuate-admin .actuate-canvas …` instead of `.actuate-admin`.
|
|
218
|
+
* The canvas re-establishes the site's typographic context: headings and
|
|
219
|
+
* body text fall back to inheritance rather than the admin scale above.
|
|
220
|
+
*/
|
|
221
|
+
.actuate-admin .actuate-canvas :is(h1, h2, h3, h4) {
|
|
222
|
+
font-size: revert-layer;
|
|
223
|
+
font-weight: revert-layer;
|
|
224
|
+
letter-spacing: revert-layer;
|
|
225
|
+
line-height: revert-layer;
|
|
226
|
+
}
|
|
201
227
|
}
|
|
202
228
|
|
|
203
229
|
.actuate-admin.reduce-motion,
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -585,7 +585,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
585
585
|
<h1 className="text-foreground">
|
|
586
586
|
{greeting}, {userName} <span aria-hidden>👋</span>
|
|
587
587
|
</h1>
|
|
588
|
-
|
|
588
|
+
{/* data-volatile: the literal date changes daily — visual tests mask it. */}
|
|
589
|
+
<p className="text-muted-foreground text-sm" data-volatile>
|
|
589
590
|
{dateStr}
|
|
590
591
|
{totalIssues > 0 && (
|
|
591
592
|
<span className="hidden sm:inline">
|
|
@@ -88,9 +88,12 @@ export function EditorCanvas({
|
|
|
88
88
|
className="mx-auto"
|
|
89
89
|
style={{ width: vp.width * scale, height: surfaceHeight ?? undefined }}
|
|
90
90
|
>
|
|
91
|
+
{/* `actuate-canvas` is the public hook for consumer brand CSS:
|
|
92
|
+
site styles that should render in this preview scope to
|
|
93
|
+
`.actuate-admin .actuate-canvas` instead of the admin root. */}
|
|
91
94
|
<div
|
|
92
95
|
ref={surfaceRef}
|
|
93
|
-
className="border-border bg-card origin-top overflow-hidden rounded-xl border shadow-sm"
|
|
96
|
+
className="actuate-canvas border-border bg-card origin-top overflow-hidden rounded-xl border shadow-sm"
|
|
94
97
|
style={{
|
|
95
98
|
width: vp.width,
|
|
96
99
|
transform: `scale(${scale})`,
|
|
@@ -91,7 +91,7 @@ export function PagePreview({ documentId, onNavigate }: PagePreviewProps) {
|
|
|
91
91
|
|
|
92
92
|
{/* The canvas is intentionally white regardless of the admin theme:
|
|
93
93
|
it emulates the public page surface, not admin chrome. */}
|
|
94
|
-
<div className="bg-card flex-1 overflow-auto">
|
|
94
|
+
<div className="actuate-canvas bg-card flex-1 overflow-auto">
|
|
95
95
|
{visible.length === 0 ? (
|
|
96
96
|
<div className="text-muted-foreground flex h-full items-center justify-center text-sm">
|
|
97
97
|
This page has no visible sections yet.
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { CSS } from '@dnd-kit/utilities'
|
|
21
21
|
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
22
22
|
import { Toggle } from '../../components/ui/Toggle.js'
|
|
23
|
+
import { RichTextField } from '../../fields/RichTextField.js'
|
|
23
24
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
24
25
|
import type { ResolvedRepeater, SectionSettings } from './section-types.js'
|
|
25
26
|
import {
|
|
@@ -37,6 +38,9 @@ interface SectionInspectorProps {
|
|
|
37
38
|
onSettingsChange: (patch: Partial<SectionSettings>) => void
|
|
38
39
|
onMetaChange: (patch: { name?: string; internalLabel?: string }) => void
|
|
39
40
|
onToggleVisibility: () => void
|
|
41
|
+
/** Post body field — shown when editing an Article Body section bound to the post field. */
|
|
42
|
+
postBody?: string
|
|
43
|
+
onPostBodyChange?: (html: string) => void
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const LABEL = 'text-foreground mb-1 block text-xs font-medium'
|
|
@@ -51,8 +55,17 @@ export function SectionInspector({
|
|
|
51
55
|
onSettingsChange,
|
|
52
56
|
onMetaChange,
|
|
53
57
|
onToggleVisibility,
|
|
58
|
+
postBody,
|
|
59
|
+
onPostBodyChange,
|
|
54
60
|
}: SectionInspectorProps) {
|
|
55
61
|
const def = getSectionType(section.sectionType)
|
|
62
|
+
const bodySource = String(section.content.source ?? 'field')
|
|
63
|
+
const editPostBodyField =
|
|
64
|
+
section.sectionType === 'article-body' &&
|
|
65
|
+
bodySource === 'field' &&
|
|
66
|
+
onPostBodyChange !== undefined
|
|
67
|
+
const contentFields =
|
|
68
|
+
def?.fields.filter((field) => !(editPostBodyField && field.key === 'body')) ?? []
|
|
56
69
|
|
|
57
70
|
// Escape closes the inspector.
|
|
58
71
|
useEffect(() => {
|
|
@@ -143,7 +156,16 @@ export function SectionInspector({
|
|
|
143
156
|
<legend className="text-muted-foreground mb-2 text-[10px] font-medium tracking-wider uppercase">
|
|
144
157
|
Content
|
|
145
158
|
</legend>
|
|
146
|
-
{
|
|
159
|
+
{editPostBodyField && (
|
|
160
|
+
<RichTextField
|
|
161
|
+
label="Body"
|
|
162
|
+
value={postBody ?? ''}
|
|
163
|
+
onChange={onPostBodyChange}
|
|
164
|
+
className="[&_.ProseMirror]:min-h-[240px]"
|
|
165
|
+
helpText="Main article copy — rendered in the canvas preview and on the live site."
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
{contentFields.map((field) => (
|
|
147
169
|
<ContentField
|
|
148
170
|
key={field.key}
|
|
149
171
|
field={field}
|
|
@@ -87,7 +87,7 @@ export function PostEditorCanvas({
|
|
|
87
87
|
>
|
|
88
88
|
<div
|
|
89
89
|
ref={surfaceRef}
|
|
90
|
-
className="border-border bg-card origin-top overflow-hidden rounded-xl border shadow-sm"
|
|
90
|
+
className="actuate-canvas border-border bg-card origin-top overflow-hidden rounded-xl border shadow-sm"
|
|
91
91
|
style={{ width: vp.width, transform: `scale(${scale})`, transformOrigin: 'top left' }}
|
|
92
92
|
>
|
|
93
93
|
<PostHeader config={header} context={context} />
|