@prenta/admin 1.0.0 → 1.1.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 (117) hide show
  1. package/CHANGELOG.md +6 -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/open-public-site-tab.test.d.ts +2 -0
  8. package/dist/__tests__/lib/open-public-site-tab.test.d.ts.map +1 -0
  9. package/dist/__tests__/lib/open-public-site-tab.test.js +64 -0
  10. package/dist/__tests__/lib/open-public-site-tab.test.js.map +1 -0
  11. package/dist/__tests__/lib/view-site-chrome.test.d.ts +2 -0
  12. package/dist/__tests__/lib/view-site-chrome.test.d.ts.map +1 -0
  13. package/dist/__tests__/lib/view-site-chrome.test.js +59 -0
  14. package/dist/__tests__/lib/view-site-chrome.test.js.map +1 -0
  15. package/dist/__tests__/router/admin-routes.test.js +4 -0
  16. package/dist/__tests__/router/admin-routes.test.js.map +1 -1
  17. package/dist/__tests__/views/canvas-surface-shared.test.js +0 -16
  18. package/dist/__tests__/views/canvas-surface-shared.test.js.map +1 -1
  19. package/dist/__tests__/views/editor-top-bar.render.test.js +44 -22
  20. package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
  21. package/dist/__tests__/views/page-section-editor.test.js +30 -9
  22. package/dist/__tests__/views/page-section-editor.test.js.map +1 -1
  23. package/dist/__tests__/views/post-section-editor.test.js +23 -0
  24. package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
  25. package/dist/components/BreakpointPicker.d.ts +3 -2
  26. package/dist/components/BreakpointPicker.d.ts.map +1 -1
  27. package/dist/components/BreakpointPicker.js +5 -3
  28. package/dist/components/BreakpointPicker.js.map +1 -1
  29. package/dist/components/CanvasEditTip.js +1 -1
  30. package/dist/components/CanvasEditTip.js.map +1 -1
  31. package/dist/lib/editor-preview.d.ts +4 -1
  32. package/dist/lib/editor-preview.d.ts.map +1 -1
  33. package/dist/lib/editor-preview.js +4 -1
  34. package/dist/lib/editor-preview.js.map +1 -1
  35. package/dist/lib/open-public-site-tab.d.ts +14 -0
  36. package/dist/lib/open-public-site-tab.d.ts.map +1 -0
  37. package/dist/lib/open-public-site-tab.js +46 -0
  38. package/dist/lib/open-public-site-tab.js.map +1 -0
  39. package/dist/lib/view-site-chrome.d.ts +16 -0
  40. package/dist/lib/view-site-chrome.d.ts.map +1 -0
  41. package/dist/lib/view-site-chrome.js +15 -0
  42. package/dist/lib/view-site-chrome.js.map +1 -0
  43. package/dist/router/admin-routes.d.ts.map +1 -1
  44. package/dist/router/admin-routes.js +0 -2
  45. package/dist/router/admin-routes.js.map +1 -1
  46. package/dist/views/page-editor/EditorCanvas.d.ts +1 -2
  47. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  48. package/dist/views/page-editor/EditorCanvas.js +2 -2
  49. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  50. package/dist/views/page-editor/EditorTopBar.d.ts +13 -17
  51. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  52. package/dist/views/page-editor/EditorTopBar.js +19 -21
  53. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  54. package/dist/views/page-editor/PageSectionEditor.d.ts +1 -7
  55. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  56. package/dist/views/page-editor/PageSectionEditor.js +29 -95
  57. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  58. package/dist/views/page-editor/StructureNotice.d.ts +2 -5
  59. package/dist/views/page-editor/StructureNotice.d.ts.map +1 -1
  60. package/dist/views/page-editor/StructureNotice.js +6 -10
  61. package/dist/views/page-editor/StructureNotice.js.map +1 -1
  62. package/dist/views/post-editor/PostEditorCanvas.d.ts +1 -3
  63. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  64. package/dist/views/post-editor/PostEditorCanvas.js +3 -3
  65. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  66. package/dist/views/post-editor/PostSectionEditor.d.ts +1 -15
  67. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  68. package/dist/views/post-editor/PostSectionEditor.js +30 -88
  69. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  70. package/package.json +4 -4
  71. package/src/AdminRoot.tsx +0 -70
  72. package/src/__tests__/components/breakpoint-controls.render.test.tsx +6 -4
  73. package/src/__tests__/lib/open-public-site-tab.test.ts +74 -0
  74. package/src/__tests__/lib/view-site-chrome.test.ts +65 -0
  75. package/src/__tests__/router/admin-routes.test.ts +5 -0
  76. package/src/__tests__/views/canvas-surface-shared.test.ts +0 -19
  77. package/src/__tests__/views/editor-top-bar.render.test.tsx +85 -22
  78. package/src/__tests__/views/page-section-editor.test.tsx +42 -9
  79. package/src/__tests__/views/post-section-editor.test.tsx +35 -0
  80. package/src/components/BreakpointPicker.tsx +6 -3
  81. package/src/components/CanvasEditTip.tsx +2 -2
  82. package/src/lib/editor-preview.ts +4 -1
  83. package/src/lib/open-public-site-tab.ts +57 -0
  84. package/src/lib/view-site-chrome.ts +34 -0
  85. package/src/router/admin-routes.ts +0 -2
  86. package/src/styles/theme.css +4 -4
  87. package/src/views/page-editor/EditorCanvas.tsx +1 -6
  88. package/src/views/page-editor/EditorTopBar.tsx +81 -82
  89. package/src/views/page-editor/PageSectionEditor.tsx +77 -157
  90. package/src/views/page-editor/StructureNotice.tsx +7 -42
  91. package/src/views/post-editor/PostEditorCanvas.tsx +2 -8
  92. package/src/views/post-editor/PostSectionEditor.tsx +66 -138
  93. package/dist/__tests__/views/use-site-preview.render.test.d.ts +0 -2
  94. package/dist/__tests__/views/use-site-preview.render.test.d.ts.map +0 -1
  95. package/dist/__tests__/views/use-site-preview.render.test.js +0 -120
  96. package/dist/__tests__/views/use-site-preview.render.test.js.map +0 -1
  97. package/dist/views/page-editor/PagePreview.d.ts +0 -13
  98. package/dist/views/page-editor/PagePreview.d.ts.map +0 -1
  99. package/dist/views/page-editor/PagePreview.js +0 -46
  100. package/dist/views/page-editor/PagePreview.js.map +0 -1
  101. package/dist/views/page-editor/SitePreviewFrame.d.ts +0 -43
  102. package/dist/views/page-editor/SitePreviewFrame.d.ts.map +0 -1
  103. package/dist/views/page-editor/SitePreviewFrame.js +0 -165
  104. package/dist/views/page-editor/SitePreviewFrame.js.map +0 -1
  105. package/dist/views/page-editor/useSitePreview.d.ts +0 -47
  106. package/dist/views/page-editor/useSitePreview.d.ts.map +0 -1
  107. package/dist/views/page-editor/useSitePreview.js +0 -96
  108. package/dist/views/page-editor/useSitePreview.js.map +0 -1
  109. package/dist/views/post-editor/PostPreview.d.ts +0 -14
  110. package/dist/views/post-editor/PostPreview.d.ts.map +0 -1
  111. package/dist/views/post-editor/PostPreview.js +0 -63
  112. package/dist/views/post-editor/PostPreview.js.map +0 -1
  113. package/src/__tests__/views/use-site-preview.render.test.tsx +0 -164
  114. package/src/views/page-editor/PagePreview.tsx +0 -105
  115. package/src/views/page-editor/SitePreviewFrame.tsx +0 -250
  116. package/src/views/page-editor/useSitePreview.ts +0 -158
  117. package/src/views/post-editor/PostPreview.tsx +0 -125
