@liiift-studio/deploy-vercel-from-sanity 0.1.7 → 0.2.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.
@@ -5,8 +5,8 @@ import {
5
5
  MenuButton, Menu, MenuItem, Code,
6
6
  } from '@sanity/ui'
7
7
  import {
8
- RocketIcon, ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
9
- CopyIcon, CheckmarkIcon, WarningOutlineIcon,
8
+ ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
9
+ CopyIcon, CheckmarkIcon, WarningOutlineIcon, ChevronDownIcon, ChevronUpIcon, EditIcon, SchemaIcon
10
10
  } from '@sanity/icons'
11
11
  import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
12
12
  import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref } from '../lib/helpers'
@@ -15,34 +15,33 @@ import { DeployHistory } from './DeployHistory'
15
15
  import type { DeployTarget, VercelDeployment } from '../types'
16
16
 
17
17
  const POLL_INTERVAL_MS = 5_000
18
+ const LABEL_WIDTH = 64
18
19
 
19
20
  interface DeployItemProps {
20
21
  target: DeployTarget
21
22
  token: string
22
23
  onDelete: (target: DeployTarget) => void
24
+ onEdit: (target: DeployTarget) => void
23
25
  }
24
26
 
25
- export function DeployItem({ target, token, onDelete }: DeployItemProps) {
27
+ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps) {
26
28
  const { projectId, hookId } = parseHookUrl(target.url)
27
29
 
28
- const [deployments, setDeployments] = useState<VercelDeployment[]>([])
30
+ const [deployments, setDeployments] = useState<VercelDeployment[]>([])
29
31
  const [loadingInitial, setLoadingInitial] = useState(true)
30
- const [triggering, setTriggering] = useState(false)
31
- const [canceling, setCanceling] = useState(false)
32
- const [deployError, setDeployError] = useState<string | null>(null)
33
- const [showHistory, setShowHistory] = useState(false)
34
- const [elapsed, setElapsed] = useState(0)
35
-
36
- // Copy URL state
37
- const [copied, setCopied] = useState(false)
38
-
39
- // Error log expansion state
40
- const [showErrorLogs, setShowErrorLogs] = useState(false)
41
- const [errorLines, setErrorLines] = useState<string[]>([])
42
- const [loadingLogs, setLoadingLogs] = useState(false)
43
- const [logError, setLogError] = useState<string | null>(null)
44
-
45
- const latest = deployments[0]
32
+ const [triggering, setTriggering] = useState(false)
33
+ const [canceling, setCanceling] = useState(false)
34
+ const [deployError, setDeployError] = useState<string | null>(null)
35
+ const [showHistory, setShowHistory] = useState(false)
36
+ const [elapsed, setElapsed] = useState(0)
37
+ const [copied, setCopied] = useState(false)
38
+ const [showDetails, setShowDetails] = useState(false)
39
+ const [showErrorLogs, setShowErrorLogs] = useState(false)
40
+ const [errorLines, setErrorLines] = useState<string[]>([])
41
+ const [loadingLogs, setLoadingLogs] = useState(false)
42
+ const [logError, setLogError] = useState<string | null>(null)
43
+
44
+ const latest = deployments[0]
46
45
  const isActive = triggering || isActiveState(latest?.state)
47
46
 
48
47
  // ── Fetch deployments ──────────────────────────────────────────────────────
@@ -70,7 +69,6 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
70
69
  if (triggering && latest && latest.state !== undefined) setTriggering(false)
71
70
  }, [triggering, latest])
72
71
 
73
- // Reset error logs when the deployment changes
74
72
  useEffect(() => {
75
73
  setShowErrorLogs(false)
76
74
  setErrorLines([])
@@ -139,13 +137,12 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
139
137
  token,
140
138
  teamId: target.teamId,
141
139
  })
142
- // Keep stderr lines and stdout lines that look like errors, in forward order
143
140
  const lines = events
