import React, { useContext, useRef, useState, useEffect, useCallback, useMemo } from 'react'; import { NodeResizeControl, useStore } from '@xyflow/react'; import { marked } from 'marked'; import { NodeContext } from './CustomNode'; marked.use({ breaks: true, gfm: true }); const NOTE_COLORS = { default: { bg: '#1e293b', border: '#334155', dot: '#475569' }, blue: { bg: '#0c1f3d', border: '#1d4ed8', dot: '#3b82f6' }, green: { bg: '#062016', border: '#15803d', dot: '#22c55e' }, yellow: { bg: '#1f1500', border: '#a16207', dot: '#eab308' }, red: { bg: '#1f0808', border: '#b91c1c', dot: '#ef4444' }, purple: { bg: '#160c2a', border: '#7c3aed', dot: '#a855f7' }, }; function TextNoteNode({ id, data }) { const ctx = useContext(NodeContext); const [isEditing, setIsEditing] = useState(false); const [collapsed, setCollapsed] = useState(false); const textareaRef = useRef(null); const selected = useStore( useCallback( (s) => { const node = s.nodeLookup?.get(id) || s.nodes?.find((n) => n.id === id); return !!node?.selected; }, [id], ), ); const text = data.widgetValues?.text ?? ''; const color = data.widgetValues?.color ?? 'default'; const palette = NOTE_COLORS[color] ?? NOTE_COLORS.default; const setField = useCallback( (name, value) => ctx?.onWidgetChange?.(id, name, value), [ctx, id], ); useEffect(() => { if (isEditing) { textareaRef.current?.focus(); } }, [isEditing]); const onDoubleClick = useCallback((e) => { e.stopPropagation(); setIsEditing(true); }, []); const onBlur = useCallback(() => setIsEditing(false), []); const onKeyDown = useCallback((e) => { // Ctrl/Cmd+Enter or Escape finishes editing if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) { e.preventDefault(); setIsEditing(false); } // Tab inserts spaces if (e.key === 'Tab') { e.preventDefault(); const ta = textareaRef.current; const start = ta.selectionStart; const end = ta.selectionEnd; const next = text.substring(0, start) + ' ' + text.substring(end); setField('text', next); requestAnimationFrame(() => { ta.selectionStart = ta.selectionEnd = start + 2; }); } }, [text, setField]); const renderedHtml = useMemo(() => { if (!text.trim()) return ''; return marked.parse(text); }, [text]); return ( <> {selected && ( )}
{/* Colour picker row */}
{Object.entries(NOTE_COLORS).map(([key, p]) => (
{/* Content area */} {!collapsed && (isEditing ? (