@@ -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
@@ -18,7 +18,6 @@ interface EditorCanvasProps {
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,16 @@ 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
52
58
  /**
53
59
  * Toggle the version history panel. When omitted the History button is
54
60
  * hidden — e.g. for unsaved documents that have no versions yet.
@@ -60,13 +66,6 @@ interface EditorTopBarProps {
60
66
  * is nothing in the chrome saying which mode the editor is in.
61
67
  */
62
68
  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
69
  onOpenSettings: () => void
71
70
  /** Open the document SEO panel (meta, social, schema, analysis). */
72
71
  onOpenSeo?: () => void
@@ -90,9 +89,8 @@ interface EditorTopBarProps {
90
89
  scheduledUnpublishAt?: string | null
91
90
  /**
92
91
  * 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.
92
+ * Used for structure notice / other chrome. View site is always a labeled
93
+ * button regardless of this list.
96
94
  */
97
95
  structuralTypes?: string[]
98
96
  }
@@ -121,12 +119,11 @@ export function EditorTopBar({
121
119
  backLabel = 'Pages',
122
120
  onViewport,
123
121
  onBack,
124
- onPreview,
125
122
  onViewOnSite,
123
+ onShare,
124
+ hasPublicSiteUrl,
126
125
  onHistory,
127
126
  historyOpen = false,
128
- sitePreview = false,
129
- onToggleSitePreview,
130
127
  onOpenSettings,
131
128
  onOpenSeo,
132
129
  seoOpen = false,
@@ -138,19 +135,16 @@ export function EditorTopBar({
138
135
  scheduleReady = true,
139
136
  scheduledAt = null,
140
137
  scheduledUnpublishAt = null,
141
- structuralTypes = [],
142
138
  }: EditorTopBarProps) {
143
139
  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'
140
+ const chrome = onViewOnSite
141
+ ? resolveViewSiteChrome({
142
+ status,
143
+ dirty,
144
+ hasPublicSiteUrl: Boolean(hasPublicSiteUrl),
145
+ hasIdentity: true,
146
+ })
147
+ : null
154
148
 
155
149
  const publishBlocked = !canPublish || !publishReady || publishing || saveState === 'saving'
156
150
  const publishTitle = publishing
@@ -281,63 +275,68 @@ export function EditorTopBar({
281
275
  </button>
282
276
  )}
283
277
 
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 ? (
278
+ {chrome && (
279
+ <>
317
280
  <button
318
281
  type="button"
319
- onClick={onViewOnSite}
282
+ onClick={() => onViewOnSite?.(chrome.primaryIntent)}
283
+ disabled={!chrome.viewEnabled}
320
284
  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'
285
+ chrome.viewDisabledReason ??
286
+ (chrome.primaryIntent === 'view-live'
287
+ ? 'Open the published page in a new tab.'
288
+ : 'Open the draft on your site in a new tab.')
324
289
  }
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"
290
+ 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
291
  >
327
292
  <ExternalLink className="h-3.5 w-3.5" aria-hidden />
328
- {isLive ? 'View live' : 'View on site'}
293
+ {chrome.primaryIntent === 'view-live' ? 'View live' : 'View site'}
329
294
  </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
- ))}
295
+ {chrome.showViewLive && (
296
+ <DropdownMenu.Root>
297
+ <DropdownMenu.Trigger asChild>
298
+ <button
299
+ type="button"
300
+ disabled={!chrome.viewEnabled}
301
+ aria-label="More view options"
302
+ 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"
303
+ >
304
+ <ChevronDown className="h-3.5 w-3.5" aria-hidden />
305
+ </button>
306
+ </DropdownMenu.Trigger>
307
+ <DropdownMenu.Portal container={adminPortalContainer()}>
308
+ <DropdownMenu.Content
309
+ align="end"
310
+ sideOffset={6}
311
+ className="border-border bg-popover z-[120] rounded-[10px] border p-[5px] shadow-[0_12px_34px_rgba(26,24,39,0.17)]"
312
+ >
313
+ <DropdownMenu.Item
314
+ onSelect={() => onViewOnSite?.('view-live')}
315
+ className="hover:bg-accent focus:bg-accent cursor-pointer rounded-md px-2.5 py-1.5 text-sm outline-none"
316
+ >
317
+ View live
318
+ </DropdownMenu.Item>
319
+ </DropdownMenu.Content>
320
+ </DropdownMenu.Portal>
321
+ </DropdownMenu.Root>
322
+ )}
323
+ {chrome.shareVisible && onShare && (
324
+ <button
325
+ type="button"
326
+ onClick={onShare}
327
+ disabled={!chrome.viewEnabled}
328
+ title={
329
+ chrome.viewDisabledReason ??
330
+ 'Copy a link so someone can view this draft without signing in.'
331
+ }
332
+ 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"
333
+ >
334
+ <Share2 className="h-3.5 w-3.5" aria-hidden />
335
+ Share
336
+ </button>
337
+ )}
338
+ </>
339
+ )}
341
340
 
342
341
  <button
343
342
  type="button"
@@ -34,11 +34,9 @@ import {
34
34
  type ValidationResult,
35
35
  } from '../../lib/page-editor-service.js'
36
36
  import { MediaPickerModal } from '../../components/MediaPickerModal.js'
37
- import {
38
- buildPublicPreviewUrl,
39
- buildPublicUrl,
40
- createPreviewToken,
41
- } from '../../lib/preview-link.js'
37
+ import { SharePreviewLinkDialog } from '../../components/SharePreviewLinkDialog.js'
38
+ import { openPublicSiteTab } from '../../lib/open-public-site-tab.js'
39
+ import type { ViewSiteIntent } from '../../lib/view-site-chrome.js'
42
40
  import { usePublicSiteUrl } from '../../hooks/usePublicSiteUrl.js'
43
41
  import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
44
42
  import type { Zoom } from '../../lib/breakpoints.js'
@@ -59,12 +57,9 @@ import { EditorTopBar, type SaveState } from './EditorTopBar.js'
59
57
  import { SectionsList } from './SectionsPanel.js'
60
58
  import { EditorCanvas } from './EditorCanvas.js'
61
59
  import { ReflowContext } from './reflow-context.js'
62
- import { SitePreviewFrame } from './SitePreviewFrame.js'
63
- import { useSitePreview } from './useSitePreview.js'
64
60
  import { SectionEditForm } from './SectionEditForm.js'
65
61
  import { PageSettingsModal } from './PageSettingsModal.js'
66
62
  import { ValidationSummary } from './ValidationSummary.js'
67
- import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
68
63
  import { buildEditorContentHtml } from '../../lib/editor-seo-content.js'
69
64
  import { companionDimensionPaths, inspectorIdFromFieldPath } from '../../lib/preview-bridge.js'
70
65
 
@@ -80,12 +75,6 @@ interface PageSectionEditorProps {
80
75
  config?: any
81
76
  session?: any
82
77
  onNavigate: (path: string) => void
83
- /**
84
- * Open a preview route in a new browser tab. `prepare` runs before the tab
85
- * navigates (used to save an unsaved draft first); returning `false` cancels
86
- * the open. Falls back to in-tab navigation when not provided.
87
- */
88
- onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
89
78
  }
90
79
 
91
80
  /**
@@ -137,7 +126,6 @@ export function PageSectionEditor({
137
126
  config,
138
127
  session,
139
128
  onNavigate,
140
- onOpenPreview,
141
129
  }: PageSectionEditorProps) {
142
130
  const [page, setPage] = useState<EditorPage | null>(null)
143
131
  const [loading, setLoading] = useState(true)
@@ -165,8 +153,7 @@ export function PageSectionEditor({
165
153
  const [canvasScale, setCanvasScale] = useState(1)
166
154
  /**
167
155
  * The live canvas surface, for the reflow guard to clone. State rather than a
168
- * ref because the context value has to update when it mounts — the canvas
169
- * unmounts whenever the site-preview frame takes over.
156
+ * ref because the context value has to update when it mounts.
170
157
  */
171
158
  const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null)
