diff --git a/frontend/src/components/Toolbar.jsx b/frontend/src/components/Toolbar.jsx index 6a6f96e..6973cd5 100644 --- a/frontend/src/components/Toolbar.jsx +++ b/frontend/src/components/Toolbar.jsx @@ -1,10 +1,64 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useEffect } from 'react' export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeChange }) { const fileRef = useRef() const [isReading, setIsReading] = useState(false) const isReadingRef = useRef(false) // ref so closure in next() always sees current value + // ── Typewriter mode ─────────────────────────────────────────────────────── + const [typewriterMode, setTypewriterMode] = useState(false) + const typewriterRef = useRef(false) + const twRafRef = useRef(null) + + // Keep ref in sync so the editor event closure always reads the latest value + useEffect(() => { typewriterRef.current = typewriterMode }, [typewriterMode]) + + // Remove the class when the component unmounts (navigating away) + useEffect(() => () => document.documentElement.classList.remove('typewriter'), []) + + // Register update / selectionUpdate listeners once editor is ready + useEffect(() => { + if (!editor) return + + function scrollToCursor() { + if (!typewriterRef.current) return + if (twRafRef.current) cancelAnimationFrame(twRafRef.current) + twRafRef.current = requestAnimationFrame(() => { + try { + const coords = editor.view.coordsAtPos(editor.state.selection.$head.pos) + const delta = Math.round(coords.top - window.innerHeight * 0.5) + if (Math.abs(delta) > 2) window.scrollBy(0, delta) + } catch { /* ignore if selection is temporarily invalid */ } + }) + } + + editor.on('update', scrollToCursor) + editor.on('selectionUpdate', scrollToCursor) + return () => { + editor.off('update', scrollToCursor) + editor.off('selectionUpdate', scrollToCursor) + if (twRafRef.current) cancelAnimationFrame(twRafRef.current) + } + }, [editor]) + + function toggleTypewriter() { + const next = !typewriterMode + setTypewriterMode(next) + typewriterRef.current = next + document.documentElement.classList.toggle('typewriter', next) + + // Immediately centre the cursor when switching on + if (next) { + requestAnimationFrame(() => { + try { + const coords = editor.view.coordsAtPos(editor.state.selection.$head.pos) + window.scrollBy(0, Math.round(coords.top - window.innerHeight * 0.5)) + } catch {} + }) + } + } + // ───────────────────────────────────────────────────────────────────────── + function startReading() { if (!('speechSynthesis' in window)) return @@ -48,6 +102,13 @@ export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeCha if (!editor) return null + // ── Alignment SVG icons (standard stacked-lines look) ───────────────────── + const AlignLeft = () => + const AlignCenter = () => + const AlignRight = () => + const AlignJustify = () => + // ────────────────────────────────────────────────────────────────────────── + async function handleImageFile(e) { const file = e.target.files[0] if (!file) return @@ -91,9 +152,18 @@ export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeCha - {tb('L', () => editor.chain().focus().setTextAlign('left').run(), editor.isActive({ textAlign: 'left' }), 'Align left')} - {tb('C', () => editor.chain().focus().setTextAlign('center').run(), editor.isActive({ textAlign: 'center' }), 'Align centre')} - {tb('R', () => editor.chain().focus().setTextAlign('right').run(), editor.isActive({ textAlign: 'right' }), 'Align right')} + {[ + { Icon: AlignLeft, align: 'left', label: 'Align left' }, + { Icon: AlignCenter, align: 'center', label: 'Align centre' }, + { Icon: AlignRight, align: 'right', label: 'Align right' }, + { Icon: AlignJustify, align: 'justify', label: 'Justify' }, + ].map(({ Icon, align, label }) => ( + + ))} @@ -129,6 +199,14 @@ export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeCha + +