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
+
+