From c6d1554215fff7a7f1215f11c3431d1511e3ac62 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 24 May 2026 21:00:14 -0400 Subject: [PATCH] Typewriter mode, SVG alignment icons, highlight readability fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typewriter mode (⌨ toolbar button): - Registers TipTap update/selectionUpdate listeners via editor.on() - On each change, uses coordsAtPos + window.scrollBy to keep cursor at 50% of viewport height — instant, no scroll-lag - Adds 50vh padding-bottom so the last line can reach screen centre - Toggles .typewriter class on ; cleans up on unmount Alignment buttons: - Replaced L/C/R text labels with inline SVG stacked-line icons (left, centre, right, justify) — standard text-editor appearance - Added Justify as a fourth option Highlight readability: - Added color: #111 !important to .editor-body .ProseMirror mark so highlighted text is always dark-on-light regardless of theme Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/Toolbar.jsx | 86 +++++++++++++++++++++++++++-- frontend/src/styles/index.css | 10 +++- 2 files changed, 91 insertions(+), 5 deletions(-) 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 + +