diff --git a/src/shared/components/BlockEditor/Block.client.js b/src/shared/components/BlockEditor/Block.client.js index 37c20e5..6c66bf7 100644 --- a/src/shared/components/BlockEditor/Block.client.js +++ b/src/shared/components/BlockEditor/Block.client.js @@ -2,6 +2,7 @@ import React, { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, forwardRef } from 'react'; import { Add01Icon, ArrowRight01Icon, Copy01Icon, Delete02Icon, DragDropVerticalIcon, RepeatIcon } from '@zen/core/shared/icons'; +import { BOX_CLASS, ITEM_CLASS, ITEM_DANGER_CLASS, SEPARATOR_CLASS } from './inline/menuStyles.js'; // Style « boîte » pour l'icône d'un type de bloc, repris du SlashMenu. const TYPE_ICON_BOX_CLASS = 'w-8 h-8 flex items-center justify-center rounded-md border border-neutral-200 dark:border-neutral-700 text-xs font-medium text-neutral-700 dark:text-neutral-300 flex-shrink-0'; @@ -111,7 +112,7 @@ function BlockInsertMenu({ {open && (
{error}
)} diff --git a/src/shared/components/BlockEditor/README.md b/src/shared/components/BlockEditor/README.md index 53abaec..8d41c16 100644 --- a/src/shared/components/BlockEditor/README.md +++ b/src/shared/components/BlockEditor/README.md @@ -123,16 +123,30 @@ Quand une sélection non-vide existe dans un bloc, un toolbar flottant apparaît au-dessus. Il propose : - **B I U S `>`** — marks simples (toggle) -- **A** — couleur du texte (popover : palette par défaut + couleurs déjà - utilisées dans le document + bouton `+` pour une couleur libre via - ``) -- **◐** — surlignage (même structure de popover) -- **🔗** — lien (popover avec input URL ; ✕ pour retirer) +- **A** — couleur du texte (sous-menu drop-down : palette par défaut + + couleurs déjà utilisées dans le document + bouton `+` pour une couleur + libre via ``) +- **◐** — surlignage (même structure de sous-menu) +- **🔗** — lien (sous-menu drop-down avec input URL ; ✕ pour retirer) - **T/** — effacer tout le formatage de la sélection (supprime toutes les marks) L'état actif est calculé à partir des marks **communes à toute la plage** (via `marksInRange`). Toggle off si toute la plage est déjà marquée. +La toolbar est rendue en `position: absolute` à l'intérieur du container +relatif de l'éditeur et **suit la sélection lors du scroll**. Les sous-menus +(couleur, surlignage, lien) s'ouvrent **au survol** avec une fenêtre de +fermeture de 120 ms — même idiome que le sous-menu *Transformer ▸* du menu +d'actions, mais en drop-down (vers le bas) plutôt qu'à droite. Une petite +flèche `▾` après le glyphe principal indique la présence du sous-menu. + +## Popover de lien + +Au simple clic dans un lien existant, un popover s'ouvre sous le `` avec +l'URL et la case « Ouvrir dans un nouvel onglet ». Il est rendu en +`position: absolute` dans le container de l'éditeur et **suit le lien lors +du scroll**. + ## Sélection multi-blocs Deux façons d'entrer en mode sélection multi-blocs : @@ -180,6 +194,16 @@ Sinon (clic sans déplacement), le menu s'ouvre normalement. Les dropdowns sont des composants maison (pas de Headless UI ici) car `MenuButton` ouvrait sur `pointerdown`, ce qui empêchait le clic-maintenu nécessaire au drag. +## Identité visuelle partagée + +Les quatre dropdowns (menu d'insertion, menu d'actions du bloc, toolbar +de formatage, popover de lien) partagent les classes `BOX_CLASS`, +`ITEM_CLASS`, `ITEM_DANGER_CLASS`, `ICON_BTN_CLASS` et `SEPARATOR_CLASS` +exportées par [`inline/menuStyles.js`](./inline/menuStyles.js). Pas de +wrapper de composant — juste des constantes Tailwind, parce que les trois +formes (liste verticale, barre horizontale, formulaire) sont trop +différentes pour qu'une abstraction soit rentable. + ## Étendre — enregistrer un bloc custom ```js diff --git a/src/shared/components/BlockEditor/inline/LinkPopover.client.js b/src/shared/components/BlockEditor/inline/LinkPopover.client.js index b86bc85..8f4111b 100644 --- a/src/shared/components/BlockEditor/inline/LinkPopover.client.js +++ b/src/shared/components/BlockEditor/inline/LinkPopover.client.js @@ -1,6 +1,13 @@ 'use client'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { BOX_CLASS } from './menuStyles.js'; + +// Popover ouvert au simple clic sur un lien existant. Rendu en `position: +// absolute` à l'intérieur du container relatif du BlockEditor : il scrolle +// avec le texte. `rect` est en coords locales au container (top/left/bottom). +// Flip-up vs flip-down se base sur la position viewport du container pour +// rester proche du lien visible. const GAP = 8; const VIEWPORT_MARGIN = 8; @@ -13,16 +20,22 @@ export default function LinkPopover({ rect, mark, onSetLink, onRemoveLink, onClo useLayoutEffect(() => { if (!rect || typeof window === 'undefined') return; - const width = ref.current?.offsetWidth ?? 280; - const height = ref.current?.offsetHeight ?? 80; - const vw = window.innerWidth; + const el = ref.current; + if (!el) return; + const w = el.offsetWidth || 280; + const h = el.offsetHeight || 80; + const container = el.offsetParent; + const containerW = container?.clientWidth ?? Infinity; + const containerVPRect = container?.getBoundingClientRect?.(); + const linkVPBottom = (containerVPRect?.top ?? 0) + rect.bottom; const vh = window.innerHeight; - let top = rect.bottom + GAP; + const spaceBelow = vh - linkVPBottom - VIEWPORT_MARGIN; + const flipUp = spaceBelow < h + GAP; + let top = flipUp ? rect.top - h - GAP : rect.bottom + GAP; let left = rect.left; - if (left + width + VIEWPORT_MARGIN > vw) left = vw - width - VIEWPORT_MARGIN; + if (left + w + VIEWPORT_MARGIN > containerW) left = containerW - w - VIEWPORT_MARGIN; if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN; - if (top + height + VIEWPORT_MARGIN > vh) top = rect.top - height - GAP; - if (top < VIEWPORT_MARGIN) top = VIEWPORT_MARGIN; + if (top < 0) top = 0; setPos({ top, left }); }, [rect]); @@ -44,8 +57,8 @@ export default function LinkPopover({ rect, mark, onSetLink, onRemoveLink, onClo