sanity 3.83.1-canary.23 → 3.84.1-next.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 (63) hide show
  1. package/lib/_chunks-cjs/PostMessageSchema.js.map +1 -1
  2. package/lib/_chunks-cjs/PostMessageTelemetry.js +1 -1
  3. package/lib/_chunks-cjs/PostMessageTelemetry.js.map +1 -1
  4. package/lib/_chunks-cjs/PresentationToolGrantsCheck.js +429 -1417
  5. package/lib/_chunks-cjs/PresentationToolGrantsCheck.js.map +1 -1
  6. package/lib/_chunks-cjs/presentation.js +70 -22
  7. package/lib/_chunks-cjs/presentation.js.map +1 -1
  8. package/lib/_chunks-cjs/resources6.js +0 -8
  9. package/lib/_chunks-cjs/resources6.js.map +1 -1
  10. package/lib/_chunks-cjs/version.js +1 -1
  11. package/lib/_chunks-es/PostMessageSchema.mjs.map +1 -1
  12. package/lib/_chunks-es/PostMessageTelemetry.mjs +1 -1
  13. package/lib/_chunks-es/PostMessageTelemetry.mjs.map +1 -1
  14. package/lib/_chunks-es/PresentationToolGrantsCheck.mjs +437 -1406
  15. package/lib/_chunks-es/PresentationToolGrantsCheck.mjs.map +1 -1
  16. package/lib/_chunks-es/presentation.mjs +73 -24
  17. package/lib/_chunks-es/presentation.mjs.map +1 -1
  18. package/lib/_chunks-es/resources6.mjs +0 -8
  19. package/lib/_chunks-es/resources6.mjs.map +1 -1
  20. package/lib/_chunks-es/version.mjs +1 -1
  21. package/lib/_singletons.d.mts +61 -2889
  22. package/lib/_singletons.d.ts +61 -2889
  23. package/lib/presentation.d.mts +57 -2887
  24. package/lib/presentation.d.ts +57 -2887
  25. package/lib/presentation.js +4 -0
  26. package/lib/presentation.js.map +1 -1
  27. package/lib/presentation.mjs +5 -1
  28. package/package.json +15 -16
  29. package/src/presentation/PostMessageTelemetry.tsx +1 -1
  30. package/src/presentation/PresentationTool.tsx +76 -85
  31. package/src/presentation/PresentationToolGrantsCheck.tsx +75 -39
  32. package/src/presentation/document/LocationsBanner.tsx +13 -52
  33. package/src/presentation/i18n/resources.ts +0 -10
  34. package/src/presentation/index.ts +13 -0
  35. package/src/presentation/{util → lib}/parse.ts +1 -2
  36. package/src/presentation/overlays/schema/PostMessageSchema.tsx +0 -1
  37. package/src/presentation/panels/usePanelsStorage.ts +1 -1
  38. package/src/presentation/preview/IFrame.tsx +35 -83
  39. package/src/presentation/preview/Preview.tsx +56 -172
  40. package/src/presentation/preview/PreviewHeader.tsx +10 -43
  41. package/src/presentation/preview/PreviewLocationInput.tsx +24 -48
  42. package/src/presentation/preview/SharePreviewMenu.tsx +2 -2
  43. package/src/presentation/reducers/presentationReducer.ts +134 -0
  44. package/src/presentation/types.ts +7 -139
  45. package/src/presentation/useMainDocument.ts +12 -4
  46. package/src/presentation/useParams.ts +3 -2
  47. package/src/presentation/usePreviewUrl.ts +133 -0
  48. package/src/presentation/actors/create-preview-secret.ts +0 -19
  49. package/src/presentation/actors/read-shared-secret.ts +0 -18
  50. package/src/presentation/actors/resolve-allow-patterns.ts +0 -55
  51. package/src/presentation/actors/resolve-initial-url.ts +0 -65
  52. package/src/presentation/actors/resolve-preview-mode-url.ts +0 -72
  53. package/src/presentation/actors/resolve-preview-mode.ts +0 -66
  54. package/src/presentation/actors/resolve-url-from-preview-search-param.ts +0 -29
  55. package/src/presentation/machines/presentation-machine.ts +0 -101
  56. package/src/presentation/machines/preview-url.ts +0 -568
  57. package/src/presentation/useAllowPatterns.ts +0 -12
  58. package/src/presentation/useId.ts +0 -7
  59. package/src/presentation/usePresentationPerspective.ts +0 -14
  60. package/src/presentation/usePreviewUrlActorRef.ts +0 -96
  61. package/src/presentation/useReportInvalidPreviewSearchParam.tsx +0 -43
  62. package/src/presentation/useTargetOrigin.ts +0 -11
  63. /package/src/presentation/{util → lib}/debounce.ts +0 -0
