sanity-plugin-iframe-pane 2.6.1 → 3.0.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.
package/src/Iframe.tsx CHANGED
@@ -1,27 +1,57 @@
1
- /* eslint-disable react/jsx-no-bind */
2
1
  import {WarningOutlineIcon} from '@sanity/icons'
2
+ import {createPreviewSecret} from '@sanity/preview-url-secret/create-secret'
3
+ import {definePreviewUrl} from '@sanity/preview-url-secret/define-preview-url'
3
4
  import {Box, Card, Container, Flex, Spinner, Stack, Text, usePrefersReducedMotion} from '@sanity/ui'
4
5
  import {AnimatePresence, motion, MotionConfig} from 'framer-motion'
5
- import React, {forwardRef, useCallback, useDeferredValue, useEffect, useRef, useState} from 'react'
6
+ import React, {
7
+ forwardRef,
8
+ memo,
9
+ Suspense,
10
+ useCallback,
11
+ useEffect,
12
+ useRef,
13
+ useState,
14
+ useTransition,
15
+ } from 'react'
6
16
  import {HTMLAttributeReferrerPolicy} from 'react'
7
- import {SanityDocument} from 'sanity'
17
+ import {SanityDocument, useClient, useCurrentUser} from 'sanity'
18
+ import {suspend} from 'suspend-react'
8
19
 
9
- import {UrlResolver} from './defineUrlResolver'
10
- import {GetUrlSecret} from './GetUrlSecret'
11
- import {UrlSecretId} from './isValidSecret'
12
20
  import {DEFAULT_SIZE, sizes, Toolbar} from './Toolbar'
13
- import {IframeSizeKey, MissingSlug, SetError, type UrlState} from './types'
21
+ import {IframeSizeKey} from './types'
14
22
 
15
- export type {IframeSizeKey, UrlResolver, UrlSecretId}
23
+ export type UrlResolver = (
24
+ document: SanityDocument | null,
25
+ ) => string | Error | undefined | Promise<string | Error | undefined>
16
26
 
