@prenta/admin 1.1.0 → 1.2.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/__tests__/lib/canvas-edit-chrome.test.js +1 -22
  3. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -1
  4. package/dist/__tests__/lib/preview-bridge.test.js +19 -79
  5. package/dist/__tests__/lib/preview-bridge.test.js.map +1 -1
  6. package/dist/__tests__/lib/view-site-chrome.test.js +11 -1
  7. package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -1
  8. package/dist/__tests__/views/canvas-review-mode.render.test.js +11 -1
  9. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -1
  10. package/dist/__tests__/views/editor-top-bar.render.test.js +8 -0
  11. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  12. package/dist/__tests__/views/page-section-editor.test.js +23 -9
  13. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  14. package/dist/lib/canvas-edit-chrome.d.ts +0 -17
  15. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -1
  16. package/dist/lib/canvas-edit-chrome.js +0 -62
  17. package/dist/lib/canvas-edit-chrome.js.map +1 -1
  18. package/dist/lib/preview-bridge.d.ts +7 -74
  19. package/dist/lib/preview-bridge.d.ts.map +1 -1
  20. package/dist/lib/preview-bridge.js +12 -73
  21. package/dist/lib/preview-bridge.js.map +1 -1
  22. package/dist/lib/view-site-chrome.d.ts +3 -0
  23. package/dist/lib/view-site-chrome.d.ts.map +1 -1
  24. package/dist/lib/view-site-chrome.js +11 -2
  25. package/dist/lib/view-site-chrome.js.map +1 -1
  26. package/dist/views/page-editor/EditorCanvas.d.ts +1 -1
  27. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  28. package/dist/views/page-editor/EditorTopBar.d.ts +3 -1
  29. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  30. package/dist/views/page-editor/EditorTopBar.js +2 -1
  31. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  32. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  33. package/dist/views/page-editor/PageSectionEditor.js +8 -47
  34. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  35. package/dist/views/page-editor/sections/SectionRenderer.d.ts +1 -1
  36. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  37. package/dist/views/page-editor/sections/SectionRenderer.js +2 -1
  38. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  39. package/dist/views/post-editor/PostEditorCanvas.d.ts +1 -1
  40. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  41. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  42. package/dist/views/post-editor/PostSectionEditor.js +7 -41
  43. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  44. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  45. package/dist/views/settings/SeoSettingsTab.js +2 -1
  46. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  47. package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
  48. package/dist/views/settings/SiteInformationCard.js +3 -1
  49. package/dist/views/settings/SiteInformationCard.js.map +1 -1
  50. package/package.json +3 -3
  51. package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
  52. package/src/__tests__/lib/preview-bridge.test.ts +21 -110
  53. package/src/__tests__/lib/view-site-chrome.test.ts +16 -1
  54. package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
  55. package/src/__tests__/views/editor-top-bar.render.test.tsx +19 -0
  56. package/src/__tests__/views/page-section-editor.test.tsx +23 -13
  57. package/src/lib/canvas-edit-chrome.ts +0 -64
  58. package/src/lib/preview-bridge.ts +11 -157
  59. package/src/lib/view-site-chrome.ts +15 -3
  60. package/src/views/page-editor/EditorCanvas.tsx +1 -1
  61. package/src/views/page-editor/EditorTopBar.tsx +4 -0
  62. package/src/views/page-editor/PageSectionEditor.tsx +6 -63
  63. package/src/views/page-editor/sections/SectionRenderer.tsx +5 -2
  64. package/src/views/post-editor/PostEditorCanvas.tsx +1 -1
  65. package/src/views/post-editor/PostSectionEditor.tsx +5 -57
  66. package/src/views/settings/SeoSettingsTab.tsx +21 -0
  67. package/src/views/settings/SiteInformationCard.tsx +7 -0
@@ -27,13 +27,11 @@ import {
27
27
  savePageDraft,
28
28
  toggleSectionVisibility,
29
29
  updateSectionContent,
30
- updateSectionContentAtPath,
31
30
  updateSectionMeta,
32
31
  validatePage,
33
32
  type EditorPage,
34
33
  type ValidationResult,
35
34
  } from '../../lib/page-editor-service.js'
36
- import { MediaPickerModal } from '../../components/MediaPickerModal.js'
37
35
  import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
38
36
  import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