@@ -110,26 +110,19 @@ export function LocationsBanner(props: {
110
110
  </Flex>
111
111
  </Card>
112
112
  <Stack hidden={!expanded} marginTop={1} space={1}>
113
- {locations.map((l) => {
114
- let active = false
115
- if (
116
- (options.name || DEFAULT_TOOL_NAME) === presentationName &&
117
- presentation?.params.preview
118
- ) {
119
- active = areUrlsMatching(presentation.params.preview, l.href)
120
- }
121
-
122
- return (
123
- <LocationItem
124
- active={active}
125
- documentId={documentId}
126
- documentType={schemaType.name}
127
- key={l.href}
128
- node={l}
129
- toolName={options.name || DEFAULT_TOOL_NAME}
130
- />
131
- )
132
- })}
113
+ {locations.map((l, index) => (
114
+ <LocationItem
115
+ active={
116
+ (options.name || DEFAULT_TOOL_NAME) === presentationName &&
117
+ l.href === presentation?.params.preview
118
+ }
119
+ documentId={documentId}
120
+ documentType={schemaType.name}
121
+ key={index}
122
+ node={l}
123
+ toolName={options.name || DEFAULT_TOOL_NAME}
124
+ />
125
+ ))}
133
126
  </Stack>
134
127
  </>
135
128
  )}
@@ -196,35 +189,3 @@ function LocationItem(props: {
196
189
  </Card>
197
190
  )
198
191
  }
