feat(ui): add MediaPicker integration to BlockEditor image block

- import MediaPicker from features/media to avoid circular dependency with @zen/core
- add "Choisir un média" button in ImageUrlForm alongside existing URL input
- insert image block with `/zen/api/media/file/<slug>` src on media selection
- update README to document dual-source form (external URL + media library) and revise known limitations
This commit is contained in:
2026-04-26 17:40:58 -04:00
parent 9723f40df2
commit 56c334684f
2 changed files with 63 additions and 28 deletions
+12 -2
View File
@@ -287,6 +287,15 @@ descendants. Les styles CSS inline (`font-weight`, `font-style`,
## Bloc image ## Bloc image
Le formulaire d'insertion accepte deux sources :
- **URL externe** : champ libre + bouton « Insérer ».
- **Bibliothèque media** : bouton « Choisir un média » qui ouvre le `MediaPicker`
de `@zen/core/features/media`. À la sélection, le bloc est inséré avec
`block.src = "/zen/api/media/file/<slug>"`. Les nouveaux uploads passent par
ce picker en visibilité `public` (les blocs image vivent dans du contenu
publié — un média privé serait illisible côté frontal).
Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante** Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
(coin haut-droit) reprenant le style `BOX_CLASS` partagé : (coin haut-droit) reprenant le style `BOX_CLASS` partagé :
@@ -316,6 +325,7 @@ quand l'`<img>` y est imbriqué pour récupérer `href` / `target`.
## Limitations connues ## Limitations connues
- Pas d'imbrication de listes. - Pas d'imbrication de listes.
- Image : URL uniquement, pas d'upload de fichier. La caption est une - Image : insertion par URL externe ou via le `MediaPicker` du module
string plate (pas de formatage inline pour l'instant). `media` (les uploads y sont gérés). La caption est une string plate
(pas de formatage inline pour l'instant).
- Tables : Phase 3. - Tables : Phase 3.
@@ -18,6 +18,9 @@ import {
SEPARATOR_VERTICAL_CLASS, SEPARATOR_VERTICAL_CLASS,
} from '../inline/menuStyles.js'; } from '../inline/menuStyles.js';
import { newBlockId } from '../utils/ids.js'; import { newBlockId } from '../utils/ids.js';
// Import relatif pour éviter une dépendance circulaire avec @zen/core (le bloc
// image vit dans `shared` et tire MediaPicker depuis `features/media`).
import MediaPicker from '../../../../features/media/components/MediaPicker.client.js';
// Bloc image. État vide = formulaire d'insertion d'URL. État rempli = image // Bloc image. État vide = formulaire d'insertion d'URL. État rempli = image
// rendue + toolbar flottante (alignement, lien, remplacer, supprimer) + // rendue + toolbar flottante (alignement, lien, remplacer, supprimer) +
@@ -44,6 +47,7 @@ function getAlign(block) {
function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) { function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
const [url, setUrl] = useState(initialUrl ?? ''); const [url, setUrl] = useState(initialUrl ?? '');
const [pickerOpen, setPickerOpen] = useState(false);
const inputRef = useRef(null); const inputRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -67,6 +71,7 @@ function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
} }
return ( return (
<>
<div className="flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/40 px-3 py-3"> <div className="flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/40 px-3 py-3">
<Image01Icon width={18} height={18} className="text-neutral-500" /> <Image01Icon width={18} height={18} className="text-neutral-500" />
<input <input
@@ -87,6 +92,14 @@ function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
> >
Insérer Insérer
</button> </button>
<button
type="button"
onClick={() => setPickerOpen(true)}
disabled={disabled}
className="px-3 py-1 text-sm rounded border border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700/60 disabled:opacity-50"
>
Choisir un média
</button>
{onCancel && ( {onCancel && (
<button <button
type="button" type="button"
@@ -97,6 +110,18 @@ function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
</button> </button>
)} )}
</div> </div>
<MediaPicker
isOpen={pickerOpen}
onClose={() => setPickerOpen(false)}
onSelect={(media) => {
if (media?.slug) onSubmit?.(`/zen/api/media/file/${media.slug}`);
}}
accept="image/*"
visibility="any"
uploadVisibility="public"
title="Insérer une image depuis la bibliothèque"
/>
</>
); );
} }