@actuate-media/cms-admin 0.48.0 → 0.49.1

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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/AdminRoot.d.ts +10 -0
  3. package/dist/AdminRoot.d.ts.map +1 -1
  4. package/dist/AdminRoot.js +25 -43
  5. package/dist/AdminRoot.js.map +1 -1
  6. package/dist/LoginRoot.d.ts +54 -0
  7. package/dist/LoginRoot.d.ts.map +1 -0
  8. package/dist/LoginRoot.js +59 -0
  9. package/dist/LoginRoot.js.map +1 -0
  10. package/dist/__tests__/components/modal.render.test.d.ts +2 -0
  11. package/dist/__tests__/components/modal.render.test.d.ts.map +1 -0
  12. package/dist/__tests__/components/modal.render.test.js +37 -0
  13. package/dist/__tests__/components/modal.render.test.js.map +1 -0
  14. package/dist/__tests__/views/login-root.render.test.d.ts +2 -0
  15. package/dist/__tests__/views/login-root.render.test.d.ts.map +1 -0
  16. package/dist/__tests__/views/login-root.render.test.js +79 -0
  17. package/dist/__tests__/views/login-root.render.test.js.map +1 -0
  18. package/dist/actuate-admin.css +1 -1
  19. package/dist/components/MediaPickerModal.d.ts +1 -1
  20. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  21. package/dist/components/MediaPickerModal.js +12 -11
  22. package/dist/components/MediaPickerModal.js.map +1 -1
  23. package/dist/components/SchedulePublishDialog.d.ts +1 -1
  24. package/dist/components/SchedulePublishDialog.d.ts.map +1 -1
  25. package/dist/components/SchedulePublishDialog.js +6 -4
  26. package/dist/components/SchedulePublishDialog.js.map +1 -1
  27. package/dist/components/SharePreviewLinkDialog.d.ts +1 -1
  28. package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -1
  29. package/dist/components/SharePreviewLinkDialog.js +10 -9
  30. package/dist/components/SharePreviewLinkDialog.js.map +1 -1
  31. package/dist/components/VersionHistory.d.ts +1 -1
  32. package/dist/components/VersionHistory.d.ts.map +1 -1
  33. package/dist/components/VersionHistory.js +14 -14
  34. package/dist/components/VersionHistory.js.map +1 -1
  35. package/dist/components/ui/Modal.d.ts +17 -1
  36. package/dist/components/ui/Modal.d.ts.map +1 -1
  37. package/dist/components/ui/Modal.js +16 -15
  38. package/dist/components/ui/Modal.js.map +1 -1
  39. package/dist/fields/BlockBuilderField.d.ts.map +1 -1
  40. package/dist/fields/BlockBuilderField.js +6 -4
  41. package/dist/fields/BlockBuilderField.js.map +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/root-chrome.d.ts +15 -0
  47. package/dist/lib/root-chrome.d.ts.map +1 -0
  48. package/dist/lib/root-chrome.js +41 -0
  49. package/dist/lib/root-chrome.js.map +1 -0
  50. package/dist/views/HiddenLoginNotFound.d.ts +8 -0
  51. package/dist/views/HiddenLoginNotFound.d.ts.map +1 -0
  52. package/dist/views/HiddenLoginNotFound.js +29 -0
  53. package/dist/views/HiddenLoginNotFound.js.map +1 -0
  54. package/dist/views/page-builder/AIGenerateDialog.d.ts +1 -1
  55. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
  56. package/dist/views/page-builder/AIGenerateDialog.js +23 -23
  57. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/AdminRoot.tsx +61 -59
  60. package/src/LoginRoot.tsx +166 -0
  61. package/src/__tests__/components/modal.render.test.tsx +57 -0
  62. package/src/__tests__/views/login-root.render.test.tsx +115 -0
  63. package/src/components/MediaPickerModal.tsx +143 -119
  64. package/src/components/SchedulePublishDialog.tsx +118 -93
  65. package/src/components/SharePreviewLinkDialog.tsx +115 -93
  66. package/src/components/VersionHistory.tsx +98 -82
  67. package/src/components/ui/Modal.tsx +48 -39
  68. package/src/fields/BlockBuilderField.tsx +26 -19
  69. package/src/index.ts +2 -0
  70. package/src/lib/root-chrome.ts +40 -0
  71. package/src/views/HiddenLoginNotFound.tsx +45 -0
  72. package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
@@ -1,9 +1,11 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
3
+ import { useEffect, useId, useState } from 'react'
4
+ import * as Dialog from '@radix-ui/react-dialog'
4
5
  import { Calendar, Clock, Loader2, X } from 'lucide-react'