39
37
  import type { ViewSiteIntent } from '../../lib/view-site-chrome.js'
@@ -61,7 +59,7 @@ import { SectionEditForm } from './SectionEditForm.js'
61
59
  import { PageSettingsModal } from './PageSettingsModal.js'
62
60
  import { ValidationSummary } from './ValidationSummary.js'
63
61
  import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
64
- import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
62
+ import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
65
63
 
66
64
  interface PageSectionEditorProps {
67
65
  /** undefined when creating a new page (`/pages/new`) */
@@ -134,13 +132,6 @@ export function PageSectionEditor({
134
132
  const [selectedId, setSelectedId] = useState<string | null>(null)
135
133
  /** Canvas field click — focused after the inspector mounts for `selectedId`. */
136
134
  const [pendingFocusPath, setPendingFocusPath] = useState<string | null>(null)
137
- /** Bump to skip the next `prenta-preview:sections` echo after a canvas text edit. */
138
- const [suppressSectionsEcho, setSuppressSectionsEcho] = useState(0)
139
- /** Canvas image Replace — opens MediaPickerModal for this field path. */
140
- const [canvasMediaTarget, setCanvasMediaTarget] = useState<{
141
- sectionId: string
142
- fieldPath: string
143
- } | null>(null)
144
135
  const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
145
136
  const [zoom, setZoom] = useState<Zoom>('fit')
146
137
  /**
@@ -227,7 +218,7 @@ export function PageSectionEditor({
227
218
 
228
219
  const { canEdit, canPublish, canCreateRedirect } = useMemo(() => deriveCan(session), [session])
229
220
  const customRenderers = useAdminSectionRenderers()
230
- const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
221
+ const { siteUrl: publicSiteUrl, matchesAdminOrigin } = usePublicSiteUrl(config?.seo?.siteUrl)
231
222
 
232
223
  // A misconfigured `pages` collection (missing the fields the editor
233
224
  // writes) would silently drop content on save — detect it up front.
@@ -716,39 +707,14 @@ export function PageSectionEditor({
716
707
  // rail has no close affordance at all — the chevron and the row both
717
708
  // re-select what is already selected and nothing happens.
718
709
  //
719
- // Except when a fieldPath came with it: that is the site preview saying
720
- // "the editor clicked THIS field", which must open and focus it. Making
721
- // that toggle would close the form on the second click of a field the
722
- // user is trying to edit.
710
+ // Except when a fieldPath came with it: a canvas field click must open
711
+ // and focus that input. Toggling would close the form on the second
712
+ // click of a field the user is trying to inspect.
723
713
  if (fieldPath) return id
724
714
  return current === id ? null : id
725
715
  })
726
716
  }, [])
727
717
 
728
- // `effectiveCanEdit`, not `canEdit`: the site preview is a second editing
729
- // surface, so a role check alone let it write through both the version-review
730
- // lock and the builder-owned lock the rest of the editor honours.
731
- const handleCanvasEdit = useCallback(
732
- (edit: { sectionId: string; fieldPath: string; value: string }) => {
733
- if (!effectiveCanEdit) return
734
- setSuppressSectionsEcho((n) => n + 1)
735
- mutateSections((s) =>
736
- updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
737
- )
738
- setSelectedId(edit.sectionId)
739
- },
740
- [effectiveCanEdit, mutateSections],
741
- )
742
-
743
- const handleCanvasRequestMedia = useCallback(
744
- (request: { sectionId: string; fieldPath: string }) => {
745
- if (!effectiveCanEdit) return
746
- setSelectedId(request.sectionId)
747
- setCanvasMediaTarget(request)
748
- },
749
- [effectiveCanEdit],
750
- )
751
-
752
718
  // After a canvas field click, focus the matching inspector input once painted.
753
719
  useEffect(() => {
754
720
  if (!pendingFocusPath || !selectedId) return
@@ -878,6 +844,7 @@ export function PageSectionEditor({
878
844
  onViewOnSite={page.id && page.slug ? handleViewOnSite : undefined}
879
845
  onShare={page.id ? handleShare : undefined}
880
846
  hasPublicSiteUrl={Boolean(publicSiteUrl)}
847
+ matchesAdminOrigin={matchesAdminOrigin}
881
848
  historyOpen={historyOpen}
882
849
  onHistory={page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
883
850
  onOpenSettings={() => setSettingsOpen(true)}
@@ -1114,30 +1081,6 @@ export function PageSectionEditor({
1114
1081
  cancelLabel="Stay"
1115
1082
  destructive
1116
1083
  />
1117
-
1118
- <MediaPickerModal
1119
- open={canvasMediaTarget !== null}
1120
- onClose={() => setCanvasMediaTarget(null)}
1121
- accept="image/*"
1122
- onSelectItem={(item) => {
1123
- if (!canvasMediaTarget) return
1124
- const { sectionId, fieldPath } = canvasMediaTarget
1125
- const { widthPath, heightPath } = companionDimensionPaths(fieldPath)
1126
- // Media Replace must echo sections so the canvas image updates.
1127
- mutateSections((s) => {
1128
- let next = updateSectionContentAtPath(s, sectionId, fieldPath, item.url)
1129
- if (typeof item.width === 'number') {
1130
- next = updateSectionContentAtPath(next, sectionId, widthPath, item.width)
1131
- }
1132
- if (typeof item.height === 'number') {
1133
- next = updateSectionContentAtPath(next, sectionId, heightPath, item.height)
1134
- }
1135
- return next
1136
- })
1137
- setCanvasMediaTarget(null)
1138
- }}
1139
- onSelect={() => setCanvasMediaTarget(null)}
1140
- />
1141
1084
  </ErrorBoundary>
1142
1085
  )
1143
1086
  }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { Puzzle } from 'lucide-react'
4
+ import { fieldPathFromEventTarget } from '../../../lib/preview-bridge.js'
4
5
  import type { PageSection } from '../../../lib/page-editor-service.js'
5
6
  import { ErrorBoundary } from '../../../components/ErrorBoundary.js'
6
7
  import { getSectionType } from '../section-types.js'
@@ -163,7 +164,7 @@ export interface SectionRendererProps {
163
164
  * renders hidden sections muted instead of removing them. */
164
165
  editable?: boolean
165
166
  selected?: boolean
166
- onSelect?: (id: string) => void
167
+ onSelect?: (id: string, fieldPath?: string) => void
167
168
  /** Lets the canvas register the DOM node for scroll-to-section. */
168
169
  registerRef?: (id: string, el: HTMLElement | null) => void
169
170
  /** Live post data for post-oriented section renderers (pages omit this). */
@@ -234,7 +235,9 @@ export function SectionRenderer({
234
235
  ? 'outline-transparent hover:outline hover:outline-[color-mix(in_oklab,var(--acc)_40%,transparent)]'
235
236
  : 'outline-transparent'
236
237
  }`}
237
- onClick={interactive ? () => onSelect?.(section.id) : undefined}
238
+ onClick={
239
+ interactive ? (e) => onSelect?.(section.id, fieldPathFromEventTarget(e.target)) : undefined
240
+ }
238
241
  // Selection is a convenience in the editor; the section list is the
239
242
  // primary, fully-accessible control surface, so this stays a region.
240
243
  aria-label={
@@ -17,7 +17,7 @@ interface PostEditorCanvasProps {
17
17
  selectedId: string | null
18
18
  viewport: ViewportId
19
19
  zoom?: Zoom
20
- onSelect: (id: string) => void
20
+ onSelect: (id: string, fieldPath?: string) => void
21
21
  onScaleChange?: (scale: number) => void
22
22
  onCanvasElement?: (el: HTMLElement | null) => void
23
23
  /** Add-section CTA on the empty canvas. Omitted by design-owned editors. */
@@ -20,14 +20,12 @@ import {
20
20
  savePostDraft,
21
21
  toggleSectionVisibility,
22
22
  updateSectionContent,
23
- updateSectionContentAtPath,
24
23
  updateSectionMeta,
25
24
  validatePost,
26
25
  type EditorPost,
27
26
  type PostHeaderConfig,
28
27
  type ValidationResult,
29
28
  } from '../../lib/post-editor-service.js'
30
- import { MediaPickerModal } from '../../components/MediaPickerModal.js'
31
29
  import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
32
30
  import { defaultPostHeader } from '@prenta/core/page-sections'
33
31
  import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
@@ -47,7 +45,7 @@ import { buildEditorContentHtml, computeLiveSeoScore } from '../../lib/editor-se
47
45
  import { snapshotBaseline, type ReflowBaseline } from '../../lib/reflow-baseline.js'
48
46
  import { ReflowContext } from '../page-editor/reflow-context.js'
49
47
  import type { Zoom } from '../../lib/breakpoints.js'
50
- import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
48
+ import { inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
51
49
  import { PostEditorCanvas } from './PostEditorCanvas.js'
52
50
  import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'
53
51
 
@@ -108,13 +106,6 @@ export function PostSectionEditor({
108
106
  const [selectedId, setSelectedId] = useState<string | null>(null)
109
107
  /** Canvas field click — focused after the inspector mounts for `selectedId`. */
110
108
  const [pendingFocusPath, setPendingFocusPath] = useState<string | null>(null)
111
- /** Bump to skip the next `prenta-preview:sections` echo after a canvas text edit. */
112
- const [suppressSectionsEcho, setSuppressSectionsEcho] = useState(0)
113
- /** Canvas image Replace — opens MediaPickerModal for this field path. */
114
- const [canvasMediaTarget, setCanvasMediaTarget] = useState<{
115
- sectionId: string
116
- fieldPath: string
117
- } | null>(null)
118
109
  const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
119
110
  const [zoom, setZoom] = useState<Zoom>('fit')
120
111
  /** Scale the canvas measured — reported upward so the zoom chip shows a real %. */
@@ -158,7 +149,7 @@ export function PostSectionEditor({
158
149
 
159
150
  const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
160
151
  const customRenderers = useAdminSectionRenderers()
161
- const { siteUrl: publicSiteUrl } = usePublicSiteUrl(config?.seo?.siteUrl)
152
+ const { siteUrl: publicSiteUrl, matchesAdminOrigin } = usePublicSiteUrl(config?.seo?.siteUrl)
162
153
 
163
154
  const typeLabel = useMemo(() => {
164
155
  const col = config?.collections?.[postType]
@@ -485,32 +476,11 @@ export function PostSectionEditor({
485
476
  setRailTab('sections')
486
477
  setPendingFocusPath(fieldPath ?? null)
487
478
  // Toggles, because selection is now disclosure — see the note on the page
488
- // editor's copy. A `fieldPath` means the site preview is pointing at a
489
- // specific field, which must open rather than toggle.
479
+ // editor's copy. A `fieldPath` means a canvas field click, which must
480
+ // open rather than toggle.
490
481
  setSelectedId((current) => (fieldPath ? id : current === id ? null : id))
491
482
  }, [])
492
483
 
493
- const handleCanvasEdit = useCallback(
494
- (edit: { sectionId: string; fieldPath: string; value: string }) => {
495
- if (!canEdit) return
496
- setSuppressSectionsEcho((n) => n + 1)
497
- mutateSections((s) =>
498
- updateSectionContentAtPath(s, edit.sectionId, edit.fieldPath, edit.value),
499
- )
500
- setSelectedId(edit.sectionId)
501
- },
502
- [canEdit, mutateSections],
503
- )
504
-
505
- const handleCanvasRequestMedia = useCallback(
506
- (request: { sectionId: string; fieldPath: string }) => {
507
- if (!canEdit) return
508
- setSelectedId(request.sectionId)
509
- setCanvasMediaTarget(request)
510
- },
511
- [canEdit],
512
- )
513
-
514
484
  // After a canvas field click, focus the matching inspector input once painted.
515
485
  useEffect(() => {
516
486
  if (!pendingFocusPath || !selectedId) return
@@ -634,6 +604,7 @@ export function PostSectionEditor({
634
604
  onViewOnSite={post.id && post.slug ? handleViewOnSite : undefined}
635
605
  onShare={post.id ? handleShare : undefined}
636
606
  hasPublicSiteUrl={Boolean(publicSiteUrl)}
607
+ matchesAdminOrigin={matchesAdminOrigin}
637
608
  onHistory={post.id ? () => setHistoryOpen(true) : undefined}
638
609
  onOpenSettings={() => setFieldsOpen(true)}
639
610
  onOpenSeo={handleToggleSeo}
@@ -817,29 +788,6 @@ export function PostSectionEditor({
817
788
  cancelLabel="Stay"
818
789
  destructive
819
790
  />
820
-
821
- <MediaPickerModal
822
- open={canvasMediaTarget !== null}
823
- onClose={() => setCanvasMediaTarget(null)}
824
- accept="image/*"
825
- onSelectItem={(item) => {
826
- if (!canvasMediaTarget) return
827
- const { sectionId, fieldPath } = canvasMediaTarget
828
- const { widthPath, heightPath } = companionDimensionPaths(fieldPath)
829
- mutateSections((s) => {
830
- let next = updateSectionContentAtPath(s, sectionId, fieldPath, item.url)
831
- if (typeof item.width === 'number') {
832
- next = updateSectionContentAtPath(next, sectionId, widthPath, item.width)
833
- }
834
- if (typeof item.height === 'number') {
835
- next = updateSectionContentAtPath(next, sectionId, heightPath, item.height)
836
- }
837
- return next
838
- })
839
- setCanvasMediaTarget(null)
840
- }}
841
- onSelect={() => setCanvasMediaTarget(null)}
842
- />
843
791
  </ErrorBoundary>
844
792
  )
845
793
  }
@@ -13,6 +13,7 @@ import {
13
13
  Upload,
14
14
  X,
15
15
  } from 'lucide-react'
16
+ import { siteUrlMatchesAdminOrigin } from '../../lib/preview-link.js'
16
17
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
17
18
  import { SEOConfigPanel } from '../../components/SEOConfigPanel.js'
18
19
  import {
@@ -125,6 +126,26 @@ export function SeoSettingsTab({ onNavigate, role }: SeoSettingsTabProps) {
125
126
 
126
127
  <SettingsValidationSummary issues={seo.issues} />
127
128
 
129
+ {!seo.siteUrl ? (
130
+ <div
131
+ role="status"
132
+ className="border-warning/30 bg-warning/10 text-warning flex items-start gap-2 rounded-lg border p-4 text-sm"
133
+ >
134
+ <AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
135
+ Set the public site URL in Settings → General. View site and Share stay disabled until it
136
+ is set.
137
+ </div>
138
+ ) : siteUrlMatchesAdminOrigin(seo.siteUrl) ? (
139
+ <div
140
+ role="status"
141
+ className="border-warning/30 bg-warning/10 text-warning flex items-start gap-2 rounded-lg border p-4 text-sm"
142
+ >
143
+ <AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
144
+ This origin is the admin, not the public site. Set the public site URL to the live apex —
145
+ View site and Share stay disabled while it matches this CMS host.
146
+ </div>
147
+ ) : null}
148
+
128
149
  <MetaDefaultsCard seo={seo} canEdit={canEdit} />
129
150
  <OrganizationCard seo={seo} canEdit={canEdit} />
130
151
  <SearchEngineVerificationCard seo={seo} canEdit={canEdit} />
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useId } from 'react'
4
4
  import { CheckCircle2 } from 'lucide-react'
5
+ import { siteUrlMatchesAdminOrigin } from '../../lib/preview-link.js'
5
6
  import { SettingsCard, SettingsSourceBadge } from './components.js'
6
7
  import type {
7
8
  GeneralSettingsForm,
@@ -37,6 +38,7 @@ export function SiteInformationCard({
37
38
  const urlIssue = issues.find((i) => i.field === 'siteUrl')
38
39
  const urlEditable = sources.siteUrl?.editable !== false && canEdit
39
40
  const urlValid = !urlIssue && form.siteUrl.trim().length > 0
41
+ const urlIsAdminOrigin = siteUrlMatchesAdminOrigin(form.siteUrl)
40
42
 
41
43
  return (
42
44
  <SettingsCard title="Site Information" description="Your site's name, tagline, and public URL.">
@@ -99,6 +101,11 @@ export function SiteInformationCard({
99
101
  >
100
102
  {urlIssue.message}
101
103
  </p>
104
+ ) : urlIsAdminOrigin ? (
105
+ <p role="status" className="text-warning mt-1 text-sm">
106
+ This origin is the admin, not the public site. Set the public apex (Astro or the live
107
+ Next site), not this CMS host — View site and Share will stay disabled until you do.
108
+ </p>
102
109
  ) : urlValid ? (
103
110
  <p className="text-muted-foreground mt-1 flex items-center gap-1.5 text-sm">
104
111
  <CheckCircle2 size={13} className="text-success" aria-hidden="true" />