@liiift-studio/deploy-vercel-from-sanity 1.0.9 → 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.
- package/README.md +15 -3
- package/dist/index.d.mts +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +587 -398
- package/dist/index.mjs +584 -420
- package/package.json +8 -5
- package/src/components/DeployHistory.tsx +3 -2
- package/src/components/DeployItem.tsx +61 -79
- package/src/components/DeployTargetForm.tsx +6 -5
- package/src/components/DeployTool.tsx +6 -2
- package/src/components/TokenSetup.tsx +5 -2
- package/src/icons.tsx +71 -0
- package/src/index.ts +1 -1
- package/src/schema/vercelDeploy.ts +1 -1
- package/src/ui.tsx +337 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports v3
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports Studio v3 through v6.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
7
7
|
"keywords": [
|
|
@@ -39,15 +39,18 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
+
"@sanity/icons": ">=3",
|
|
43
|
+
"@sanity/ui": ">=2",
|
|
42
44
|
"react": ">=18",
|
|
43
45
|
"sanity": ">=3"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
|
-
"@sanity/icons": "^
|
|
47
|
-
"@sanity/ui": "^
|
|
48
|
+
"@sanity/icons": "^5",
|
|
49
|
+
"@sanity/ui": "^4",
|
|
48
50
|
"@types/react": "^19",
|
|
51
|
+
"@types/react-dom": "^19.2.4",
|
|
49
52
|
"react": "^19",
|
|
50
|
-
"sanity": "^
|
|
53
|
+
"sanity": "^6",
|
|
51
54
|
"tsup": "^8",
|
|
52
55
|
"typescript": "^5"
|
|
53
56
|
},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Deployment history modal — shows last 10 deployments for a target
|
|
2
2
|
import { useEffect, useState, useCallback } from 'react'
|
|
3
3
|
import {
|
|
4
|
-
Dialog, Card, Box,
|
|
4
|
+
Dialog, Card, Box, Flex, Text, Badge, Spinner, Button,
|
|
5
5
|
} from '@sanity/ui'
|
|
6
|
-
import {
|
|
6
|
+
import { Stack } from '../ui'
|
|
7
|
+
import { LaunchIcon, CloseIcon } from '../icons'
|
|
7
8
|
import { listDeployments } from '../lib/api'
|
|
8
9
|
import { parseHookUrl, stateLabel, timeAgo, shortSha, safeHref } from '../lib/helpers'
|
|
9
10
|
import type { DeployTarget, VercelDeployment } from '../types'
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
3
3
|
import { flushSync } from 'react-dom'
|
|
4
4
|
import {
|
|
5
|
-
Card, Box,
|
|
6
|
-
MenuButton, Menu, MenuItem, Code, useToast,
|
|
5
|
+
Card, Box, Flex, Text, Button, Badge, Spinner,
|
|
7
6
|
} from '@sanity/ui'
|
|
7
|
+
import { Stack, useToast, Tooltip, ActionMenu, Code } from '../ui'
|
|
8
8
|
import {
|
|
9
9
|
ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
|
|
10
10
|
CopyIcon, CheckmarkIcon, WarningOutlineIcon, ChevronDownIcon, ChevronUpIcon, EditIcon, SchemaIcon
|
|
11
|
-
} from '
|
|
11
|
+
} from '../icons'
|
|
12
12
|
import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
|
|
13
13
|
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref, githubCommitHref } from '../lib/helpers'
|
|
14
14
|
import { StatusBadge } from './StatusBadge'
|
|
@@ -17,6 +17,8 @@ import type { DeployTarget, VercelDeployment } from '../types'
|
|
|
17
17
|
|
|
18
18
|
const POLL_INTERVAL_MS = 5_000
|
|
19
19
|
const LABEL_WIDTH = 64
|
|
20
|
+
/** Max time (ms) to hold the optimistic pending state before giving up and deferring to real API status */
|
|
21
|
+
const PENDING_TIMEOUT_MS = 60_000
|
|
20
22
|
|
|
21
23
|
interface DeployItemProps {
|
|
22
24
|
target: DeployTarget
|
|
@@ -31,7 +33,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
31
33
|
|
|
32
34
|
const [deployments, setDeployments] = useState<VercelDeployment[]>([])
|
|
33
35
|
const [loadingInitial, setLoadingInitial] = useState(true)
|
|
34
|
-
const [
|
|
36
|
+
const [pendingSince, setPendingSince] = useState<number | null>(null)
|
|
35
37
|
const [canceling, setCanceling] = useState(false)
|
|
36
38
|
const [deployError, setDeployError] = useState<string | null>(null)
|
|
37
39
|
const [showHistory, setShowHistory] = useState(false)
|
|
@@ -43,8 +45,13 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
43
45
|
const [loadingLogs, setLoadingLogs] = useState(false)
|
|
44
46
|
const [logError, setLogError] = useState<string | null>(null)
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
+
/** uid of the deployment that was latest when Deploy was clicked — lets us tell the optimistic state apart from a genuinely new deployment */
|
|
49
|
+
const triggeredFromUidRef = useRef<string | undefined>(undefined)
|
|
50
|
+
|
|
51
|
+
const latest = deployments[0]
|
|
52
|
+
/** True between the click and the API returning a new deployment — drives the optimistic "Queued" state */
|
|
53
|
+
const isPending = pendingSince !== null
|
|
54
|
+
const isActive = isPending || isActiveState(latest?.state)
|
|
48
55
|
|
|
49
56
|
// ── Fetch deployments ──────────────────────────────────────────────────────
|
|
50
57
|
const fetchDeployments = useCallback(async () => {
|
|
@@ -67,9 +74,20 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
67
74
|
return () => clearInterval(id)
|
|
68
75
|
}, [isActive, fetchDeployments])
|
|
69
76
|
|
|
77
|
+
// Hand off from the optimistic state only once the API returns a deployment
|
|
78
|
+
// that is not the one which was already latest when Deploy was clicked.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isPending) return
|
|
81
|
+
if (latest?.uid && latest.uid !== triggeredFromUidRef.current) setPendingSince(null)
|
|
82
|
+
}, [isPending, latest?.uid])
|
|
83
|
+
|
|
84
|
+
// Safety net — never strand the card in the optimistic state if the new
|
|
85
|
+
// deployment never appears (hook accepted but nothing was queued).
|
|
70
86
|
useEffect(() => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
87
|
+
if (!isPending) return
|
|
88
|
+
const id = setTimeout(() => setPendingSince(null), PENDING_TIMEOUT_MS)
|
|
89
|
+
return () => clearTimeout(id)
|
|
90
|
+
}, [isPending])
|
|
73
91
|
|
|
74
92
|
//── Deploy-complete toast ─────────────────────────────────────────────────
|
|
75
93
|
const prevStateRef = useRef<string | undefined>(undefined)
|
|
@@ -98,7 +116,8 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
98
116
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
99
117
|
useEffect(() => {
|
|
100
118
|
if (isActive) {
|
|
101
|
-
|
|
119
|
+
// While optimistic, count from the click; once real, count from the deployment's own timestamp
|
|
120
|
+
const start = pendingSince ?? latest?.created ?? Date.now()
|
|
102
121
|
setElapsed(Math.floor((Date.now() - start) / 1000))
|
|
103
122
|
timerRef.current = setInterval(() => {
|
|
104
123
|
setElapsed(Math.floor((Date.now() - start) / 1000))
|
|
@@ -108,24 +127,27 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
108
127
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
109
128
|
}
|
|
110
129
|
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
|
111
|
-
}, [isActive, latest?.created])
|
|
130
|
+
}, [isActive, pendingSince, latest?.created])
|
|
112
131
|
|
|
113
132
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
114
133
|
const deploy = useCallback(() => {
|
|
134
|
+
// Paint the optimistic "Queued" state in the same frame as the click,
|
|
135
|
+
// before the hook request is even sent
|
|
115
136
|
flushSync(() => {
|
|
116
137
|
setDeployError(null)
|
|
117
|
-
|
|
138
|
+
triggeredFromUidRef.current = latest?.uid
|
|
139
|
+
setPendingSince(Date.now())
|
|
118
140
|
})
|
|
119
141
|
void (async () => {
|
|
120
142
|
try {
|
|
121
143
|
await triggerDeploy(target.url)
|
|
122
144
|
setTimeout(fetchDeployments, 2000)
|
|
123
145
|
} catch (err) {
|
|
124
|
-
|
|
146
|
+
setPendingSince(null)
|
|
125
147
|
setDeployError(err instanceof Error ? err.message : 'Deploy failed')
|
|
126
148
|
}
|
|
127
149
|
})()
|
|
128
|
-
}, [target.url, fetchDeployments])
|
|
150
|
+
}, [target.url, fetchDeployments, latest?.uid])
|
|
129
151
|
|
|
130
152
|
const cancel = useCallback(async () => {
|
|
131
153
|
if (!latest?.uid) return
|
|
@@ -218,8 +240,8 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
218
240
|
)}
|
|
219
241
|
{token && !loadingInitial && (
|
|
220
242
|
<>
|
|
221
|
-
{
|
|
222
|
-
<
|
|
243
|
+
{isPending ? (
|
|
244
|
+
<StatusBadge state="QUEUED" />
|
|
223
245
|
) : (
|
|
224
246
|
<StatusBadge state={latest?.state} />
|
|
225
247
|
)}
|
|
@@ -234,63 +256,36 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
234
256
|
style={{ cursor: 'pointer' }}
|
|
235
257
|
/>
|
|
236
258
|
)}
|
|
237
|
-
{
|
|
259
|
+
{isPending ? (
|
|
260
|
+
/* Optimistic — dimmed spinner only; no elapsed time until a real build exists */
|
|
261
|
+
<Spinner muted style={{ marginLeft: 4, opacity: 0.5 }} />
|
|
262
|
+
) : isActive ? (
|
|
238
263
|
<Flex align="center" gap={1}>
|
|
239
264
|
<Spinner muted style={{ marginLeft: 4 }} />
|
|
240
265
|
<Text size={1} muted>{formatDuration(elapsed)}</Text>
|
|
241
266
|
</Flex>
|
|
242
|
-
) :
|
|
267
|
+
) : deployedAt ? (
|
|
243
268
|
<Text size={1} muted>{deployedAt}</Text>
|
|
244
269
|
) : null}
|
|
245
270
|
</>
|
|
246
271
|
)}
|
|
247
272
|
</Flex>
|
|
248
|
-
<
|
|
249
|
-
button={<Button mode="ghost" icon={EllipsisVerticalIcon} padding={2} />}
|
|
273
|
+
<ActionMenu
|
|
250
274
|
id={`menu-${target._id}`}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
text="Build logs"
|
|
266
|
-
icon={LaunchIcon}
|
|
267
|
-
as="a"
|
|
268
|
-
href={safeHref(latest?.inspectorUrl)}
|
|
269
|
-
target="_blank"
|
|
270
|
-
rel="noreferrer"
|
|
271
|
-
/>
|
|
272
|
-
)}
|
|
273
|
-
{vercelProjectUrl && (
|
|
274
|
-
<MenuItem
|
|
275
|
-
text="Open in Vercel"
|
|
276
|
-
icon={LaunchIcon}
|
|
277
|
-
as="a"
|
|
278
|
-
href={vercelProjectUrl}
|
|
279
|
-
target="_blank"
|
|
280
|
-
rel="noreferrer"
|
|
281
|
-
/>
|
|
282
|
-
)}
|
|
283
|
-
{!target.disableDeleteAction && (
|
|
284
|
-
<MenuItem
|
|
285
|
-
text="Delete"
|
|
286
|
-
icon={TrashIcon}
|
|
287
|
-
tone="critical"
|
|
288
|
-
onClick={() => onDelete(target)}
|
|
289
|
-
/>
|
|
290
|
-
)}
|
|
291
|
-
</Menu>
|
|
292
|
-
}
|
|
293
|
-
popover={{ placement: 'bottom-end' }}
|
|
275
|
+
buttonIcon={EllipsisVerticalIcon}
|
|
276
|
+
items={[
|
|
277
|
+
{ text: 'Edit target', icon: EditIcon, onClick: () => onEdit(target) },
|
|
278
|
+
{ text: 'History', icon: ClockIcon, onClick: () => setShowHistory(true) },
|
|
279
|
+
...(safeHref(latest?.inspectorUrl)
|
|
280
|
+
? [{ text: 'Build logs', icon: LaunchIcon, href: safeHref(latest?.inspectorUrl)! }]
|
|
281
|
+
: []),
|
|
282
|
+
...(vercelProjectUrl
|
|
283
|
+
? [{ text: 'Open in Vercel', icon: LaunchIcon, href: vercelProjectUrl }]
|
|
284
|
+
: []),
|
|
285
|
+
...(!target.disableDeleteAction
|
|
286
|
+
? [{ text: 'Delete', icon: TrashIcon, tone: 'critical' as const, onClick: () => onDelete(target) }]
|
|
287
|
+
: []),
|
|
288
|
+
]}
|
|
294
289
|
/>
|
|
295
290
|
</Flex>
|
|
296
291
|
|
|
@@ -329,14 +324,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
329
324
|
|
|
330
325
|
{/* Commit SHA — links to GitHub if repo info available, tooltip shows full message */}
|
|
331
326
|
{sha && (
|
|
332
|
-
<Tooltip
|
|
333
|
-
content={
|
|
334
|
-
<Box padding={2}>
|
|
335
|
-
<Text size={1}>{commitMsg ?? sha}</Text>
|
|
336
|
-
</Box>
|
|
337
|
-
}
|
|
338
|
-
portal
|
|
339
|
-
>
|
|
327
|
+
<Tooltip text={commitMsg ?? sha}>
|
|
340
328
|
{commitHref ? (
|
|
341
329
|
<a
|
|
342
330
|
href={commitHref}
|
|
@@ -367,14 +355,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
367
355
|
{/* Visit link + copy URL */}
|
|
368
356
|
{latest?.url && latest.state === 'READY' && (
|
|
369
357
|
<>
|
|
370
|
-
<Tooltip
|
|
371
|
-
content={
|
|
372
|
-
<Box padding={2}>
|
|
373
|
-
<Text size={1}>{copied ? 'Copied!' : 'Copy URL'}</Text>
|
|
374
|
-
</Box>
|
|
375
|
-
}
|
|
376
|
-
portal
|
|
377
|
-
>
|
|
358
|
+
<Tooltip text={copied ? 'Copied!' : 'Copy URL'}>
|
|
378
359
|
<Button
|
|
379
360
|
mode="ghost"
|
|
380
361
|
icon={copied ? CheckmarkIcon : CopyIcon}
|
|
@@ -438,7 +419,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
438
419
|
<Stack space={2}>
|
|
439
420
|
<Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize: 13, lineHeight: 1.6 }}>
|
|
440
421
|
{errorLines.map((line, i) => (
|
|
441
|
-
<Code key={i}
|
|
422
|
+
<Code key={i} style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
|
442
423
|
{line}
|
|
443
424
|
</Code>
|
|
444
425
|
))}
|
|
@@ -562,6 +543,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
562
543
|
<Button
|
|
563
544
|
text="Deploy"
|
|
564
545
|
tone="primary"
|
|
546
|
+
loading={isPending}
|
|
565
547
|
disabled={isActive}
|
|
566
548
|
onClick={deploy}
|
|
567
549
|
style={{
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { useState, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
4
|
import {
|
|
5
|
-
Dialog, Box,
|
|
5
|
+
Dialog, Box, Flex, Text, TextInput, Button, Switch, Label, Card,
|
|
6
6
|
} from '@sanity/ui'
|
|
7
|
-
import {
|
|
7
|
+
import { Stack } from '../ui'
|
|
8
|
+
import { CheckmarkCircleIcon } from '../icons'
|
|
8
9
|
import type { DeployTarget } from '../types'
|
|
9
10
|
|
|
10
11
|
const VERCEL_HOOK_RE = /^https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\//
|
|
@@ -34,10 +35,10 @@ export function DeployTargetForm({ initial, onSaved, onClose }: DeployTargetForm
|
|
|
34
35
|
if (!canSave) return
|
|
35
36
|
setSaving(true)
|
|
36
37
|
setError(null)
|
|
37
|
-
const fields = {
|
|
38
|
+
const fields: { name: string; url: string; teamId: string | null; disableDeleteAction: boolean } = {
|
|
38
39
|
name: name.trim(),
|
|
39
40
|
url: url.trim(),
|
|
40
|
-
|
|
41
|
+
teamId: teamId.trim() || null,
|
|
41
42
|
disableDeleteAction: disableDelete,
|
|
42
43
|
}
|
|
43
44
|
try {
|
|
@@ -127,7 +128,7 @@ export function DeployTargetForm({ initial, onSaved, onClose }: DeployTargetForm
|
|
|
127
128
|
id="disable-delete"
|
|
128
129
|
/>
|
|
129
130
|
<Stack space={1}>
|
|
130
|
-
<Label size={1} htmlFor="disable-delete">Disable delete action</Label>
|
|
131
|
+
<Label as="label" size={1} htmlFor="disable-delete">Disable delete action</Label>
|
|
131
132
|
<Text size={0} muted>Hides the delete button for this target in the studio.</Text>
|
|
132
133
|
</Stack>
|
|
133
134
|
</Flex>
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
4
|
import {
|
|
5
|
-
Card, Box,
|
|
5
|
+
Card, Box, Flex, Text, Heading, Spinner, Button, Dialog,
|
|
6
6
|
} from '@sanity/ui'
|
|
7
|
-
import {
|
|
7
|
+
import { Stack, useToast, ToastViewport } from '../ui'
|
|
8
|
+
import { TokenIcon, TrashIcon, WarningOutlineIcon, AddIcon } from '../icons'
|
|
8
9
|
import { DeployItem } from './DeployItem'
|
|
9
10
|
import { TokenSetup } from './TokenSetup'
|
|
10
11
|
import { DeployTargetForm } from './DeployTargetForm'
|
|
@@ -287,6 +288,9 @@ export function DeployTool() {
|
|
|
287
288
|
</Box>
|
|
288
289
|
</Dialog>
|
|
289
290
|
)}
|
|
291
|
+
|
|
292
|
+
{/* Fallback toast surface — renders nothing when @sanity/ui exports its own useToast */}
|
|
293
|
+
<ToastViewport />
|
|
290
294
|
</Card>
|
|
291
295
|
)
|
|
292
296
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Vercel API token form — rendered inside a Dialog by DeployTool
|
|
2
2
|
import { useState, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Text, TextInput, Button, Card, Dialog, Flex,
|
|
6
|
+
} from '@sanity/ui'
|
|
7
|
+
import { Stack } from '../ui'
|
|
8
|
+
import { CheckmarkCircleIcon } from '../icons'
|
|
6
9
|
|
|
7
10
|
interface TokenSetupProps {
|
|
8
11
|
/** Called after the token is successfully saved */
|
package/src/icons.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Version-agnostic access to @sanity/icons — resolves named exports (icons v3/v4) or <Icon symbol> (icons v5+)
|
|
2
|
+
import { forwardRef } from 'react'
|
|
3
|
+
import type { ComponentType, SVGProps } from 'react'
|
|
4
|
+
import * as sanityIcons from '@sanity/icons'
|
|
5
|
+
|
|
6
|
+
/** Props every Sanity icon accepts — it renders a plain sized SVG. */
|
|
7
|
+
export type IconProps = SVGProps<SVGSVGElement>
|
|
8
|
+
|
|
9
|
+
/** An icon component, whichever shape the installed @sanity/icons exposes it in. */
|
|
10
|
+
export type IconComponent = ComponentType<IconProps>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The installed @sanity/icons namespace, read through an index signature.
|
|
14
|
+
*
|
|
15
|
+
* icons v3 and v4 export one named component per glyph (`RocketIcon`). v5.0.0
|
|
16
|
+
* dropped those from the barrel and replaced them with a single `<Icon symbol>`
|
|
17
|
+
* that lazy-loads from an internal map. Its `index.d.ts` still declares the old
|
|
18
|
+
* named exports, so the mismatch is invisible to TypeScript and only surfaces at
|
|
19
|
+
* runtime — reading the namespace dynamically lets one build serve every major.
|
|
20
|
+
*/
|
|
21
|
+
const INSTALLED = sanityIcons as unknown as Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
/** Sizing of a Sanity icon glyph — 1em square on a 25-unit viewBox, matching @sanity/icons. */
|
|
24
|
+
const GLYPH = { width: '1em', height: '1em', viewBox: '0 0 25 25', fill: 'none' } as const
|
|
25
|
+
|
|
26
|
+
/** Last-resort placeholder when the installed @sanity/icons exposes neither shape. Holds layout, draws nothing. */
|
|
27
|
+
const MissingIcon = forwardRef<SVGSVGElement, IconProps>(function MissingIcon(props, ref) {
|
|
28
|
+
return <svg {...GLYPH} xmlns="http://www.w3.org/2000/svg" {...props} ref={ref} />
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/** The v5+ `<Icon>` component, absent on icons v3 and v4. */
|
|
32
|
+
type SymbolIcon = ComponentType<IconProps & { symbol: string }>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve one glyph against whichever @sanity/icons the host Studio installed.
|
|
36
|
+
* Always reads from the host package, so new and revised artwork is picked up
|
|
37
|
+
* on the consumer's next `@sanity/icons` update without a release here.
|
|
38
|
+
*
|
|
39
|
+
* @param name Named export used by icons v3 and v4, e.g. `RocketIcon`.
|
|
40
|
+
* @param symbol Kebab-case symbol used by the v5+ `<Icon>` component, e.g. `rocket`.
|
|
41
|
+
*/
|
|
42
|
+
function resolveIcon(name: string, symbol: string): IconComponent {
|
|
43
|
+
const named = INSTALLED[name] as IconComponent | undefined
|
|
44
|
+
if (named) return named
|
|
45
|
+
|
|
46
|
+
const Icon = INSTALLED.Icon as SymbolIcon | undefined
|
|
47
|
+
if (!Icon) return MissingIcon
|
|
48
|
+
|
|
49
|
+
const Resolved = forwardRef<SVGSVGElement, IconProps>(function SanityIcon(props, ref) {
|
|
50
|
+
return <Icon symbol={symbol} {...props} ref={ref as never} />
|
|
51
|
+
})
|
|
52
|
+
Resolved.displayName = name
|
|
53
|
+
return Resolved as IconComponent
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const AddIcon = resolveIcon('AddIcon', 'add')
|
|
57
|
+
export const CheckmarkCircleIcon = resolveIcon('CheckmarkCircleIcon', 'checkmark-circle')
|
|
58
|
+
export const CheckmarkIcon = resolveIcon('CheckmarkIcon', 'checkmark')
|
|
59
|
+
export const ChevronDownIcon = resolveIcon('ChevronDownIcon', 'chevron-down')
|
|
60
|
+
export const ChevronUpIcon = resolveIcon('ChevronUpIcon', 'chevron-up')
|
|
61
|
+
export const ClockIcon = resolveIcon('ClockIcon', 'clock')
|
|
62
|
+
export const CloseIcon = resolveIcon('CloseIcon', 'close')
|
|
63
|
+
export const CopyIcon = resolveIcon('CopyIcon', 'copy')
|
|
64
|
+
export const EditIcon = resolveIcon('EditIcon', 'edit')
|
|
65
|
+
export const EllipsisVerticalIcon = resolveIcon('EllipsisVerticalIcon', 'ellipsis-vertical')
|
|
66
|
+
export const LaunchIcon = resolveIcon('LaunchIcon', 'launch')
|
|
67
|
+
export const RocketIcon = resolveIcon('RocketIcon', 'rocket')
|
|
68
|
+
export const SchemaIcon = resolveIcon('SchemaIcon', 'schema')
|
|
69
|
+
export const TokenIcon = resolveIcon('TokenIcon', 'token')
|
|
70
|
+
export const TrashIcon = resolveIcon('TrashIcon', 'trash')
|
|
71
|
+
export const WarningOutlineIcon = resolveIcon('WarningOutlineIcon', 'warning-outline')
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// deploy-vercel-from-sanity — Sanity Studio v5 plugin for Vercel deployments
|
|
2
2
|
import { definePlugin } from 'sanity'
|
|
3
|
-
import { RocketIcon } from '
|
|
3
|
+
import { RocketIcon } from './icons'
|
|
4
4
|
import { DeployTool } from './components/DeployTool'
|
|
5
5
|
import { vercelDeploySchema } from './schema/vercelDeploy'
|
|
6
6
|
import type { VercelDeployPluginConfig } from './types'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Sanity schema for vercel_deploy documents — stores deploy hook targets
|
|
2
2
|
import { defineField, defineType } from 'sanity'
|
|
3
|
-
import { RocketIcon } from '
|
|
3
|
+
import { RocketIcon } from '../icons'
|
|
4
4
|
|
|
5
5
|
export const vercelDeploySchema = defineType({
|
|
6
6
|
name: 'vercel_deploy',
|