@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/__tests__/components/toggle.test.d.ts +2 -0
  3. package/dist/__tests__/components/toggle.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/toggle.test.js +17 -0
  5. package/dist/__tests__/components/toggle.test.js.map +1 -0
  6. package/dist/__tests__/views/post-fields-modal.render.test.js +38 -0
  7. package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
  8. package/dist/actuate-admin.css +1 -1
  9. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  10. package/dist/components/MediaPickerModal.js +1 -1
  11. package/dist/components/MediaPickerModal.js.map +1 -1
  12. package/dist/components/TagInput.js +1 -1
  13. package/dist/components/TagInput.js.map +1 -1
  14. package/dist/components/ui/Modal.d.ts +17 -1
  15. package/dist/components/ui/Modal.d.ts.map +1 -1
  16. package/dist/components/ui/Modal.js +29 -3
  17. package/dist/components/ui/Modal.js.map +1 -1
  18. package/dist/components/ui/Toggle.d.ts.map +1 -1
  19. package/dist/components/ui/Toggle.js +4 -3
  20. package/dist/components/ui/Toggle.js.map +1 -1
  21. package/dist/fields/RichTextField.d.ts +3 -1
  22. package/dist/fields/RichTextField.d.ts.map +1 -1
  23. package/dist/fields/RichTextField.js +2 -2
  24. package/dist/fields/RichTextField.js.map +1 -1
  25. package/dist/views/Dashboard.d.ts.map +1 -1
  26. package/dist/views/Dashboard.js +1 -1
  27. package/dist/views/Dashboard.js.map +1 -1
  28. package/dist/views/page-editor/SectionInspector.d.ts +4 -1
  29. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  30. package/dist/views/page-editor/SectionInspector.js +8 -2
  31. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  32. package/dist/views/post-editor/PostFieldsModal.d.ts +1 -1
  33. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -1
  34. package/dist/views/post-editor/PostFieldsModal.js +152 -13
  35. package/dist/views/post-editor/PostFieldsModal.js.map +1 -1
  36. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  37. package/dist/views/post-editor/PostSectionEditor.js +6 -1
  38. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/__tests__/components/toggle.test.tsx +17 -0
  41. package/src/__tests__/views/post-fields-modal.render.test.tsx +54 -0
  42. package/src/components/MediaPickerModal.tsx +6 -2
  43. package/src/components/TagInput.tsx +1 -1
  44. package/src/components/ui/Modal.tsx +46 -3
  45. package/src/components/ui/Toggle.tsx +4 -3
  46. package/src/fields/RichTextField.tsx +4 -1
  47. package/src/styles/actuate-tokens.css +7 -2
  48. package/src/views/Dashboard.tsx +2 -1
  49. package/src/views/page-editor/SectionInspector.tsx +23 -1
  50. package/src/views/post-editor/PostFieldsModal.tsx +412 -174
  51. 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 open={open} onOpenChange={(next) => !next && onClose()}>
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="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none"
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="px-6 py-4">{children}</div>
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-flex shrink-0 cursor-pointer rounded-[var(--r-pill)] 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',
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-transform duration-[var(--motion-base)]',
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
- md: 'h-4 w-4 translate-x-0.5 data-[state=checked]:translate-x-[17.5px]',
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
- --muted: #b0adbe;
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
- --muted: #504e68;
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;
@@ -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
- <p className="text-muted-foreground text-sm">
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
- {def?.fields.map((field) => (
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}