@actuate-media/cms-admin 0.38.1 → 0.40.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.
Files changed (61) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +25 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/breadcrumbs-is-id.test.d.ts +2 -0
  5. package/dist/__tests__/components/breadcrumbs-is-id.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/breadcrumbs-is-id.test.js +28 -0
  7. package/dist/__tests__/components/breadcrumbs-is-id.test.js.map +1 -0
  8. package/dist/__tests__/lib/page-editor-service-api.test.js +40 -0
  9. package/dist/__tests__/lib/page-editor-service-api.test.js.map +1 -1
  10. package/dist/__tests__/views/appearance-settings.render.test.js +34 -0
  11. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
  12. package/dist/actuate-admin.css +1 -1
  13. package/dist/components/Breadcrumbs.d.ts +1 -0
  14. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  15. package/dist/components/Breadcrumbs.js +10 -2
  16. package/dist/components/Breadcrumbs.js.map +1 -1
  17. package/dist/components/MediaPickerModal.d.ts +9 -1
  18. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  19. package/dist/components/MediaPickerModal.js +11 -5
  20. package/dist/components/MediaPickerModal.js.map +1 -1
  21. package/dist/lib/page-editor-service.d.ts.map +1 -1
  22. package/dist/lib/page-editor-service.js +10 -3
  23. package/dist/lib/page-editor-service.js.map +1 -1
  24. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  25. package/dist/views/page-editor/PageSectionEditor.js +5 -2
  26. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  27. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  28. package/dist/views/page-editor/SectionInspector.js +65 -27
  29. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  30. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -1
  31. package/dist/views/page-editor/SectionsPanel.js +7 -4
  32. package/dist/views/page-editor/SectionsPanel.js.map +1 -1
  33. package/dist/views/page-editor/section-types.d.ts +2 -2
  34. package/dist/views/page-editor/section-types.d.ts.map +1 -1
  35. package/dist/views/page-editor/section-types.js +1 -1
  36. package/dist/views/page-editor/section-types.js.map +1 -1
  37. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
  38. package/dist/views/page-editor/sections/SectionRenderer.js +14 -1
  39. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
  40. package/dist/views/settings/BrandingCard.d.ts.map +1 -1
  41. package/dist/views/settings/BrandingCard.js +15 -4
  42. package/dist/views/settings/BrandingCard.js.map +1 -1
  43. package/dist/views/settings/useAppearanceSettings.d.ts +10 -0
  44. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  45. package/dist/views/settings/useAppearanceSettings.js +24 -0
  46. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/AdminRoot.tsx +31 -0
  49. package/src/__tests__/components/breadcrumbs-is-id.test.ts +32 -0
  50. package/src/__tests__/lib/page-editor-service-api.test.ts +43 -0
  51. package/src/__tests__/views/appearance-settings.render.test.tsx +45 -0
  52. package/src/components/Breadcrumbs.tsx +11 -2
  53. package/src/components/MediaPickerModal.tsx +68 -39
  54. package/src/lib/page-editor-service.ts +10 -3
  55. package/src/views/page-editor/PageSectionEditor.tsx +24 -1
  56. package/src/views/page-editor/SectionInspector.tsx +235 -121
  57. package/src/views/page-editor/SectionsPanel.tsx +54 -38
  58. package/src/views/page-editor/section-types.ts +5 -0
  59. package/src/views/page-editor/sections/SectionRenderer.tsx +25 -1
  60. package/src/views/settings/BrandingCard.tsx +37 -2
  61. package/src/views/settings/useAppearanceSettings.ts +39 -0
@@ -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 { BrandAssetField, UseAppearanceSettings } from './useAppearanceSettings.js'
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,