@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.
- package/CHANGELOG.md +22 -0
- package/dist/AdminRoot.d.ts +10 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +25 -43
- package/dist/AdminRoot.js.map +1 -1
- package/dist/LoginRoot.d.ts +54 -0
- package/dist/LoginRoot.d.ts.map +1 -0
- package/dist/LoginRoot.js +59 -0
- package/dist/LoginRoot.js.map +1 -0
- 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/__tests__/views/login-root.render.test.d.ts +2 -0
- package/dist/__tests__/views/login-root.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/login-root.render.test.js +79 -0
- package/dist/__tests__/views/login-root.render.test.js.map +1 -0
- package/dist/actuate-admin.css +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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/root-chrome.d.ts +15 -0
- package/dist/lib/root-chrome.d.ts.map +1 -0
- package/dist/lib/root-chrome.js +41 -0
- package/dist/lib/root-chrome.js.map +1 -0
- package/dist/views/HiddenLoginNotFound.d.ts +8 -0
- package/dist/views/HiddenLoginNotFound.d.ts.map +1 -0
- package/dist/views/HiddenLoginNotFound.js +29 -0
- package/dist/views/HiddenLoginNotFound.js.map +1 -0
- 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 +2 -2
- package/src/AdminRoot.tsx +61 -59
- package/src/LoginRoot.tsx +166 -0
- package/src/__tests__/components/modal.render.test.tsx +57 -0
- package/src/__tests__/views/login-root.render.test.tsx +115 -0
- 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/index.ts +2 -0
- package/src/lib/root-chrome.ts +40 -0
- package/src/views/HiddenLoginNotFound.tsx +45 -0
- package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
|
@@ -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>
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AdminRoot } from './AdminRoot.js'
|
|
2
2
|
export type { AdminRootProps } from './AdminRoot.js'
|
|
3
|
+
export { LoginRoot } from './LoginRoot.js'
|
|
4
|
+
export type { LoginRootProps } from './LoginRoot.js'
|
|
3
5
|
|
|
4
6
|
// Pluggable canvas renderers for custom section types (see `AdminRoot`'s
|
|
5
7
|
// `sectionRenderers` prop). The types structurally mirror
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Full-viewport overlay style shared by the admin and login mounts so the CMS
|
|
6
|
+
* UI renders above any consumer page chrome, with its own stacking context.
|
|
7
|
+
*/
|
|
8
|
+
export const ISOLATION_STYLE: React.CSSProperties = {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
inset: '0',
|
|
11
|
+
zIndex: 50,
|
|
12
|
+
overflow: 'auto',
|
|
13
|
+
isolation: 'isolate',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The admin (and its login screens) are private application UI and must never
|
|
18
|
+
* be indexed. Ensure a `noindex, nofollow` robots meta tag is present while
|
|
19
|
+
* mounted, as defense-in-depth alongside the server `robots.txt` Disallow
|
|
20
|
+
* rule. This covers every consumer install (the admin page is typically a
|
|
21
|
+
* client component that can't export Next.js route metadata).
|
|
22
|
+
*/
|
|
23
|
+
export function useRobotsNoIndex(): void {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (typeof document === 'undefined') return
|
|
26
|
+
const prev = document.querySelector<HTMLMetaElement>('meta[name="robots"]')
|
|
27
|
+
const prevContent = prev?.getAttribute('content') ?? null
|
|
28
|
+
const meta = prev ?? document.createElement('meta')
|
|
29
|
+
meta.setAttribute('name', 'robots')
|
|
30
|
+
meta.setAttribute('content', 'noindex, nofollow')
|
|
31
|
+
if (!prev) document.head.appendChild(meta)
|
|
32
|
+
return () => {
|
|
33
|
+
if (prevContent === null) {
|
|
34
|
+
meta.remove()
|
|
35
|
+
} else {
|
|
36
|
+
meta.setAttribute('content', prevContent)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, [])
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rendered at the admin mount for signed-out visitors on split-login installs
|
|
5
|
+
* (`config.admin.loginPath` set). Mimics the framework's default 404 page so
|
|
6
|
+
* hitting `/admin` reveals nothing — no login form, no admin branding, and no
|
|
7
|
+
* redirect that would leak the real login location.
|
|
8
|
+
*/
|
|
9
|
+
export function HiddenLoginNotFound() {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
style={{
|
|
13
|
+
fontFamily: 'system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
14
|
+
height: '100vh',
|
|
15
|
+
display: 'flex',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
background: '#fff',
|
|
19
|
+
color: '#000',
|
|
20
|
+
textAlign: 'center',
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<div style={{ display: 'flex', alignItems: 'center', lineHeight: '49px' }}>
|
|
24
|
+
<h1
|
|
25
|
+
style={{
|
|
26
|
+
display: 'inline-block',
|
|
27
|
+
margin: '0 20px 0 0',
|
|
28
|
+
paddingRight: 23,
|
|
29
|
+
fontSize: 24,
|
|
30
|
+
fontWeight: 500,
|
|
31
|
+
verticalAlign: 'top',
|
|
32
|
+
borderRight: '1px solid rgba(0, 0, 0, .3)',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
404
|
|
36
|
+
</h1>
|
|
37
|
+
<div style={{ display: 'inline-block' }}>
|
|
38
|
+
<h2 style={{ margin: 0, fontSize: 14, fontWeight: 400, lineHeight: '49px' }}>
|
|
39
|
+
This page could not be found.
|
|
40
|
+
</h2>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|