@actuate-media/cms-admin 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +8 -0
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/breadcrumbs-is-id.test.d.ts +2 -0
- package/dist/__tests__/components/breadcrumbs-is-id.test.d.ts.map +1 -0
- package/dist/__tests__/components/breadcrumbs-is-id.test.js +28 -0
- package/dist/__tests__/components/breadcrumbs-is-id.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service-api.test.js +40 -0
- package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
- package/dist/__tests__/no-server-barrel-imports.test.d.ts +2 -0
- package/dist/__tests__/no-server-barrel-imports.test.d.ts.map +1 -0
- package/dist/__tests__/no-server-barrel-imports.test.js +50 -0
- package/dist/__tests__/no-server-barrel-imports.test.js.map +1 -0
- package/dist/__tests__/router/navigate-pushstate.test.d.ts +2 -0
- package/dist/__tests__/router/navigate-pushstate.test.d.ts.map +1 -0
- package/dist/__tests__/router/navigate-pushstate.test.js +37 -0
- package/dist/__tests__/router/navigate-pushstate.test.js.map +1 -0
- package/dist/__tests__/views/appearance-settings.render.test.js +34 -0
- package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
- package/dist/components/Breadcrumbs.d.ts +1 -0
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +10 -2
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/MediaPickerModal.d.ts +9 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +11 -5
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/hooks/useBuilderState.js +1 -1
- package/dist/hooks/useBuilderState.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +10 -3
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +6 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/page-builder/BlockEditor.js +1 -1
- package/dist/views/page-builder/BlockEditor.js.map +1 -1
- package/dist/views/page-builder/BlockPicker.js +1 -1
- package/dist/views/page-builder/BlockPicker.js.map +1 -1
- package/dist/views/page-builder/DesignScore.js +1 -1
- package/dist/views/page-builder/DesignScore.js.map +1 -1
- package/dist/views/page-builder/SEOPanel.js +1 -1
- package/dist/views/page-builder/SEOPanel.js.map +1 -1
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -1
- package/dist/views/page-builder/SavedSections.js +1 -1
- package/dist/views/page-builder/SavedSections.js.map +1 -1
- package/dist/views/page-builder/TemplatePicker.js +1 -1
- package/dist/views/page-builder/TemplatePicker.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +5 -2
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +6 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +7 -4
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +14 -1
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/dist/views/settings/BrandingCard.d.ts.map +1 -1
- package/dist/views/settings/BrandingCard.js +15 -4
- package/dist/views/settings/BrandingCard.js.map +1 -1
- package/dist/views/settings/useAppearanceSettings.d.ts +10 -0
- package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
- package/dist/views/settings/useAppearanceSettings.js +24 -0
- package/dist/views/settings/useAppearanceSettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +12 -0
- package/src/__tests__/components/breadcrumbs-is-id.test.ts +32 -0
- package/src/__tests__/lib/page-editor-service-api.test.ts +43 -0
- package/src/__tests__/no-server-barrel-imports.test.ts +58 -0
- package/src/__tests__/router/navigate-pushstate.test.ts +46 -0
- package/src/__tests__/views/appearance-settings.render.test.tsx +45 -0
- package/src/components/Breadcrumbs.tsx +11 -2
- package/src/components/MediaPickerModal.tsx +68 -39
- package/src/hooks/useBuilderState.ts +1 -1
- package/src/lib/page-editor-service.ts +10 -3
- package/src/router/index.ts +6 -1
- package/src/views/page-builder/BlockEditor.tsx +1 -1
- package/src/views/page-builder/BlockPicker.tsx +1 -1
- package/src/views/page-builder/DesignScore.tsx +1 -1
- package/src/views/page-builder/SEOPanel.tsx +1 -1
- package/src/views/page-builder/SavedSections.tsx +6 -1
- package/src/views/page-builder/TemplatePicker.tsx +1 -1
- package/src/views/page-editor/PageSectionEditor.tsx +24 -1
- package/src/views/page-editor/SectionInspector.tsx +50 -1
- package/src/views/page-editor/SectionsPanel.tsx +54 -38
- package/src/views/page-editor/sections/SectionRenderer.tsx +25 -1
- package/src/views/settings/BrandingCard.tsx +37 -2
- package/src/views/settings/useAppearanceSettings.ts +39 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
|
-
import { Eye, EyeOff, Plus, X } from 'lucide-react'
|
|
4
|
+
import { Eye, EyeOff, Lock, Plus, X } from 'lucide-react'
|
|
5
5
|
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
6
6
|
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
7
7
|
import type { SectionSettings } from './section-types.js'
|
|
@@ -41,6 +41,55 @@ export function SectionInspector({
|
|
|
41
41
|
return () => document.removeEventListener('keydown', onKey)
|
|
42
42
|
}, [onClose])
|
|
43
43
|
|
|
44
|
+
// Unmanaged sections (no registered type) are read-only: show why instead of
|
|
45
|
+
// editable controls, so the externally-managed content is never altered here.
|
|
46
|
+
if (section.unmanaged) {
|
|
47
|
+
return (
|
|
48
|
+
<aside
|
|
49
|
+
role="dialog"
|
|
50
|
+
aria-label={`${section.name} (read-only)`}
|
|
51
|
+
className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
|
|
52
|
+
>
|
|
53
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
54
|
+
<div className="min-w-0">
|
|
55
|
+
<h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
|
|
56
|
+
<p className="text-muted-foreground text-xs">Managed outside the editor</p>
|
|
57
|
+
</div>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={onClose}
|
|
61
|
+
aria-label="Close inspector"
|
|
62
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
|
|
63
|
+
>
|
|
64
|
+
<X className="h-4 w-4" aria-hidden />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex-1 space-y-3 overflow-y-auto p-4">
|
|
68
|
+
<div className="border-border bg-muted text-muted-foreground space-y-2 rounded-md border p-3 text-sm">
|
|
69
|
+
<p className="text-foreground flex items-center gap-2 font-medium">
|
|
70
|
+
<Lock size={14} aria-hidden /> Read-only section
|
|
71
|
+
</p>
|
|
72
|
+
<p>
|
|
73
|
+
The section type{' '}
|
|
74
|
+
<code className="bg-background rounded px-1 py-0.5 text-xs">
|
|
75
|
+
{section.sectionType}
|
|
76
|
+
</code>{' '}
|
|
77
|
+
isn't registered in this CMS, so it can't be edited here. It is preserved
|
|
78
|
+
exactly as-is on every save.
|
|
79
|
+
</p>
|
|
80
|
+
<p>
|
|
81
|
+
Edit it in your seed script or code, or register it via{' '}
|
|
82
|
+
<code className="bg-background rounded px-1 py-0.5 text-xs">
|
|
83
|
+
sections.customTypes
|
|
84
|
+
</code>{' '}
|
|
85
|
+
in your config to make it editable.
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</aside>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
44
93
|
return (
|
|
45
94
|
<aside
|
|
46
95
|
role="dialog"
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Eye,
|
|
6
6
|
EyeOff,
|
|
7
7
|
GripVertical,
|
|
8
|
+
Lock,
|
|
8
9
|
type LucideIcon,
|
|
9
10
|
MoreVertical,
|
|
10
11
|
Plus,
|
|
@@ -153,9 +154,12 @@ function SortableSectionItem({
|
|
|
153
154
|
onDuplicate,
|
|
154
155
|
onRequestDelete,
|
|
155
156
|
}: SortableItemProps) {
|
|
157
|
+
// Unmanaged (externally-managed) sections are read-only: no drag, duplicate,
|
|
158
|
+
// delete, or visibility toggle — only selectable to view the read-only notice.
|
|
159
|
+
const locked = section.unmanaged === true
|
|
156
160
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
157
161
|
id: section.id,
|
|
158
|
-
disabled: !canEdit,
|
|
162
|
+
disabled: !canEdit || locked,
|
|
159
163
|
})
|
|
160
164
|
const Icon = sectionIconFor(section.sectionType)
|
|
161
165
|
|
|
@@ -176,7 +180,7 @@ function SortableSectionItem({
|
|
|
176
180
|
type="button"
|
|
177
181
|
className="text-muted-foreground hover:text-foreground shrink-0 cursor-grab touch-none disabled:cursor-not-allowed disabled:opacity-40"
|
|
178
182
|
aria-label={`Drag to reorder ${section.name}`}
|
|
179
|
-
disabled={!canEdit}
|
|
183
|
+
disabled={!canEdit || locked}
|
|
180
184
|
{...attributes}
|
|
181
185
|
{...listeners}
|
|
182
186
|
>
|
|
@@ -198,48 +202,60 @@ function SortableSectionItem({
|
|
|
198
202
|
</span>
|
|
199
203
|
</button>
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
) : (
|
|
212
|
-
<EyeOff className="h-4 w-4" aria-hidden />
|
|
213
|
-
)}
|
|
214
|
-
</button>
|
|
215
|
-
|
|
216
|
-
<DropdownMenu.Root>
|
|
217
|
-
<DropdownMenu.Trigger asChild>
|
|
205
|
+
{locked ? (
|
|
206
|
+
<span
|
|
207
|
+
className="text-muted-foreground shrink-0 p-1"
|
|
208
|
+
title="Managed outside the editor — read-only"
|
|
209
|
+
aria-label={`${section.name} is managed outside the editor and is read-only`}
|
|
210
|
+
>
|
|
211
|
+
<Lock className="h-4 w-4" aria-hidden />
|
|
212
|
+
</span>
|
|
213
|
+
) : (
|
|
214
|
+
<>
|
|
218
215
|
<button
|
|
219
216
|
type="button"
|
|
217
|
+
onClick={() => onToggleVisibility(section.id)}
|
|
220
218
|
disabled={!canEdit}
|
|
221
|
-
aria-
|
|
219
|
+
aria-pressed={!section.visible}
|
|
220
|
+
aria-label={section.visible ? `Hide ${section.name}` : `Show ${section.name}`}
|
|
222
221
|
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
|
|
223
222
|
>
|
|
224
|
-
|
|
223
|
+
{section.visible ? (
|
|
224
|
+
<Eye className="h-4 w-4" aria-hidden />
|
|
225
|
+
) : (
|
|
226
|
+
<EyeOff className="h-4 w-4" aria-hidden />
|
|
227
|
+
)}
|
|
225
228
|
</button>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
229
|
+
|
|
230
|
+
<DropdownMenu.Root>
|
|
231
|
+
<DropdownMenu.Trigger asChild>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
disabled={!canEdit}
|
|
235
|
+
aria-label={`More actions for ${section.name}`}
|
|
236
|
+
className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
|
|
237
|
+
>
|
|
238
|
+
<MoreVertical className="h-4 w-4" aria-hidden />
|
|
239
|
+
</button>
|
|
240
|
+
</DropdownMenu.Trigger>
|
|
241
|
+
<DropdownMenu.Portal>
|
|
242
|
+
<DropdownMenu.Content
|
|
243
|
+
align="end"
|
|
244
|
+
sideOffset={4}
|
|
245
|
+
className="border-border bg-popover z-[70] min-w-44 rounded-lg border p-1 shadow-md"
|
|
246
|
+
>
|
|
247
|
+
<MenuItem onSelect={() => onDuplicate(section.id)} icon={Copy}>
|
|
248
|
+
Duplicate
|
|
249
|
+
</MenuItem>
|
|
250
|
+
<DropdownMenu.Separator className="bg-border my-1 h-px" />
|
|
251
|
+
<MenuItem onSelect={() => onRequestDelete(section.id)} icon={Trash2} destructive>
|
|
252
|
+
Delete
|
|
253
|
+
</MenuItem>
|
|
254
|
+
</DropdownMenu.Content>
|
|
255
|
+
</DropdownMenu.Portal>
|
|
256
|
+
</DropdownMenu.Root>
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
243
259
|
</div>
|
|
244
260
|
</li>
|
|
245
261
|
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Puzzle } from 'lucide-react'
|
|
3
4
|
import type { PageSection } from '../../../lib/page-editor-service.js'
|
|
5
|
+
import { getSectionType } from '../section-types.js'
|
|
4
6
|
import { backgroundClass, paddingClass, type PostRenderContext } from './parts.js'
|
|
5
7
|
import { HeroBannerSection } from './HeroBannerSection.js'
|
|
6
8
|
import { MissionSection } from './MissionSection.js'
|
|
@@ -37,10 +39,32 @@ function SectionBody({ section, context }: { section: PageSection; context?: Pos
|
|
|
37
39
|
case 'gallery':
|
|
38
40
|
return <GallerySection section={section} />
|
|
39
41
|
default:
|
|
40
|
-
|
|
42
|
+
// Custom (config-declared) types and unmanaged (externally-managed)
|
|
43
|
+
// sections have no built-in body component. Render a neutral placeholder
|
|
44
|
+
// so the canvas reflects their presence + order instead of a blank gap.
|
|
45
|
+
return <PlaceholderSection section={section} />
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/** Fallback canvas card for section types without a built-in renderer. */
|
|
50
|
+
function PlaceholderSection({ section }: { section: PageSection }) {
|
|
51
|
+
const def = getSectionType(section.sectionType)
|
|
52
|
+
const label = def?.name ?? section.name
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center">
|
|
55
|
+
<span className="bg-muted text-muted-foreground flex h-10 w-10 items-center justify-center rounded-lg">
|
|
56
|
+
<Puzzle className="h-5 w-5" aria-hidden />
|
|
57
|
+
</span>
|
|
58
|
+
<p className="text-foreground text-sm font-medium">{label}</p>
|
|
59
|
+
<p className="text-muted-foreground text-xs">
|
|
60
|
+
{section.unmanaged
|
|
61
|
+
? `Managed outside the editor (${section.sectionType}) — preview not available here.`
|
|
62
|
+
: `Custom section "${section.sectionType}" — rendered by your site's renderer.`}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
export interface SectionRendererProps {
|
|
45
69
|
section: PageSection
|
|
46
70
|
/** Editor mode: adds click-to-select, the selection outline + label, and
|
|
@@ -5,14 +5,20 @@ import {
|
|
|
5
5
|
ChevronDown,
|
|
6
6
|
ChevronRight,
|
|
7
7
|
ImageIcon,
|
|
8
|
+
Library,
|
|
8
9
|
Loader2,
|
|
9
10
|
Trash2,
|
|
10
11
|
Upload,
|
|
11
12
|
} from 'lucide-react'
|
|
12
|
-
import { useRef } from 'react'
|
|
13
|
+
import { useRef, useState } from 'react'
|
|
13
14
|
|
|
15
|
+
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
14
16
|
import { SettingsCard, SettingsControlRow, SettingsSubsection } from './components.js'
|
|
15
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
BrandAssetField,
|
|
19
|
+
SelectedBrandAsset,
|
|
20
|
+
UseAppearanceSettings,
|
|
21
|
+
} from './useAppearanceSettings.js'
|
|
16
22
|
|
|
17
23
|
const LOGO_ACCEPT = 'image/svg+xml,image/png'
|
|
18
24
|
|
|
@@ -33,6 +39,7 @@ function LogoField({
|
|
|
33
39
|
canEdit,
|
|
34
40
|
warning,
|
|
35
41
|
onUpload,
|
|
42
|
+
onSelectFromLibrary,
|
|
36
43
|
onRemove,
|
|
37
44
|
}: {
|
|
38
45
|
label: string
|
|
@@ -47,9 +54,11 @@ function LogoField({
|
|
|
47
54
|
canEdit: boolean
|
|
48
55
|
warning?: string
|
|
49
56
|
onUpload: (field: BrandAssetField, file: File) => void
|
|
57
|
+
onSelectFromLibrary: (field: BrandAssetField, item: SelectedBrandAsset) => void
|
|
50
58
|
onRemove: (field: BrandAssetField) => void
|
|
51
59
|
}) {
|
|
52
60
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
61
|
+
const [pickerOpen, setPickerOpen] = useState(false)
|
|
53
62
|
const assetId = brand[field]
|
|
54
63
|
const asset = assetId ? assets[assetId] : undefined
|
|
55
64
|
const uploading = uploadingField === field
|
|
@@ -102,6 +111,15 @@ function LogoField({
|
|
|
102
111
|
<Trash2 size={14} aria-hidden="true" /> Remove
|
|
103
112
|
</button>
|
|
104
113
|
)}
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => setPickerOpen(true)}
|
|
117
|
+
disabled={!canEdit || uploading}
|
|
118
|
+
aria-label={`Choose ${label.toLowerCase()} from media library`}
|
|
119
|
+
className="border-border text-foreground hover:bg-accent inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
120
|
+
>
|
|
121
|
+
<Library size={14} aria-hidden="true" /> Media library
|
|
122
|
+
</button>
|
|
105
123
|
<button
|
|
106
124
|
type="button"
|
|
107
125
|
onClick={() => inputRef.current?.click()}
|
|
@@ -116,6 +134,22 @@ function LogoField({
|
|
|
116
134
|
)}
|
|
117
135
|
{asset ? 'Replace' : 'Upload'}
|
|
118
136
|
</button>
|
|
137
|
+
<MediaPickerModal
|
|
138
|
+
open={pickerOpen}
|
|
139
|
+
onClose={() => setPickerOpen(false)}
|
|
140
|
+
accept={accept}
|
|
141
|
+
libraryOnly
|
|
142
|
+
onSelect={() => {}}
|
|
143
|
+
onSelectItem={(item) => {
|
|
144
|
+
onSelectFromLibrary(field, {
|
|
145
|
+
id: item.id,
|
|
146
|
+
url: item.url,
|
|
147
|
+
mimeType: item.mimeType,
|
|
148
|
+
width: item.width,
|
|
149
|
+
height: item.height,
|
|
150
|
+
})
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
119
153
|
</>
|
|
120
154
|
)
|
|
121
155
|
|
|
@@ -197,6 +231,7 @@ export function BrandingCard({ s, canEdit }: { s: UseAppearanceSettings; canEdit
|
|
|
197
231
|
uploadingField: s.uploadingField,
|
|
198
232
|
canEdit,
|
|
199
233
|
onUpload: s.uploadAsset,
|
|
234
|
+
onSelectFromLibrary: s.selectAsset,
|
|
200
235
|
onRemove: s.removeAsset,
|
|
201
236
|
}
|
|
202
237
|
|
|
@@ -75,6 +75,15 @@ export interface AssetPreview {
|
|
|
75
75
|
shape: LogoShape
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** An existing media-library asset chosen for a brand field via the picker. */
|
|
79
|
+
export interface SelectedBrandAsset {
|
|
80
|
+
id: string
|
|
81
|
+
url: string
|
|
82
|
+
mimeType: string
|
|
83
|
+
width?: number
|
|
84
|
+
height?: number
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
const LOGO_MIME = ['image/svg+xml', 'image/png']
|
|
79
88
|
const FAVICON_MIME = ['image/svg+xml', 'image/png']
|
|
80
89
|
const MAX_BYTES = 1024 * 1024 // 1 MB
|
|
@@ -103,6 +112,8 @@ export interface UseAppearanceSettings {
|
|
|
103
112
|
setBrandingMode: (v: AdminBrandingMode) => void
|
|
104
113
|
setAltText: (v: string) => void
|
|
105
114
|
uploadAsset: (field: BrandAssetField, file: File) => Promise<void>
|
|
115
|
+
/** Point a brand field at an EXISTING media asset (no re-upload, no duplicate). */
|
|
116
|
+
selectAsset: (field: BrandAssetField, item: SelectedBrandAsset) => void
|
|
106
117
|
removeAsset: (field: BrandAssetField) => void
|
|
107
118
|
save: () => Promise<void>
|
|
108
119
|
reset: () => void
|
|
@@ -375,6 +386,33 @@ export function useAppearanceSettings(
|
|
|
375
386
|
[appearance, brand, assets, update],
|
|
376
387
|
)
|
|
377
388
|
|
|
389
|
+
const selectAsset = useCallback(
|
|
390
|
+
(field: BrandAssetField, item: SelectedBrandAsset) => {
|
|
391
|
+
// Same allow-list as upload: brand assets must be SVG or PNG, regardless
|
|
392
|
+
// of what else lives in the media library.
|
|
393
|
+
const isFavicon = field === 'faviconSourceAssetId'
|
|
394
|
+
const allowed = isFavicon ? FAVICON_MIME : LOGO_MIME
|
|
395
|
+
if (!allowed.includes(item.mimeType)) {
|
|
396
|
+
setSaveError(`Unsupported file type. Brand assets must be ${allowed.join(' or ')}.`)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
setSaveError(null)
|
|
400
|
+
const nextAssets: Record<string, AssetPreview> = {
|
|
401
|
+
...assets,
|
|
402
|
+
[item.id]: {
|
|
403
|
+
id: item.id,
|
|
404
|
+
url: item.url,
|
|
405
|
+
mimeType: item.mimeType,
|
|
406
|
+
width: item.width,
|
|
407
|
+
height: item.height,
|
|
408
|
+
shape: classifyLogoShape(item.width, item.height),
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
update(appearance, { ...brand, [field]: item.id }, nextAssets)
|
|
412
|
+
},
|
|
413
|
+
[appearance, brand, assets, update],
|
|
414
|
+
)
|
|
415
|
+
|
|
378
416
|
const removeAsset = useCallback(
|
|
379
417
|
(field: BrandAssetField) => {
|
|
380
418
|
const next = { ...brand }
|
|
@@ -505,6 +543,7 @@ export function useAppearanceSettings(
|
|
|
505
543
|
setBrandingMode,
|
|
506
544
|
setAltText,
|
|
507
545
|
uploadAsset,
|
|
546
|
+
selectAsset,
|
|
508
547
|
removeAsset,
|
|
509
548
|
save,
|
|
510
549
|
reset,
|