5
6
  import { toast } from 'sonner'
6
7
  import { cmsApi } from '../lib/api.js'
8
+ import { adminPortalContainer } from '../lib/portal-container.js'
7
9
 
8
10
  export interface SchedulePublishDialogProps {
9
11
  collectionSlug: string
@@ -56,6 +58,8 @@ export function SchedulePublishDialog({
56
58
  const [includeUnpublish, setIncludeUnpublish] = useState(false)
57
59
  const [saving, setSaving] = useState(false)
58
60
  const [cancelling, setCancelling] = useState(false)
61
+ const publishAtId = useId()
62
+ const unpublishAtId = useId()
59
63
 
60
64
  const hasExistingSchedule = Boolean(scheduledAt || scheduledUnpublishAt)
61
65
 
@@ -133,109 +137,130 @@ export function SchedulePublishDialog({
133
137
  onClose()
134
138
  }
135
139
 
136
- if (!open) return null
137
-
138
140
  return (
139
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
140
- <div className="fixed inset-0 bg-black/40" onClick={onClose} />
141
- <div className="relative w-full max-w-md rounded-lg bg-white shadow-xl">
142
- <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
143
- <div className="flex items-center gap-2">
144
- <Calendar className="h-5 w-5 text-gray-600" />
145
- <h2 className="text-lg font-semibold text-gray-900">Schedule publishing</h2>
146
- </div>
147
- <button
148
- onClick={onClose}
149
- className="rounded-lg p-1.5 transition-colors hover:bg-gray-100"
150
- >
151
- <X className="h-5 w-5 text-gray-500" />
152
- </button>
153
- </div>
154
-
155
- <div className="space-y-4 px-4 py-4">
156
- <div className="space-y-1.5">
157
- <label className="text-sm font-medium text-gray-700">Publish at</label>
158
- <div className="relative">
159
- <Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
160
- <input
161
- type="datetime-local"
162
- value={publishAt}
163
- onChange={(e) => setPublishAt(e.target.value)}
164
- min={toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
165
- className="w-full rounded-md border border-gray-300 bg-white py-2 pr-3 pl-9 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
166
- />
141
+ <Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
142
+ <Dialog.Portal container={adminPortalContainer()}>
143
+ <Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
144
+ <Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
145
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
146
+ <div className="flex items-center gap-2">
147
+ <Calendar className="text-muted-foreground h-5 w-5" aria-hidden />
148
+ <Dialog.Title className="text-foreground text-lg font-medium">
149
+ Schedule publishing
150
+ </Dialog.Title>
167
151
  </div>
168
- <p className="text-xs text-gray-500">
169
- The document will move from DRAFT to PUBLISHED at this time (your local timezone).
170
- </p>
152
+ <Dialog.Close asChild>
153
+ <button
154
+ type="button"
155
+ aria-label="Close dialog"
156
+ className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-lg p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
157
+ >
158
+ <X className="h-5 w-5" aria-hidden />
159
+ </button>
160
+ </Dialog.Close>
171
161
  </div>
172
162
 
173
- <label className="flex items-center gap-2 text-sm text-gray-700">
174
- <input
175
- type="checkbox"
176
- checked={includeUnpublish}
177
- onChange={(e) => setIncludeUnpublish(e.target.checked)}
178
- className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
179
- />
180
- Also schedule an unpublish
181
- </label>
182
-
183
- {includeUnpublish && (
163
+ <div className="space-y-4 px-4 py-4">
184
164
  <div className="space-y-1.5">
185
- <label className="text-sm font-medium text-gray-700">Unpublish at</label>
165
+ <label htmlFor={publishAtId} className="text-foreground text-sm font-medium">
166
+ Publish at
167
+ </label>
186
168
  <div className="relative">
187
- <Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
169
+ <Clock
170
+ className="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 h-4 w-4"
171
+ aria-hidden
172
+ />
188
173
  <input
174
+ id={publishAtId}
189
175
  type="datetime-local"
190
- value={unpublishAt}
191
- onChange={(e) => setUnpublishAt(e.target.value)}
192
- min={publishAt || toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
193
- className="w-full rounded-md border border-gray-300 bg-white py-2 pr-3 pl-9 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
176
+ value={publishAt}
177
+ onChange={(e) => setPublishAt(e.target.value)}
178
+ min={toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
179
+ className="border-input bg-background text-foreground focus:border-ring focus:ring-ring w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-1 focus:outline-none"
194
180
  />
195
181
  </div>
182
+ <p className="text-muted-foreground text-xs">
183
+ The document will move from DRAFT to PUBLISHED at this time (your local timezone).
184
+ </p>
185
+ </div>
186
+
187
+ <label className="text-foreground flex items-center gap-2 text-sm">
188
+ <input
189
+ type="checkbox"
190
+ checked={includeUnpublish}
191
+ onChange={(e) => setIncludeUnpublish(e.target.checked)}
192
+ className="border-input text-primary focus:ring-ring h-4 w-4 rounded"
193
+ />
194
+ Also schedule an unpublish
195
+ </label>
196
+
197
+ {includeUnpublish && (
198
+ <div className="space-y-1.5">
199
+ <label htmlFor={unpublishAtId} className="text-foreground text-sm font-medium">
200
+ Unpublish at
201
+ </label>
202
+ <div className="relative">
203
+ <Clock
204
+ className="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 h-4 w-4"
205
+ aria-hidden
206
+ />
207
+ <input
208
+ id={unpublishAtId}
209
+ type="datetime-local"
210
+ value={unpublishAt}
211
+ onChange={(e) => setUnpublishAt(e.target.value)}
212
+ min={publishAt || toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
213
+ className="border-input bg-background text-foreground focus:border-ring focus:ring-ring w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-1 focus:outline-none"
214
+ />
215
+ </div>
216
+ </div>
217
+ )}
218
+ </div>
219
+
220
+ <div className="border-border flex items-center justify-between gap-2 border-t px-4 py-3">
221
+ {hasExistingSchedule ? (
222
+ <button
223
+ type="button"
224
+ onClick={handleCancelSchedule}
225
+ disabled={cancelling || saving}
226
+ className="text-destructive hover:bg-destructive/10 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium disabled:opacity-50"
227
+ >
228
+ {cancelling ? (
229
+ <Loader2 className="h-4 w-4 animate-spin" aria-hidden />
230
+ ) : (
231
+ <X className="h-4 w-4" aria-hidden />
232
+ )}
233
+ Cancel schedule
234
+ </button>
235
+ ) : (
236
+ <span />
237
+ )}
238
+ <div className="flex items-center gap-2">
239
+ <Dialog.Close asChild>
240
+ <button
241
+ type="button"
242
+ className="text-foreground hover:bg-accent rounded-md px-3 py-1.5 text-sm"
243
+ >
244
+ Close
245
+ </button>
246
+ </Dialog.Close>
247
+ <button
248
+ type="button"
249
+ onClick={handleSave}
250
+ disabled={saving || cancelling}
251
+ className="bg-primary text-primary-foreground inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium hover:opacity-90 disabled:opacity-50"
252
+ >
253
+ {saving ? (
254
+ <Loader2 className="h-4 w-4 animate-spin" aria-hidden />
255
+ ) : (
256
+ <Calendar className="h-4 w-4" aria-hidden />
257
+ )}
258
+ {hasExistingSchedule ? 'Reschedule' : 'Schedule'}
259
+ </button>
196
260
  </div>
197
- )}
198
- </div>
199
-
200
- <div className="flex items-center justify-between gap-2 border-t border-gray-200 px-4 py-3">
201
- {hasExistingSchedule ? (
202
- <button
203
- onClick={handleCancelSchedule}
204
- disabled={cancelling || saving}
205
- className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 disabled:opacity-50"
206
- >
207
- {cancelling ? (
208
- <Loader2 className="h-4 w-4 animate-spin" />
209
- ) : (
210
- <X className="h-4 w-4" />
211
- )}
212
- Cancel schedule
213
- </button>
214
- ) : (
215
- <span />
216
- )}
217
- <div className="flex items-center gap-2">
218
- <button
219
- onClick={onClose}
220
- className="rounded-md px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100"
221
- >
222
- Close
223
- </button>
224
- <button
225
- onClick={handleSave}
226
- disabled={saving || cancelling}
227
- className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
228
- >
229
- {saving ? (
230
- <Loader2 className="h-4 w-4 animate-spin" />
231
- ) : (
232
- <Calendar className="h-4 w-4" />
233
- )}
234
- {hasExistingSchedule ? 'Reschedule' : 'Schedule'}
235
- </button>
236
261
  </div>
237
- </div>
238
- </div>
239
- </div>
262
+ </Dialog.Content>
263
+ </Dialog.Portal>
264
+ </Dialog.Root>
240
265
  )
241
266
  }
@@ -1,9 +1,11 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useId, useState } from 'react'
4
+ import * as Dialog from '@radix-ui/react-dialog'
4
5
  import { Check, Copy, Link2, Loader2, Share2, X } from 'lucide-react'
5
6
  import { toast } from 'sonner'
6
7
  import { cmsApi } from '../lib/api.js'
8
+ import { adminPortalContainer } from '../lib/portal-container.js'
7
9
 
8
10
  export interface SharePreviewLinkDialogProps {
9
11
  collectionSlug: string
@@ -75,6 +77,7 @@ export function SharePreviewLinkDialog({
75
77
  const [expiresAt, setExpiresAt] = useState<string>('')
76
78
  const [generating, setGenerating] = useState(false)
77
79
  const [copied, setCopied] = useState(false)
80
+ const previewUrlId = useId()
78
81
 
79
82
  async function generate() {
80
83
  setGenerating(true)
@@ -121,107 +124,126 @@ export function SharePreviewLinkDialog({
121
124
  }
122
125
  }
123
126
 
124
- if (!open) return null
125
-
126
127
  return (
127
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
128
- <div className="fixed inset-0 bg-black/40" onClick={onClose} />
129
- <div className="relative w-full max-w-lg rounded-lg bg-white shadow-xl">
130
- <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
131
- <div className="flex items-center gap-2">
132
- <Share2 className="h-5 w-5 text-gray-600" />
133
- <h2 className="text-lg font-semibold text-gray-900">Share preview link</h2>
128
+ <Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
129
+ <Dialog.Portal container={adminPortalContainer()}>
130
+ <Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
131
+ <Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
132
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
133
+ <div className="flex items-center gap-2">
134
+ <Share2 className="text-muted-foreground h-5 w-5" aria-hidden />
135
+ <Dialog.Title className="text-foreground text-lg font-medium">
136
+ Share preview link
137
+ </Dialog.Title>
138
+ </div>
139
+ <Dialog.Close asChild>
140
+ <button
141
+ type="button"
142
+ aria-label="Close dialog"
143
+ className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-lg p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
144
+ >
145
+ <X className="h-5 w-5" aria-hidden />
146
+ </button>
147
+ </Dialog.Close>
134
148
  </div>
135
- <button
136
- onClick={onClose}
137
- className="rounded-lg p-1.5 transition-colors hover:bg-gray-100"
138
- >
139
- <X className="h-5 w-5 text-gray-500" />
140
- </button>
141
- </div>
142
149
 
143
- <div className="space-y-4 px-4 py-4">
144
- <p className="text-sm text-gray-600">
145
- Generate a signed URL that lets anyone with the link view the current draft of this
146
- document — even if it's never been published.
147
- </p>
150
+ <div className="space-y-4 px-4 py-4">
151
+ <Dialog.Description className="text-muted-foreground text-sm">
152
+ Generate a signed URL that lets anyone with the link view the current draft of this
153
+ document — even if it's never been published.
154
+ </Dialog.Description>
148
155
 
149
- <div className="space-y-2">
150
- <label className="text-sm font-medium text-gray-700">Link lifetime</label>
151
- <div className="grid grid-cols-2 gap-2">
152
- {TTL_CHOICES.map((choice) => {
153
- const selected = ttl === choice.seconds
154
- return (
156
+ <div className="space-y-2">
157
+ <span className="text-foreground text-sm font-medium">Link lifetime</span>
158
+ <div className="grid grid-cols-2 gap-2">
159
+ {TTL_CHOICES.map((choice) => {
160
+ const selected = ttl === choice.seconds
161
+ return (
162
+ <button
163
+ key={choice.seconds}
164
+ type="button"
165
+ aria-pressed={selected}
166
+ onClick={() => setTtl(choice.seconds)}
167
+ className={`rounded-md border px-3 py-2 text-left text-sm transition-colors ${
168
+ selected
169
+ ? 'border-primary bg-primary/10 text-foreground'
170
+ : 'border-border hover:border-input hover:bg-accent'
171
+ }`}
172
+ >
173
+ <div className="font-medium">{choice.label}</div>
174
+ <div className="text-muted-foreground text-xs">{choice.hint}</div>
175
+ </button>
176
+ )
177
+ })}
178
+ </div>
179
+ <p className="text-muted-foreground text-xs">
180
+ Preview tokens can't be revoked individually — pick the shortest TTL that fits.
181
+ </p>
182
+ </div>
183
+
184
+ {previewUrl ? (
185
+ <div className="border-border bg-muted space-y-2 rounded-lg border p-3">
186
+ <label
187
+ htmlFor={previewUrlId}
188
+ className="text-muted-foreground text-xs font-medium tracking-wide uppercase"
189
+ >
190
+ Preview URL
191
+ </label>
192
+ <div className="flex items-stretch gap-2">
193
+ <input
194
+ id={previewUrlId}
195
+ readOnly
196
+ value={previewUrl}
197
+ onFocus={(e) => e.currentTarget.select()}
198
+ className="border-input bg-background text-foreground flex-1 rounded-md border px-2 py-1.5 font-mono text-xs"
199
+ />
155
200
  <button
156
- key={choice.seconds}
157
- onClick={() => setTtl(choice.seconds)}
158
- className={`rounded-md border px-3 py-2 text-left text-sm transition-colors ${
159
- selected
160
- ? 'border-blue-500 bg-blue-50 text-blue-900'
161
- : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
162
- }`}
201
+ type="button"
202
+ onClick={copyToClipboard}
203
+ className="bg-primary text-primary-foreground inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium hover:opacity-90"
163
204
  >
164
- <div className="font-medium">{choice.label}</div>
165
- <div className="text-xs text-gray-500">{choice.hint}</div>
205
+ {copied ? (
206
+ <Check className="h-3.5 w-3.5" aria-hidden />
207
+ ) : (
208
+ <Copy className="h-3.5 w-3.5" aria-hidden />
209
+ )}
210
+ {copied ? 'Copied' : 'Copy'}
166
211
  </button>
167
- )
168
- })}
169
- </div>
170
- <p className="text-xs text-gray-500">
171
- Preview tokens can't be revoked individually — pick the shortest TTL that fits.
172
- </p>
212
+ </div>
213
+ {expiresAt && (
214
+ <p className="text-muted-foreground text-xs">
215
+ Expires {new Date(expiresAt).toLocaleString()}
216
+ </p>
217
+ )}
218
+ </div>
219
+ ) : null}
173
220
  </div>
174
221
 
175
- {previewUrl ? (
176
- <div className="space-y-2 rounded-lg border border-gray-200 bg-gray-50 p-3">
177
- <label className="text-xs font-medium tracking-wide text-gray-500 uppercase">
178
- Preview URL
179
- </label>
180
- <div className="flex items-stretch gap-2">
181
- <input
182
- readOnly
183
- value={previewUrl}
184
- onFocus={(e) => e.currentTarget.select()}
185
- className="flex-1 rounded-md border border-gray-300 bg-white px-2 py-1.5 font-mono text-xs"
186
- />
187
- <button
188
- onClick={copyToClipboard}
189
- className="inline-flex items-center gap-1.5 rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700"
190
- >
191
- {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
192
- {copied ? 'Copied' : 'Copy'}
193
- </button>
194
- </div>
195
- {expiresAt && (
196
- <p className="text-xs text-gray-500">
197
- Expires {new Date(expiresAt).toLocaleString()}
198
- </p>
222
+ <div className="border-border flex items-center justify-end gap-2 border-t px-4 py-3">
223
+ <Dialog.Close asChild>
224
+ <button
225
+ type="button"
226
+ className="text-foreground hover:bg-accent rounded-md px-3 py-1.5 text-sm"
227
+ >
228
+ Close
229
+ </button>
230
+ </Dialog.Close>
231
+ <button
232
+ type="button"
233
+ onClick={generate}
234
+ disabled={generating}
235
+ className="bg-primary text-primary-foreground inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium hover:opacity-90 disabled:opacity-50"
236
+ >
237
+ {generating ? (
238
+ <Loader2 className="h-4 w-4 animate-spin" aria-hidden />
239
+ ) : (
240
+ <Link2 className="h-4 w-4" aria-hidden />
199
241
  )}
200
- </div>
201
- ) : null}
202
- </div>
203
-
204
- <div className="flex items-center justify-end gap-2 border-t border-gray-200 px-4 py-3">
205
- <button
206
- onClick={onClose}
207
- className="rounded-md px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100"
208
- >
209
- Close
210
- </button>
211
- <button
212
- onClick={generate}
213
- disabled={generating}
214
- className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
215
- >
216
- {generating ? (
217
- <Loader2 className="h-4 w-4 animate-spin" />
218
- ) : (
219
- <Link2 className="h-4 w-4" />
220
- )}
221
- {previewUrl ? 'Generate new link' : 'Generate link'}
222
- </button>
223
- </div>
224
- </div>
225
- </div>
242
+ {previewUrl ? 'Generate new link' : 'Generate link'}
243
+ </button>
244
+ </div>
245
+ </Dialog.Content>
246
+ </Dialog.Portal>
247
+ </Dialog.Root>
226
248
  )
227
249
  }