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
@@ -1,15 +1,13 @@
1
- /* eslint-disable no-nested-ternary */
2
1
  import {DesktopIcon, MobileDeviceIcon, PanelLeftIcon, RefreshIcon} from '@sanity/icons'
3
2
  import {withoutSecretSearchParams} from '@sanity/preview-url-secret/without-secret-search-params'
4
3
  import {Box, Card, Flex, Hotkeys, Switch, Text} from '@sanity/ui'
5
- import {useSelector} from '@xstate/react'
6
4
  import {type RefObject, useCallback, useMemo} from 'react'
7
5
  import {useTranslation} from 'sanity'
8
6
 
9
7
  import {Button, Tooltip} from '../../ui-components'
10
8
  import {presentationLocaleNamespace} from '../i18n'
9
+ import {ACTION_IFRAME_RELOAD} from '../reducers/presentationReducer'
11
10
  import {type HeaderOptions} from '../types'
12
- import {useId} from '../useId'
13
11
  import {OpenPreviewButton} from './OpenPreviewButton'
14
12
  import {type PreviewProps} from './Preview'
15
13
  import {PreviewLocationInput} from './PreviewLocationInput'
@@ -26,6 +24,8 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
26
24
  canSharePreviewAccess,
27
25
  canToggleSharePreviewAccess,
28
26
  canUseSharedPreviewAccess,
27
+ dispatch,
28
+ iframe,
29
29
  iframeRef,
30
30
  initialUrl,
31
31
  navigatorEnabled,
@@ -33,7 +33,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
33
33
  onRefresh,
34
34
  openPopup,
35
35
  overlaysConnection,
36
- presentationRef,
37
36
  perspective,
38
37
  previewUrl,
39
38
  setViewport,
@@ -41,7 +40,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
41
40
  toggleNavigator,
42
41
  toggleOverlay,
43
42
  viewport,
44
- previewUrlRef,
43
+ visualEditing: {overlaysEnabled},
45
44
  } = props
46
45
 
47
46
  const {t} = useTranslation(presentationLocaleNamespace)
@@ -60,7 +59,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
60
59
  if (!iframeRef.current) {
61
60
  return
62
61
  }
63
- presentationRef.send({type: 'iframe reload'})
62
+ dispatch({type: ACTION_IFRAME_RELOAD})
64
63
  // Funky way to reload an iframe without CORS issues
65
64
  // eslint-disable-next-line no-self-assign
66
65
  // ref.current.src = ref.current.src
@@ -68,17 +67,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
68
67
  })
69
68
  }
70
69
 
71
- const isLoading = useSelector(presentationRef, (state) => state.matches('loading'))
72
- const isLoaded = useSelector(presentationRef, (state) => state.matches('loaded'))
73
- const isRefreshing = useSelector(presentationRef, (state) =>
74
- state.matches({loaded: 'refreshing'}),
75
- )
76
- const isReloading = useSelector(presentationRef, (state) => state.matches({loaded: 'reloading'}))
77
- const overlaysEnabled = useSelector(
78
- presentationRef,
79
- (state) => state.context.visualEditingOverlaysEnabled,
80
- )
81
-
82
70
  const previewLocationRoute = useMemo(() => {
83
71
  const previewURL = new URL(previewUrl || '/', targetOrigin)
84
72
  const {pathname, search} = withoutSecretSearchParams(previewURL)
@@ -86,14 +74,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
86
74
  return `${pathname}${search}`
87
75
  }, [previewUrl, targetOrigin])
88
76
 