199
-
200
- /**
201
- * Compares two URLs to determine if they match based on origin, pathname, and search parameters
202
- * The previewUrl should have all the search parameters that are in the locationUrl
203
- */
204
- function areUrlsMatching(previewUrlString: string, locationUrlString: string): boolean {
205
- try {
206
- const previewUrl = new URL(previewUrlString, location.origin)
207
- const locationUrl = new URL(locationUrlString, previewUrl.origin)
208
-
209
- // First compare origin and pathname
210
- if (previewUrl.origin !== locationUrl.origin || previewUrl.pathname !== locationUrl.pathname) {
211
- return false
212
- }
213
-
214
- // Then check search params
215
- // All search params in locationUrl must exist with the same values in previewUrl
216
- const locationParams = new URLSearchParams(locationUrl.search)
217
- const previewParams = new URLSearchParams(previewUrl.search)
218
-
219
- for (const [key, value] of locationParams.entries()) {
220
- if (previewParams.get(key) !== value) {
221
- return false
222
- }
223
- }
224
-
225
- return true
226
- } catch {
227
- // If URL parsing fails, URLs don't match
228
- return false
229
- }
230
- }
@@ -35,16 +35,6 @@ export default {
35
35
  'presentation-error.label': 'Error message',
36
36
  /** The text shown when the preview frame cannot connect to Presentation */
37
37
  'preview-frame.connection.error.text': 'Could not connect to the preview',
38
- /** The title of the error toast that shows when the preview iframe times out while waiting for a connection over comlink to establish, and the root cause is discovered to be that the iframe is loading on an URL origin that's not in the allow list. */
39
- 'preview-frame.configuration.error.title': 'Preview URL origin mismatch',
40
- /** The toast description for the error message explaining that the iframe is blocked from loading due to a security mismatch. */
41
- 'preview-frame.configuration.error.description':
42
- 'The preview iframe is configured to load <Code>{{targetOrigin}}</Code>, but the reported origin is <Code>{{reportedOrigin}}</Code>. Presentation Tool is unable to connect to unknown origins for security purposes. Update your <Code>presentationTool.allowOrigins</Code> configuration to allow connecting to Visual Editing and Loaders.',
43
- /** The title of the error toast that shows when attempting to navigate to a preview URL origin that's not in the allow list. */
44
- 'preview-search-param.configuration.error.title': 'Blocked preview URL',
45
- /** The toast description for the error message explaining that the iframe won't navigate to the new preview URL due to an URL origin security mismatch. */
46
- 'preview-search-param.configuration.error.description':
47
- 'The router wants to navigate to <Code>{{previewSearchParam}}</Code>, but the origin <Code>{{blockedOrigin}}</Code> is not allowed. Update your <Code>presentationTool.allowOrigins</Code> configuration to allow it.',
48
38
  /** The text shown on the button for dismissing the error overlay after a timeout */
49
39
  'preview-frame.continue-button.text': 'Continue anyway',
50
40
  /** The label for the loader's connection status */
@@ -2,6 +2,19 @@ export {useSharedState} from './overlays/useSharedState'
2
2
  export * from './plugin'
3
3
  export type {PreviewProps} from './preview/Preview'
4
4
  export type {PreviewHeaderProps} from './preview/PreviewHeader'
5
+ export {
6
+ ACTION_IFRAME_LOADED,
7
+ ACTION_IFRAME_REFRESH,
8
+ ACTION_IFRAME_RELOAD,
9
+ ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE,
10
+ type DispatchPresentationAction,
11
+ type IframeLoadedAction,
12
+ type IframeRefreshAction,
13
+ type IframeReloadAction,
14
+ type PresentationAction,
15
+ type PresentationState,
16
+ type VisualEditingOverlaysToggleAction,
17
+ } from './reducers/presentationReducer'
5
18
  export type {
6
19
  CombinedSearchParams,
7
20
  ConnectionStatus,
@@ -1,7 +1,6 @@
1
1
  import {studioPath} from '@sanity/client/csm'
2
2
  import {urlStringToPath} from '@sanity/visual-editing-csm'
3
-
4
- import {type PresentationStateParams} from '../types'
3
+ import type {PresentationStateParams} from '../types'
5
4
 
6
5
  export function parseId(rawId: string | undefined): string | undefined {
7
6
  if (rawId === undefined) {
@@ -75,7 +75,6 @@ function PostMessageSchema(props: PostMessageSchemaProps): React.JSX.Element | n
75
75
  const arr = Array.from(paths)
76
76
  const projection = arr.map((path, i) => `"${i}": ${path}[0]._type`).join(',')
77
77
  const query = `*[_id == $id][0]{${projection}}`
78
- // Should implement max 25 concurrent queries here
79
78
  const result = await client.fetch(
80
79
  query,
81
80
  {id: getPublishedId(id)},
@@ -1,6 +1,6 @@
1
1
  import {useMemo} from 'react'
2
2
 
3
- import {debounce} from '../util/debounce'
3
+ import {debounce} from '../lib/debounce'
4
4
  import {type PanelElement} from './types'
5
5
 
6
6
  const itemKey = 'presentation/panels'
@@ -1,75 +1,7 @@
1
1
  import {Box} from '@sanity/ui'
2
2
  import {motion, type VariantLabels, type Variants} from 'framer-motion'
3
- import {forwardRef, type ReactEventHandler, useEffect, useImperativeHandle, useRef} from 'react'
4
- import {createGlobalStyle, styled} from 'styled-components'
5
-
6
- import {useId} from '../useId'
7
-
8
- interface IFrameProps {
9
- animate: VariantLabels
10
- initial: VariantLabels
11
- onLoad: ReactEventHandler<HTMLIFrameElement>
12
- preventClick: boolean
13
- src: string
14
- variants: Variants
15
- style: React.CSSProperties
16
- }
17
-
18
- export const IFrame = forwardRef<HTMLIFrameElement, IFrameProps>(
19
- function IFrame(props, forwardedRef) {
20
- const {animate, initial, onLoad, preventClick, src, variants, style} = props
21
-
22
- const ref = useRef<HTMLIFrameElement | null>(null)
23
- // Forward the iframe ref to the parent component
24
- useImperativeHandle<HTMLIFrameElement | null, HTMLIFrameElement | null>(
25
- forwardedRef,
26
- () => ref.current,
27
- )
28
-
29
- /**
30
- * Ensure that clicking outside of menus and dialogs will close as focus shifts to the iframe
31
- */
32
-
33
- useEffect(() => {
34
- if (!ref.current) {
35
- return undefined
36
- }
37
- const instance = ref.current
38
- function handleBlur() {
39
- if (instance !== document.activeElement) {
40
- return
41
- }
42
-
43
- instance.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
44
- }
45
- window.addEventListener('blur', handleBlur)
46
- return () => {
47
- window.removeEventListener('blur', handleBlur)
48
- }
49
- }, [])
50
-
51
- const viewTransitionName = useId()
52
-
53
- return (
54
- <>
55
- <IFrameElement
56
- style={{
57
- ...style,
58
- viewTransitionName,
59
- }}
60
- animate={animate}
61
- initial={initial}
62
- onLoad={onLoad}
63
- ref={ref}
64
- src={src}
65
- variants={variants}
66
- />
67
- {preventClick && <IFrameOverlay />}
68
- <GlobalViewTransition />
69
- </>
70
- )
71
- },
72
- )
3
+ import {forwardRef, type ReactEventHandler, useId} from 'react'
4
+ import {styled} from 'styled-components'
73
5
 
74
6
  const IFrameElement = motion.create(styled.iframe`
75
7
  box-shadow: 0 0 0 1px var(--card-border-color);
@@ -85,17 +17,37 @@ const IFrameOverlay = styled(Box)`
85
17
  background: transparent;
86
18
  `
87
19
 
88
- const GlobalViewTransition = createGlobalStyle`
89
- html:active-view-transition-type(sanity-iframe-viewport) {
90
- view-transition-name: none;
91
- &::view-transition {
92
- pointer-events: none;
93
- }
94
- /* &::view-transition-old(root) {
95
- display: none;
96
- }
97
- &::view-transition-new(root) {
98
- animation: none;
99
- } */
20
+ interface IFrameProps {
21
+ animate: VariantLabels
22
+ initial: VariantLabels
23
+ onLoad: ReactEventHandler<HTMLIFrameElement>
24
+ preventClick: boolean
25
+ src: string
26
+ variants: Variants
27
+ style: React.CSSProperties
100
28
  }
101
- `
29
+
30
+ export const IFrame = forwardRef<HTMLIFrameElement, IFrameProps>(function IFrame(props, ref) {
31
+ const {animate, initial, onLoad, preventClick, src, variants, style} = props
32
+ const id = useId()
33
+
34
+ return (
35
+ <>
36
+ <IFrameElement
37
+ style={{
38
+ ...style,
39
+ // useId() guarantees that the ID will be unique, even if we add support for multiple iframe instances,
40
+ // while `view-transition-class: presentation-tool-iframe` provides userland a way to customize the transition with CSS if they wish
41
+ viewTransitionName: `presentation-tool-iframe-${id.replace(/[^a-zA-Z0-9-_]/g, '_')}`,
42
+ }}
43
+ animate={animate}
44
+ initial={initial}
45
+ onLoad={onLoad}
46
+ ref={ref}
47
+ src={src}
48
+ variants={variants}
49
+ />
50
+ {preventClick && <IFrameOverlay />}
51
+ </>
52
+ )
53
+ })
@@ -1,28 +1,11 @@
1
1
  /* eslint-disable react/no-unused-prop-types,no-nested-ternary */
2
- import {createConnectionMachine, createController} from '@sanity/comlink'
3
- import {
4
- createCompatibilityActors,
5
- type VisualEditingControllerMsg,
6
- type VisualEditingNodeMsg,
7
- } from '@sanity/presentation-comlink'
8
2
  import {
9
3
  urlSearchParamPreviewPerspective,
10
4
  urlSearchParamVercelProtectionBypass,
11
5
  urlSearchParamVercelSetBypassCookie,
12
6
  type VercelSetBypassCookieValue,
13
7
  } from '@sanity/preview-url-secret/constants'
14
- import {
15
- Card,
16
- Code,
17
- Flex,
18
- Label,
19
- Spinner,
20
- Stack,
21
- Text,
22
- usePrefersReducedMotion,
23
- useToast,
24
- } from '@sanity/ui'
25
- import {useSelector} from '@xstate/react'
8
+ import {Card, Code, Flex, Label, Spinner, Stack, Text, usePrefersReducedMotion} from '@sanity/ui'
26
9
  import {AnimatePresence, motion, MotionConfig} from 'framer-motion'
27
10
  import {
28
11
  forwardRef,
@@ -36,23 +19,24 @@ import {
36
19
  useSyncExternalStore,
37
20
  } from 'react'
38
21
  import {flushSync} from 'react-dom'
39
- import {Translate, useTranslation} from 'sanity'
40
- import {useEffectEvent} from 'use-effect-event'
22
+ import {useTranslation} from 'sanity'
41
23
 
42
24
  import {Button, TooltipDelayGroupProvider} from '../../ui-components'
43
25
  import {ErrorCard} from '../components/ErrorCard'
44
26
  import {MAX_TIME_TO_OVERLAYS_CONNECTION} from '../constants'
45
27
  import {presentationLocaleNamespace} from '../i18n'
46
- import {type PresentationMachineRef} from '../machines/presentation-machine'
47
- import {type PreviewUrlRef} from '../machines/preview-url'
28
+ import {
29
+ ACTION_IFRAME_LOADED,
30
+ ACTION_IFRAME_RELOAD,
31
+ type DispatchPresentationAction,
32
+ type PresentationState,
33
+ } from '../reducers/presentationReducer'
48
34
  import {
49
35
  type ConnectionStatus,
50
36
  type HeaderOptions,
51
37
  type PresentationPerspective,
52
38
  type PresentationViewport,
53
39
  } from '../types'
54
- import {useAllowPatterns} from '../useAllowPatterns'
55
- import {usePresentationNavigate} from '../usePresentationNavigate'
56
40
  import {usePresentationTool} from '../usePresentationTool'
57
41
  import {encodeStudioPerspective} from '../util/encodeStudioPerspective'
58
42
  import {IFrame} from './IFrame'
@@ -61,10 +45,11 @@ import {PreviewHeader} from './PreviewHeader'
61
45
  const MotionFlex = motion.create(Flex)
62
46
 
63
47
  /** @public */
64
- export interface PreviewProps {
48
+ export interface PreviewProps extends Pick<PresentationState, 'iframe' | 'visualEditing'> {
65
49
  canSharePreviewAccess: boolean
66
50
  canToggleSharePreviewAccess: boolean
67
51
  canUseSharedPreviewAccess: boolean
52
+ dispatch: DispatchPresentationAction
68
53
  header?: HeaderOptions
69
54
  initialUrl: URL
70
55
  loadersConnection: ConnectionStatus
@@ -73,7 +58,6 @@ export interface PreviewProps {
73
58
  onRefresh: (fallback: () => void) => void
74
59
  openPopup: (url: string) => void
75
60
  overlaysConnection: ConnectionStatus
76
- presentationRef: PresentationMachineRef
77
61
  perspective: PresentationPerspective
78
62
  previewUrl?: string
79
63
  setViewport: (mode: 'desktop' | 'mobile') => void
@@ -82,12 +66,13 @@ export interface PreviewProps {
82
66
  toggleOverlay: () => void
83
67
  viewport: PresentationViewport
84
68
  vercelProtectionBypass: string | null
85
- previewUrlRef: PreviewUrlRef
86
69
  }
87
70
 
88
71
  export const Preview = memo(
89
72
  forwardRef<HTMLIFrameElement, PreviewProps>(function PreviewComponent(props, forwardedRef) {
90
73
  const {
74
+ dispatch,
75
+ iframe,
91
76
  header,
92
77
  initialUrl,
93
78
  loadersConnection,
@@ -95,8 +80,6 @@ export const Preview = memo(
95
80
  perspective,
96
81
  viewport,
97
82
  vercelProtectionBypass,
98
- presentationRef,
99
- previewUrlRef,
100
83
  } = props
101
84
 
102
85
  const [stablePerspective, setStablePerspective] = useState<typeof perspective | null>(null)
@@ -150,17 +133,11 @@ export const Preview = memo(
150
133
  () => ref.current,
151
134
  )
152
135
 
153
- const isLoading = useSelector(
154
- presentationRef,
155
- (state) => state.matches('loading') || state.matches({loaded: 'reloading'}),
156
- )
157
-
136
+ const loading = iframe.status === 'loading' || iframe.status === 'reloading'
158
137
  const [timedOut, setTimedOut] = useState(false)
159
- const isRefreshing = useSelector(presentationRef, (state) =>
160
- state.matches({loaded: 'refreshing'}),
161
- )
138
+ const refreshing = iframe.status === 'refreshing'
162
139
  const [somethingIsWrong, setSomethingIsWrong] = useState(false)
163
- const iframeIsBusy = isLoading || isRefreshing || overlaysConnection === 'connecting'
140
+ const iframeIsBusy = loading || refreshing || overlaysConnection === 'connecting'
164
141
 
165
142
  const handleRetry = useCallback(() => {
166
143
  if (!ref.current) {
@@ -169,8 +146,8 @@ export const Preview = memo(
169
146
 
170
147
  ref.current.src = previewUrl.toString()
171
148
 
172
- presentationRef.send({type: 'iframe reload'})
173
- }, [presentationRef, previewUrl])
149
+ dispatch({type: ACTION_IFRAME_RELOAD})
150
+ }, [dispatch, previewUrl])
174
151
  const handleContinueAnyway = useCallback(() => {
175
152
  setContinueAnyway(true)
176
153
  }, [])
@@ -178,7 +155,7 @@ export const Preview = memo(
178
155
  const [continueAnyway, setContinueAnyway] = useState(false)
179
156
  const [showOverlaysConnectionStatus, setShowOverlaysConnectionState] = useState(false)
180
157
  useEffect(() => {
181
- if (isLoading || isRefreshing) {
158
+ if (loading || refreshing) {
182
159
  return undefined
183
160
  }
184
161
 
@@ -189,10 +166,10 @@ export const Preview = memo(
189
166
  return () => clearTimeout(timeout)
190
167
  }
191
168
  return undefined
192
- }, [overlaysConnection, isLoading, isRefreshing])
169
+ }, [overlaysConnection, loading, refreshing])
193
170
 
194
171
  useEffect(() => {
195
- if (isLoading || isRefreshing || !showOverlaysConnectionStatus) {
172
+ if (loading || refreshing || !showOverlaysConnectionStatus) {
196
173
  return undefined
197
174
  }
198
175
  if (overlaysConnection === 'connected') {
@@ -219,17 +196,39 @@ export const Preview = memo(
219
196
  return () => clearTimeout(timeout)
220
197
  }
221
198
  return undefined
222
- }, [isLoading, overlaysConnection, isRefreshing, showOverlaysConnectionStatus])
199
+ }, [loading, overlaysConnection, refreshing, showOverlaysConnectionStatus])
223
200
 
224
201
  const onIFrameLoad = useCallback(() => {
225
- presentationRef.send({type: 'iframe loaded'})
226
- }, [presentationRef])
202
+ dispatch({type: ACTION_IFRAME_LOADED})
203
+ }, [dispatch])
204
+
205
+ /**
206
+ * Ensure that clicking outside of menus and dialogs will close as focus shifts to the iframe
207
+ */
208
+ useEffect(() => {
209
+ if (!ref.current) {
210
+ return undefined
211
+ }
212
+ const instance = ref.current
213
+ function handleBlur() {
214
+ if (instance !== document.activeElement) {
215
+ return
216
+ }
217
+
218
+ instance.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
219
+ }
220
+ window.addEventListener('blur', handleBlur)
221
+ return () => {
222
+ window.removeEventListener('blur', handleBlur)
223
+ }
224
+ }, [])
227
225
 
228
226
  const preventIframeInteraction = useMemo(() => {
229
227
  return (
230
- (isLoading || (overlaysConnection === 'connecting' && !isRefreshing)) && !continueAnyway
228
+ (loading || (overlaysConnection === 'connecting' && iframe.status !== 'refreshing')) &&
229
+ !continueAnyway
231
230
  )
232
- }, [continueAnyway, isLoading, isRefreshing, overlaysConnection])
231
+ }, [continueAnyway, iframe.status, loading, overlaysConnection])
233
232
 
234
233
  const canUseViewTransition = useSyncExternalStore(
235
234
  // eslint-disable-next-line no-empty-function
@@ -239,7 +238,7 @@ export const Preview = memo(
239
238
  const iframeAnimations = useMemo(() => {
240
239
  return [
241
240
  preventIframeInteraction ? 'background' : 'active',
242
- isLoading ? 'reloading' : 'idle',
241
+ loading ? 'reloading' : 'idle',
243
242
  // If CSS View Transitions are supported, then transition iframe viewport dimensions with that instead of Motion
244
243
  canUseViewTransition ? '' : viewport,
245
244
  showOverlaysConnectionStatus && !continueAnyway ? 'timedOut' : '',
@@ -247,7 +246,7 @@ export const Preview = memo(
247
246
  }, [
248
247
  canUseViewTransition,
249
248
  continueAnyway,
250
- isLoading,
249
+ loading,
251
250
  preventIframeInteraction,
252
251
  showOverlaysConnectionStatus,
253
252
  viewport,
@@ -266,135 +265,19 @@ export const Preview = memo(
266
265
  'startViewTransition' in document &&
267
266
  typeof document.startViewTransition === 'function'
268
267
  ) {
269
- document.startViewTransition({
270
- // @ts-expect-error - fix typings
271
- update: () => flushSync(() => update()),
272
- types: ['sanity-iframe-viewport'],
273
- })
268
+ document.startViewTransition(() => flushSync(() => update()))
274
269
  } else {
275
270
  update()
276
271
  }
277
272
  }
278
273
  }, [canUseViewTransition, prefersReducedMotion, currentViewport, viewport])
279
274
 
280
- const toast = useToast()
281
- const allowOrigins = useAllowPatterns(previewUrlRef)
282
- const [checkOrigin, setCheckOrigin] = useState<false | string>(false)
283
- const [reportedMismatches] = useState(new Set<string>())
284
- const reportMismatchingOrigin = useEffectEvent((reportedOrigin: string) => {
285
- if (allowOrigins.some((allow) => allow.test(reportedOrigin))) {
286
- setCheckOrigin(reportedOrigin)
287
- return
288
- }
289
- if (reportedMismatches.has(reportedOrigin)) return
290
- reportedMismatches.add(reportedOrigin)
291
- console.warn('Visual Editing is here but misconfigured', {reportedOrigin})
292
- toast.push({
293
- closable: true,
294
- id: `presentation-iframe-origin-mismatch-${reportedOrigin}`,
295
- status: 'error',
296
- duration: Infinity,
297
- title: t('preview-frame.configuration.error.title'),
298
- description: (
299
- <Translate
300
- t={t}
301
- i18nKey="preview-frame.configuration.error.description"
302
- components={{Code: 'code'}}
303
- values={{
304
- targetOrigin: previewUrl.origin,
305
- reportedOrigin,
306
- }}
307
- />
308
- ),
309
- })
310
- })
311
- const navigate = usePresentationNavigate()
312
- const navigateEvent = useEffectEvent((url: string) => {
313
- if (!checkOrigin) return
314
- const nextUrl = new URL(url, checkOrigin)
315
- navigate(`${checkOrigin}${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`)
316
- })
317
- useEffect(() => {
318
- if (!checkOrigin) {
319
- return undefined
320
- }
321
- const target = ref.current?.contentWindow
322
- if (!target) {
323
- return undefined
324
- }
325
- const controller = createController({targetOrigin: checkOrigin})
326
- controller.addTarget(target)
327
- const comlink = controller.createChannel<VisualEditingControllerMsg, VisualEditingNodeMsg>(
328
- {
329
- name: 'presentation',
330
- heartbeat: true,
331
- connectTo: 'visual-editing',
332
- },
333
- createConnectionMachine<VisualEditingControllerMsg, VisualEditingNodeMsg>().provide({
334
- actors: createCompatibilityActors<VisualEditingControllerMsg>(),
335
- }),
336
- )
337
-
338
- comlink.on('visual-editing/navigate', (data) => {
339
- navigateEvent(data.url)
340
- })
341
- const stop = comlink.start()
342
-
343
- return () => {
344
- stop()
345
- controller.destroy()
346
- }
347
- }, [checkOrigin])
348
- useEffect(() => {
349
- if (overlaysConnection === 'connecting' || overlaysConnection === 'reconnecting') {
350
- const interval = setInterval(() => {
351
- ref.current?.contentWindow?.postMessage(
352
- {domain: 'sanity/channels', from: 'presentation', type: 'presentation/status'},
353
- /**
354
- * The targetOrigin is set to '*' intentionally here, as we need to find out if the iframe is misconfigured and has the wrong origin
355
- */
356
- '*',
357
- )
358
- }, 1_000)
359
-
360
- const controller = new AbortController()
361
- window.addEventListener(
362
- 'message',
363
- ({data}: MessageEvent<unknown>) => {
364
- /**
365
- * Listen for replies to presentation/status
366
- */
367
- if (
368
- data &&
369
- typeof data === 'object' &&
370
- 'domain' in data &&
371
- data.domain === 'sanity/channels' &&
372
- 'type' in data &&
373
- data.type === 'visual-editing/status' &&
374
- 'data' in data &&
375
- typeof data.data === 'object' &&
376
- data.data &&
377
- 'origin' in data.data &&
378
- typeof data.data.origin === 'string'
379
- ) {
380
- reportMismatchingOrigin(data.data.origin)
381
- }
382
- },
383
- {signal: controller.signal},
384
- )
385
-
386
- return () => {
387
- controller.abort()
388
- clearInterval(interval)
389
- }
390
- }
391
- return undefined
392
- }, [overlaysConnection, timedOut])
393
-
394
275
  return (
395
276
  <MotionConfig transition={prefersReducedMotion ? {duration: 0} : undefined}>
396
277
  <TooltipDelayGroupProvider>
397
278
  {previewHeader}
279
+
280
+ {/* @TODO: Move this to <PreviewFrame /> */}
398
281
  <Card flex={1} tone="transparent">
399
282
  <Flex
400
283
  align="center"
@@ -409,8 +292,8 @@ export const Preview = memo(
409
292
  >
410
293
  <AnimatePresence>
411
294
  {!somethingIsWrong &&
412
- !isLoading &&
413
- !isRefreshing &&
295
+ !loading &&
296
+ !refreshing &&
414
297
  // viewport, // using CSS View Transitions instead of framer motion to drive this
415
298
  showOverlaysConnectionStatus &&
416
299
  !continueAnyway ? (
@@ -476,7 +359,8 @@ export const Preview = memo(
476
359
  )}
477
360
  </Flex>
478
361
  </MotionFlex>
479
- ) : (isLoading || (overlaysConnection === 'connecting' && !isRefreshing)) &&
362
+ ) : (loading ||
363
+ (overlaysConnection === 'connecting' && iframe.status !== 'refreshing')) &&
480
364
  !continueAnyway ? (
481
365
  <MotionFlex
482
366
  initial="initial"