Spaces:
Running
Running
fix: implement useUpdateNodeInternals across all node types to resolve connection offset issues on size change
4555b70 | import React, { useRef } from 'react'; | |
| import { NodeResizer, Handle, Position, useUpdateNodeInternals } from '@xyflow/react'; | |
| import { Settings, StickyNote, Pin } from 'lucide-react'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { useTheme } from '../../../../context/ThemeContext'; | |
| const getInitials = (displayName, email) => { | |
| if (displayName && displayName.trim()) { | |
| const parts = displayName.trim().split(/\s+/); | |
| if (parts.length >= 2) { | |
| return (parts[0][0] + parts[1][0]).toUpperCase(); | |
| } | |
| return parts[0].substring(0, 2).toUpperCase(); | |
| } | |
| if (email && email.trim()) { | |
| return email.substring(0, 2).toUpperCase(); | |
| } | |
| return 'U'; | |
| }; | |
| const PASTEL_COLORS = { | |
| yellow: { light: '#fef9c3', dark: '#2e2b14', borderLight: '#eab308', borderDark: '#ca8a04' }, | |
| blue: { light: '#dbeafe', dark: '#111827', borderLight: '#3b82f6', borderDark: '#2563eb' }, | |
| green: { light: '#dcfce7', dark: '#062f17', borderLight: '#22c55e', borderDark: '#16a34a' }, | |
| pink: { light: '#fce7f3', dark: '#37061d', borderLight: '#ec4899', borderDark: '#db2777' }, | |
| purple: { light: '#f3e8ff', dark: '#210638', borderLight: '#a855f7', borderDark: '#9333ea' }, | |
| orange: { light: '#ffedd5', dark: '#2e1208', borderLight: '#f97316', borderDark: '#ea580c' }, | |
| white: { light: '#f5f5f4', dark: '#1c1917', borderLight: '#d6d3d1', borderDark: '#44403c' } | |
| }; | |
| const CommentNode = ({ id, data, selected }) => { | |
| const { t } = useTranslation(); | |
| const { isDarkMode } = useTheme(); | |
| const updateNodeInternals = useUpdateNodeInternals(); | |
| // Re-calculate handles when node size might change | |
| React.useEffect(() => { | |
| updateNodeInternals(id); | |
| }, [id, data.label, data.description, updateNodeInternals]); | |
| // Create a slight premium dynamic rotation based on node ID to make it look like a physical sticky note | |
| const rotateAngle = useRef(((parseInt(id.slice(0, 2), 16) % 3) - 1.5).toFixed(1)); | |
| const openConfigureModal = (e) => { | |
| e.stopPropagation(); | |
| window.dispatchEvent(new CustomEvent('configure-sketch-node', { detail: { nodeId: id } })); | |
| }; | |
| const togglePin = (e) => { | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| if (data.updateNodeData) { | |
| data.updateNodeData(id, { isPinned: !data.isPinned }); | |
| } | |
| }; | |
| // Background and border style resolution based on selected color or default to yellow | |
| const colorKey = data.color && data.color !== 'default' ? data.color : 'yellow'; | |
| const selectedColor = PASTEL_COLORS[colorKey]; | |
| const nodeBg = isDarkMode ? selectedColor.dark : selectedColor.light; | |
| const nodeBorder = isDarkMode ? selectedColor.borderDark : selectedColor.borderLight; | |
| return ( | |
| <div | |
| onDoubleClick={openConfigureModal} | |
| className={`px-5 py-4 w-full h-full min-w-[150px] min-h-[150px] rough-border transition-all group relative cursor-pointer select-none ${ | |
| selected ? 'shadow-[8px_8px_0px_0px_rgba(245,158,11,0.25)] -translate-y-1 border-amber-400 ring-4 ring-amber-400/20 scale-[1.01]' : 'shadow-[4px_4px_0px_0px_rgba(0,0,0,0.08)]' | |
| }`} | |
| style={{ | |
| transform: `rotate(${rotateAngle.current}deg)`, | |
| backgroundColor: nodeBg, | |
| borderColor: nodeBorder, | |
| borderRadius: '2px 14px 4px 16px' // Hand-drawn sticker fold style | |
| }} | |
| > | |
| {/* | |
| Single Integrated Pin Handle | |
| Styled as a sleek dark drawing pin to fit the sketchbook aesthetic. | |
| */} | |
| <Handle | |
| type="target" | |
| position={Position.Top} | |
| id="sticker-target" | |
| className="!w-4 !h-4 !bg-primary !border-2 !border-primary !shadow-[2px_2px_0px_0px_rgba(0,0,0,0.15)] !z-50 hover:!scale-110 transition-transform !opacity-100 !rounded-full !absolute" | |
| style={{ | |
| top: '-8px', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| }} | |
| > | |
| {/* Metallic/Plastic highlight to give it volume */} | |
| <div className="pointer-events-none absolute top-0.5 left-0.5 w-1 h-1 bg-white/20 rounded-full" /> | |
| </Handle> | |
| {/* Invisible source handle sharing the same space */} | |
| <Handle | |
| type="source" | |
| position={Position.Top} | |
| id="sticker-source" | |
| className="!w-4 !h-4 !opacity-0 !z-[51] !pointer-events-none" | |
| style={{ | |
| top: '-8px', | |
| left: '50%', | |
| transform: 'translateX(-50%)' | |
| }} | |
| /> | |
| {/* Resizer */} | |
| <NodeResizer | |
| minWidth={150} | |
| minHeight={150} | |
| isVisible={selected && !data.isPinned} | |
| lineClassName="border-amber-400" | |
| handleClassName="w-3 h-3 bg-white border-2 border-amber-400 rounded" | |
| /> | |
| {/* Folded Corner Accent */} | |
| <div | |
| className="absolute top-0 right-0 w-5 h-5 rounded-bl-lg border-l-2 border-b-2 opacity-30 pointer-events-none" | |
| style={{ borderColor: isDarkMode ? '#ffffff' : '#000000' }} | |
| /> | |
| <div className="flex flex-col h-full gap-3"> | |
| <div> | |
| {/* Header */} | |
| <div className="flex justify-between items-start gap-2 border-b border-primary/10 pb-1 mb-2"> | |
| <div className="flex items-center gap-1.5 min-w-0"> | |
| <StickyNote size={14} className="text-primary/60 shrink-0" /> | |
| <span className="font-extrabold text-[10px] uppercase tracking-widest text-primary/60 truncate"> | |
| {data.label || t('canvas.comment_title', 'Стікер')} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1 shrink-0"> | |
| <button | |
| onClick={togglePin} | |
| className={`transition-all p-0.5 rounded-md cursor-pointer ${ | |
| data.isPinned | |
| ? 'text-amber-500 bg-amber-500/10 hover:bg-amber-500/20' | |
| : 'opacity-0 group-hover:opacity-100 text-primary/60 hover:text-primary hover:bg-primary/5' | |
| }`} | |
| title={data.isPinned ? t('canvas.unpin', 'Відкріпити') : t('canvas.pin', 'Закріпити')} | |
| > | |
| <Pin size={13} className={data.isPinned ? 'rotate-45 fill-amber-500' : ''} /> | |
| </button> | |
| <button | |
| onClick={openConfigureModal} | |
| className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 text-primary/60 hover:text-primary hover:bg-primary/5 rounded-md cursor-pointer" | |
| title={t('canvas.configure', 'Configure')} | |
| > | |
| <Settings size={13} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Sticker content in a gorgeous handwriting font */} | |
| <p className="text-base font-accent-note text-primary leading-tight break-words whitespace-pre-wrap italic"> | |
| {data.description || t('canvas.comment_placeholder', 'Двоклацніть, щоб додати коментар...')} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default CommentNode; | |