"use client";
// Port of Julio Caggiano's approved Editor Toolbar experiment.
// Undo/Redo preview motion independently; this component has no drawing canvas.
// Approved Lucide paths are distributed with their ISC license. Tooltip uses system monospace.
import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
import { Blocks, Brackets, ChevronDown, Circle, Frame, Hand, Minus, MousePointer2, PenTool, PencilLine, Redo2, Scaling, Square, Terminal, Type, Undo2 } from "./EditorToolbarIcons.jsx";
import { AnimatePresence, motion } from "motion/react";
import "./EditorToolbarDemo.css";
// Keep the standalone code example responsive to preference changes after mount.
function subscribeReducedMotion(onChange) {
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
query.addEventListener("change", onChange);
return () => query.removeEventListener("change", onChange);
}
function reducedMotionSnapshot() {
return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
const reducedMotionServerSnapshot = () => false;
const GROUP_TOOLS = {
cursor: ["cursor", "hand", "scale"],
frame: ["frame", "rectangle", "ellipse", "line", "section"],
pen: ["pen", "pencil"]
};
function SectionGlyph(props) {
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...props}>
<rect x="3.5" y="4" width="17" height="16" rx="2" strokeDasharray="2.5 2.5" />
<path d="M7.5 9h9" />
</svg>;
}
const TOOL_META = {
cursor: { label: "Cursor", Icon: MousePointer2 },
hand: { label: "Hand", Icon: Hand },
scale: { label: "Scale", Icon: Scaling },
frame: { label: "Frame", Icon: Frame },
rectangle: { label: "Rectangle", Icon: Square },
ellipse: { label: "Ellipse", Icon: Circle },
line: { label: "Line", Icon: Minus },
section: { label: "Section", Icon: SectionGlyph },
pen: { label: "Pen", Icon: PenTool },
pencil: { label: "Pencil", Icon: PencilLine },
text: { label: "Text", Icon: Type }
};
const TOOL_SHORTCUTS = {
cursor: "V",
hand: "H",
scale: "K",
frame: "F",
rectangle: "R",
ellipse: "O",
line: "L",
section: "\u21E7S",
pen: "P",
pencil: "\u21E7P",
text: "T"
};
const KEY_TO_TOOL = {
v: "cursor",
h: "hand",
k: "scale",
f: "frame",
r: "rectangle",
o: "ellipse",
l: "line",
p: "pen",
t: "text"
};
const TOOL_GROUP = {
cursor: "cursor",
hand: "cursor",
scale: "cursor",
frame: "frame",
rectangle: "frame",
ellipse: "frame",
line: "frame",
section: "frame",
pen: "pen",
pencil: "pen"
};
const toolTipLabel = (tool) => `${TOOL_META[tool].label} (${TOOL_SHORTCUTS[tool]})`;
const ariaShortcut = (tool) => TOOL_SHORTCUTS[tool].replace("\u21E7", "Shift+");
const appleShortcuts = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
const HISTORY_PATHS = {
undo: ["M9 14 4 9l5-5", "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5 5.5 5.5 0 0 1-5.5 5.5H11"],
redo: ["m15 14 5-5-5-5", "M20 9H9.5A5.5 5.5 0 0 0 4 14.5 5.5 5.5 0 0 0 9.5 20H13"]
};
function MoreIcon() {
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false">
<circle cx="4" cy="10" r="1.25" />
<circle cx="10" cy="10" r="1.25" />
<circle cx="16" cy="10" r="1.25" />
</svg>;
}
function InkTrace({ tool }) {
return <motion.svg
className="tlpg-editor-toolbar__ink-trace"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 0.55, 0] }}
transition={{ duration: 0.22, times: [0, 0.24, 1], ease: "easeOut" }}
>
{HISTORY_PATHS[tool].map((path) => <motion.path
key={path}
d={path}
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
/>)}
</motion.svg>;
}
export function EditorToolbarDemo({ compact = false, reducedMotion: reducedMotionOverride = false, historyRef }) {
const motionVariant = "quiet";
const moreOptions = [
{ id: "ai", label: "Generative AI", Icon: Terminal },
{ id: "plugins", label: "Plugins", Icon: Brackets },
{ id: "widgets", label: "Widgets", Icon: Blocks }
];
const [activeTool, setActiveTool] = useState(() => historyRef?.current?.activeTool ?? "cursor");
const [rememberedTools, setRememberedTools] = useState(() => historyRef?.current?.rememberedTools ?? {
cursor: "cursor",
frame: "frame",
pen: "pen"
});
const [stageWidth, setStageWidth] = useState(0);
const [pressedTool, setPressedTool] = useState(null);
const [historyFlash, setHistoryFlash] = useState(null);
const [historyPulse, setHistoryPulse] = useState(null);
const [openMenuId, setOpenMenuId] = useState(null);
const [moreFlash, setMoreFlash] = useState(false);
const [tooltip, setTooltip] = useState(null);
const [announcement, setAnnouncement] = useState("");
const [previewHover, setPreviewHover] = useState(null);
const [previewPointer, setPreviewPointer] = useState({ x: 364, y: 92, opacity: 0, pressed: false });
const stageRef = useRef(null);
const toolbarRef = useRef(null);
const menuWrapRefs = useRef({});
const menuTriggerRefs = useRef({});
const menuItemRefs = useRef({});
const openMenuRef = useRef(null);
const pinnedMenuRef = useRef(null);
const focusOnOpenRef = useRef(null);
const menuCloseTimer = useRef(null);
const moreFlashTimer = useRef(null);
const pulseSerial = useRef(0);
const historyFlashTimer = useRef(null);
const tooltipTimer = useRef(null);
const prefersReducedMotion = useSyncExternalStore(subscribeReducedMotion, reducedMotionSnapshot, reducedMotionServerSnapshot);
const reducedMotion = reducedMotionOverride || prefersReducedMotion;
useLayoutEffect(() => {
if (historyRef && !compact) historyRef.current = { activeTool, rememberedTools };
}, [activeTool, rememberedTools, compact, historyRef]);
useEffect(() => {
if (!compact) return;
const stage = stageRef.current;
if (!stage) return;
let visible = false;
let running = false;
let timers = [];
let nextTool = "pencil";
const resetPreview = () => {
nextTool = "pencil";
setActiveTool("cursor");
setRememberedTools({ cursor: "cursor", frame: "frame", pen: "pen" });
setOpenMenuId(null);
setHistoryPulse(null);
setPressedTool(null);
setPreviewHover(null);
setPreviewPointer({ x: 364, y: 92, opacity: 0, pressed: false });
};
const stopPreview = () => {
timers.forEach(clearTimeout);
timers = [];
running = false;
};
resetPreview();
if (reducedMotion) return;
// One legible action: approach the menu, choose a tool, inspect it, return.
// Coordinates follow the real controls through both thumbnail scale layers.
// Alternate Pen/Pencil so a visible remembered glyph never snaps back on loop.
const pointAt = (element) => {
const bar = toolbarRef.current;
if (!bar || !element) return;
const bounds = bar.getBoundingClientRect();
const target = element.getBoundingClientRect();
const scale = bounds.width / bar.offsetWidth;
if (!scale) return;
setPreviewPointer(current => ({ ...current,
x: (target.left + target.width / 2 - bounds.left) / scale,
y: (target.top + target.height / 2 - bounds.top) / scale,
}));
};
const press = (pressed) => setPreviewPointer(current => ({ ...current, pressed }));
const playCycle = () => {
timers = [];
const chosen = nextTool;
nextTool = chosen === "pencil" ? "pen" : "pencil";
const at = (delay, update) => timers.push(setTimeout(update, delay));
at(600, () => setPreviewPointer(current => ({ ...current, opacity: 1 })));
at(850, () => pointAt(menuTriggerRefs.current.pen));
at(1570, () => setPreviewHover("pen-trigger"));
at(1780, () => press(true));
at(1870, () => { press(false); setOpenMenuId("pen"); });
at(2280, () => { setPreviewHover(null); pointAt(menuItemRefs.current.pen?.[chosen === "pencil" ? 1 : 0]); });
at(3000, () => setPreviewHover(chosen));
at(3220, () => press(true));
at(3310, () => {
press(false);
setRememberedTools(current => ({ ...current, pen: chosen }));
setActiveTool(chosen);
});
at(3500, () => { setOpenMenuId(null); setPreviewHover(null); });
at(3850, () => setPreviewPointer(current => ({ ...current, x: 364, y: 92 })));
at(4800, () => pointAt(stage.querySelector(".tlpg-editor-toolbar__split-group--cursor .tlpg-editor-toolbar__tool")));
at(5520, () => setPreviewHover("cursor"));
at(5720, () => { press(true); setPressedTool("cursor"); });
at(5810, () => { press(false); setPressedTool(null); setActiveTool("cursor"); });
at(6120, () => {
setPreviewHover(null);
setPreviewPointer(current => ({ ...current, x: 364, y: 92 }));
});
at(6900, () => setPreviewPointer(current => ({ ...current, opacity: 0 })));
at(8400, playCycle);
};
const updatePlayback = () => {
if (visible && !document.hidden) {
if (!running) { running = true; playCycle(); }
} else {
stopPreview();
resetPreview();
}
};
const observer = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting && entry.intersectionRatio >= 0.35;
updatePlayback();
}, { threshold: 0.35 });
observer.observe(stage);
document.addEventListener("visibilitychange", updatePlayback);
return () => {
stopPreview();
observer.disconnect();
document.removeEventListener("visibilitychange", updatePlayback);
};
}, [compact, reducedMotion]);
useLayoutEffect(() => {
const stage = stageRef.current;
if (!stage) return;
// Measure the layout box: collection thumbnails are scaled by their host.
const resize = () => setStageWidth(stage.clientWidth);
resize();
const observer = new ResizeObserver(resize);
observer.observe(stage);
return () => {
observer.disconnect();
if (historyFlashTimer.current) clearTimeout(historyFlashTimer.current);
if (menuCloseTimer.current) clearTimeout(menuCloseTimer.current);
if (moreFlashTimer.current) clearTimeout(moreFlashTimer.current);
if (tooltipTimer.current) clearTimeout(tooltipTimer.current);
};
}, []);
const hideTooltip = () => {
if (tooltipTimer.current) clearTimeout(tooltipTimer.current);
tooltipTimer.current = null;
setTooltip(null);
};
const showTooltip = (button, label, immediate = false) => {
if (compact || openMenuRef.current) return;
const stage = stageRef.current;
if (!stage) return;
const stageBox = stage.getBoundingClientRect();
const buttonBox = button.getBoundingClientRect();
const position = {
label,
x: Math.min(Math.max(buttonBox.left + buttonBox.width / 2 - stageBox.left, Math.max(70, label.length * 4 + 11)), stageBox.width - Math.max(70, label.length * 4 + 11)),
y: buttonBox.top - stageBox.top - 13
};
if (tooltipTimer.current) clearTimeout(tooltipTimer.current);
if (immediate) setTooltip(position);
else tooltipTimer.current = setTimeout(() => setTooltip(position), 180);
};
const tooltipHandlers = (label) => ({
onPointerEnter: (event) => {
if (event.pointerType === "mouse") showTooltip(event.currentTarget, label);
},
onPointerLeave: hideTooltip,
onFocus: (event) => {
if (event.currentTarget.matches(":focus-visible")) showTooltip(event.currentTarget, label, true);
},
onBlur: hideTooltip
});
const closeMenu = useCallback((menuId, restoreFocus = false) => {
if (openMenuRef.current !== menuId) return;
if (menuCloseTimer.current) clearTimeout(menuCloseTimer.current);
openMenuRef.current = null;
pinnedMenuRef.current = null;
focusOnOpenRef.current = null;
setOpenMenuId(null);
if (restoreFocus) menuTriggerRefs.current[menuId]?.focus({ preventScroll: true });
}, []);
const showMenu = (menuId, focusItem, pin) => {
hideTooltip();
if (menuCloseTimer.current) clearTimeout(menuCloseTimer.current);
const wasOpen = openMenuRef.current === menuId;
openMenuRef.current = menuId;
pinnedMenuRef.current = pin ? menuId : null;
focusOnOpenRef.current = focusItem ? menuId : null;
setOpenMenuId(menuId);
if (wasOpen && focusItem) {
focusOnOpenRef.current = null;
const firstIndex = menuId === "more" ? 0 : GROUP_TOOLS[menuId].indexOf(rememberedTools[menuId]);
menuItemRefs.current[menuId]?.[Math.max(0, firstIndex)]?.focus({ preventScroll: true });
}
};
useEffect(() => {
if (compact || !openMenuId) return;
if (focusOnOpenRef.current === openMenuId) {
focusOnOpenRef.current = null;
const firstIndex = openMenuId === "more" ? 0 : GROUP_TOOLS[openMenuId].indexOf(rememberedTools[openMenuId]);
menuItemRefs.current[openMenuId]?.[Math.max(0, firstIndex)]?.focus({ preventScroll: true });
}
const onPointerDown = (event) => {
if (!menuWrapRefs.current[openMenuId]?.contains(event.target)) closeMenu(openMenuId);
};
const onKeyDown2 = (event) => {
if (event.key !== "Escape" || stageRef.current?.closest("[inert]") || !stageRef.current?.contains(event.target)) return;
event.preventDefault();
closeMenu(openMenuId, true);
};
globalThis.document.addEventListener("pointerdown", onPointerDown, true);
globalThis.document.addEventListener("keydown", onKeyDown2);
return () => {
globalThis.document.removeEventListener("pointerdown", onPointerDown, true);
globalThis.document.removeEventListener("keydown", onKeyDown2);
};
}, [compact, openMenuId, rememberedTools, closeMenu]);
const animateHistory = (tool) => {
if (reducedMotion) {
setHistoryFlash(tool);
if (historyFlashTimer.current) clearTimeout(historyFlashTimer.current);
historyFlashTimer.current = setTimeout(() => setHistoryFlash(null), 135);
} else {
setHistoryPulse({ tool, serial: ++pulseSerial.current });
}
};
const undo = () => {
hideTooltip();
animateHistory("undo");
setAnnouncement("Undo motion preview");
};
const redo = () => {
hideTooltip();
animateHistory("redo");
setAnnouncement("Redo motion preview");
};
const selectTool = (tool) => {
hideTooltip();
setActiveTool(tool);
setAnnouncement(`${TOOL_META[tool].label} tool selected`);
};
const selectToolByKey = (tool) => {
const group = TOOL_GROUP[tool];
if (group) setRememberedTools((current) => ({ ...current, [group]: tool }));
selectTool(tool);
};
const chooseGroupTool = (group, tool) => {
setRememberedTools((current) => ({ ...current, [group]: tool }));
selectTool(tool);
closeMenu(group, true);
};
const chooseMoreOption = (label) => {
setAnnouncement(`${label} option previewed`);
setMoreFlash(true);
if (moreFlashTimer.current) clearTimeout(moreFlashTimer.current);
moreFlashTimer.current = setTimeout(() => setMoreFlash(false), 240);
closeMenu("more", true);
};
const onMenuKeyDown = (event, menuId) => {
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
event.preventDefault();
const items = (menuItemRefs.current[menuId] ?? []).filter((item) => item !== null);
if (items.length === 0) return;
const current = items.indexOf(globalThis.document.activeElement);
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowDown" ? (current + 1) % items.length : (current - 1 + items.length) % items.length;
items[next]?.focus({ preventScroll: true });
};
const onKeyDown = (event) => {
if (event.key === "Escape" && !openMenuRef.current) {
selectToolByKey("cursor");
return;
}
const key = event.key.toLowerCase();
if (event.metaKey || event.ctrlKey) {
if (key === "z") {
event.preventDefault();
if (event.shiftKey) redo();
else undo();
}
return;
}
if (event.altKey || event.repeat || openMenuRef.current) return;
if (event.target.closest('input, textarea, select, [contenteditable="true"]')) return;
const tool = event.shiftKey ? key === "p" ? "pencil" : key === "s" ? "section" : null : KEY_TO_TOOL[key];
if (!tool) return;
event.preventDefault();
selectToolByKey(tool);
};
const pressHandlers = (tool, label) => ({
onPointerDown: () => {
hideTooltip();
setPressedTool(tool);
},
onPointerUp: () => setPressedTool(null),
onPointerCancel: () => setPressedTool(null),
onPointerEnter: tooltipHandlers(label).onPointerEnter,
onPointerLeave: () => {
hideTooltip();
setPressedTool(null);
},
onFocus: tooltipHandlers(label).onFocus,
onKeyDown: (event) => {
if (event.key === " " || event.key === "Enter") setPressedTool(tool);
},
onKeyUp: (event) => {
if (event.key === " " || event.key === "Enter") setPressedTool(null);
},
onBlur: () => {
hideTooltip();
setPressedTool(null);
}
});
const glyphMotion = (tool) => ({
animate: { x: 0, y: pressedTool === tool && !reducedMotion ? 1 : 0, scale: 1 },
transition: reducedMotion ? { duration: 0 } : { type: "spring", stiffness: 900, damping: 56, mass: 0.6 }
});
const selectionFace = () => <span className="tlpg-editor-toolbar__face" aria-hidden="true" />;
const mobile = !compact && stageWidth > 0 && stageWidth < 530;
const scale = 0.88 * (stageWidth === 0 ? 1 : Math.min(1, Math.max(0.2, (stageWidth - (compact ? 20 : 28)) / 451)));
const stageStyle = { "--tlpg-editor-toolbar-scale": scale };
const onMenuWrapBlur = (menuId, event) => {
if (event.currentTarget.contains(event.relatedTarget)) return;
const wrap = menuWrapRefs.current[menuId];
globalThis.requestAnimationFrame(() => {
if (wrap?.isConnected && !wrap.contains(globalThis.document.activeElement)) closeMenu(menuId);
});
};
const renderSplitGroup = (group) => {
const currentTool = rememberedTools[group];
const { Icon, label } = TOOL_META[currentTool];
const isOpen = openMenuId === group;
return <div
key={group}
ref={(node) => {
menuWrapRefs.current[group] = node;
}}
className={`tlpg-editor-toolbar__split-group tlpg-editor-toolbar__split-group--${group}`}
data-open={isOpen}
onBlur={(event) => onMenuWrapBlur(group, event)}
>
<button
type="button"
className={`tlpg-editor-toolbar__tool tlpg-editor-toolbar__tool--split${activeTool === currentTool ? " is-selected" : ""}`}
aria-label={`${label} tool`}
aria-keyshortcuts={ariaShortcut(currentTool)}
aria-pressed={activeTool === currentTool}
data-preview-hover={compact && previewHover === currentTool}
data-pressed={pressedTool === currentTool}
tabIndex={compact ? -1 : void 0}
onClick={() => {
if (openMenuRef.current) closeMenu(openMenuRef.current);
selectTool(currentTool);
}}
{...pressHandlers(currentTool, toolTipLabel(currentTool))}
>
{selectionFace()}
<motion.span className="tlpg-editor-toolbar__glyph" {...glyphMotion(currentTool)}>
{reducedMotion ? <Icon aria-hidden="true" strokeWidth={1.8} /> : <AnimatePresence initial={false}>
<motion.span
key={currentTool}
className="tlpg-editor-toolbar__glyph-swap"
initial={{ opacity: 0, y: 2, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -1, scale: 0.99, transition: { duration: 0.08, ease: "easeOut" } }}
transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
>
<Icon aria-hidden="true" strokeWidth={1.8} />
</motion.span>
</AnimatePresence>}
</motion.span>
</button>
<button
ref={(node) => {
menuTriggerRefs.current[group] = node;
}}
type="button"
className="tlpg-editor-toolbar__split-trigger"
data-preview-hover={compact && previewHover === `${group}-trigger`}
data-preview-pressed={compact && previewHover === `${group}-trigger` && previewPointer.pressed}
aria-label={`Choose ${TOOL_META[group].label} tool`}
aria-haspopup="menu"
aria-expanded={isOpen}
data-open={isOpen}
tabIndex={compact ? -1 : void 0}
onClick={() => {
if (openMenuRef.current === group) closeMenu(group);
else showMenu(group, true, true);
}}
onKeyDown={(event) => {
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
event.preventDefault();
showMenu(group, true, true);
}}
>
<ChevronDown aria-hidden="true" strokeWidth={1.8} />
</button>
<div className="tlpg-editor-toolbar__menu-placement" data-open={isOpen}>
<div
className="tlpg-editor-toolbar__menu"
role="menu"
aria-label={`${TOOL_META[group].label} tools`}
aria-hidden={!isOpen}
inert={!isOpen}
onKeyDown={(event) => onMenuKeyDown(event, group)}
>
{GROUP_TOOLS[group].map((tool, index) => {
const { Icon: RowIcon, label: rowLabel } = TOOL_META[tool];
const isCurrent = rememberedTools[group] === tool;
return <button
key={tool}
ref={(node) => {
if (!menuItemRefs.current[group]) menuItemRefs.current[group] = [];
menuItemRefs.current[group][index] = node;
}}
type="button"
className="tlpg-editor-toolbar__menu-item"
role="menuitemradio"
aria-checked={isCurrent}
data-current={isCurrent}
data-preview-hover={compact && previewHover === tool}
tabIndex={isOpen && isCurrent ? 0 : -1}
onClick={() => chooseGroupTool(group, tool)}
>
<RowIcon aria-hidden="true" strokeWidth={1.8} />
{rowLabel}
</button>;
})}
</div>
</div>
</div>;
};
return <div
ref={stageRef}
className={`tlpg-editor-toolbar-stage tlpg-editor-toolbar-stage--${motionVariant}${compact ? " tlpg-editor-toolbar-stage--compact" : ""}${mobile ? " tlpg-editor-toolbar-stage--mobile" : ""}`}
data-motion-variant={motionVariant}
data-reduced={reducedMotion ? "true" : void 0}
style={stageStyle}
onKeyDown={compact ? void 0 : onKeyDown}
role={compact ? void 0 : "group"}
inert={compact}
aria-hidden={compact ? true : void 0}
aria-label={compact ? void 0 : "Creative editor toolbar microinteraction experiment"}
>
<div ref={toolbarRef} className="tlpg-editor-toolbar" role="toolbar" aria-label="Creative tools">
<div className="tlpg-editor-toolbar__history" role="group" aria-label="Undo and redo previews">
<button
type="button"
className="tlpg-editor-toolbar__history-button"
aria-label="Undo"
aria-keyshortcuts="Meta+Z Control+Z"
data-pressed={pressedTool === "undo" || historyFlash === "undo"}
tabIndex={compact ? -1 : void 0}
onClick={undo}
{...pressHandlers("undo", appleShortcuts ? "Undo (\u2318Z)" : "Undo (Ctrl+Z)")}
>
<span className="tlpg-editor-toolbar__history-glyph">
<Undo2 aria-hidden="true" strokeWidth={1.8} />
{historyPulse?.tool === "undo" && !reducedMotion && <InkTrace key={historyPulse.serial} tool="undo" />}
</span>
</button>
<button
type="button"
className="tlpg-editor-toolbar__history-button"
aria-label="Redo"
aria-keyshortcuts="Meta+Shift+Z Control+Shift+Z"
data-pressed={pressedTool === "redo" || historyFlash === "redo"}
tabIndex={compact ? -1 : void 0}
onClick={redo}
{...pressHandlers("redo", appleShortcuts ? "Redo (\u2318\u21E7Z)" : "Redo (Ctrl+Shift+Z)")}
>
<span className="tlpg-editor-toolbar__history-glyph">
<Redo2 aria-hidden="true" strokeWidth={1.8} />
{historyPulse?.tool === "redo" && !reducedMotion && <InkTrace key={historyPulse.serial} tool="redo" />}
</span>
</button>
</div>
<span className="tlpg-editor-toolbar__divider" aria-hidden="true" />
<div className="tlpg-editor-toolbar__tools" role="group" aria-label="Canvas tools">
{renderSplitGroup("cursor")}
{renderSplitGroup("frame")}
{renderSplitGroup("pen")}
<button
type="button"
className={`tlpg-editor-toolbar__tool${activeTool === "text" ? " is-selected" : ""}`}
aria-label="Text"
aria-keyshortcuts="T"
aria-pressed={activeTool === "text"}
data-pressed={pressedTool === "text"}
tabIndex={compact ? -1 : void 0}
onClick={() => {
if (openMenuRef.current) closeMenu(openMenuRef.current);
selectTool("text");
}}
{...pressHandlers("text", toolTipLabel("text"))}
>
{selectionFace()}
<motion.span className="tlpg-editor-toolbar__glyph" {...glyphMotion("text")}><Type aria-hidden="true" strokeWidth={1.8} /></motion.span>
</button>
</div>
<div
ref={(node) => {
menuWrapRefs.current.more = node;
}}
className="tlpg-editor-toolbar__more-wrap"
onPointerEnter={(event) => {
if (event.pointerType === "mouse" && !compact && openMenuRef.current !== "more") showMenu("more", false, false);
}}
onPointerLeave={() => {
if (pinnedMenuRef.current === "more" || menuWrapRefs.current.more?.contains(globalThis.document.activeElement)) return;
if (menuCloseTimer.current) clearTimeout(menuCloseTimer.current);
menuCloseTimer.current = setTimeout(() => closeMenu("more"), 120);
}}
onBlur={(event) => onMenuWrapBlur("more", event)}
>
<button
ref={(node) => {
menuTriggerRefs.current.more = node;
}}
type="button"
className="tlpg-editor-toolbar__tool tlpg-editor-toolbar__tool--more"
aria-label="More options"
aria-haspopup="menu"
aria-expanded={openMenuId === "more"}
data-open={openMenuId === "more"}
data-flash={moreFlash}
data-pressed={pressedTool === "more"}
tabIndex={compact ? -1 : void 0}
{...pressHandlers("more", "More options")}
onClick={() => {
if (pinnedMenuRef.current === "more") closeMenu("more");
else showMenu("more", true, true);
}}
onKeyDown={(event) => {
if (event.key === " " || event.key === "Enter") setPressedTool("more");
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
event.preventDefault();
showMenu("more", true, true);
}}
>
{selectionFace()}
<motion.span className="tlpg-editor-toolbar__glyph" {...glyphMotion("more")}><MoreIcon /></motion.span>
</button>
<div className="tlpg-editor-toolbar__menu-placement" data-open={openMenuId === "more"}>
<div
className="tlpg-editor-toolbar__menu"
role="menu"
aria-label="More options"
aria-hidden={openMenuId !== "more"}
inert={openMenuId !== "more"}
onKeyDown={(event) => onMenuKeyDown(event, "more")}
>
{moreOptions.map(({ id, label, Icon }, index) => <button
key={id}
ref={(node) => {
if (!menuItemRefs.current.more) menuItemRefs.current.more = [];
menuItemRefs.current.more[index] = node;
}}
type="button"
className="tlpg-editor-toolbar__menu-item"
role="menuitem"
tabIndex={openMenuId === "more" && index === 0 ? 0 : -1}
onClick={() => chooseMoreOption(label)}
>
<Icon aria-hidden="true" strokeWidth={1.8} />
{label}
</button>)}
</div>
</div>
</div>
{compact && !reducedMotion && <motion.span
className="tlpg-editor-toolbar__preview-pointer"
aria-hidden="true"
initial={false}
animate={{ x: previewPointer.x, y: previewPointer.y, opacity: previewPointer.opacity }}
transition={{ x: { duration: 0.7, ease: [0.45, 0, 0.2, 1] }, y: { duration: 0.7, ease: [0.45, 0, 0.2, 1] }, opacity: { duration: 0.18 } }}
>
<motion.svg viewBox="0 0 24 28" fill="#202020" stroke="#fafafa" strokeWidth="1.4" strokeLinejoin="round"
animate={{ scale: previewPointer.pressed ? 0.88 : 1 }}
transition={{ duration: 0.1, ease: [0.22, 1, 0.36, 1] }}
><path d="M2 2v20l5.5-5 4.4 9 4-2-4.4-8.5H20Z" /></motion.svg>
</motion.span>}
</div>
<AnimatePresence>
{tooltip && !compact && <motion.div
key="tool-tip"
className="tlpg-editor-toolbar__tooltip-placement"
style={{ left: tooltip.x, top: tooltip.y }}
aria-hidden="true"
initial={{ opacity: 0, y: reducedMotion ? 0 : 3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: reducedMotion ? 0 : 2 }}
transition={{ duration: reducedMotion ? 0 : 0.13, ease: [0.22, 1, 0.36, 1] }}
>
<span className="tlpg-editor-toolbar__tooltip">{tooltip.label}</span>
</motion.div>}
</AnimatePresence>
<span className="tlpg-editor-toolbar__sr-only" role="status" aria-live="polite">{announcement}</span>
</div>;
}