refactor(block-editor): extract shared menu styles into dedicated module
- add `menuStyles.js` with reusable `BOX_CLASS`, `ITEM_CLASS`, `ITEM_DANGER_CLASS`, and `SEPARATOR_CLASS` constants - replace inline tailwind strings in `Block.client.js` with imported style constants - update `BlockEditor.client.js`, `LinkPopover.client.js`, and `Toolbar.client.js` to use shared menu styles - update `README.md` to document the new `menuStyles.js` file
This commit is contained in:
@@ -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
|
||||
<div
|
||||
ref={ref}
|
||||
data-link-popover
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 50 }}
|
||||
className="flex flex-col gap-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5"
|
||||
style={{ top: pos.top, left: pos.left }}
|
||||
className={`absolute z-50 flex flex-col gap-1.5 p-2 ${BOX_CLASS}`}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { TextColorIcon, HighlighterIcon, Link02Icon, CodeSimpleIcon, TextClearIcon } from '@zen/core/shared/icons';
|
||||
import {
|
||||
TextColorIcon,
|
||||
HighlighterIcon,
|
||||
Link02Icon,
|
||||
CodeSimpleIcon,
|
||||
TextClearIcon,
|
||||
ArrowDown01Icon,
|
||||
} from '@zen/core/shared/icons';
|
||||
import { INLINE_COLORS, INLINE_COLOR_KEYS, markKey } from './types.js';
|
||||
import {
|
||||
BOX_CLASS,
|
||||
ICON_BTN_CLASS,
|
||||
ICON_BTN_ACTIVE_CLASS,
|
||||
SEPARATOR_VERTICAL_CLASS,
|
||||
} from './menuStyles.js';
|
||||
|
||||
// Toolbar flottant de formatage. Affiché tant qu'une sélection non-vide
|
||||
// existe dans un bloc. Ancré au-dessus du rect de sélection ; flip en
|
||||
// dessous si pas assez de place.
|
||||
//
|
||||
// Ne contient pas d'état métier — tous les changements remontent via
|
||||
// `onToggleMark(mark)`. Le parent recalcule `activeMarks` à chaque rendu.
|
||||
// Toolbar flottant de formatage. Rendu en `position: absolute` à l'intérieur
|
||||
// du container relatif du BlockEditor : il scrolle naturellement avec le
|
||||
// texte. `rect` est exprimé en coords locales au container (top/left/bottom
|
||||
// relatifs au content origin du container). La décision flip-up vs flip-down
|
||||
// utilise la position viewport du container pour rester proche de la
|
||||
// sélection visible.
|
||||
|
||||
const TOOLBAR_HEIGHT = 36;
|
||||
const TOOLBAR_GAP = 8;
|
||||
const VIEWPORT_MARGIN = 8;
|
||||
const SUBMENU_CLOSE_DELAY = 120;
|
||||
|
||||
const SIMPLE_BUTTONS = [
|
||||
{ type: 'bold', label: 'B', title: 'Gras (Ctrl+B)', className: 'font-bold' },
|
||||
@@ -26,33 +40,63 @@ const SIMPLE_BUTTONS = [
|
||||
export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMark, onClearMarks, onPinChange, usedColors }) {
|
||||
const ref = useRef(null);
|
||||
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
|
||||
const [popover, setPopover] = useState(null);
|
||||
const [openSubmenu, setOpenSubmenu] = useState(null);
|
||||
const submenuTimerRef = useRef(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [linkNewTab, setLinkNewTab] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onPinChange?.(popover !== null);
|
||||
}, [popover, onPinChange]);
|
||||
onPinChange?.(openSubmenu !== null);
|
||||
}, [openSubmenu, onPinChange]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!rect || typeof window === 'undefined') return;
|
||||
const width = ref.current?.offsetWidth ?? 280;
|
||||
const height = ref.current?.offsetHeight ?? TOOLBAR_HEIGHT;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const spaceAbove = rect.top - VIEWPORT_MARGIN;
|
||||
const flipBelow = spaceAbove < height + TOOLBAR_GAP;
|
||||
let top = flipBelow
|
||||
? rect.bottom + TOOLBAR_GAP
|
||||
: rect.top - height - TOOLBAR_GAP;
|
||||
let left = rect.left + rect.width / 2 - width / 2;
|
||||
if (left + width + VIEWPORT_MARGIN > vw) left = vw - width - VIEWPORT_MARGIN;
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const w = el.offsetWidth || 280;
|
||||
const h = el.offsetHeight || TOOLBAR_HEIGHT;
|
||||
const container = el.offsetParent;
|
||||
const containerW = container?.clientWidth ?? Infinity;
|
||||
const containerVPTop = container?.getBoundingClientRect?.().top ?? 0;
|
||||
const selectionVPTop = containerVPTop + rect.top;
|
||||
const spaceAbove = selectionVPTop - VIEWPORT_MARGIN;
|
||||
const flipBelow = spaceAbove < h + TOOLBAR_GAP;
|
||||
let top = flipBelow ? rect.bottom + TOOLBAR_GAP : rect.top - h - TOOLBAR_GAP;
|
||||
let left = rect.left + rect.width / 2 - w / 2;
|
||||
if (left + w + VIEWPORT_MARGIN > containerW) left = containerW - w - VIEWPORT_MARGIN;
|
||||
if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
|
||||
if (top < VIEWPORT_MARGIN) top = VIEWPORT_MARGIN;
|
||||
if (top + height + VIEWPORT_MARGIN > vh) top = vh - height - VIEWPORT_MARGIN;
|
||||
if (top < 0) top = 0;
|
||||
setPos({ top, left, flipped: flipBelow });
|
||||
}, [rect]);
|
||||
|
||||
function scheduleClose() {
|
||||
if (submenuTimerRef.current) clearTimeout(submenuTimerRef.current);
|
||||
submenuTimerRef.current = setTimeout(() => setOpenSubmenu(null), SUBMENU_CLOSE_DELAY);
|
||||
}
|
||||
function cancelClose() {
|
||||
if (submenuTimerRef.current) {
|
||||
clearTimeout(submenuTimerRef.current);
|
||||
submenuTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
useEffect(() => () => {
|
||||
if (submenuTimerRef.current) clearTimeout(submenuTimerRef.current);
|
||||
}, []);
|
||||
|
||||
function openSubmenuFor(key) {
|
||||
cancelClose();
|
||||
if (openSubmenu !== key && key === 'link') {
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
setLinkUrl(link?.href ?? '');
|
||||
setLinkNewTab(link ? !!link.newTab : false);
|
||||
}
|
||||
setOpenSubmenu(key);
|
||||
}
|
||||
|
||||
function closeSubmenu() {
|
||||
cancelClose();
|
||||
setOpenSubmenu(null);
|
||||
}
|
||||
|
||||
function isActive(type, payloadKey) {
|
||||
if (!Array.isArray(activeMarks)) return false;
|
||||
@@ -66,41 +110,33 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
|
||||
function handleColor(color) {
|
||||
onToggleMark?.({ type: 'color', color });
|
||||
setPopover(null);
|
||||
closeSubmenu();
|
||||
}
|
||||
|
||||
function handleHighlight(color) {
|
||||
onToggleMark?.({ type: 'highlight', color });
|
||||
setPopover(null);
|
||||
closeSubmenu();
|
||||
}
|
||||
|
||||
function handleLinkSubmit(e) {
|
||||
e.preventDefault?.();
|
||||
e?.preventDefault?.();
|
||||
if (!linkUrl) return;
|
||||
onSetMark?.({ type: 'link', href: linkUrl, newTab: linkNewTab });
|
||||
setLinkUrl('');
|
||||
setPopover(null);
|
||||
closeSubmenu();
|
||||
}
|
||||
|
||||
function handleLinkRemove() {
|
||||
// Trouver la mark active pour reproduire la même clé (toggle off).
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
if (link) onToggleMark?.({ type: 'link', href: link.href, ...(link.newTab ? { newTab: true } : {}) });
|
||||
setPopover(null);
|
||||
}
|
||||
|
||||
function openLinkPopover() {
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
setLinkUrl(link?.href ?? '');
|
||||
setLinkNewTab(link ? !!link.newTab : false);
|
||||
setPopover(p => (p === 'link' ? null : 'link'));
|
||||
closeSubmenu();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-inline-toolbar
|
||||
className="fixed z-50 flex items-center gap-0.5 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-1 py-1"
|
||||
className={`absolute z-50 flex items-center gap-0.5 p-1 ${BOX_CLASS}`}
|
||||
style={{ top: pos.top, left: pos.left }}
|
||||
>
|
||||
{SIMPLE_BUTTONS.map(btn => (
|
||||
@@ -110,117 +146,166 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
title={btn.title}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleSimple(btn.type)}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 ${btn.className} ${isActive(btn.type) ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
className={`${ICON_BTN_CLASS} ${btn.className} ${isActive(btn.type) ? ICON_BTN_ACTIVE_CLASS : ''}`}
|
||||
>
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<span className="mx-1 w-px h-5 bg-neutral-200 dark:bg-neutral-700" aria-hidden />
|
||||
<span className={SEPARATOR_VERTICAL_CLASS} aria-hidden />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<SubmenuTrigger
|
||||
title="Couleur du texte"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setPopover(p => (p === 'color' ? null : 'color'))}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 ${isActive('color') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
active={isActive('color')}
|
||||
open={openSubmenu === 'color'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('color')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
icon={<TextColorIcon width={16} height={16} />}
|
||||
>
|
||||
<TextColorIcon width={16} height={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Surlignage"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setPopover(p => (p === 'highlight' ? null : 'highlight'))}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 ${isActive('highlight') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
>
|
||||
<HighlighterIcon width={16} height={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Lien (Ctrl+K)"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={openLinkPopover}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 ${isActive('link') ? 'bg-neutral-100 dark:bg-neutral-800' : ''}`}
|
||||
>
|
||||
<Link02Icon width={16} height={16} />
|
||||
</button>
|
||||
|
||||
<span className="mx-1 w-px h-5 bg-neutral-200 dark:bg-neutral-700" aria-hidden />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Effacer le formatage"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onClearMarks?.()}
|
||||
className="w-7 h-7 flex items-center justify-center rounded text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<TextClearIcon width={16} height={16} />
|
||||
</button>
|
||||
|
||||
{popover === 'color' && (
|
||||
<ColorGrid
|
||||
mode="text"
|
||||
activeKey={activeMarks.find(m => m.type === 'color')?.color}
|
||||
usedColors={usedColors?.color}
|
||||
onPick={handleColor}
|
||||
/>
|
||||
)}
|
||||
{popover === 'highlight' && (
|
||||
</SubmenuTrigger>
|
||||
|
||||
<SubmenuTrigger
|
||||
title="Surlignage"
|
||||
active={isActive('highlight')}
|
||||
open={openSubmenu === 'highlight'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('highlight')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
icon={<HighlighterIcon width={16} height={16} />}
|
||||
>
|
||||
<ColorGrid
|
||||
mode="highlight"
|
||||
activeKey={activeMarks.find(m => m.type === 'highlight')?.color}
|
||||
usedColors={usedColors?.highlight}
|
||||
onPick={handleHighlight}
|
||||
/>
|
||||
)}
|
||||
{popover === 'link' && (
|
||||
</SubmenuTrigger>
|
||||
|
||||
<SubmenuTrigger
|
||||
title="Lien (Ctrl+K)"
|
||||
active={isActive('link')}
|
||||
open={openSubmenu === 'link'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('link')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
icon={<Link02Icon width={16} height={16} />}
|
||||
>
|
||||
<LinkForm
|
||||
url={linkUrl}
|
||||
newTab={linkNewTab}
|
||||
showRemove={isActive('link')}
|
||||
onUrlChange={setLinkUrl}
|
||||
onNewTabChange={setLinkNewTab}
|
||||
onSubmit={handleLinkSubmit}
|
||||
onRemove={handleLinkRemove}
|
||||
/>
|
||||
</SubmenuTrigger>
|
||||
|
||||
<span className={SEPARATOR_VERTICAL_CLASS} aria-hidden />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Effacer le formatage"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onClearMarks?.()}
|
||||
className={ICON_BTN_CLASS}
|
||||
>
|
||||
<TextClearIcon width={16} height={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Wrapper hover-to-open pour les boutons de la toolbar avec sous-menu. La
|
||||
// fenêtre de tolérance (120 ms) entre `mouseleave` du trigger et
|
||||
// `mouseenter` du panneau permet de traverser le gap visuel.
|
||||
function SubmenuTrigger({ title, active, open, flipUp, icon, children, onMouseEnter, onMouseLeave, onPanelEnter, onPanelLeave }) {
|
||||
const panelPosition = flipUp ? 'bottom-full mb-1' : 'top-full mt-1';
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className={`h-7 px-1.5 flex items-center gap-0.5 rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out ${active || open ? ICON_BTN_ACTIVE_CLASS : ''}`}
|
||||
>
|
||||
{icon}
|
||||
<ArrowDown01Icon width={10} height={10} className="opacity-60" />
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
className="absolute top-full left-0 mt-1 flex flex-col gap-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5"
|
||||
className={`absolute ${panelPosition} left-0 ${BOX_CLASS} p-1.5 z-50`}
|
||||
onMouseEnter={onPanelEnter}
|
||||
onMouseLeave={onPanelLeave}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleLinkSubmit(e); } }}
|
||||
className="w-56 px-2 py-1 text-sm rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLinkSubmit}
|
||||
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
{isActive('link') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLinkRemove}
|
||||
title="Retirer le lien"
|
||||
className="px-2 py-1 text-xs rounded text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-neutral-700 dark:text-neutral-300 select-none cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={linkNewTab}
|
||||
onChange={(e) => setLinkNewTab(e.target.checked)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Ouvrir dans un nouvel onglet
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkForm({ url, newTab, showRemove, onUrlChange, onNewTabChange, onSubmit, onRemove }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
value={url}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); onSubmit(e); } }}
|
||||
className="w-56 px-2 py-1 text-sm rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
className="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
{showRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
title="Retirer le lien"
|
||||
className="px-2 py-1 text-xs rounded text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-neutral-700 dark:text-neutral-300 select-none cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newTab}
|
||||
onChange={(e) => onNewTabChange(e.target.checked)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Ouvrir dans un nouvel onglet
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const USED_COLORS_LIMIT = 8;
|
||||
|
||||
function ColorGrid({ mode, activeKey, usedColors, onPick }) {
|
||||
@@ -238,7 +323,7 @@ function ColorGrid({ mode, activeKey, usedColors, onPick }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-full left-0 mt-1 flex items-center gap-1 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md px-2 py-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
{INLINE_COLOR_KEYS.map(key => {
|
||||
const palette = INLINE_COLORS[key];
|
||||
const tw = isText ? palette.text : palette.highlight;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Identité visuelle partagée par les dropdowns du BlockEditor (BlockActionsMenu,
|
||||
// BlockInsertMenu, InlineToolbar, LinkPopover). Constantes de className
|
||||
// uniquement — pas de JSX, pas de logique. Le but est d'aligner les
|
||||
// quatre menus sur la même boîte arrondie / mêmes hover states sans
|
||||
// imposer un wrapper de composant qui ne conviendrait pas aux trois
|
||||
// formes très différentes (liste verticale, barre horizontale, formulaire).
|
||||
|
||||
export const BOX_CLASS =
|
||||
'rounded-xl border border-black/8 dark:border-white/8 bg-white dark:bg-[#0B0B0B] shadow-lg';
|
||||
|
||||
export const ITEM_CLASS =
|
||||
'cursor-pointer w-full flex items-center gap-2 px-[7px] py-[10px] rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out text-left';
|
||||
|
||||
export const ITEM_DANGER_CLASS =
|
||||
'cursor-pointer w-full flex items-center gap-2 px-[7px] py-[10px] rounded-lg text-[13px] leading-none text-red-700 dark:text-red-600 hover:bg-red-700/10 dark:hover:bg-red-700/20 transition-colors duration-150 text-left';
|
||||
|
||||
export const ICON_BTN_CLASS =
|
||||
'w-7 h-7 flex items-center justify-center rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out';
|
||||
|
||||
export const ICON_BTN_ACTIVE_CLASS =
|
||||
'bg-neutral-100 dark:bg-white/5 text-neutral-900 dark:text-white';
|
||||
|
||||
export const SEPARATOR_CLASS = 'h-px bg-black/6 dark:bg-white/6 my-0.5';
|
||||
|
||||
export const SEPARATOR_VERTICAL_CLASS = 'w-px h-5 bg-black/6 dark:bg-white/6 mx-1 self-center';
|
||||
Reference in New Issue
Block a user