144
141
  .filter(e => e.type === 'stderr' || e.type === 'stdout')
145
142
  .map(e => e.text ?? '')
146
143
  .filter(Boolean)
147
- .reverse() // API returns backward — flip to chronological
148
- .slice(-30) // last 30 lines
144
+ .reverse()
145
+ .slice(-30)
149
146
  setErrorLines(lines.length > 0 ? lines : ['No log output captured.'])
150
147
  } catch (err) {
151
148
  setLogError(err instanceof Error ? err.message : 'Failed to load build logs')
@@ -155,295 +152,363 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
155
152
  }, [latest?.uid, token, target.teamId])
156
153
 
157
154
  const toggleErrorLogs = useCallback(() => {
158
- if (!showErrorLogs && errorLines.length === 0 && !logError) {
159
- fetchErrorLogs()
160
- }
155
+ if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs()
161
156
  setShowErrorLogs(v => !v)
162
157
  }, [showErrorLogs, errorLines.length, logError, fetchErrorLogs])
163
158
 
164
159
  // ── Derived display values ────────────────────────────────────────────────
165
- const branch = latest?.meta?.githubCommitRef
166
- const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
167
- const sha = shortSha(latest?.meta?.githubCommitSha)
168
- const creator = latest?.creator?.username
169
- const deployedAt = latest?.created ? timeAgo(latest.created) : null
160
+ const branch = latest?.meta?.githubCommitRef
161
+ const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
162
+ const sha = shortSha(latest?.meta?.githubCommitSha)
163
+ const fullSha = latest?.meta?.githubCommitSha
164
+ const creator = latest?.creator?.username
165
+ const deployedAt = latest?.created ? timeAgo(latest.created) : null
170
166
  const vercelProjectUrl = projectHref(latest?.inspectorUrl)
171
- const isError = latest?.state === 'ERROR'
167
+ const isError = latest?.state === 'ERROR'
172
168
 