17
- export type IframeOptions = {
18
- urlSecretId?: UrlSecretId
19
- url: UrlState | UrlResolver
27
+ export type {IframeSizeKey}
28
+
29
+ export interface IframeOptions {
30
+ url:
31
+ | string
32
+ | UrlResolver
33
+ | {
34
+ /**
35
+ * The URL origin of where the preview is hosted, for example `https://example.com`.
36
+ * If it's an embedded Studio then set it to `'same-origin'`.
37
+ */
38
+ origin: 'same-origin' | string
39
+ /**
40
+ * The route to redirect to after enabling Draft Mode.
41
+ * If you don't have enough data to build the URL, return an `Error` instance to show an error message.
42
+ * @example `return new Error('Missing slug')`
43
+ * To prolong the loading state, return `undefined`
44
+ */
45
+ preview: string | UrlResolver
46
+ /**
47
+ * The route that enables Draft Mode
48
+ * @example '/api/draft'
49
+ */
50
+ draftMode: string
51
+ }
20
52
  defaultSize?: IframeSizeKey
21
- loader?: string | boolean
22
53
  showDisplayUrl?: boolean
23
54
  reload?: {
24
- revision?: boolean | number
25
55
  button?: boolean
26
56
  }
27
57
  attributes?: Partial<{
@@ -36,44 +66,155 @@ const MotionFlex = motion(Flex)
36
66
 
37
67
  export interface IframeProps {
38
68
  document: {
39
- displayed: SanityDocument
69
+ draft: SanityDocument | null
70
+ published: SanityDocument | null
40
71
  }
41
72
  options: IframeOptions
42
73
  }
43
74
 
44
75
  export function Iframe(props: IframeProps) {
45
- const [error, setError] = useState<unknown>(null)
46
- if (error) {
47
- throw error
48
- }
76
+ const {
77
+ document: {published, draft = published},
78
+ options,
79
+ } = props
80
+ const {defaultSize = DEFAULT_SIZE, reload, attributes, showDisplayUrl = true} = options
81
+
82
+ const urlRef = useRef(options.url)
83
+ const [draftSnapshot, setDraftSnapshot] = useState(() => draft)
84
+ useEffect(() => {
85
+ urlRef.current = options.url
86
+ }, [options.url])
87
+ useEffect(() => {
88
+ if (JSON.stringify(draft) !== JSON.stringify(draftSnapshot)) {
89
+ startTransition(() => setDraftSnapshot(draft))
90
+ }
91
+ }, [draft, draftSnapshot])
92
+ const currentUser = useCurrentUser()
93
+ const client = useClient({apiVersion: '2023-10-16'})
94
+ const [expiresAt, setExpiresAt] = useState<number | undefined>()
95
+ const previewSecretRef = useRef<string | undefined>()
96
+ const [isResolvingUrl, startTransition] = useTransition()
97
+ const url = useCallback(
98
+ // eslint-disable-next-line @typescript-eslint/no-shadow
99
+ async (draft: SanityDocument | null) => {
100
+ if (typeof location === 'undefined') {
101
+ return undefined
102
+ }
103
+ const urlProp = urlRef.current
104
+ if (typeof urlProp === 'string') {
105
+ return new URL(urlProp, location.origin)
106
+ }
107
+ if (typeof urlProp === 'function') {
108
+ // eslint-disable-next-line @typescript-eslint/no-shadow
109
+ const url = await urlProp(draft)
110
+ return typeof url === 'string' ? new URL(url, location.origin) : url
111
+ }
112
+ if (typeof urlProp === 'object') {
113
+ const preview =
114
+ typeof urlProp.preview === 'function' ? await urlProp.preview(draft) : urlProp.preview
115
+ if (typeof preview !== 'string') {
116
+ return preview
117
+ }
118
+ if (!previewSecretRef.current) {
119
+ // eslint-disable-next-line @typescript-eslint/no-shadow
120
+ const {secret, expiresAt} = await createPreviewSecret(
121
+ client,
122
+ 'sanity-plugin-iframe-pane',
123
+ location.href,
124
+ currentUser?.id,
125
+ )
126
+ previewSecretRef.current = secret
127
+ startTransition(() => setExpiresAt(expiresAt.getTime()))
128
+ }
49
129
 
50
- const {document: sanityDocument, options} = props
130
+ const resolvePreviewUrl = definePreviewUrl({
131
+ origin: urlProp.origin === 'same-origin' ? location.origin : urlProp.origin,
132
+ preview,
133
+ draftMode: {
134
+ enable: urlProp.draftMode,
135
+ },
136
+ })
137
+ // eslint-disable-next-line @typescript-eslint/no-shadow
138
+ const url = await resolvePreviewUrl({
139
+ client,
140
+ previewUrlSecret: previewSecretRef.current,
141
+ previewSearchParam: null,
142
+ })
143
+ return new URL(url, location.origin)
144
+ }
145
+ return undefined
146
+ },
147
+ [client, currentUser?.id],
148
+ )
149
+ useEffect(() => {
150
+ if (expiresAt) {
151
+ const timeout = setTimeout(
152
+ () => {
153
+ startTransition(() => setExpiresAt(undefined))
154
+ previewSecretRef.current = undefined
155
+ },
156
+ Math.max(0, expiresAt - Date.now()),
157
+ )
158
+ return () => clearTimeout(timeout)
159
+ }
160
+ return undefined
161
+ }, [expiresAt])
162
+
163
+ return (
164
+ <Suspense fallback={<Loading iframeSize="desktop" />}>
165
+ <IframeInner
166
+ draftSnapshot={draftSnapshot}
167
+ url={url}
168
+ isResolvingUrl={isResolvingUrl}
169
+ attributes={attributes}
170
+ defaultSize={defaultSize}
171
+ reload={reload}
172
+ showDisplayUrl={showDisplayUrl}
173
+ userId={currentUser?.id}
174
+ />
175
+ </Suspense>
176
+ )
177
+ }
178
+
179
+ export interface IframeInnerProps extends Omit<IframeOptions, 'url'> {
180
+ url: (draftSnapshot: SanityDocument | null) => Promise<URL | Error | undefined>
181
+ isResolvingUrl: boolean
182
+ draftSnapshot: SanityDocument | null
183
+ userId?: string
184
+ expiresAt?: number
185
+ }
186
+ const IframeInner = memo(function IframeInner(props: IframeInnerProps) {
51
187
  const {
52
- url,
53
- urlSecretId,
188
+ isResolvingUrl,
54
189
  defaultSize = DEFAULT_SIZE,
55
190
  reload,
56
- loader = 'Loading…',
57
191
  attributes = {},
58
192
  showDisplayUrl = true,
59
- } = options
193
+ draftSnapshot,
194
+ userId,
195
+ expiresAt,
196
+ } = props
60
197
  const [iframeSize, setIframeSize] = useState(sizes?.[defaultSize] ? defaultSize : DEFAULT_SIZE)
61
198
 
62
- // Workaround documents that initially appears to be an empty new document but just hasen't loaded yet
63
- const [workaroundEmptyDocument, setWorkaroundEmptyDocument] = useState(true)
64
- useEffect(() => {
65
- const timeout = setTimeout(() => setWorkaroundEmptyDocument(false), 1000)
66
- return () => clearTimeout(timeout)
67
- }, [])
68
-
69
199
  const prefersReducedMotion = usePrefersReducedMotion()
70
- const [urlState, setUrlState] = useState<UrlState>(() => (typeof url === 'function' ? '' : url))
200
+
201
+ const url = suspend(
202
+ () => props.url(draftSnapshot),
203
+ [
204
+ // Cache based on a few specific conditions
205
+ 'sanity-plugin-iframe-pane',
206
+ draftSnapshot,
207
+ userId,
208
+ expiresAt,
209
+ resolveUUID,
210
+ ],
211
+ )
71
212
 
72
213
  const [loading, setLoading] = useState(true)
73
- const [reloading, setReloading] = useState(false)
214
+ const [_reloading, setReloading] = useState(false)
215
+ const reloading = _reloading || isResolvingUrl
74
216
 
75
217
  const iframe = useRef<HTMLIFrameElement>(null)
76
- const {displayed} = sanityDocument
77
218
 
78
219
  const handleReload = useCallback(() => {
79
220
  if (!iframe?.current) {
@@ -87,78 +228,52 @@ export function Iframe(props: IframeProps) {
87
228
  setReloading(true)
88
229
  }, [])
89
230
 
90
- const deferredRevision = useDeferredValue(displayed._rev)
91
- const displayUrl = typeof urlState === 'string' ? urlState : ''
92
-
93
231
  return (
94
232
  <MotionConfig transition={prefersReducedMotion ? {duration: 0} : undefined}>
95
233
  <Flex direction="column" style={{height: `100%`}}>
96
234
  <Toolbar
97
- displayUrl={displayUrl}
235
+ url={url}
98
236
  iframeSize={iframeSize}
99
237
  reloading={reloading}
100
238
  setIframeSize={setIframeSize}
101
- showDisplayUrl={showDisplayUrl}
239
+ showUrl={showDisplayUrl}
102
240
  reloadButton={!!reload?.button}
103
241
  handleReload={handleReload}
104
242
  />
105
- {urlState === MissingSlug && !workaroundEmptyDocument ? (
106
- <MissingSlugScreen />
243
+ {url instanceof Error ? (
244
+ <ErrorCard error={url} />
107
245
  ) : (
108
246
  <Card tone="transparent" style={{height: `100%`}}>
109
247
  <Frame
110
248
  ref={iframe}
111
- loader={loader}
112
249
  loading={loading}
113
250
  reloading={reloading}
114
251
  iframeSize={iframeSize}
115
252
  setReloading={setReloading}
116
253
  setLoading={setLoading}
117
- displayUrl={displayUrl}
254
+ url={url}
118
255
  attributes={attributes}
119
256
  />
120
257
  </Card>
121
258
  )}
122
- {typeof url === 'function' && (
123
- <AsyncUrl
124
- // We use the revision as a key, to force a re-render when the revision changes
125
- // This allows us to respond to changed props (maybe the url function itself changes)
126
- // But avoid calling async logic on every render accidentally
127
- key={deferredRevision}
128
- url={url}
129
- displayed={displayed}
130
- urlSecretId={urlSecretId}
131
- setDisplayUrl={setUrlState}
132
- setError={setError}
133
- />
134
- )}
135
- {displayUrl && (reload?.revision || reload?.revision === 0) && (
136
- <ReloadOnRevision
137
- revision={reload.revision}
138
- _rev={deferredRevision}
139
- handleReload={handleReload}
140
- />
141
- )}
142
259
  </Flex>
143
260
  </MotionConfig>
144
261
  )
145
- }
262
+ })
146
263
 
147
- interface FrameProps extends Required<Pick<IframeOptions, 'loader' | 'attributes'>> {
148
- loader: string | boolean
264
+ interface FrameProps extends Required<Pick<IframeOptions, 'attributes'>> {
149
265
  loading: boolean
150
266
  reloading: boolean
151
267
  setLoading: (loading: boolean) => void
152
268
  setReloading: (reloading: boolean) => void
153
269
  iframeSize: IframeSizeKey
154
- displayUrl: string
270
+ url: URL | undefined
155
271
  }
156
272
  const Frame = forwardRef(function Frame(
157
273
  props: FrameProps,
158
274
  iframe: React.ForwardedRef<HTMLIFrameElement>,
159
275
  ) {
160
- const {loader, loading, setLoading, iframeSize, attributes, reloading, displayUrl, setReloading} =
161
- props
276
+ const {loading, setLoading, iframeSize, attributes, reloading, url, setReloading} = props
162
277
 
163
278
  function handleIframeLoad() {
164
279
  setLoading(false)
@@ -172,49 +287,39 @@ const Frame = forwardRef(function Frame(
172
287
  return (
173
288
  <Flex align="center" justify="center" style={{height: `100%`, position: `relative`}}>
174
289
  <AnimatePresence>
175
- {loader && loading && (
176
- <MotionFlex
177
- initial="initial"
178
- animate="animate"
179
- exit="exit"
180
- variants={spinnerVariants}
181
- justify="center"
182
- align="center"
183
- style={{inset: `0`, position: `absolute`}}
184
- >
185
- <Flex
186
- style={{...sizes[iframeSize]}}
290
+ {!url ||
291
+ (loading && (
292
+ <MotionFlex
293
+ initial="initial"
294
+ animate="animate"
295
+ exit="exit"
296
+ variants={spinnerVariants}
187
297
  justify="center"
188
298
  align="center"
189
- direction="column"
190
- gap={4}
299
+ style={{inset: `0`, position: `absolute`}}
191
300
  >
192
- <Spinner muted />
193
- {loader && typeof loader === 'string' && (
194
- <Text muted size={1}>
195
- {loader}
196
- </Text>
197
- )}
198
- </Flex>
199
- </MotionFlex>
200
- )}
301
+ <Loading iframeSize={iframeSize} />
302
+ </MotionFlex>
303
+ ))}
201
304
  </AnimatePresence>
202
- <motion.iframe
203
- ref={iframe}
204
- title="preview"
205
- frameBorder="0"
206
- style={{maxHeight: '100%'}}
207
- src={displayUrl}
208
- initial={['background', iframeSize]}
209
- variants={iframeVariants}
210
- animate={[
211
- loader && loading ? 'background' : 'active',
212
- reloading ? 'reloading' : 'idle',
213
- iframeSize,
214
- ]}
215
- {...attributes}
216
- onLoad={handleIframeLoad}
217
- />
305
+ {url && (
306
+ <motion.iframe
307
+ ref={iframe}
308
+ title="preview"
309
+ frameBorder="0"
310
+ style={{maxHeight: '100%'}}
311
+ src={url.toString()}
312
+ initial={['background', iframeSize]}
313
+ variants={iframeVariants}
314
+ animate={[
315
+ loading ? 'background' : 'active',
316
+ reloading ? 'reloading' : 'idle',
317
+ iframeSize,
318
+ ]}
319
+ {...attributes}
320
+ onLoad={handleIframeLoad}
321
+ />
322
+ )}
218
323
  </Flex>
219
324
  )
220
325
  })
@@ -251,74 +356,18 @@ const iframeVariants = {
251
356
  },
252
357
  }
253
358
 
254
- interface ReloadOnRevisionProps {
255
- _rev?: string
256
- revision: number | boolean
257
- handleReload: () => void
258
- }
259
- function ReloadOnRevision(props: ReloadOnRevisionProps) {
260
- const {revision, handleReload, _rev} = props
261
- const [initialRev] = useState(_rev)
262
- // Reload on new revisions
263
- // eslint-disable-next-line consistent-return
264
- useEffect(() => {
265
- if (_rev !== initialRev) {
266
- const timeout = setTimeout(handleReload, Number(revision === true ? 300 : revision))
267
- return () => clearTimeout(timeout)
268
- }
269
- }, [_rev, revision, handleReload, initialRev])
270
-
271
- return null
272
- }
273
-
274
- interface AsyncUrlProps {
275
- displayed: SanityDocument
276
- url: UrlResolver
277
- urlSecretId?: UrlSecretId
278
- setDisplayUrl: (url: UrlState) => void
279
- setError: SetError
280
- }
281
- function AsyncUrl(props: AsyncUrlProps) {
282
- const {urlSecretId, setDisplayUrl, setError} = props
283
- // Snapshot values we only care about when the revision changes, done by changing the `key` prop
284
- const [displayed] = useState(props.displayed)
285
- const [url] = useState(() => props.url)
286
- const [urlSecret, setUrlSecret] = useState<null | string>(null)
287
-
288
- // Set initial URL and refresh on new revisions
289
- useEffect(() => {
290
- if (urlSecretId && !urlSecret) return
291
-
292
- const getUrl = async (signal: AbortSignal) => {
293
- const resolveUrl = await url(displayed, urlSecret, abort.signal)
294
-
295
- // Only update state if URL has changed
296
- if (!signal.aborted && resolveUrl) {
297
- setDisplayUrl(resolveUrl)
298
- }
299
- }
300
-
301
- const abort = new AbortController()
302
- getUrl(abort.signal).catch((error) => error.name !== 'AbortError' && setError(error))
303
- // eslint-disable-next-line consistent-return
304
- return () => abort.abort()
305
- }, [displayed, setDisplayUrl, setError, url, urlSecret, urlSecretId])
306
-
307
- if (urlSecretId) {
308
- return (
309
- <GetUrlSecret
310
- urlSecretId={urlSecretId}
311
- urlSecret={urlSecret}
312
- setUrlSecret={setUrlSecret}
313
- setError={setError}
314
- />
315
- )
316
- }
317
-
318
- return null
359
+ function Loading({iframeSize}: {iframeSize: IframeSizeKey}) {
360
+ return (
361
+ <Flex style={{...sizes[iframeSize]}} justify="center" align="center" direction="column" gap={4}>
362
+ <Spinner muted />
363
+ <Text muted size={1}>
364
+ Loading…
365
+ </Text>
366
+ </Flex>
367
+ )
319
368
  }
320
369
 
321
- export function MissingSlugScreen() {
370
+ export function ErrorCard({error}: {error: Error}) {
322
371
  return (
323
372
  <Card height="fill">
324
373
  <Flex align="center" height="fill" justify="center" padding={4} sizing="border">
@@ -332,10 +381,10 @@ export function MissingSlugScreen() {
332
381
  </Box>
333
382
  <Stack flex={1} marginLeft={3} space={3}>
334
383
  <Text as="h1" size={1} weight="bold">
335
- Missing slug
384
+ {error.name}
336
385
  </Text>
337
386
  <Text as="p" muted size={1}>
338
- Add a slug to see the preview.
387
+ {error.message}
339
388
  </Text>
340
389
  </Stack>
341
390
  </Flex>
@@ -345,3 +394,6 @@ export function MissingSlugScreen() {
345
394
  </Card>
346
395
  )
347
396
  }
397
+
398
+ // https://github.com/pmndrs/suspend-react?tab=readme-ov-file#making-cache-keys-unique
399
+ const resolveUUID = Symbol()
package/src/Toolbar.tsx CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/jsx-no-bind */
2
1
  import {CopyIcon, LaunchIcon, MobileDeviceIcon, RefreshIcon} from '@sanity/icons'
3
2
  import {Box, Button, Card, Flex, Text, Tooltip, useToast} from '@sanity/ui'
4
3
  import React, {useRef} from 'react'
@@ -21,24 +20,17 @@ export const sizes: SizeProps = {
21
20
  export const DEFAULT_SIZE = `desktop`
22
21
 
23
22
  export interface ToolbarProps {
24
- displayUrl: string
23
+ url: URL | Error | undefined
25
24
  iframeSize: IframeSizeKey
26
25
  setIframeSize: (size: IframeSizeKey) => void
27
- showDisplayUrl: boolean
26
+ showUrl: boolean
28
27
  reloading: boolean
29
28
  reloadButton: boolean
30
29
  handleReload: () => void
31
30
  }
32
31
  export function Toolbar(props: ToolbarProps) {
33
- const {
34
- displayUrl,
35
- iframeSize,
36
- setIframeSize,
37
- reloading,
38
- showDisplayUrl,
39
- reloadButton,
40
- handleReload,
41
- } = props
32
+ const {url, iframeSize, setIframeSize, reloading, showUrl, reloadButton, handleReload} = props
33
+ const validUrl = url instanceof URL
42
34
 
43
35
  const input = useRef<HTMLTextAreaElement>(null)
44
36
  const {push: pushToast} = useToast()
@@ -49,7 +41,7 @@ export function Toolbar(props: ToolbarProps) {
49
41
  <textarea
50
42
  style={{position: `absolute`, pointerEvents: `none`, opacity: 0}}
51
43
  ref={input}
52
- value={displayUrl}
44
+ value={validUrl ? url.toString() : ''}
53
45
  readOnly
54
46
  tabIndex={-1}
55
47
  />
@@ -66,7 +58,7 @@ export function Toolbar(props: ToolbarProps) {
66
58
  placement="bottom-start"
67
59
  >
68
60
  <Button
69
- disabled={!displayUrl}
61
+ disabled={!validUrl}
70
62
  fontSize={[1]}
71
63
  padding={2}
72
64
  mode={iframeSize === 'mobile' ? 'default' : 'ghost'}
@@ -75,9 +67,7 @@ export function Toolbar(props: ToolbarProps) {
75
67
  />
76
68
  </Tooltip>
77
69
  </Flex>
78
- <Box flex={1}>
79
- {showDisplayUrl && displayUrl && <DisplayUrl displayUrl={displayUrl} />}
80
- </Box>
70
+ <Box flex={1}>{showUrl && validUrl && <DisplayUrl url={url} />}</Box>
81
71
  <Flex align="center" gap={1}>
82
72
  {reloadButton ? (
83
73
  <Tooltip
@@ -89,7 +79,7 @@ export function Toolbar(props: ToolbarProps) {
89
79
  padding={2}
90
80
  >
91
81
  <Button
92
- disabled={!displayUrl}
82
+ disabled={!validUrl}
93
83
  mode="bleed"
94
84
  fontSize={[1]}
95
85
  padding={2}
@@ -110,7 +100,7 @@ export function Toolbar(props: ToolbarProps) {
110
100
  >
111
101
  <Button
112
102
  mode="bleed"
113
- disabled={!displayUrl}
103
+ disabled={!validUrl}
114
104
  fontSize={[1]}
115
105
  icon={CopyIcon}
116
106
  padding={[2]}
@@ -137,14 +127,14 @@ export function Toolbar(props: ToolbarProps) {
137
127
  placement="bottom-end"
138
128
  >
139
129
  <Button
140
- disabled={!displayUrl}
130
+ disabled={!validUrl}
141
131
  fontSize={[1]}
142
132
  icon={LaunchIcon}
143
133
  mode="ghost"
144
134
  paddingY={[2]}
145
135
  text="Open"
146
136
  aria-label="Open URL in a new tab"
147
- onClick={() => window.open(displayUrl)}
137
+ onClick={validUrl ? () => window.open(url.toString()) : undefined}
148
138
  />
149
139
  </Tooltip>
150
140
  </Flex>
package/src/index.ts CHANGED
@@ -1,15 +1,8 @@
1
1
  export {
2
- defineUrlResolver,
3
- type DefineUrlResolverOptions,
4
- type UrlResolver,
5
- type UrlState,
6
- } from './defineUrlResolver'
7
- export {
8
- Iframe as default,
9
2
  Iframe,
10
3
  type IframeOptions,
11
4
  type IframeProps,
12
5
  type IframeSizeKey,
13
- type UrlSecretId,
6
+ type UrlResolver,
14
7
  } from './Iframe'
15
8
  export type {Size, SizeProps} from './types'
package/src/types.ts CHANGED
@@ -1,7 +1,3 @@
1
- export const MissingSlug = Symbol('MissingSlug')
2
-
3
- export type UrlState = string | typeof MissingSlug
4
-
5
1
  export type IframeSizeKey = keyof SizeProps
6
2
 
7
3
  export type Size = 'desktop' | 'mobile'
@@ -13,5 +9,3 @@ export type SizeProps = {
13
9
  height: string | number
14
10
  }
15
11
  }
16
-
17
- export type SetError = (error: unknown) => void