@actuate-media/cms-admin 0.49.0 → 0.49.3
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/CHANGELOG.md +34 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +10 -11
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/modal.render.test.d.ts +2 -0
- package/dist/__tests__/components/modal.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/modal.render.test.js +37 -0
- package/dist/__tests__/components/modal.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/FolderTree.js +19 -19
- package/dist/components/FolderTree.js.map +1 -1
- package/dist/components/MediaPickerModal.d.ts +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +12 -11
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/SchedulePublishDialog.d.ts +1 -1
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -1
- package/dist/components/SchedulePublishDialog.js +6 -4
- package/dist/components/SchedulePublishDialog.js.map +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -1
- package/dist/components/SharePreviewLinkDialog.js +10 -9
- package/dist/components/SharePreviewLinkDialog.js.map +1 -1
- package/dist/components/VersionHistory.d.ts +1 -1
- package/dist/components/VersionHistory.d.ts.map +1 -1
- package/dist/components/VersionHistory.js +14 -14
- package/dist/components/VersionHistory.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +17 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +16 -15
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/fields/BlockBuilderField.d.ts.map +1 -1
- package/dist/fields/BlockBuilderField.js +6 -4
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +3 -3
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +3 -3
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/Redirects.js +7 -7
- package/dist/views/Redirects.js.map +1 -1
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTagEditor.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +5 -5
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +23 -23
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +35 -30
- package/src/__tests__/components/modal.render.test.tsx +57 -0
- package/src/components/FolderTree.tsx +32 -32
- package/src/components/MediaPickerModal.tsx +143 -119
- package/src/components/SchedulePublishDialog.tsx +118 -93
- package/src/components/SharePreviewLinkDialog.tsx +115 -93
- package/src/components/VersionHistory.tsx +98 -82
- package/src/components/ui/Modal.tsx +48 -39
- package/src/fields/BlockBuilderField.tsx +26 -19
- package/src/views/MediaBrowser.tsx +128 -120
- package/src/views/Pages/PagesListView.tsx +123 -115
- package/src/views/Posts/PostsListView.tsx +114 -110
- package/src/views/Redirects.tsx +48 -44
- package/src/views/ScriptTagEditor.tsx +40 -40
- package/src/views/ScriptTags.tsx +34 -30
- package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
|
@@ -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
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
<div className="flex items-center
|
|
132
|
-
<
|
|
133
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
onClick={
|
|
158
|
-
className=
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
<
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
5
|
import { X, RotateCcw, Clock, User, Loader2 } 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
|
interface Version {
|
|
9
11
|
id: string
|
|
@@ -38,10 +40,10 @@ function timeAgo(dateStr: string): string {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const CHANGE_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
|
41
|
-
CREATE: { label: 'Created', color: 'bg-
|
|
42
|
-
UPDATE: { label: 'Updated', color: 'bg-
|
|
43
|
-
DELETE: { label: 'Deleted', color: 'bg-
|
|
44
|
-
RESTORE: { label: 'Restored', color: 'bg-
|
|
43
|
+
CREATE: { label: 'Created', color: 'bg-success/15 text-success' },
|
|
44
|
+
UPDATE: { label: 'Updated', color: 'bg-primary/15 text-primary' },
|
|
45
|
+
DELETE: { label: 'Deleted', color: 'bg-destructive/15 text-destructive' },
|
|
46
|
+
RESTORE: { label: 'Restored', color: 'bg-accent text-accent-foreground' },
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function VersionHistory({
|
|
@@ -88,91 +90,105 @@ export function VersionHistory({
|
|
|
88
90
|
setRestoring(null)
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
if (!open) return null
|
|
92
|
-
|
|
93
93
|
return (
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
|
|
95
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
96
|
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/30" />
|
|
97
|
+
<Dialog.Content
|
|
98
|
+
aria-describedby={undefined}
|
|
99
|
+
className="bg-card text-card-foreground border-border animate-in slide-in-from-right fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col border-l shadow-xl focus:outline-none"
|
|
100
|
+
>
|
|
101
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<Clock className="text-muted-foreground h-5 w-5" aria-hidden />
|
|
104
|
+
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
105
|
+
Version History
|
|
106
|
+
</Dialog.Title>
|
|
107
|
+
</div>
|
|
108
|
+
<Dialog.Close asChild>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
aria-label="Close version history"
|
|
112
|
+
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"
|
|
113
|
+
>
|
|
114
|
+
<X className="h-5 w-5" aria-hidden />
|
|
115
|
+
</button>
|
|
116
|
+
</Dialog.Close>
|
|
101
117
|
</div>
|
|
102
|
-
<button
|
|
103
|
-
onClick={onClose}
|
|
104
|
-
className="rounded-lg p-1.5 transition-colors hover:bg-gray-100"
|
|
105
|
-
>
|
|
106
|
-
<X className="h-5 w-5 text-gray-500" />
|
|
107
|
-
</button>
|
|
108
|
-
</div>
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
<div className="flex-1 overflow-y-auto">
|
|
120
|
+
{loading ? (
|
|
121
|
+
<div className="flex items-center justify-center py-16">
|
|
122
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
|
|
123
|
+
</div>
|
|
124
|
+
) : versions.length === 0 ? (
|
|
125
|
+
<div className="text-muted-foreground py-16 text-center text-sm">
|
|
126
|
+
No version history available
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div className="divide-border divide-y">
|
|
130
|
+
{versions.map((version, index) => {
|
|
131
|
+
const typeInfo = CHANGE_TYPE_LABELS[version.changeType] ?? {
|
|
132
|
+
label: version.changeType,
|
|
133
|
+
color: 'bg-muted text-muted-foreground',
|
|
134
|
+
}
|
|
135
|
+
const isLatest = index === 0
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
key={version.id}
|
|
140
|
+
className="hover:bg-accent/50 px-4 py-3 transition-colors"
|
|
141
|
+
>
|
|
142
|
+
<div className="flex items-start justify-between gap-3">
|
|
143
|
+
<div className="min-w-0 flex-1">
|
|
144
|
+
<div className="mb-1 flex items-center gap-2">
|
|
145
|
+
<span
|
|
146
|
+
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${typeInfo.color}`}
|
|
147
|
+
>
|
|
148
|
+
{typeInfo.label}
|
|
149
|
+
</span>
|
|
150
|
+
{isLatest && (
|
|
151
|
+
<span className="text-muted-foreground text-xs font-medium">
|
|
152
|
+
Current
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
<div className="text-muted-foreground flex items-center gap-3 text-xs">
|
|
157
|
+
{version.changedBy && (
|
|
158
|
+
<span className="flex items-center gap-1">
|
|
159
|
+
<User className="h-3 w-3" aria-hidden />
|
|
160
|
+
{version.changedBy.name ?? version.changedBy.email ?? 'Unknown'}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
<span title={new Date(version.createdAt).toLocaleString()}>
|
|
164
|
+
{timeAgo(version.createdAt)}
|
|
147
165
|
</span>
|
|
148
|
-
|
|
149
|
-
<span title={new Date(version.createdAt).toLocaleString()}>
|
|
150
|
-
{timeAgo(version.createdAt)}
|
|
151
|
-
</span>
|
|
166
|
+
</div>
|
|
152
167
|
</div>
|
|
168
|
+
{!isLatest && (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => handleRestore(version)}
|
|
172
|
+
disabled={restoring === version.id}
|
|
173
|
+
className="bg-primary/10 text-primary hover:bg-primary/20 flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium transition-colors disabled:opacity-50"
|
|
174
|
+
>
|
|
175
|
+
{restoring === version.id ? (
|
|
176
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
|
|
177
|
+
) : (
|
|
178
|
+
<RotateCcw className="h-3.5 w-3.5" aria-hidden />
|
|
179
|
+
)}
|
|
180
|
+
Restore
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
153
183
|
</div>
|
|
154
|
-
{!isLatest && (
|
|
155
|
-
<button
|
|
156
|
-
onClick={() => handleRestore(version)}
|
|
157
|
-
disabled={restoring === version.id}
|
|
158
|
-
className="flex items-center gap-1.5 rounded-lg bg-blue-50 px-2.5 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 disabled:opacity-50"
|
|
159
|
-
>
|
|
160
|
-
{restoring === version.id ? (
|
|
161
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
162
|
-
) : (
|
|
163
|
-
<RotateCcw className="h-3.5 w-3.5" />
|
|
164
|
-
)}
|
|
165
|
-
Restore
|
|
166
|
-
</button>
|
|
167
|
-
)}
|
|
168
184
|
</div>
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</
|
|
175
|
-
</
|
|
176
|
-
</
|
|
185
|
+
)
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</Dialog.Content>
|
|
191
|
+
</Dialog.Portal>
|
|
192
|
+
</Dialog.Root>
|
|
177
193
|
)
|
|
178
194
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
|
+
import { X } from 'lucide-react'
|
|
5
|
+
import { type ReactNode } from 'react'
|
|
6
|
+
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
4
7
|
|
|
5
8
|
export interface ModalProps {
|
|
6
9
|
open: boolean
|
|
@@ -8,47 +11,53 @@ export interface ModalProps {
|
|
|
8
11
|
title: string
|
|
9
12
|
children: ReactNode
|
|
10
13
|
actions?: ReactNode
|
|
14
|
+
/**
|
|
15
|
+
* Accessible description for the dialog, announced after the title. Optional —
|
|
16
|
+
* omit when the body content is self-explanatory.
|
|
17
|
+
*/
|
|
18
|
+
description?: string
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!open) return null
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The canonical admin modal. Built on Radix `Dialog`, which provides the
|
|
23
|
+
* accessibility contract for free: focus is trapped while open and restored to
|
|
24
|
+
* the trigger on close, Escape and overlay-click dismiss, the surface is
|
|
25
|
+
* exposed as `role="dialog"` + `aria-modal`, and the title/description are wired
|
|
26
|
+
* via `aria-labelledby`/`aria-describedby`.
|
|
27
|
+
*
|
|
28
|
+
* The portal mounts inside `.actuate-admin` (see `adminPortalContainer`) so the
|
|
29
|
+
* dialog inherits theme tokens and dark mode instead of rendering unstyled on
|
|
30
|
+
* `document.body`.
|
|
31
|
+
*/
|
|
32
|
+
export function Modal({ open, onClose, title, children, actions, description }: ModalProps) {
|
|
27
33
|
return (
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</
|
|
43
|
-
</button>
|
|
44
|
-
</div>
|
|
45
|
-
<div className="px-6 py-4">{children}</div>
|
|
46
|
-
{actions && (
|
|
47
|
-
<div className="flex justify-end gap-2 border-t border-[var(--border)] px-6 py-4">
|
|
48
|
-
{actions}
|
|
34
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
|
|
35
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
36
|
+
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/50" />
|
|
37
|
+
<Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-60 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
|
|
38
|
+
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
|
39
|
+
<Dialog.Title className="text-foreground text-lg font-medium">{title}</Dialog.Title>
|
|
40
|
+
<Dialog.Close asChild>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
aria-label="Close dialog"
|
|
44
|
+
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-md p-1 transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
45
|
+
>
|
|
46
|
+
<X className="h-4 w-4" aria-hidden />
|
|
47
|
+
</button>
|
|
48
|
+
</Dialog.Close>
|
|
49
49
|
</div>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
{description && (
|
|
51
|
+
<Dialog.Description className="text-muted-foreground px-6 pt-4 text-sm">
|
|
52
|
+
{description}
|
|
53
|
+
</Dialog.Description>
|
|
54
|
+
)}
|
|
55
|
+
<div className="px-6 py-4">{children}</div>
|
|
56
|
+
{actions && (
|
|
57
|
+
<div className="border-border flex justify-end gap-2 border-t px-6 py-4">{actions}</div>
|
|
58
|
+
)}
|
|
59
|
+
</Dialog.Content>
|
|
60
|
+
</Dialog.Portal>
|
|
61
|
+
</Dialog.Root>
|
|
53
62
|
)
|
|
54
63
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
arrayMove,
|
|
19
19
|
} from '@dnd-kit/sortable'
|
|
20
20
|
import { CSS } from '@dnd-kit/utilities'
|
|
21
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
21
22
|
import {
|
|
22
23
|
Plus,
|
|
23
24
|
GripVertical,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
X,
|
|
34
35
|
} from 'lucide-react'
|
|
35
36
|
import { PRESET_BLOCKS, type BlockTypeDefinition } from './block-types.js'
|
|
37
|
+
import { adminPortalContainer } from '../lib/portal-container.js'
|
|
36
38
|
|
|
37
39
|
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
38
40
|
layout: Layout,
|
|
@@ -359,19 +361,24 @@ export function BlockBuilderField({
|
|
|
359
361
|
</button>
|
|
360
362
|
</div>
|
|
361
363
|
|
|
362
|
-
{pickerOpen
|
|
363
|
-
<
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
364
|
+
<Dialog.Root open={pickerOpen} onOpenChange={setPickerOpen}>
|
|
365
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
366
|
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/50" />
|
|
367
|
+
<Dialog.Content
|
|
368
|
+
aria-describedby={undefined}
|
|
369
|
+
className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none"
|
|
370
|
+
>
|
|
371
|
+
<div className="border-border flex items-center justify-between border-b px-5 py-4">
|
|
372
|
+
<Dialog.Title className="text-base font-medium">Add Block</Dialog.Title>
|
|
373
|
+
<Dialog.Close asChild>
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md p-1 focus-visible:ring-2 focus-visible:outline-none"
|
|
377
|
+
aria-label="Close block picker"
|
|
378
|
+
>
|
|
379
|
+
<X className="h-5 w-5" aria-hidden />
|
|
380
|
+
</button>
|
|
381
|
+
</Dialog.Close>
|
|
375
382
|
</div>
|
|
376
383
|
<div className="grid grid-cols-2 gap-3 p-5 sm:grid-cols-3">
|
|
377
384
|
{allBlockTypes.map((bt) => {
|
|
@@ -381,20 +388,20 @@ export function BlockBuilderField({
|
|
|
381
388
|
key={bt.type}
|
|
382
389
|
type="button"
|
|
383
390
|
onClick={() => addBlock(bt.type)}
|
|
384
|
-
className="flex flex-col items-start gap-1.5 rounded-md border
|
|
391
|
+
className="border-border hover:border-primary hover:bg-accent flex flex-col items-start gap-1.5 rounded-md border p-3 text-left transition-colors"
|
|
385
392
|
>
|
|
386
|
-
{Icon && <Icon className="h-5 w-5
|
|
393
|
+
{Icon && <Icon className="text-primary h-5 w-5" aria-hidden />}
|
|
387
394
|
<span className="text-sm font-medium">{bt.label}</span>
|
|
388
|
-
<span className="text-xs leading-snug
|
|
395
|
+
<span className="text-muted-foreground text-xs leading-snug">
|
|
389
396
|
{bt.description}
|
|
390
397
|
</span>
|
|
391
398
|
</button>
|
|
392
399
|
)
|
|
393
400
|
})}
|
|
394
401
|
</div>
|
|
395
|
-
</
|
|
396
|
-
</
|
|
397
|
-
|
|
402
|
+
</Dialog.Content>
|
|
403
|
+
</Dialog.Portal>
|
|
404
|
+
</Dialog.Root>
|
|
398
405
|
|
|
399
406
|
{helpText && <p className="mt-1 text-xs text-[var(--muted-foreground)]">{helpText}</p>}
|
|
400
407
|
</div>
|