@actuate-media/cms-admin 0.72.2 → 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 +33 -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/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/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/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/views/Dashboard.tsx +2 -1
- package/src/views/page-editor/SectionInspector.tsx +23 -1
- package/src/views/post-editor/PostFieldsModal.tsx +412 -174
- package/src/views/post-editor/PostSectionEditor.tsx +8 -0
|
@@ -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/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">
|
|
@@ -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}
|