@prenta/admin 1.0.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 (149) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +5 -56
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/breakpoint-controls.render.test.js +4 -4
  6. package/dist/__tests__/components/breakpoint-controls.render.test.js.map +1 -1
  7. package/dist/__tests__/lib/canvas-edit-chrome.test.js +1 -22
  8. package/dist/__tests__/lib/canvas-edit-chrome.test.js.map +1 -1
  9. package/dist/__tests__/lib/open-public-site-tab.test.d.ts +2 -0
  10. package/dist/__tests__/lib/open-public-site-tab.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/open-public-site-tab.test.js +64 -0
  12. package/dist/__tests__/lib/open-public-site-tab.test.js.map +1 -0
  13. package/dist/__tests__/lib/preview-bridge.test.js +19 -79
  14. package/dist/__tests__/lib/preview-bridge.test.js.map +1 -1
  15. package/dist/__tests__/lib/view-site-chrome.test.d.ts +2 -0
  16. package/dist/__tests__/lib/view-site-chrome.test.d.ts.map +1 -0
  17. package/dist/__tests__/lib/view-site-chrome.test.js +69 -0
  18. package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -0
  19. package/dist/__tests__/router/admin-routes.test.js +4 -0
  20. package/dist/__tests__/router/admin-routes.test.js.map +1 -1
  21. package/dist/__tests__/views/canvas-review-mode.render.test.js +11 -1
  22. package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -1
  23. package/dist/__tests__/views/canvas-surface-shared.test.js +0 -16
  24. package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -1
  25. package/dist/__tests__/views/editor-top-bar.render.test.js +52 -22
  26. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  27. package/dist/__tests__/views/page-section-editor.test.js +53 -18
  28. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  29. package/dist/__tests__/views/post-section-editor.test.js +23 -0
  30. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  31. package/dist/components/BreakpointPicker.d.ts +3 -2
  32. package/dist/components/BreakpointPicker.d.ts.map +1 -1
  33. package/dist/components/BreakpointPicker.js +5 -3
  34. package/dist/components/BreakpointPicker.js.map +1 -1
  35. package/dist/components/CanvasEditTip.js +1 -1
  36. package/dist/components/CanvasEditTip.js.map +1 -1
  37. package/dist/lib/canvas-edit-chrome.d.ts +0 -17
  38. package/dist/lib/canvas-edit-chrome.d.ts.map +1 -1
  39. package/dist/lib/canvas-edit-chrome.js +0 -62
  40. package/dist/lib/canvas-edit-chrome.js.map +1 -1
  41. package/dist/lib/editor-preview.d.ts +4 -1
  42. package/dist/lib/editor-preview.d.ts.map +1 -1
  43. package/dist/lib/editor-preview.js +4 -1
  44. package/dist/lib/editor-preview.js.map +1 -1
  45. package/dist/lib/open-public-site-tab.d.ts +14 -0
  46. package/dist/lib/open-public-site-tab.d.ts.map +1 -0
  47. package/dist/lib/open-public-site-tab.js +46 -0
  48. package/dist/lib/open-public-site-tab.js.map +1 -0
  49. package/dist/lib/preview-bridge.d.ts +7 -74
  50. package/dist/lib/preview-bridge.d.ts.map +1 -1
  51. package/dist/lib/preview-bridge.js +12 -73
  52. package/dist/lib/preview-bridge.js.map +1 -1
  53. package/dist/lib/view-site-chrome.d.ts +19 -0
  54. package/dist/lib/view-site-chrome.d.ts.map +1 -0
  55. package/dist/lib/view-site-chrome.js +24 -0
  56. package/dist/lib/view-site-chrome.js.map +1 -0
  57. package/dist/router/admin-routes.d.ts.map +1 -1
  58. package/dist/router/admin-routes.js +0 -2
  59. package/dist/router/admin-routes.js.map +1 -1
  60. package/dist/views/page-editor/EditorCanvas.d.ts +2 -3
  61. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  62. package/dist/views/page-editor/EditorCanvas.js +2 -2
  63. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  64. package/dist/views/page-editor/EditorTopBar.d.ts +15 -17
  65. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  66. package/dist/views/page-editor/EditorTopBar.js +20 -21
  67. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  68. package/dist/views/page-editor/PageSectionEditor.d.ts +1 -7
  69. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  70. package/dist/views/page-editor/PageSectionEditor.js +36 -141
  71. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  72. package/dist/views/page-editor/StructureNotice.d.ts +2 -5
  73. package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
  74. package/dist/views/page-editor/StructureNotice.js +6 -10
  75. package/dist/views/page-editor/StructureNotice.js.map +1 -1
  76. package/dist/views/page-editor/sections/SectionRenderer.d.ts +1 -1
  77. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  78. package/dist/views/page-editor/sections/SectionRenderer.js +2 -1
  79. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  80. package/dist/views/post-editor/PostEditorCanvas.d.ts +2 -4
  81. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  82. package/dist/views/post-editor/PostEditorCanvas.js +3 -3
  83. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  84. package/dist/views/post-editor/PostSectionEditor.d.ts +1 -15
  85. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  86. package/dist/views/post-editor/PostSectionEditor.js +36 -128
  87. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  88. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  89. package/dist/views/settings/SeoSettingsTab.js +2 -1
  90. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  91. package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
  92. package/dist/views/settings/SiteInformationCard.js +3 -1
  93. package/dist/views/settings/SiteInformationCard.js.map +1 -1
  94. package/package.json +6 -6
  95. package/src/AdminRoot.tsx +0 -70
  96. package/src/__tests__/components/breakpoint-controls.render.test.tsx +6 -4
  97. package/src/__tests__/lib/canvas-edit-chrome.test.ts +0 -27
  98. package/src/__tests__/lib/open-public-site-tab.test.ts +74 -0
  99. package/src/__tests__/lib/preview-bridge.test.ts +21 -110
  100. package/src/__tests__/lib/view-site-chrome.test.ts +80 -0
  101. package/src/__tests__/router/admin-routes.test.ts +5 -0
  102. package/src/__tests__/views/canvas-review-mode.render.test.tsx +14 -1
  103. package/src/__tests__/views/canvas-surface-shared.test.ts +0 -19
  104. package/src/__tests__/views/editor-top-bar.render.test.tsx +104 -22
  105. package/src/__tests__/views/page-section-editor.test.tsx +65 -22
  106. package/src/__tests__/views/post-section-editor.test.tsx +35 -0
  107. package/src/components/BreakpointPicker.tsx +6 -3
  108. package/src/components/CanvasEditTip.tsx +2 -2
  109. package/src/lib/canvas-edit-chrome.ts +0 -64
  110. package/src/lib/editor-preview.ts +4 -1
  111. package/src/lib/open-public-site-tab.ts +57 -0
  112. package/src/lib/preview-bridge.ts +11 -157
  113. package/src/lib/view-site-chrome.ts +46 -0
  114. package/src/router/admin-routes.ts +0 -2
  115. package/src/styles/theme.css +4 -4
  116. package/src/views/page-editor/EditorCanvas.tsx +2 -7
  117. package/src/views/page-editor/EditorTopBar.tsx +85 -82
  118. package/src/views/page-editor/PageSectionEditor.tsx +83 -220
  119. package/src/views/page-editor/StructureNotice.tsx +7 -42
  120. package/src/views/page-editor/sections/SectionRenderer.tsx +5 -2
  121. package/src/views/post-editor/PostEditorCanvas.tsx +3 -9
  122. package/src/views/post-editor/PostSectionEditor.tsx +71 -195
  123. package/src/views/settings/SeoSettingsTab.tsx +21 -0
  124. package/src/views/settings/SiteInformationCard.tsx +7 -0
  125. package/dist/__tests__/views/use-site-preview.render.test.d.ts +0 -2
  126. package/dist/__tests__/views/use-site-preview.render.test.d.ts.map +0 -1
  127. package/dist/__tests__/views/use-site-preview.render.test.js +0 -120
  128. package/dist/__tests__/views/use-site-preview.render.test.js.map +0 -1
  129. package/dist/views/page-editor/PagePreview.d.ts +0 -13
  130. package/dist/views/page-editor/PagePreview.d.ts.map +0 -1
  131. package/dist/views/page-editor/PagePreview.js +0 -46
  132. package/dist/views/page-editor/PagePreview.js.map +0 -1
  133. package/dist/views/page-editor/SitePreviewFrame.d.ts +0 -43
  134. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +0 -1
  135. package/dist/views/page-editor/SitePreviewFrame.js +0 -165
  136. package/dist/views/page-editor/SitePreviewFrame.js.map +0 -1
  137. package/dist/views/page-editor/useSitePreview.d.ts +0 -47
  138. package/dist/views/page-editor/useSitePreview.d.ts.map +0 -1
  139. package/dist/views/page-editor/useSitePreview.js +0 -96
  140. package/dist/views/page-editor/useSitePreview.js.map +0 -1
  141. package/dist/views/post-editor/PostPreview.d.ts +0 -14
  142. package/dist/views/post-editor/PostPreview.d.ts.map +0 -1
  143. package/dist/views/post-editor/PostPreview.js +0 -63
  144. package/dist/views/post-editor/PostPreview.js.map +0 -1
  145. package/src/__tests__/views/use-site-preview.render.test.tsx +0 -164
  146. package/src/views/page-editor/PagePreview.tsx +0 -105
  147. package/src/views/page-editor/SitePreviewFrame.tsx +0 -250
  148. package/src/views/page-editor/useSitePreview.ts +0 -158
  149. package/src/views/post-editor/PostPreview.tsx +0 -125