89
- const perspectiveToggleTooltipId = useId()
90
-
91
- /**
92
- * If the preview URL machine is busy it means it's about to change the target origin and reload the iframe,
93
- * so we show a spinner
94
- */
95
- const previewUrlBusy = useSelector(previewUrlRef, (state) => state.hasTag('busy'))
96
-
97
77
  return (
98
78
  <Flex align="center" gap={1} paddingX={1} style={{width: '100%'}}>
99
79
  {toggleNavigator && (
@@ -146,10 +126,9 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
146
126
  <Flex align="center" gap={3}>
147
127
  <div style={{margin: -4}}>
148
128
  <Switch
149
- indeterminate={!isLoaded}
150
129
  checked={overlaysEnabled}
151
130
  onChange={toggleOverlay}
152
- disabled={isLoading || overlaysConnection !== 'connected'}
131
+ disabled={iframe.status === 'loading' || overlaysConnection !== 'connected'}
153
132
  />
154
133
  </div>
155
134
  <Box>
@@ -163,24 +142,15 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
163
142
 
164
143
  <Box flex={1}>
165
144
  <PreviewLocationInput
166
- previewUrlRef={previewUrlRef}
167
145
  prefix={
168
146
  <Box padding={1}>
169
147
  <Tooltip
170
148
  animate
171
149
  content={
172
150
  <Text size={1}>
173
- {isLoaded
151
+ {iframe.status === 'loaded'
174
152
  ? t('preview-frame.refresh-button.tooltip')
175
- : t('preview-frame.status', {
176
- context: isLoading
177
- ? 'loading'
178
- : isRefreshing
179
- ? 'refreshing'
180
- : isReloading
181
- ? 'reloading'
182
- : 'unknown',
183
- })}
153
+ : t('preview-frame.status', {context: iframe.status})}
184
154
  </Text>
185
155
  }
186
156
  fallbackPlacements={['bottom-end']}
@@ -191,7 +161,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
191
161
  aria-label={t('preview-frame.refresh-button.aria-label')}
192
162
  icon={RefreshIcon}
193
163
  mode="bleed"
194
- loading={isReloading || isRefreshing || previewUrlBusy}
164
+ loading={iframe.status === 'reloading' || iframe.status === 'refreshing'}
195
165
  onClick={handleRefresh}
196
166
  tooltipProps={null}
197
167
  />
@@ -199,6 +169,7 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
199
169
  </Box>
200
170
  }
201
171
  onChange={onPathChange}
172
+ origin={previewLocationOrigin}
202
173
  suffix={
203
174
  <Box padding={1}>
204
175
  <OpenPreviewButton
@@ -217,10 +188,6 @@ const PreviewHeaderDefault = (props: Omit<PreviewHeaderProps, 'renderDefault'>)
217
188
  <Flex align="center" flex="none" gap={1}>
218
189
  <Tooltip
219
190
  animate
220
- // eslint-disable-next-line react/jsx-no-bind
221
- ref={(node) => {
222
- node?.style.setProperty('view-transition-name', perspectiveToggleTooltipId)
223
- }}
224
191
  content={
225
192
  <Text size={1}>
226
193
  {t('preview-frame.viewport-button.tooltip', {
@@ -2,6 +2,7 @@ import {ResetIcon} from '@sanity/icons'
2
2
  import {TextInput, type TextInputClearButtonProps} from '@sanity/ui'
3
3
  import {
4
4
  type ChangeEvent,
5
+ type FunctionComponent,
5
6
  type KeyboardEvent,
6
7
  type ReactNode,
7
8
  useCallback,
@@ -13,22 +14,17 @@ import {
13
14
  import {useActiveWorkspace, useTranslation} from 'sanity'
14
15
 
15
16
  import {presentationLocaleNamespace} from '../i18n'
16
- import {type PreviewUrlRef} from '../machines/preview-url'
17
- import {useAllowPatterns} from '../useAllowPatterns'
18
- import {useTargetOrigin} from '../useTargetOrigin'
19
17
 
20
- export function PreviewLocationInput(props: {
18
+ export const PreviewLocationInput: FunctionComponent<{
21
19
  fontSize?: number
22
20
  onChange: (value: string) => void
23
- previewUrlRef: PreviewUrlRef
21
+ origin: string
24
22
  padding?: number
25
23
  prefix?: ReactNode
26
24
  suffix?: ReactNode
27
25
  value: string
28
- }): React.JSX.Element {
29
- const {fontSize = 1, onChange, padding = 3, prefix, suffix, value, previewUrlRef} = props
30
- const allowOrigins = useAllowPatterns(previewUrlRef)
31
- const targetOrigin = useTargetOrigin(previewUrlRef)
26
+ }> = function (props) {
27
+ const {fontSize = 1, onChange, origin, padding = 3, prefix, suffix, value} = props
32
28
 
33
29
  const {t} = useTranslation(presentationLocaleNamespace)
34
30
  const {basePath = '/'} = useActiveWorkspace()?.activeWorkspace || {}
@@ -48,41 +44,29 @@ export function PreviewLocationInput(props: {
48
44
  return
49
45
  }
50
46
 
51
- let absoluteValue = sessionValue
52
- try {
53
- absoluteValue = new URL(sessionValue, targetOrigin).toString()
54
- } catch {
55
- // ignore
56
- }
47
+ const absoluteValue =
48
+ sessionValue.startsWith('/') || sessionValue === ''
49
+ ? `${origin}${sessionValue}`
50
+ : sessionValue
57
51
 
58
- if (Array.isArray(allowOrigins)) {
59
- if (!allowOrigins.some((pattern) => pattern.test(absoluteValue))) {
60
- setCustomValidity(
61
- t('preview-location-input.error', {
62
- origin: targetOrigin,
63
- context: 'origin-not-allowed',
64
- }),
65
- )
66
- event.currentTarget.reportValidity()
67
- return
68
- }
69
- // `origin` is an empty string '' if the Studio is embedded, and that's when we need to protect against recursion
70
- } else if (
71
- !targetOrigin &&
72
- (absoluteValue.startsWith(`${basePath}/`) || absoluteValue === basePath)
73
- ) {
52
+ if (!absoluteValue.startsWith(`${origin}/`) && absoluteValue !== origin) {
53
+ setCustomValidity(t('preview-location-input.error', {origin, context: 'missing-origin'}))
54
+ return
55
+ }
56
+ // `origin` is an empty string '' if the Studio is embedded, and that's when we need to protect against recursion
57
+ if (!origin && (absoluteValue.startsWith(`${basePath}/`) || absoluteValue === basePath)) {
74
58
  setCustomValidity(
75
59
  t('preview-location-input.error', {basePath, context: 'same-base-path'}),
76
60
  )
77
61
  return
78
62
  }
79
63
 
80
- const nextValue = absoluteValue === targetOrigin ? `${targetOrigin}/` : absoluteValue
64
+ const nextValue = absoluteValue === origin ? `${origin}/` : absoluteValue
81
65
 
82
66
  setCustomValidity(undefined)
83
67
  setSessionValue(undefined)
84
68
 
85
- onChange(nextValue)
69
+ onChange(nextValue.slice(origin.length))
86
70
 
87
71
  inputRef.current?.blur()
88
72
  }
@@ -92,7 +76,7 @@ export function PreviewLocationInput(props: {
92
76
  setSessionValue(undefined)
93
77
  }
94
78
  },
95
- [allowOrigins, basePath, onChange, sessionValue, t, targetOrigin],
79
+ [basePath, onChange, origin, sessionValue, t],
96
80
  )
97
81
 
98
82
  const handleBlur = useCallback(() => {
@@ -100,21 +84,10 @@ export function PreviewLocationInput(props: {
100
84
  setSessionValue(undefined)
101
85
  }, [])
102
86
 
103
- const handleClear = useCallback(() => {
104
- setCustomValidity(undefined)
105
- let nextValue = value
106
- try {
107
- nextValue = new URL(value, targetOrigin).toString()
108
- } catch {
109
- // ignore
110
- }
111
- setSessionValue(nextValue)
112
- }, [targetOrigin, value])
113
-
114
87
  useEffect(() => {
115
88
  setCustomValidity(undefined)
116
89
  setSessionValue(undefined)
117
- }, [targetOrigin, value])
90
+ }, [origin, value])
118
91
 
119
92
  const resetButton: TextInputClearButtonProps = useMemo(() => ({icon: ResetIcon}), [])
120
93
 
@@ -125,7 +98,10 @@ export function PreviewLocationInput(props: {
125
98
  customValidity={customValidity}
126
99
  fontSize={fontSize}
127
100
  onBlur={handleBlur}
128
- onClear={handleClear}
101
+ onClear={() => {
102
+ setCustomValidity(undefined)
103
+ setSessionValue(origin + value)
104
+ }}
129
105
  onChange={handleChange}
130
106
  onKeyDownCapture={handleKeyDown}
131
107
  padding={padding}
@@ -135,7 +111,7 @@ export function PreviewLocationInput(props: {
135
111
  ref={inputRef}
136
112
  space={padding}
137
113
  suffix={suffix}
138
- value={sessionValue === undefined ? new URL(value, targetOrigin).toString() : sessionValue}
114
+ value={sessionValue === undefined ? `${origin}${value}` : sessionValue}
139
115
  />
140
116
  </>
141
117
  )
@@ -108,7 +108,7 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent(
108
108
  setDisabling(true)
109
109
  await disablePreviewAccessSharing(
110
110
  client,
111
- 'sanity/presentation',
111
+ '@sanity/presentation',
112
112
  typeof window === 'undefined' ? '' : location.href,
113
113
  currentUser?.id,
114
114
  )
@@ -125,7 +125,7 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent(
125
125
 
126
126
  const previewUrlSecret = await enablePreviewAccessSharing(
127
127
  client,
128
- 'sanity/presentation',
128
+ '@sanity/presentation',
129
129
  typeof window === 'undefined' ? '' : location.href,
130
130
  currentUser?.id,
131
131
  )
@@ -0,0 +1,134 @@
1
+ import {type Dispatch, type Reducer} from 'react'
2
+ import {boolean, fallback, object, parse, picklist} from 'valibot'
3
+
4
+ /** @public */
5
+ export interface PresentationState {
6
+ mainDocument: boolean
7
+ iframe: {
8
+ status: 'loading' | 'loaded' | 'refreshing' | 'reloading'
9
+ }
10
+ visualEditing: {
11
+ overlaysEnabled: boolean
12
+ }
13
+ }
14
+
15
+ /** @public */
16
+ export const ACTION_IFRAME_LOADED = 'ACTION_IFRAME_LOADED'
17
+ /** @public */
18
+ export const ACTION_IFRAME_REFRESH = 'ACTION_IFRAME_REFRESH'
19
+ /** @public */
20
+ export const ACTION_IFRAME_RELOAD = 'ACTION_IFRAME_RELOAD'
21
+ /** @public */
22
+ export const ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE = 'ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE'
23
+
24
+ /** @public */
25
+ export interface IframeLoadedAction {
26
+ type: typeof ACTION_IFRAME_LOADED
27
+ }
28
+ /** @public */
29
+ export interface IframeRefreshAction {
30
+ type: typeof ACTION_IFRAME_REFRESH
31
+ }
32
+ /** @public */
33
+ export interface IframeReloadAction {
34
+ type: typeof ACTION_IFRAME_RELOAD
35
+ }
36
+ /** @public */
37
+ export interface VisualEditingOverlaysToggleAction {
38
+ type: typeof ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE
39
+ enabled: boolean
40
+ }
41
+
42
+ /** @public */
43
+ export type PresentationAction =
44
+ | IframeLoadedAction
45
+ | IframeRefreshAction
46
+ | IframeReloadAction
47
+ | VisualEditingOverlaysToggleAction
48
+
49
+ export const presentationReducer: Reducer<
50
+ Readonly<PresentationState>,
51
+ Readonly<PresentationAction>
52
+ > = (state, action) => {
53
+ switch (action.type) {
54
+ case ACTION_IFRAME_LOADED:
55
+ return state.iframe.status === 'loaded'
56
+ ? state
57
+ : {
58
+ ...state,
59
+ iframe: {
60
+ ...state.iframe,
61
+ status: 'loaded',
62
+ },
63
+ }
64
+ case ACTION_IFRAME_REFRESH:
65
+ return state.iframe.status === 'refreshing'
66
+ ? state
67
+ : {
68
+ ...state,
69
+ iframe: {
70
+ ...state.iframe,
71
+ status: 'refreshing',
72
+ },
73
+ }
74
+ case ACTION_IFRAME_RELOAD:
75
+ return state.iframe.status === 'reloading'
76
+ ? state
77
+ : {
78
+ ...state,
79
+ iframe: {
80
+ ...state.iframe,
81
+ status: 'reloading',
82
+ },
83
+ }
84
+ case ACTION_VISUAL_EDITING_OVERLAYS_TOGGLE:
85
+ return toggleVisualEditingOverlays(state, action)
86
+ default:
87
+ return state
88
+ }
89
+ }
90
+
91
+ const toggleVisualEditingOverlays: Reducer<
92
+ Readonly<PresentationState>,
93
+ Readonly<VisualEditingOverlaysToggleAction>
94
+ > = (state, action) => {
95
+ if (state.visualEditing.overlaysEnabled === action.enabled) return state
96
+ return {
97
+ ...state,
98
+ visualEditing: {
99
+ ...state.visualEditing,
100
+ overlaysEnabled: action.enabled,
101
+ },
102
+ }
103
+ }
104
+
105
+ const mainDocumentSchema = fallback(boolean(), false)
106
+
107
+ const iframeStatusSchema = picklist(['loading', 'loaded', 'refreshing', 'reloading'])
108
+
109
+ const initStateSchema = object({
110
+ mainDocument: mainDocumentSchema,
111
+ iframe: object({
112
+ status: iframeStatusSchema,
113
+ }),
114
+ visualEditing: object({overlaysEnabled: boolean()}),
115
+ })
116
+
117
+ const INITIAL_PRESENTATION_STATE = {
118
+ mainDocument: false,
119
+ iframe: {
120
+ status: 'loading',
121
+ },
122
+ visualEditing: {
123
+ overlaysEnabled: false,
124
+ },
125
+ } as const satisfies PresentationState
126
+
127
+ export function presentationReducerInit(
128
+ state: Readonly<Partial<PresentationState>>,
129
+ ): Readonly<PresentationState> {
130
+ return parse(initStateSchema, {...INITIAL_PRESENTATION_STATE, ...state})
131
+ }
132
+
133
+ /** @public */
134
+ export type DispatchPresentationAction = Dispatch<Readonly<PresentationAction>>
@@ -6,14 +6,17 @@ import {
6
6
  type VisualEditingControllerMsg,
7
7
  type VisualEditingNodeMsg,
8
8
  } from '@sanity/presentation-comlink'
9
- import {type PreviewUrlResolver} from '@sanity/preview-url-secret/define-preview-url'
9
+ import {
10
+ type PreviewUrlResolver,
11
+ type PreviewUrlResolverOptions,
12
+ } from '@sanity/preview-url-secret/define-preview-url'
10
13
  import {type ComponentType} from 'react'
11
14
  import {type Observable} from 'rxjs'
12
15
  import {type DocumentStore, type SanityClient} from 'sanity'
13
16
 
14
17
  import {type PreviewHeaderProps} from './preview/PreviewHeader'
15
18
 
16
- export type {PreviewUrlResolver}
19
+ export type {PreviewUrlResolver, PreviewUrlResolverOptions}
17
20
 
18
21
  /**
19
22
  * Represents a document location
@@ -79,141 +82,7 @@ export interface HeaderOptions {
79
82
  }
80
83
 
81
84
  /** @public */
82
- export interface PreviewUrlAllowOptionContext {
83
- client: SanityClient
84
- /**
85
- * Equivalent to `location.origin`
86
- */
87
- origin: string
88
- /**
89
- * The initial URL of the preview
90
- */
91
- initialUrl: URL
92
- }
93
-
94
- /** @public */
95
- export interface PreviewUrlInitialOptionContext {
96
- client: SanityClient
97
- /**
98
- * Equivalent to `location.origin`
99
- */
100
- origin: string
101
- }
102
-
103
- /** @public */
104
- export interface PreviewUrlPreviewModeOptionContext {
105
- client: SanityClient
106
- /**
107
- * Equivalent to `location.origin`
108
- */
109
- origin: string
110
- /**
111
- * The origin on the URL that will be used in the preview iframe
112
- */
113
- targetOrigin: string
114
- }
115
-
116
- /** @public */
117
- export type PreviewUrlAllowOption =
118
- | string
119
- | string[]
120
- | ((context: PreviewUrlAllowOptionContext) => string | string[] | Promise<string | string[]>)
121
-
122
- /** @public */
123
- export type PreviewUrlInitialOption =
124
- | string
125
- | ((context: PreviewUrlInitialOptionContext) => string | Promise<string>)
126
-
127
- /** @public */
128
- export type PreviewUrlPreviewModeOption =
129
- | PreviewUrlPreviewMode
130
- | ((
131
- context: PreviewUrlPreviewModeOptionContext,
132
- ) => false | PreviewUrlPreviewMode | Promise<false | PreviewUrlPreviewMode>)
133
-
134
- /** @public */
135
- export interface PreviewUrlPreviewMode {
136
- /**
137
- * The route that enables Preview Mode
138
- * @example '/api/preview'
139
- * @example '/api/draft-mode/enable'
140
- */
141
- enable: string
142
- /**
143
- * Allow sharing access to a preview with others.
144
- * This is enabled/disabled in the Presentation Tool. It's initially disabled, and can be enabled by someone who has access to creating draft documents in the Studio.
145
- * Custom roles can limit access to `_id in path("drafts.**") && _type == "sanity.previewUrlSecret"`.
146
- * This will create a secret that is valid until sharing is disabled. Turning sharing off and on again will create a new secret and can be used to remove access for folks that got the link in an email but should no longer have access.
147
- * Share URLs to previews will append this secret and give access to anyone who is given the URL, they don't need to be logged into the Studio or to Vercel.
148
- */
149
- shareAccess?: boolean
150
- /**
151
- * The route that reports if Preview Mode is enabled or not, useful for debugging
152
- * @example '/api/check-preview'
153
- * @deprecated - this API is not yet implemented
154
- */
155
- check?: string
156
- /**
157
- * The route that disables Preview Mode, useful for debugging
158
- * @example '/api/disable-preview'
159
- * @deprecated - this API is not yet implemented
160
- */
161
- disable?: string
162
- }
163
-
164
- /**
165
- * @public
166
- */
167
- export interface PreviewUrlResolverOptions {
168
- /**
169
- * The default preview URL, used when the URL to use is not yet known, or there's no `&preview=...` search param in the studio URL.
170
- * @example '/en/preview?q=shoes'
171
- * @example 'https://example.com'
172
- * @defaultValue `location.origin`
173
- */
174
- initial?: PreviewUrlInitialOption
175
- previewMode?: PreviewUrlPreviewModeOption
176
- /**
177
- * @defaultValue `location.origin`
178
- * @deprecated - use `previewMode.initial` instead
179
- */
180
- origin?: string
181
- /**
182
- * @defaultValue '/'
183
- * @deprecated - use `previewMode.initial` instead
184
- */
185
- preview?: string
186
- /**
187
- * @deprecated - use `previewMode` instead
188
- */
189
- draftMode?: {
190
- /**
191
- * @deprecated - use `previewMode.enable` instead
192
- */
193
- enable: string
194
- /**
195
- * @deprecated - use `previewMode.shareAccess` instead
196
- */
197
- shareAccess?: boolean
198
- /**
199
- * @deprecated - use `previewMode.check` instead
200
- */
201
- check?: string
202
- /**
203
- * @deprecated - use `previewMode.disable` instead
204
- */
205
- disable?: string
206
- }
207
- }
208
-
209
- /**
210
- * @deprecated the `previewUrl.initial`, `previewUrl.allowOrigins` and `previewUrl.previewMode.enable` supports async functions that offer advanced control over how preview URLs are resolved
211
- * @public
212
- */
213
- export type DeprecatedPreviewUrlResolver = PreviewUrlResolver<SanityClient>
214
-
215
- /** @public */
216
- export type PreviewUrlOption = string | DeprecatedPreviewUrlResolver | PreviewUrlResolverOptions
85
+ export type PreviewUrlOption = string | PreviewUrlResolver<SanityClient> | PreviewUrlResolverOptions
217
86
 
218
87
  /**
219
88
  * Object of document location resolver definitions per document type
@@ -291,8 +160,6 @@ export interface PresentationPluginOptions {
291
160
  icon?: ComponentType
292
161
  name?: string
293
162
  title?: string
294
- allowOrigins?: PreviewUrlAllowOption
295
- previewUrl: PreviewUrlOption
296
163
  /**
297
164
  * @deprecated use `resolve.locations` instead
298
165
  */
@@ -301,6 +168,7 @@ export interface PresentationPluginOptions {
301
168
  mainDocuments?: DocumentResolver[]
302
169
  locations?: DocumentLocationResolvers | DocumentLocationResolver
303
170
  }
171
+ previewUrl: PreviewUrlOption
304
172
  components?: {
305
173
  unstable_header?: HeaderOptions
306
174
  unstable_navigator?: NavigatorOptions
@@ -12,6 +12,7 @@ import {
12
12
  type MainDocument,
13
13
  type MainDocumentState,
14
14
  type PresentationNavigate,
15
+ type PreviewUrlOption,
15
16
  } from './types'
16
17
 
17
18
  // Helper function to "unwrap" a result when it is either explicitly provided or
@@ -96,10 +97,10 @@ export function useMainDocument(props: {
96
97
  navigate?: PresentationNavigate
97
98
  navigationHistory: RouterState[]
98
99
  path?: string
99
- targetOrigin: string
100
+ previewUrl?: PreviewUrlOption
100
101
  resolvers?: DocumentResolver[]
101
102
  }): MainDocumentState | undefined {
102
- const {navigate, navigationHistory, path, targetOrigin, resolvers = []} = props
103
+ const {navigate, navigationHistory, path, previewUrl, resolvers = []} = props
103
104
  const {state: routerState} = useRouter()
104
105
  const {perspectiveStack} = usePerspective()
105
106
  const client = useClient({apiVersion: API_VERSION})
@@ -135,7 +136,14 @@ export function useMainDocument(props: {
135
136
  })
136
137
 
137
138
  useEffect(() => {
138
- const url = new URL(relativeUrl, targetOrigin)
139
+ const base =
140
+ // eslint-disable-next-line no-nested-ternary
141
+ typeof previewUrl === 'string'
142
+ ? previewUrl
143
+ : typeof previewUrl === 'object'
144
+ ? previewUrl?.origin || location.origin
145
+ : location.origin
146
+ const url = new URL(relativeUrl, base)
139
147
 
140
148
  if (resolvers.length) {
141
149
  let result:
@@ -180,7 +188,7 @@ export function useMainDocument(props: {
180
188
  setMainDocumentState(undefined)
181
189
  mainDocumentIdRef.current = undefined
182
190
  return undefined
183
- }, [client, perspectiveStack, relativeUrl, resolvers, targetOrigin])
191
+ }, [client, previewUrl, relativeUrl, resolvers, perspectiveStack])
184
192
 
185
193
  return mainDocumentState
186
194
  }
@@ -3,6 +3,7 @@ import {type MutableRefObject, useCallback, useEffect, useMemo, useRef, useState
3
3
  import {getPublishedId} from 'sanity'
4
4
  import {type RouterContextValue, type RouterState, type SearchParam} from 'sanity/router'
5
5
 
6
+ import {parseRouterState} from './lib/parse'
6
7
  import {
7
8
  type CombinedSearchParams,
8
9
  type FrameState,
@@ -12,7 +13,6 @@ import {
12
13
  type PresentationStateParams,
13
14
  type StructureDocumentPaneParams,
14
15
  } from './types'
15
- import {parseRouterState} from './util/parse'
16
16
 
17
17
  function pruneObject<T extends RouterState | PresentationParamsContextValue>(obj: T): T {
18
18
  return Object.fromEntries(
@@ -50,7 +50,8 @@ export function useParams({
50
50
  id,
51
51
  type,
52
52
  path,
53
- preview: routerSearchParams.preview || initialPreviewUrl.toString(),
53
+ preview:
54
+ routerSearchParams.preview || `${initialPreviewUrl.pathname}${initialPreviewUrl.search}`,
54
55
  perspective: routerSearchParams.perspective,
55
56
  viewport: routerSearchParams.viewport,
56
57
  inspect: routerSearchParams.inspect,