172
159
  /**
@@ -234,6 +221,7 @@ export function PageSectionEditor({
234
221
  const [restoring, setRestoring] = useState(false)
235
222
  const [confirmPublish, setConfirmPublish] = useState(false)
236
223
  const [scheduleOpen, setScheduleOpen] = useState(false)
224
+ const [shareOpen, setShareOpen] = useState(false)
237
225
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
238
226
  const [validation, setValidation] = useState<ValidationResult | null>(null)
239
227
 
@@ -579,22 +567,6 @@ export function PageSectionEditor({
579
567
  }
580
568
  }, [page, documentId, onNavigate, saveDirectives, acceptPage])
581
569
 
582
- // Inline branded preview — Next builder canvas (same origin as admin), not
583
- // the Astro public apex. "View on site" still uses publicSiteUrl below.
584
- const sitePreview = useSitePreview({
585
- collection: 'pages',
586
- documentId: page?.id,
587
- slug: page?.slug,
588
- path: page?.path,
589
- urlPrefix: '',
590
- dirty,
591
- saveState,
592
- saveDraft: handleSaveDraft,
593
- defaultActive: resolveDefaultSitePreview(config?.admin?.editor, 'pages'),
594
- })
595
-
596
- const sitePreviewAvailable = Boolean(page?.id && page?.slug)
597
-
598
570
  const requestPublish = useCallback(() => {
599
571
  if (!page) return
600
572
  const result = validatePage(page, true)
@@ -797,82 +769,36 @@ export function PageSectionEditor({
797
769
  [dirty, onNavigate],
798
770
  )
799
771
 
800
- const handlePreview = useCallback(() => {
801
- if (!page?.id) {
802
- toast.error('Save the page before previewing')
803
- return
804
- }
805
- const path = `/pages/${page.id}/preview`
806
- // New-tab preview: save first when there are unsaved edits so the preview
807
- // (which reads the persisted document) reflects what's on screen.
808
- if (onOpenPreview) {
809
- onOpenPreview(path, dirty ? handleSaveDraft : undefined)
810
- } else {
811
- guardedNavigate(path)
812
- }
813
- }, [page, dirty, onOpenPreview, handleSaveDraft, guardedNavigate])
814
-
815
- // "View on site": render the draft through the consumer's real public route
816
- // via a signed preview token. The blank tab is opened synchronously inside
817
- // the click (popup-safe), then pointed at the public URL once any unsaved
818
- // draft is persisted and the token is minted.
819
- const handleViewOnSite = useCallback(() => {
820
- if (!page?.id || !page.slug) {
821
- toast.error('Save the page before viewing it on the site')
822
- return
823
- }
824
- // Published → jump straight to the LIVE page: no preview token, no forced
825
- // save. This is the fast "see it live" path. Pending edits stay previewable
826
- // via the eye (canvas) and globe (inline site preview).
827
- if (page.status === 'PUBLISHED') {
828
- window.open(
829
- buildPublicUrl({
830
- siteUrl: publicSiteUrl,
831
- urlPrefix: '',
832
- slug: page.slug,
833
- path: page.path,
834
- }),
835
- '_blank',
836
- 'noopener',
837
- )
838
- return
839
- }
840
- // Not yet public (draft / scheduled / archived) → token-gated preview of
841
- // the current draft through the real front-end.
842
- const tab = window.open('about:blank', '_blank')
843
- if (!tab) {
844
- toast.error('Enable pop-ups for this site to open the preview in a new tab.')
845
- return
846
- }
847
- void (async () => {
848
- try {
849
- if (dirty) {
850
- const ok = await handleSaveDraft()
851
- if (!ok) {
852
- tab.close()
853
- return
854
- }
855
- }
856
- const { token } = await createPreviewToken('pages', page.id!)
857
- const target = buildPublicPreviewUrl({
858
- siteUrl: publicSiteUrl,
859
- urlPrefix: '',
860
- slug: page.slug,
861
- path: page.path,
862
- token,
863
- })
864
- try {
865
- tab.opener = null
866
- } catch {
867
- /* same-origin guard */
868
- }
869
- tab.location.replace(target)
870
- } catch (err) {
871
- tab.close()
872
- toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
772
+ const handleViewOnSite = useCallback(
773
+ (intent: ViewSiteIntent) => {
774
+ if (!page?.id || !page.slug) {
775
+ toast.error('Save the page before viewing it on the site')
776
+ return
873
777
  }
874
- })()
875
- }, [page, dirty, handleSaveDraft, publicSiteUrl])
778
+ void openPublicSiteTab({
779
+ intent,
780
+ siteUrl: publicSiteUrl,
781
+ collection: 'pages',
782
+ documentId: page.id,
783
+ slug: page.slug,
784
+ path: page.path,
785
+ dirty,
786
+ saveDraft: handleSaveDraft,
787
+ }).catch((err) => {
788
+ toast.error(err instanceof Error ? err.message : 'Failed to open the site')
789
+ })
790
+ },
791
+ [page, dirty, handleSaveDraft, publicSiteUrl],
792
+ )
793
+
794
+ const handleShare = useCallback(async () => {
795
+ if (!page?.id) return
796
+ if (dirty) {
797
+ const ok = await handleSaveDraft()
798
+ if (!ok) return
799
+ }
800
+ setShareOpen(true)
801
+ }, [page, dirty, handleSaveDraft])
876
802
 