173
169
  return (
174
170
  <>
175
- <Card padding={5} radius={2} shadow={1} tone="default">
176
- <Flex gap={4} align="stretch">
171
+ <Card radius={2} shadow={1} tone="default">
172
+ <Flex align="stretch" className="dvfs-card-flex">
177
173
 
178
174
  {/* ── Left: info column ──────────────────────────────────── */}
179
- <Stack space={3} flex={1} style={{ minWidth: 0 }}>
180
-
181
- {/* Name + menu */}
182
- <Flex align="center" justify="space-between" gap={2}>
183
- <Text size={2} weight="semibold">{target.name}</Text>
184
- <MenuButton
185
- button={<Button mode="ghost" icon={EllipsisVerticalIcon} padding={2} />}
186
- id={`menu-${target._id}`}
187
- menu={
188
- <Menu>
189
- <MenuItem
190
- text="History"
191
- icon={ClockIcon}
192
- onClick={() => setShowHistory(true)}
193
- />
194
- {safeHref(latest?.inspectorUrl) && (
195
- <MenuItem
196
- text="Build logs"
197
- icon={LaunchIcon}
198
- as="a"
199
- href={safeHref(latest?.inspectorUrl)}
200
- target="_blank"
201
- rel="noreferrer"
202
- />
203
- )}
204
- {vercelProjectUrl && (
205
- <MenuItem
206
- text="Open in Vercel"
207
- icon={LaunchIcon}
208
- as="a"
209
- href={vercelProjectUrl}
210
- target="_blank"
211
- rel="noreferrer"
212
- />
213
- )}
214
- {!target.disableDeleteAction && (
215
- <MenuItem
216
- text="Delete"
217
- icon={TrashIcon}
218
- tone="critical"
219
- onClick={() => onDelete(target)}
220
- />
221
- )}
222
- </Menu>
223
- }
224
- popover={{ placement: 'bottom-end' }}
225
- />
226
- </Flex>
227
-
228
- {/* Subtitle: project + hook IDs */}
229
- <Flex gap={2} wrap="wrap">
230
- <Text size={0} muted>
231
- {projectId ? `${projectId.slice(0, 18)}…` : '—'}
232
- </Text>
233
- <Text size={0} muted>·</Text>
234
- <Text size={0} muted>Hook: {hookId || '—'}</Text>
235
- {target.teamId && (
236
- <>
237
- <Text size={0} muted>·</Text>
238
- <Text size={0} muted>Team: {target.teamId}</Text>
239
- </>
240
- )}
241
- </Flex>
242
-
243
- {/* Status + metadata */}
244
- {loadingInitial ? (
245
- <Flex align="center" gap={2}>
246
- <Spinner muted />
247
- <Text size={1} muted>Loading…</Text>
248
- </Flex>
249
- ) : (
250
- <Stack space={2}>
251
- <Flex align="center" gap={2} wrap="wrap">
252
- {/* Status */}
253
- {triggering ? (
254
- <Flex align="center" gap={2}>
255
- <Spinner muted />
256
- <Badge tone="caution" mode="outline">Triggering…</Badge>
257
- </Flex>
258
- ) : (
259
- <StatusBadge state={latest?.state} showSpinner />
260
- )}
175
+ <Flex direction="column" flex={1} style={{ minWidth: 0 }}>
261
176
 
262
- {/* Build timer (active) or time since deploy (idle) */}
263
- {isActive && elapsed > 0 ? (
264
- <Flex align="center" gap={1}>
265
- <ClockIcon />
266
- <Text size={1} muted>{formatDuration(elapsed)}</Text>
267
- </Flex>
268
- ) : (!isActive && deployedAt) ? (
269
- <Text size={1} muted>{deployedAt}</Text>
270
- ) : null}
177
+ <Stack space={3} padding={3} style={{ flex: 1 }}>
271
178
 
272
- {/* Branch */}
179
+ {/* ── Title row: name + branch + status + menu ──────── */}
180
+ <Flex align="center" justify="space-between" gap={2}>
181
+ <Flex align="center" gap={2} style={{ minWidth: 0, flexWrap: 'wrap' }}>
182
+ <Text size={2} weight="semibold" style={{ flexShrink: 0 }}>{target.name}</Text>
273
183
  {branch && (
184
+ <Badge tone="default" padding={2}>
185
+ <Flex align="center" gap={1}>
186
+ {branch}
187
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{ marginLeft: "-0.1em", opacity: 0.5 }}>
188
+ <path d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" />
189
+ </svg>
190
+ </Flex>
191
+ </Badge>
192
+ )}
193
+ {token && !loadingInitial && (
274
194
  <>
275
- <Text size={1} muted>·</Text>
276
- <Badge tone="default" mode="outline">{branch}</Badge>
195
+ {triggering ? (
196
+ <Flex align="center" gap={1}>
197
+ <Spinner muted />
198
+ <Badge tone="caution" padding={2}>Triggering…</Badge>
199
+ </Flex>
200
+ ) : (
201
+ <StatusBadge state={latest?.state} showSpinner />
202
+ )}
203
+ {isActive && elapsed > 0 ? (
204
+ <Flex align="center" gap={1}>
205
+ <ClockIcon />
206
+ <Text size={1} muted>{formatDuration(elapsed)}</Text>
207
+ </Flex>
208
+ ) : (!isActive && deployedAt) ? (
209
+ <Text size={1} muted>{deployedAt}</Text>
210
+ ) : null}
277
211
  </>
278
212
  )}
213
+ </Flex>
214
+ <MenuButton
215
+ button={<Button mode="ghost" icon={EllipsisVerticalIcon} padding={2} />}
216
+ id={`menu-${target._id}`}
217
+ menu={
218
+ <Menu>
219
+ <MenuItem
220
+ text="Edit target"
221
+ icon={EditIcon}
222
+ onClick={() => onEdit(target)}
223
+ />
224
+ <MenuItem
225
+ text="History"
226
+ icon={ClockIcon}
227
+ onClick={() => setShowHistory(true)}
228
+ />
229
+ {safeHref(latest?.inspectorUrl) && (
230
+ <MenuItem
231
+ text="Build logs"
232
+ icon={LaunchIcon}
233
+ as="a"
234
+ href={safeHref(latest?.inspectorUrl)}
235
+ target="_blank"
236
+ rel="noreferrer"
237
+ />
238
+ )}
239
+ {vercelProjectUrl && (
240
+ <MenuItem
241
+ text="Open in Vercel"
242
+ icon={LaunchIcon}
243
+ as="a"
244
+ href={vercelProjectUrl}
245
+ target="_blank"
246
+ rel="noreferrer"
247
+ />
248
+ )}
249
+ {!target.disableDeleteAction && (
250
+ <MenuItem
251
+ text="Delete"
252
+ icon={TrashIcon}
253
+ tone="critical"
254
+ onClick={() => onDelete(target)}
255
+ />
256
+ )}
257
+ </Menu>
258
+ }
259
+ popover={{ placement: 'bottom-end' }}
260
+ />
261
+ </Flex>
279
262
 
280
- {/* Commit SHA tooltip shows full message */}
281
- {sha && (
282
- <Tooltip
283
- content={
284
- <Box padding={2}>
285
- <Text size={1}>{commitMsg ?? sha}</Text>
286
- </Box>
287
- }
288
- portal
289
- >
290
- <Text
291
- size={1}
292
- muted
293
- style={{ cursor: 'default', fontFamily: 'monospace' }}
294
- >
295
- {sha}
296
- </Text>
297
- </Tooltip>
298
- )}
263
+ {/* ── Divider below title ────────────────────────────── */}
264
+ <hr style={{ border: 'none', borderTop: '1px solid currentColor', opacity: 0.1, margin: 0 }} />
299
265
 
300
- {/* Creator */}
301
- {creator && <Text size={1} muted>by {creator}</Text>}
266
+ {/* ── Status + metadata ──────────────────────────────── */}
267
+ {!token ? (
268
+ <Text size={1} muted>Connect a Vercel API token to see deployment status.</Text>
269
+ ) : loadingInitial ? (
270
+ <Flex align="center" gap={2}>
271
+ <Spinner muted />
272
+ <Text size={1} muted>Loading…</Text>
273
+ </Flex>
274
+ ) : (
275
+ <Stack space={2}>
276
+
277
+ {/* Metadata row */}
278
+ <Flex align="center" gap={2} wrap="wrap">
279
+
280
+ {/* Visit link + copy URL */}
281
+ {latest?.url && latest.state === 'READY' && (
282
+ <>
283
+ <a
284
+ href={`https://${latest.url}`}
285
+ target="_blank"
286
+ rel="noreferrer"
287
+ style={{ color: 'inherit' }}
288
+ >
289
+ <Flex align="center" gap={1}>
290
+ <Text size={1}>{latest.url}</Text>
291
+ </Flex>
292
+ </a>
293
+ </>
294
+ )}
302
295
 
303
- {/* Preview link + copy URL */}
304
- {latest?.url && latest.state === 'READY' && (
305
- <>
306
- <a
307
- href={`https://${latest.url}`}
308
- target="_blank"
309
- rel="noreferrer"
310
- style={{ color: 'inherit' }}
311
- >
312
- <Flex align="center" gap={1}>
313
- <LaunchIcon />
314
- <Text size={1}>Preview</Text>
315
- </Flex>
316
- </a>
296
+ {/* Commit SHA tooltip shows full message */}
297
+ {sha && (
317
298
  <Tooltip
318
299
  content={
319
300
  <Box padding={2}>
320
- <Text size={1}>{copied ? 'Copied!' : 'Copy URL'}</Text>
301
+ <Text size={1}>{commitMsg ?? sha}</Text>
321
302
  </Box>
322
303
  }
323
304
  portal
324
305
  >
306
+ <Text
307
+ size={1}
308
+ muted
309
+ style={{ cursor: 'default', fontFamily: 'monospace' }}
310
+ >
311
+ {sha}
312
+ </Text>
313
+ </Tooltip>
314
+ )}
315
+
316
+ {/* Creator */}
317
+ {creator && <Text size={1} muted>by {creator}</Text>}
318
+
319
+ {/* Visit link + copy URL */}
320
+ {latest?.url && latest.state === 'READY' && (
321
+ <>
322
+ <Tooltip
323
+ content={
324
+ <Box padding={2}>
325
+ <Text size={1}>{copied ? 'Copied!' : 'Copy URL'}</Text>
326
+ </Box>
327
+ }
328
+ portal
329
+ >
330
+ <Button
331
+ mode="ghost"
332
+ icon={copied ? CheckmarkIcon : CopyIcon}
333
+ padding={1}
334
+ tone={copied ? 'positive' : 'default'}
335
+ onClick={copyUrl}
336
+ style={{ cursor: 'pointer' }}
337
+ />
338
+ </Tooltip>
339
+ </>
340
+ )}
341
+ </Flex>
342
+
343
+ <Flex align="center" gap={2} wrap="wrap">
344
+ {/* Commit message */}
345
+ {commitMsg && (
346
+ <Text
347
+ size={0}
348
+ muted
349
+ style={{ fontStyle: 'italic'}}
350
+ >
351
+ {commitMsg}
352
+ </Text>
353
+ )}
354
+
355
+ {/* Error expansion */}
356
+ {isError && (
357
+ <Stack space={2}>
325
358
  <Button
359
+ text={showErrorLogs ? 'Hide error details' : 'Show error details'}
326
360
  mode="ghost"
327
- icon={copied ? CheckmarkIcon : CopyIcon}
328
- padding={1}
329
- tone={copied ? 'positive' : 'default'}
330
- onClick={copyUrl}
361
+ tone="critical"
362
+ icon={WarningOutlineIcon}
363
+ fontSize={1}
364
+ padding={2}
365
+ onClick={toggleErrorLogs}
366
+ style={{ alignSelf: 'flex-start', cursor: 'pointer' }}
331
367
  />
332
- </Tooltip>
333
- </>
334
- )}
335
- </Flex>
336
-
337
- {/* Commit message */}
338
- {commitMsg && (
339
- <Text
340
- size={0}
341
- muted
342
- style={{
343
- overflow: 'hidden',
344
- textOverflow: 'ellipsis',
345
- whiteSpace: 'nowrap',
346
- }}
347
- >
348
- {commitMsg}
349
- </Text>
350
- )}
351
-
352
- {/* Error expansion */}
353
- {isError && (
354
- <Stack space={2}>
355
- <Button
356
- text={showErrorLogs ? 'Hide error details' : 'Show error details'}
357
- mode="ghost"
358
- tone="critical"
359
- icon={WarningOutlineIcon}
360
- fontSize={1}
361
- padding={2}
362
- onClick={toggleErrorLogs}
363
- style={{ alignSelf: 'flex-start' }}
364
- />
365
-
366
- {showErrorLogs && (
367
- <Card tone="critical" radius={2} padding={3}>
368
- {loadingLogs && (
369
- <Flex align="center" gap={2}>
370
- <Spinner muted />
371
- <Text size={1} muted>Loading logs…</Text>
372
- </Flex>
373
- )}
374
- {logError && (
375
- <Stack space={2}>
376
- <Text size={1}>{logError}</Text>
377
- {safeHref(latest?.inspectorUrl) && (
378
- <a
379
- href={safeHref(latest?.inspectorUrl)}
380
- target="_blank"
381
- rel="noreferrer"
382
- style={{ color: 'inherit' }}
383
- >
384
- <Flex align="center" gap={1}>
385
- <LaunchIcon />
386
- <Text size={1}>View full logs in Vercel</Text>
387
- </Flex>
388
- </a>
368
+ {showErrorLogs && (
369
+ <Card tone="critical" radius={2} padding={3}>
370
+ {loadingLogs && (
371
+ <Flex align="center" gap={2}>
372
+ <Spinner muted />
373
+ <Text size={1} muted>Loading logs…</Text>
374
+ </Flex>
389
375
  )}
390
- </Stack>
391
- )}
392
- {!loadingLogs && !logError && errorLines.length > 0 && (
393
- <Stack space={2}>
394
- <Box
395
- style={{
396
- maxHeight: 240,
397
- overflowY: 'auto',
398
- fontFamily: 'monospace',
399
- fontSize: 11,
400
- lineHeight: 1.6,
401
- }}
402
- >
403
- {errorLines.map((line, i) => (
404
- <Code
405
- key={i}
406
- size={1}
407
- style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
408
- >
409
- {line}
410
- </Code>
411
- ))}
412
- </Box>
413
- {safeHref(latest?.inspectorUrl) && (
414
- <a
415
- href={safeHref(latest?.inspectorUrl)}
416
- target="_blank"
417
- rel="noreferrer"
418
- style={{ color: 'inherit' }}
419
- >
420
- <Flex align="center" gap={1}>
421
- <LaunchIcon />
422
- <Text size={1}>View full logs in Vercel</Text>
423
- </Flex>
424
- </a>
376
+ {logError && (
377
+ <Stack space={2}>
378
+ <Text size={1}>{logError}</Text>
379
+ {safeHref(latest?.inspectorUrl) && (
380
+ <a href={safeHref(latest?.inspectorUrl)} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
381
+ <Flex align="center" gap={1}>
382
+ <LaunchIcon />
383
+ <Text size={1}>View full logs in Vercel</Text>
384
+ </Flex>
385
+ </a>
386
+ )}
387
+ </Stack>
388
+ )}
389
+ {!loadingLogs && !logError && errorLines.length > 0 && (
390
+ <Stack space={2}>
391
+ <Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize: 11, lineHeight: 1.6 }}>
392
+ {errorLines.map((line, i) => (
393
+ <Code key={i} size={1} style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
394
+ {line}
395
+ </Code>
396
+ ))}
397
+ </Box>
398
+ {safeHref(latest?.inspectorUrl) && (
399
+ <a href={safeHref(latest?.inspectorUrl)} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
400
+ <Flex align="center" gap={1}>
401
+ <LaunchIcon />
402
+ <Text size={1}>View full logs in Vercel</Text>
403
+ </Flex>
404
+ </a>
405
+ )}
406
+ </Stack>
425
407
  )}
426
- </Stack>
408
+ </Card>
427
409
  )}
410
+ </Stack>
411
+ )}
412
+
413
+ {/* Trigger error */}
414
+ {deployError && (
415
+ <Card tone="critical" padding={2} radius={2}>
416
+ <Text size={1}>{deployError}</Text>
428
417
  </Card>
429
418
  )}
419
+ </Flex>
420
+ </Stack>
421
+ )}
422
+
423
+ </Stack>
424
+
425
+ {/* ── Details accordion — flush to left/bottom/right ─────── */}
426
+ <Box>
427
+ <Button
428
+ mode="ghost"
429
+ iconRight={showDetails ? ChevronUpIcon : ChevronDownIcon}
430
+ text="Details"
431
+ fontSize={0}
432
+ padding={3}
433
+ onClick={() => setShowDetails(v => !v)}
434
+ style={{ width: '100%', justifyContent: 'flex-start', borderRadius: 0, cursor: 'pointer' }}
435
+ />
436
+ {showDetails && (
437
+ <Card tone="primary" padding={3} style={{ borderRadius: 0 }}>
438
+ <Stack space={2}>
439
+ <Flex gap={2} align="center">
440
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Project</Text>
441
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>{projectId || '—'}</Text>
442
+ </Flex>
443
+ <Flex gap={2} align="center">
444
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Hook</Text>
445
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>{hookId || '—'}</Text>
446
+ </Flex>
447
+ {target.teamId && (
448
+ <Flex gap={2} align="center">
449
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Team</Text>
450
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>{target.teamId}</Text>
451
+ </Flex>
452
+ )}
453
+ {fullSha && (
454
+ <Flex gap={2} align="flex-start">
455
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Commit</Text>
456
+ <Text size={0} muted style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>{fullSha}</Text>
457
+ </Flex>
458
+ )}
459
+ {latest?.meta?.githubCommitAuthorName && (
460
+ <Flex gap={2} align="center">
461
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Author</Text>
462
+ <Text size={0} muted>{latest.meta.githubCommitAuthorName}</Text>
463
+ </Flex>
464
+ )}
465
+ {branch && (
466
+ <Flex gap={2} align="center">
467
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Branch</Text>
468
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>{branch}</Text>
469
+ </Flex>
470
+ )}
471
+ {latest?.uid && (
472
+ <Flex gap={2} align="center">
473
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Deploy ID</Text>
474
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>{latest.uid}</Text>
475
+ </Flex>
476
+ )}
477
+ {latest?.url && (
478
+ <Flex gap={2} align="flex-start">
479
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>URL</Text>
480
+ <Text size={0} muted style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>{latest.url}</Text>
481
+ </Flex>
482
+ )}
483
+ {safeHref(latest?.inspectorUrl) && (
484
+ <Flex gap={2} align="center">
485
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Inspector</Text>
486
+ <a href={safeHref(latest?.inspectorUrl)!} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
487
+ <Flex align="center" gap={1}>
488
+ <Text size={0} muted style={{ fontFamily: 'monospace' }}>Open in Vercel</Text>
489
+ <LaunchIcon style={{ width: 10, height: 10 }} />
490
+ </Flex>
491
+ </a>
492
+ </Flex>
493
+ )}
494
+ {latest?.created && (
495
+ <Flex gap={2} align="center">
496
+ <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Created</Text>
497
+ <Text size={0} muted>{new Date(latest.created).toLocaleString()}</Text>
498
+ </Flex>
499
+ )}
430
500
  </Stack>
431
- )}
432
-
433
- {/* Trigger error */}
434
- {deployError && (
435
- <Card tone="critical" padding={2} radius={2}>
436
- <Text size={1}>{deployError}</Text>
437
- </Card>
438
- )}
439
- </Stack>
440
- )}
441
- </Stack>
501
+ </Card>
502
+ )}
503
+ </Box>
504
+
505
+ </Flex>
442
506
 
443
507
  {/* ── Right: action buttons — stretch full card height ── */}
444
508
  <Flex
445
509
  direction="column"
446
510
  gap={2}
511
+ className="dvfs-deploy-col"
447
512
  style={{ flexShrink: 0, alignSelf: 'stretch' }}
448
513
  >
449
514
  {isActiveState(latest?.state) && (
@@ -454,16 +519,22 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
454
519
  loading={canceling}
455
520
  disabled={canceling}
456
521
  onClick={cancel}
522
+ style={{ cursor: 'pointer' }}
457
523
  />
458
524
  )}
459
525
  <Button
460
526
  text="Deploy"
461
527
  tone="primary"
462
- icon={RocketIcon}
463
528
  loading={triggering}
464
529
  disabled={isActive}
465
530
  onClick={deploy}
466
- style={{ flex: 1 }}
531
+ style={{
532
+ flex: 1,
533
+ borderRadius: 0,
534
+ borderTopRightRadius: 3,
535
+ borderBottomRightRadius: 3,
536
+ cursor: 'pointer',
537
+ }}
467
538
  />
468
539
  </Flex>
469
540