@@ -1,140 +1,11 @@
1
1
  /**
2
- * Versioned postMessage protocol between the page editor (parent) and the
3
- * Next editing canvas (iframe). Namespaced with colons so it does not collide
4
- * with the legacy `prenta-preview-update` DocumentEdit bus.
2
+ * Field-path helpers shared by the editor canvas and the inspector.
5
3
  *
6
- * @see prenta docs/superpowers/specs/2026-07-29-visual-editing-design.md
4
+ * The old `prenta-preview:*` postMessage protocol is gone — View site opens a
5
+ * normal tab and does not talk back. `data-cms-field` on first-party section
6
+ * renderers still feeds the reflow guard and canvas field-focus.
7
7
  */
8
8
 
9
- export const PREVIEW_BRIDGE_VERSION = 1 as const
10
-
11
- /** Debounce for live section pushes while typing in the inspector. */
12
- export const PREVIEW_SECTIONS_DEBOUNCE_MS = 120
13
-
14
- export type PreviewBridgeMessageType =
15
- | 'prenta-preview:ready'
16
- | 'prenta-preview:select'
17
- | 'prenta-preview:highlight'
18
- | 'prenta-preview:sections'
19
- | 'prenta-preview:mode'
20
- | 'prenta-preview:edit'
21
- | 'prenta-preview:request-media'
22
-
23
- export interface PreviewBridgeEnvelope {
24
- type: PreviewBridgeMessageType
25
- v: typeof PREVIEW_BRIDGE_VERSION
26
- }
27
-
28
- export interface PreviewReadyMessage extends PreviewBridgeEnvelope {
29
- type: 'prenta-preview:ready'
30
- }
31
-
32
- export interface PreviewSelectMessage extends PreviewBridgeEnvelope {
33
- type: 'prenta-preview:select'
34
- sectionId: string
35
- fieldPath?: string
36
- }
37
-
38
- export interface PreviewHighlightMessage extends PreviewBridgeEnvelope {
39
- type: 'prenta-preview:highlight'
40
- sectionId: string | null
41
- }
42
-
43
- /**
44
- * Opaque JSON object on the wire. Uses `object` (not `Record<string, unknown>`)
45
- * so concrete editor interfaces like `SectionSettings` remain assignable.
46
- */
47
- export type PreviewObjectPayload = object
48
-
49
- /** Wire shape for live canvas updates — matches `PublicSection` / editor sections. */
50
- export interface PreviewSectionPayload {
51
- id: string
52
- sectionType: string
53
- name?: string
54
- visible?: boolean
55
- content?: PreviewObjectPayload
56
- settings?: PreviewObjectPayload
57
- unmanaged?: boolean
58
- }
59
-
60
- export interface PreviewSectionsMessage extends PreviewBridgeEnvelope {
61
- type: 'prenta-preview:sections'
62
- sections: PreviewSectionPayload[]
63
- }
64
-
65
- /** Admin → canvas: enable/disable inline editing affordances. */
66
- export interface PreviewModeMessage extends PreviewBridgeEnvelope {
67
- type: 'prenta-preview:mode'
68
- editing: boolean
69
- }
70
-
71
- /** Canvas → admin: text field edit from contenteditable. */
72
- export interface PreviewEditMessage extends PreviewBridgeEnvelope {
73
- type: 'prenta-preview:edit'
74
- sectionId: string
75
- fieldPath: string
76
- value: string
77
- }
78
-
79
- /** Canvas → admin: open media picker for an image field. */
80
- export interface PreviewRequestMediaMessage extends PreviewBridgeEnvelope {
81
- type: 'prenta-preview:request-media'
82
- sectionId: string
83
- fieldPath: string
84
- }
85
-
86
- export type PreviewBridgeMessage =
87
- | PreviewReadyMessage
88
- | PreviewSelectMessage
89
- | PreviewHighlightMessage
90
- | PreviewSectionsMessage
91
- | PreviewModeMessage
92
- | PreviewEditMessage
93
- | PreviewRequestMediaMessage
94
-
95
- export function isPreviewBridgeMessage(data: unknown): data is PreviewBridgeMessage {
96
- if (!data || typeof data !== 'object') return false
97
- const msg = data as Record<string, unknown>
98
- if (msg.v !== PREVIEW_BRIDGE_VERSION) return false
99
- if (typeof msg.type !== 'string' || !msg.type.startsWith('prenta-preview:')) return false
100
- return true
101
- }
102
-
103
- export function isPreviewEditMessage(data: unknown): data is PreviewEditMessage {
104
- if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:edit') return false
105
- return (
106
- typeof data.sectionId === 'string' &&
107
- data.sectionId.length > 0 &&
108
- typeof data.fieldPath === 'string' &&
109
- data.fieldPath.length > 0 &&
110
- typeof data.value === 'string'
111
- )
112
- }
113
-
114
- export function isPreviewRequestMediaMessage(data: unknown): data is PreviewRequestMediaMessage {
115
- if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:request-media') return false
116
- return (
117
- typeof data.sectionId === 'string' &&
118
- data.sectionId.length > 0 &&
119
- typeof data.fieldPath === 'string' &&
120
- data.fieldPath.length > 0
121
- )
122
- }
123
-
124
- export function isPreviewModeMessage(data: unknown): data is PreviewModeMessage {
125
- if (!isPreviewBridgeMessage(data) || data.type !== 'prenta-preview:mode') return false
126
- return typeof data.editing === 'boolean'
127
- }
128
-
129
- export function canvasOriginFromSrc(src: string | null | undefined): string | null {
130
- if (!src) return null
131
- try {
132
- return new URL(src, typeof window !== 'undefined' ? window.location.origin : undefined).origin
133
- } catch {
134
- return null
135
- }
136
- }
137
-
138
9
  /**
139
10
  * Map a dotted content field path (`lead`, `stats.0.num`) to the inspector
140
11
  * input id (`insp-field-lead`, `insp-field-stats-0-num`). Same string shape
@@ -181,29 +52,12 @@ export function companionDimensionPaths(fieldPath: string): {
181
52
  }
182
53
 
183
54
  /**
184
- * Media URL slots are not contenteditable. Prefer `data-cms-field-kind="media"`
185
- * on the canvas; fall back to common image field leaf names.
55
+ * Read `data-cms-field` from a canvas click target (or its ancestor).
56
+ * Used so a field click opens the accordion on the matching inspector input.
186
57
  */