877
803
  // ─── Render states ───────────────────────────────────────────────────
878
804
  if (loading) {
@@ -945,16 +871,13 @@ export function PageSectionEditor({
945
871
  publishDisabledReason={publishDisabledReason}
946
872
  viewport={viewport}
947
873
  onViewport={setViewport}
948
- {...(sitePreview.active
949
- ? // The site preview is an iframe of the real front end, not a
950
- // scaled canvas — a zoom percentage there would be a lie.
951
- {}
952
- : { zoom, canvasScale, onZoom: setZoom })}
874
+ zoom={zoom}
875
+ canvasScale={canvasScale}
876
+ onZoom={setZoom}
953
877
  onBack={() => guardedNavigate('/pages')}
954
- onPreview={handlePreview}
955
- onViewOnSite={page.id ? handleViewOnSite : undefined}
956
- sitePreview={sitePreview.active}
957
- onToggleSitePreview={page.id && page.slug ? sitePreview.toggle : undefined}
878
+ onViewOnSite={page.id && page.slug ? handleViewOnSite : undefined}
879
+ onShare={page.id ? handleShare : undefined}
880
+ hasPublicSiteUrl={Boolean(publicSiteUrl)}
958
881
  historyOpen={historyOpen}
959
882
  onHistory={page.id ? () => (historyOpen ? closeHistory() : openHistory()) : undefined}
960
883
  onOpenSettings={() => setSettingsOpen(true)}
@@ -1105,46 +1028,30 @@ export function PageSectionEditor({
1105
1028
  </EditorRail>
1106
1029
 
1107
1030
  <div className="flex-1 overflow-hidden">
1108
- {sitePreview.active ? (
1109
- <SitePreviewFrame
1110
- src={sitePreview.url}
1111
- viewport={viewport}
1112
- reloadKey={sitePreview.reloadKey}
1113
- title={`Site preview: ${page.title || 'Untitled page'}`}
1114
- selectedId={selectedId}
1115
- sections={page.sections}
1116
- suppressSectionsEcho={suppressSectionsEcho}
1117
- onSelectSection={handleSelectSection}
1118
- onCanvasEdit={handleCanvasEdit}
1119
- onCanvasRequestMedia={handleCanvasRequestMedia}
1120
- />
1121
- ) : (
1122
- <EditorCanvas
1123
- sections={canvasSections}
1124
- selectedId={selectedId}
1125
- breakpoint={viewport}
1126
- zoom={zoom}
1127
- onScaleChange={setCanvasScale}
1128
- onCanvasElement={setCanvasEl}
1129
- onSelect={handleSelectSection}
1130
- structuralTypes={structuralTypes}
1131
- sitePreviewAvailable={sitePreviewAvailable && !sitePreview.active}
1132
- reviewing={reviewing}
1133
- changedSectionIds={reviewChangedIds}
1134
- reviewBar={
1135
- reviewVersion ? (
1136
- <ReviewBar
1137
- version={reviewVersion}
1138
- changedCount={reviewChangedIds.size}
1139
- onRestore={() => void handleRestoreVersion(reviewVersion)}
1140
- onBackToCurrent={() => setVersionIndex(0)}
1141
- restoring={restoring}
1142
- canRestore={canRestoreVersion}
1143
- />
1144
- ) : null
1145
- }
1146
- />
1147
- )}
1031
+ <EditorCanvas
1032
+ sections={canvasSections}
1033
+ selectedId={selectedId}
1034
+ breakpoint={viewport}
1035
+ zoom={zoom}
1036
+ onScaleChange={setCanvasScale}
1037
+ onCanvasElement={setCanvasEl}
1038
+ onSelect={handleSelectSection}
1039
+ structuralTypes={structuralTypes}
1040
+ reviewing={reviewing}
1041
+ changedSectionIds={reviewChangedIds}
1042
+ reviewBar={
1043
+ reviewVersion ? (
1044
+ <ReviewBar
1045
+ version={reviewVersion}
1046
+ changedCount={reviewChangedIds.size}
1047
+ onRestore={() => void handleRestoreVersion(reviewVersion)}
1048
+ onBackToCurrent={() => setVersionIndex(0)}
1049
+ restoring={restoring}
1050
+ canRestore={canRestoreVersion}
1051
+ />
1052
+ ) : null
1053
+ }
1054
+ />
1148
1055
  </div>
1149
1056
  </div>
1150
1057
  </ReflowContext.Provider>
@@ -1179,6 +1086,19 @@ export function PageSectionEditor({
1179
1086
  />
1180
1087
  )}
1181
1088
 
1089
+ {page.id && (
1090
+ <SharePreviewLinkDialog
1091
+ collectionSlug="pages"
1092
+ documentId={page.id}
1093
+ documentSlug={page.slug}
1094
+ documentPath={page.path}
1095
+ siteUrl={publicSiteUrl}
1096
+ urlPrefix=""
1097
+ open={shareOpen}
1098
+ onClose={() => setShareOpen(false)}
1099
+ />
1100
+ )}
1101
+
1182
1102
  <ConfirmDialog
1183
1103
  open={leaveTarget !== null}
1184
1104
  onClose={() => setLeaveTarget(null)}
@@ -4,48 +4,12 @@ import { Puzzle } from 'lucide-react'
4
4
 
5
5
  /**
6
6
  * Banner shown above the canvas surface when the document contains section
7
- * types the canvas can only render as placeholders (custom / unmanaged
8
- * types), or when Site preview is available for accurate styling.
7
+ * types the canvas can only render as placeholders (custom / unmanaged types).
9
8
  */
10
- export function StructureNotice({
11
- structuralTypes,
12
- sitePreviewAvailable = false,
13
- }: {
14
- structuralTypes: string[]
15
- /** When true, nudge editors toward the Site preview toggle for WYSIWYG. */
16
- sitePreviewAvailable?: boolean
17
- }) {
18
- if (structuralTypes.length === 0 && !sitePreviewAvailable) return null
19
-
20
- if (structuralTypes.length > 0) {
21
- const count = structuralTypes.length
22
- return (
23
- <div
24
- role="note"
25
- className="border-border bg-card text-muted-foreground mx-auto mb-4 flex w-fit max-w-full items-center gap-2 rounded-lg border px-3 py-1.5 text-xs"
26
- >
27
- <Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
28
- <span>
29
- <span className="text-foreground font-medium">Structure view</span> — {count} custom
30
- section type{count > 1 ? 's' : ''} shown as placeholders.
31
- {sitePreviewAvailable ? (
32
- <>
33
- {' '}
34
- Use <span className="text-foreground font-medium">Site preview</span> for accurate
35
- styling.
36
- </>
37
- ) : (
38
- <>
39
- {' '}
40
- Use <span className="text-foreground font-medium">View on site</span> for the real
41
- design.
42
- </>
43
- )}
44
- </span>
45
- </div>
46
- )
47
- }
9
+ export function StructureNotice({ structuralTypes }: { structuralTypes: string[] }) {
10
+ if (structuralTypes.length === 0) return null
48
11
 
12
+ const count = structuralTypes.length
49
13
  return (
50
14
  <div
51
15
  role="note"
@@ -53,8 +17,9 @@ export function StructureNotice({
53
17
  >
54
18
  <Puzzle className="h-3.5 w-3.5 shrink-0" aria-hidden />
55
19
  <span>
56
- <span className="text-foreground font-medium">Structure preview</span> — use{' '}
57
- <span className="text-foreground font-medium">Site preview</span> for accurate styling.
20
+ <span className="text-foreground font-medium">Structure view</span> — {count} custom section
21
+ type{count > 1 ? 's' : ''} shown as placeholders. Use{' '}
22
+ <span className="text-foreground font-medium">View site</span> for the real design.
58
23
  </span>
59
24
  </div>
60
25
  )