187
- export function isMediaFieldPath(fieldPath: string, kindAttr?: string | null): boolean {
188
- if (kindAttr === 'media') return true
189
- if (kindAttr === 'text') return false
190
- const leaf = fieldPathLeaf(fieldPath)
191
- if (leaf === 'imageUrl' || leaf.endsWith('ImageUrl') || leaf === 'featuredImage') return true
192
- // Gallery rows use `images.N.url` (not link CTAs).
193
- return leaf === 'url' && fieldPath.includes('images.')
194
- }
195
-
196
- /** Strip editor chrome to the public wire shape before posting to the canvas. */
197
- export function toPreviewSectionPayload(
198
- section: PreviewSectionPayload & { internalLabel?: string },
199
- ): PreviewSectionPayload {
200
- return {
201
- id: section.id,
202
- sectionType: section.sectionType,
203
- name: section.name,
204
- visible: section.visible,
205
- content: section.content ?? {},
206
- settings: section.settings ?? {},
207
- unmanaged: section.unmanaged,
208
- }
58
+ export function fieldPathFromEventTarget(target: EventTarget | null): string | undefined {
59
+ if (!(target instanceof Element)) return undefined
60
+ const field = target.closest('[data-cms-field]')
61
+ const path = field?.getAttribute('data-cms-field')?.trim()
62
+ return path || undefined
209
63
  }
@@ -0,0 +1,46 @@
1
+ import type { PageStatus } from './page-editor-service.js'
2
+
3
+ export type ViewSiteIntent = 'preview-draft' | 'view-live'
4
+
5
+ export const NO_PUBLIC_SITE_URL_REASON =
6
+ 'Set the public site URL in Settings → SEO to open the live design.'
7
+
8
+ export const ADMIN_ORIGIN_SITE_URL_REASON =
9
+ 'This origin is the admin, not the public site. Set the public site URL in Settings → SEO.'
10
+
11
+ export function resolveViewSiteChrome(input: {
12
+ status: PageStatus
13
+ dirty: boolean
14
+ hasPublicSiteUrl: boolean
15
+ hasIdentity: boolean
16
+ /** True when the configured site URL is the CMS host (Astro-split misconfig). */
17
+ matchesAdminOrigin?: boolean
18
+ }): {
19
+ viewEnabled: boolean
20
+ viewDisabledReason?: string
21
+ primaryIntent: ViewSiteIntent
22
+ showViewLive: boolean
23
+ shareVisible: boolean
24
+ } {
25
+ const unpublished = input.status !== 'PUBLISHED' || input.dirty
26
+ const primaryIntent: ViewSiteIntent =
27
+ input.status === 'PUBLISHED' && !input.dirty ? 'view-live' : 'preview-draft'
28
+ const shareVisible = input.hasIdentity && unpublished
29
+ const usablePublicUrl = input.hasPublicSiteUrl && !input.matchesAdminOrigin
30
+ const viewEnabled = input.hasIdentity && usablePublicUrl
31
+
32
+ let viewDisabledReason: string | undefined
33
+ if (input.hasIdentity && !input.hasPublicSiteUrl) {
34
+ viewDisabledReason = NO_PUBLIC_SITE_URL_REASON
35
+ } else if (input.hasIdentity && input.matchesAdminOrigin) {
36
+ viewDisabledReason = ADMIN_ORIGIN_SITE_URL_REASON
37
+ }
38
+
39
+ return {
40
+ viewEnabled,
41
+ viewDisabledReason,
42
+ primaryIntent,
43
+ showViewLive: input.hasIdentity && input.status === 'PUBLISHED' && input.dirty,
44
+ shareVisible,
45
+ }
46
+ }
@@ -40,14 +40,12 @@ export const ADMIN_STATIC_ROUTE_PATTERNS: readonly string[] = [
40
40
  '/pages',
41
41
  '/pages/:id',
42
42
  '/pages/:id/edit',
43
- '/pages/:id/preview',
44
43
  '/pages/new',
45
44
  '/pages/tags',
46
45
  '/pages/tags/:id/edit',
47
46
  '/pages/tags/new',
48
47
  '/posts',
49
48
  '/posts/:type/:id/edit',
50
- '/posts/:type/:id/preview',
51
49
  '/posts/:type/new',
52
50
  '/posts/new',
53
51
  '/posts/types',
@@ -174,8 +174,8 @@
174
174
  * `views/page-editor/sections/` must use `@pv-*`, never `sm:`/`md:`/`lg:`.
175
175
  *
176
176
  * Values come from the page-builder design handoff. Any element carrying
177
- * these variants needs an ancestor with `@container` — EditorCanvas,
178
- * PostEditorCanvas, PagePreview and PostPreview all declare it.
177
+ * these variants needs an ancestor with `@container` — EditorCanvas
178
+ * and PostEditorCanvas both declare it.
179
179
  */
180
180
  @theme {
181
181
  --container-pv-sm: 460px; /* below: stats 1-up, buttons full width */
@@ -281,8 +281,8 @@
281
281
  /*
282
282
  * The canvas is the query container for every previewed section (`@pv-*`
283
283
  * variants — see the step scale above). It lives here rather than as an
284
- * `@container` utility on each host because FOUR surfaces render these
285
- * sections — EditorCanvas, PostEditorCanvas, PagePreview, PostPreview — and
284
+ * `@container` utility on each host because TWO surfaces render these
285
+ * sections — EditorCanvas and PostEditorCanvas — and
286
286
  * a host that forgets it degrades silently: with no container ancestor,
287
287
  * every `@pv-*` variant simply never matches and the page renders at its
288
288
  * narrowest layout regardless of the selected breakpoint. Tying containment
@@ -13,12 +13,11 @@ interface EditorCanvasProps {
13
13
  selectedId: string | null
14
14
  breakpoint: BreakpointId
15
15
  zoom?: Zoom
16
- onSelect: (id: string) => void
16
+ onSelect: (id: string, fieldPath?: string) => void
17
17
  onScaleChange?: (scale: number) => void
18
18
  onCanvasElement?: (el: HTMLElement | null) => void
19
19
  /** Section types rendered as placeholders — shows the "Structure view" notice. */
20
20
  structuralTypes?: string[]
21
- sitePreviewAvailable?: boolean
22
21
  /**
23
22
  * Read-only render of a past version. Suppresses selection and the editing
24
23
  * affordances, and turns on the changed-section outlines.
@@ -50,7 +49,6 @@ export function EditorCanvas({
50
49
  onScaleChange,
51
50
  onCanvasElement,
52
51
  structuralTypes = [],
53
- sitePreviewAvailable = false,
54
52
  reviewing = false,
55
53
  changedSectionIds,
56
54
  reviewBar,
@@ -84,10 +82,7 @@ export function EditorCanvas({
84
82
  ) : (
85
83
  <>
86
84
  <CanvasEditTip />
87
- <StructureNotice
88
- structuralTypes={structuralTypes}
89
- sitePreviewAvailable={sitePreviewAvailable}
90
- />
85
+ <StructureNotice structuralTypes={structuralTypes} />
91
86
  </>
92
87
  )
93
88
  }
@@ -1,21 +1,24 @@
1
1
  'use client'
2
2
 
3
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
4
  import {
4
5
  Calendar,
5
6
  Check,
7
+ ChevronDown,
6
8
  ExternalLink,
7
- Eye,
8
- Globe,
9
9
  History,
10
10
  Loader2,
11
11
  Settings2,
12
+ Share2,
12
13
  TriangleAlert,
13
14
  } from 'lucide-react'
14
15
  import { EditorSeoScoreBadge } from '../../components/seo/EditorSeoScoreBadge.js'
15
16
  import { BreakpointPicker } from '../../components/BreakpointPicker.js'
16
17
  import { ZoomChip } from '../../components/ZoomChip.js'
18
+ import { adminPortalContainer } from '../../lib/portal-container.js'
17
19
  import type { PageStatus } from '../../lib/page-editor-service.js'
18
20
  import type { Zoom } from '../../lib/breakpoints.js'
21
+ import { resolveViewSiteChrome, type ViewSiteIntent } from '../../lib/view-site-chrome.js'
19
22
  import { type ViewportId } from './viewports.js'
20
23
 
21
24
  export type SaveState = 'idle' | 'saving' | 'saved' | 'error'
@@ -33,7 +36,7 @@ interface EditorTopBarProps {
33
36
  /** Shown on hover/focus when publish is blocked by validation (not role). */
34
37
  publishDisabledReason?: string
35
38
  viewport: ViewportId
36
- /** Canvas zoom mode; omit to hide the zoom chip (e.g. the site-preview iframe). */
39
+ /** Canvas zoom mode; omit to hide the zoom chip. */
37
40
  zoom?: Zoom
38
41
  /** Scale the canvas actually measured, 0–1 — displayed by the zoom chip. */
39
42
  canvasScale?: number
@@ -42,13 +45,18 @@ interface EditorTopBarProps {
42
45
  backLabel?: string
43
46
  onViewport: (v: ViewportId) => void
44
47
  onBack: () => void
45
- onPreview: () => void
46
48
  /**
47
- * Open the draft on the public site (real front-end) in a new tab. When
48
- * omitted the "View on site" button is hidden — e.g. for unsaved documents
49
- * or installs without a public URL.
49
+ * Open the public site in a new tab. Called with `preview-draft` or
50
+ * `view-live` depending on status and dirty. Omitted for unsaved documents
51
+ * (no id/slug yet) View site / Share / View live are then hidden.
50
52
  */
51
- onViewOnSite?: () => void
53
+ onViewOnSite?: (intent: ViewSiteIntent) => void
54
+ /** Copy a shareable draft preview link. Hidden when `shareVisible` is false. */
55
+ onShare?: () => void
56
+ /** When false, View site and Share are disabled with the SEO settings reason. */
57
+ hasPublicSiteUrl?: boolean
58
+ /** When true, View site and Share stay disabled — the URL is the CMS host. */
59
+ matchesAdminOrigin?: boolean
52
60
  /**
53
61
  * Toggle the version history panel. When omitted the History button is
54
62
  * hidden — e.g. for unsaved documents that have no versions yet.
@@ -60,13 +68,6 @@ interface EditorTopBarProps {
60
68
  * is nothing in the chrome saying which mode the editor is in.
61
69
  */
62
70
  historyOpen?: boolean
63
- /** Whether the inline site preview (iframe of the public site) is showing. */
64
- sitePreview?: boolean
65
- /**
66
- * Toggle the inline site preview — renders the draft through the consumer's
67
- * real public route inside the editor. Hidden when omitted (unsaved docs).
68
- */
69
- onToggleSitePreview?: () => void
70
71
  onOpenSettings: () => void
71
72
  /** Open the document SEO panel (meta, social, schema, analysis). */
72
73
  onOpenSeo?: () => void
@@ -90,9 +91,8 @@ interface EditorTopBarProps {
90
91
  scheduledUnpublishAt?: string | null
91
92
  /**
92
93
  * Section types the canvas renders as placeholders (custom / unmanaged).
93
- * When non-empty the editor is in "structure" mode: the eye preview is
94
- * labeled as a structure preview and "View on site" is promoted to a
95
- * labeled button as the primary design preview.
94
+ * Used for structure notice / other chrome. View site is always a labeled
95
+ * button regardless of this list.
96
96
  */
97
97
  structuralTypes?: string[]
98
98
  }
@@ -121,12 +121,12 @@ export function EditorTopBar({
121
121
  backLabel = 'Pages',
122
122
  onViewport,
123
123
  onBack,
124
- onPreview,
125
124
  onViewOnSite,
125
+ onShare,
126
+ hasPublicSiteUrl,
127
+ matchesAdminOrigin,
126
128
  onHistory,
127
129
  historyOpen = false,
128
- sitePreview = false,
129
- onToggleSitePreview,
130
130
  onOpenSettings,
131
131
  onOpenSeo,
132
132
  seoOpen = false,
@@ -138,19 +138,17 @@ export function EditorTopBar({
138
138
  scheduleReady = true,
139
139
  scheduledAt = null,
140
140
  scheduledUnpublishAt = null,
141
- structuralTypes = [],
142
141
  }: EditorTopBarProps) {
143
142
  const statusInfo = STATUS_TEXT[status]
144
- // With custom section types on the page the canvas (and the eye preview,
145
- // which reuses the admin renderers) only shows structure — the real design
146
- // preview is the public site, so promote "View on site" to a labeled button.
147
- const structural = structuralTypes.length > 0
148
- const previewLabel = structural
149
- ? 'Preview structure (custom sections show placeholders)'
150
- : 'Preview page'
151
- // For a published document the external-link button opens the LIVE page (no
152
- // preview token); otherwise it opens a token-gated preview of the draft.
153
- const isLive = status === 'PUBLISHED'
143
+ const chrome = onViewOnSite
144
+ ? resolveViewSiteChrome({
145
+ status,
146
+ dirty,
147
+ hasPublicSiteUrl: Boolean(hasPublicSiteUrl),
148
+ matchesAdminOrigin: Boolean(matchesAdminOrigin),
149
+ hasIdentity: true,
150
+ })
151
+ : null
154
152
 
155
153
  const publishBlocked = !canPublish || !publishReady || publishing || saveState === 'saving'
156
154
  const publishTitle = publishing
@@ -281,63 +279,68 @@ export function EditorTopBar({
281
279
  </button>
282
280
  )}
283
281
 
284
- <button
285
- type="button"
286
- onClick={onPreview}
287
- aria-label={previewLabel}
288
- title={previewLabel}
289
- className="prenta-touch-target text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
290
- >
291
- <Eye className="h-4 w-4" aria-hidden />
292
- </button>
293
-
294
- {onToggleSitePreview && (
295
- <button
296
- type="button"
297
- onClick={onToggleSitePreview}
298
- aria-pressed={sitePreview}
299
- aria-label={sitePreview ? 'Back to editor canvas' : 'Inline site preview'}
300
- title={
301
- sitePreview
302
- ? 'Back to editor canvas'
303
- : 'Inline site preview — render the draft through your public site, in place'
304
- }
305
- className={`prenta-touch-target rounded-lg p-2 transition-colors ${
306
- sitePreview
307
- ? 'bg-primary text-primary-foreground'
308
- : 'text-muted-foreground hover:text-foreground hover:bg-accent'
309
- }`}
310
- >
311
- <Globe className="h-4 w-4" aria-hidden />
312
- </button>
313
- )}
314
-
315
- {onViewOnSite &&
316
- (structural ? (
282
+ {chrome && (
283
+ <>
317
284
  <button
318
285
  type="button"
319
- onClick={onViewOnSite}
286
+ onClick={() => onViewOnSite?.(chrome.primaryIntent)}
287
+ disabled={!chrome.viewEnabled}
320
288
  title={
321
- isLive
322
- ? 'Open the live published page in a new tab'
323
- : 'Open the draft on your public site — the real design preview'
289
+ chrome.viewDisabledReason ??
290
+ (chrome.primaryIntent === 'view-live'
291
+ ? 'Open the published page in a new tab.'
292
+ : 'Open the draft on your site in a new tab.')
324
293
  }
325
- className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
294
+ className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
326
295
  >
327
296
  <ExternalLink className="h-3.5 w-3.5" aria-hidden />
328
- {isLive ? 'View live' : 'View on site'}
297
+ {chrome.primaryIntent === 'view-live' ? 'View live' : 'View site'}
329
298
  </button>
330
- ) : (
331
- <button
332
- type="button"
333
- onClick={onViewOnSite}
334
- aria-label={isLive ? 'View live page' : 'View draft on site'}
335
- title={isLive ? 'View live page' : 'View draft on site'}
336
- className="prenta-touch-target text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg p-2"
337
- >
338
- <ExternalLink className="h-4 w-4" aria-hidden />
339
- </button>
340
- ))}
299
+ {chrome.showViewLive && (
300
+ <DropdownMenu.Root>
301
+ <DropdownMenu.Trigger asChild>
302
+ <button
303
+ type="button"
304
+ disabled={!chrome.viewEnabled}
305
+ aria-label="More view options"
306
+ className="prenta-touch-target border-border text-foreground hover:bg-accent inline-flex h-[34px] items-center rounded-lg border px-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
307
+ >
308
+ <ChevronDown className="h-3.5 w-3.5" aria-hidden />
309
+ </button>
310
+ </DropdownMenu.Trigger>
311
+ <DropdownMenu.Portal container={adminPortalContainer()}>
312
+ <DropdownMenu.Content
313
+ align="end"
314
+ sideOffset={6}
315
+ className="border-border bg-popover z-[120] rounded-[10px] border p-[5px] shadow-[0_12px_34px_rgba(26,24,39,0.17)]"
316
+ >
317
+ <DropdownMenu.Item
318
+ onSelect={() => onViewOnSite?.('view-live')}
319
+ className="hover:bg-accent focus:bg-accent cursor-pointer rounded-md px-2.5 py-1.5 text-sm outline-none"
320
+ >
321
+ View live
322
+ </DropdownMenu.Item>
323
+ </DropdownMenu.Content>
324
+ </DropdownMenu.Portal>
325
+ </DropdownMenu.Root>
326
+ )}
327
+ {chrome.shareVisible && onShare && (
328
+ <button
329
+ type="button"
330
+ onClick={onShare}
331
+ disabled={!chrome.viewEnabled}
332
+ title={
333
+ chrome.viewDisabledReason ??
334
+ 'Copy a link so someone can view this draft without signing in.'
335
+ }
336
+ className="border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
337
+ >
338
+ <Share2 className="h-3.5 w-3.5" aria-hidden />
339
+ Share
340
+ </button>
341
+ )}
342
+ </>
343
+ )}
341
344
 
342
345
  <button
343
346
  type="button"