diff --git a/keep-notes/components/masonry-grid.css b/keep-notes/components/masonry-grid.css index e6aa957..42efe18 100644 --- a/keep-notes/components/masonry-grid.css +++ b/keep-notes/components/masonry-grid.css @@ -8,14 +8,8 @@ /* Masonry Container */ .masonry-container { width: 100%; - padding: 0 16px 24px 16px; -} - -/* Grid containers for pinned and others sections */ -.masonry-container > div > div[ref*="GridRef"] { - width: 100%; - min-height: 100px; - position: relative; + /* Reduced to compensate for item padding */ + padding: 0 20px 40px 20px; } /* Masonry Item Base Styles - Width is managed by Muuri */ @@ -24,8 +18,8 @@ position: absolute; z-index: 1; box-sizing: border-box; - padding: 8px 0; - width: auto; /* Width will be set by JS based on container */ + padding: 8px; + /* 8px * 2 = 16px gap (Tighter spacing) */ } /* Masonry Item Content Wrapper */ @@ -44,39 +38,38 @@ /* Note Card - Base styles */ .note-card { - width: 100% !important; /* Force full width within grid cell */ - min-width: 0; /* Prevent overflow */ - height: auto !important; /* Let content determine height like Google Keep */ - max-height: none !important; /* No max-height restriction */ + width: 100% !important; + /* Force full width within grid cell */ + min-width: 0; + /* Prevent overflow */ } /* Note Size Styles - Desktop Default */ .note-card[data-size="small"] { min-height: 150px !important; - height: auto !important; } .note-card[data-size="medium"] { min-height: 200px !important; - height: auto !important; } .note-card[data-size="large"] { min-height: 300px !important; - height: auto !important; } -/* Drag State Styles - Improved for Google Keep-like behavior */ +/* Drag State Styles - Clean and flat behavior requested by user */ .masonry-item.muuri-item-dragging { z-index: 1000; - opacity: 0.6; - transition: none; /* No transition during drag for better performance */ + opacity: 1 !important; + /* Force opacity to 100% */ + transition: none; } .masonry-item.muuri-item-dragging .note-card { - transform: scale(1.05) rotate(2deg); - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4); - transition: none; /* No transition during drag */ + transform: none !important; + /* Force "straight" - no rotation, no scale */ + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); + transition: none; } .masonry-item.muuri-item-releasing { @@ -122,29 +115,27 @@ /* Mobile Styles (< 640px) */ @media (max-width: 639px) { .masonry-container { - padding: 0 12px 16px 12px; + padding: 0 20px 16px 20px; } - + .masonry-item { - padding: 6px 0; - } - - /* Smaller note sizes on mobile - keep same ratio */ - .note-card[data-size="small"] { - min-height: 120px !important; - height: auto !important; + padding: 8px; + /* 16px gap on mobile */ } - .note-card[data-size="medium"] { - min-height: 160px !important; - height: auto !important; + /* Smaller note sizes on mobile */ + .masonry-item-content .note-card[data-size="small"] { + min-height: 120px; } - .note-card[data-size="large"] { - min-height: 240px !important; - height: auto !important; + .masonry-item-content .note-card[data-size="medium"] { + min-height: 160px; } - + + .masonry-item-content .note-card[data-size="large"] { + min-height: 240px; + } + /* Reduced drag effect on mobile */ .masonry-item.muuri-item-dragging .note-card { transform: scale(1.01); @@ -155,31 +146,36 @@ /* Tablet Styles (640px - 1023px) */ @media (min-width: 640px) and (max-width: 1023px) { .masonry-container { - padding: 0 16px 20px 16px; + padding: 0 24px 20px 24px; } - + .masonry-item { - padding: 8px 0; + padding: 8px; + /* 16px gap */ } } /* Desktop Styles (1024px - 1279px) */ @media (min-width: 1024px) and (max-width: 1279px) { .masonry-container { - padding: 0 20px 24px 20px; + padding: 0 28px 24px 28px; + } + + .masonry-item { + padding: 8px; } } /* Large Desktop Styles (1280px+) */ @media (min-width: 1280px) { .masonry-container { - padding: 0 24px 32px 24px; - /* max-width removed for infinite columns */ - width: 100%; + padding: 0 28px 32px 28px; + max-width: 1600px; + margin: 0 auto; } - + .masonry-item { - padding: 10px 0; + padding: 8px; } } @@ -204,12 +200,13 @@ body.muuri-dragging { /* Optimize for reduced motion */ @media (prefers-reduced-motion: reduce) { + .masonry-item, .masonry-item-content, .note-card { transition: none; } - + .masonry-item.muuri-item-dragging .note-card { transform: none; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); @@ -218,13 +215,14 @@ body.muuri-dragging { /* Print styles */ @media print { + .masonry-item.muuri-item-dragging, .muuri-item-placeholder { display: none !important; } - + .masonry-item { break-inside: avoid; page-break-inside: avoid; } -} +} \ No newline at end of file diff --git a/keep-notes/components/masonry-grid.tsx b/keep-notes/components/masonry-grid.tsx index b6ea9bd..11991bc 100644 --- a/keep-notes/components/masonry-grid.tsx +++ b/keep-notes/components/masonry-grid.tsx @@ -9,7 +9,7 @@ import { useResizeObserver } from '@/hooks/use-resize-observer'; import { useNotebookDrag } from '@/context/notebook-drag-context'; import { useLanguage } from '@/lib/i18n'; import { DEFAULT_LAYOUT, calculateColumns, calculateItemWidth, isMobileViewport } from '@/config/masonry-layout'; -import './masonry-grid.css'; +import './masonry-grid.css'; // Force rebuild: Spacing update verification interface MasonryGridProps { notes: Note[]; @@ -22,9 +22,10 @@ interface MasonryItemProps { onResize: () => void; onDragStart?: (noteId: string) => void; onDragEnd?: () => void; + isDragging?: boolean; } -const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd }: MasonryItemProps) { +const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) { const resizeRef = useResizeObserver(onResize); return ( @@ -41,13 +42,14 @@ const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragSt onEdit={onEdit} onDragStart={onDragStart} onDragEnd={onDragEnd} + isDragging={isDragging} /> ); }, (prev, next) => { // Custom comparison to avoid re-render on function prop changes if note data is same - return prev.note.id === next.note.id; // Removed isDragging comparison + return prev.note.id === next.note.id && prev.isDragging === next.isDragging; }); export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { @@ -55,14 +57,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null); const { startDrag, endDrag, draggedNoteId } = useNotebookDrag(); - const lastDragEndTime = useRef(0); - const handleEdit = useCallback((note: Note, readOnly?: boolean) => { - // Prevent opening note if it was just dragged (within 200ms) - if (Date.now() - lastDragEndTime.current < 200) { - return; - } - if (onEdit) { onEdit(note, readOnly); } else { @@ -86,9 +81,6 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { ); const handleDragEnd = useCallback(async (grid: any) => { - // Record drag end time to prevent accidental clicks - lastDragEndTime.current = Date.now(); - if (!grid) return; const items = grid.getItems(); @@ -105,32 +97,21 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { } }, []); - const layoutTimeoutRef = useRef(); - - const refreshLayout = useCallback((_?: any) => { - if (layoutTimeoutRef.current) { - clearTimeout(layoutTimeoutRef.current); - } - - layoutTimeoutRef.current = setTimeout(() => { - requestAnimationFrame(() => { - if (pinnedMuuri.current) { - pinnedMuuri.current.refreshItems().layout(); - } - if (othersMuuri.current) { - othersMuuri.current.refreshItems().layout(); - } - }); - }, 100); // 100ms debounce + const refreshLayout = useCallback(() => { + requestAnimationFrame(() => { + if (pinnedMuuri.current) { + pinnedMuuri.current.refreshItems().layout(); + } + if (othersMuuri.current) { + othersMuuri.current.refreshItems().layout(); + } + }); }, []); - // Ref for container to use with ResizeObserver - const containerRef = useRef(null); - - // Centralized function to apply item dimensions based on container width const applyItemDimensions = useCallback((grid: any, containerWidth: number) => { if (!grid) return; + // Calculate columns and item width based on container width const columns = calculateColumns(containerWidth); const itemWidth = calculateItemWidth(containerWidth, columns); @@ -139,11 +120,12 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { const el = item.getElement(); if (el) { el.style.width = `${itemWidth}px`; - // Height is auto - determined by content (Google Keep style) + // Height is auto (determined by content) - Google Keep style } }); }, []); + // Initialize Muuri grids once on mount and sync when needed useEffect(() => { let isMounted = true; let muuriInitialized = false; @@ -172,34 +154,12 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { console.log(`[Masonry] Container width: ${containerWidth}px, Columns: ${columns}, Item width: ${itemWidth}px`); - // Calculate item dimensions based on note size - const getItemDimensions = (note: Note) => { - const baseWidth = itemWidth; - let baseHeight = 200; // Default medium height - - switch (note.size) { - case 'small': - baseHeight = 150; - break; - case 'medium': - baseHeight = 200; - break; - case 'large': - baseHeight = 300; - break; - default: - baseHeight = 200; - } - - return { width: baseWidth, height: baseHeight }; - }; - const layoutOptions = { dragEnabled: true, // Use drag handle for mobile devices to allow smooth scrolling // On desktop, whole card is draggable (no handle needed) dragHandle: isMobile ? '.muuri-drag-handle' : undefined, - // dragContainer: document.body, // REMOVED: Keep item in grid to prevent React conflict + dragContainer: document.body, dragStartPredicate: { distance: 10, delay: 0, @@ -208,10 +168,8 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { enabled: true, createElement: (item: any) => { const el = item.getElement().cloneNode(true); - el.style.opacity = '0.4'; - el.style.transform = 'scale(1.05)'; - el.style.boxShadow = '0 20px 40px rgba(0, 0, 0, 0.3)'; - el.classList.add('muuri-item-placeholder'); + // Styles are now handled purely by CSS (.muuri-item-placeholder) + // to avoid inline style conflicts and "grayed out/tilted" look return el; }, }, @@ -223,113 +181,35 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { threshold: 50, // Start auto-scroll earlier (50px from edge) smoothStop: true, // Smooth deceleration }, - // IMPROVED: Configuration for drag release handling - dragRelease: { - duration: 300, - easing: 'cubic-bezier(0.25, 1, 0.5, 1)', - useDragContainer: false, // REMOVED: Keep item in grid - }, - dragCssProps: { - touchAction: 'none', - userSelect: 'none', - userDrag: 'none', - tapHighlightColor: 'rgba(0, 0, 0, 0)', - touchCallout: 'none', - contentZooming: 'none', - }, - // CRITICAL: Grid layout configuration for fixed width items - layoutDuration: 30, // Much faster layout for responsiveness - layoutEasing: 'ease-out', - // Use grid layout for better control over item placement + // LAYOUT OPTIONS - Configure masonry grid behavior + // These options are critical for proper masonry layout with different item sizes + layoutDuration: 300, + layoutEasing: 'cubic-bezier(0.25, 1, 0.5, 1)', + fillGaps: true, + horizontal: false, + alignRight: false, + alignBottom: false, + rounding: false, + // CRITICAL: Enable true masonry layout for different item sizes layout: { - // Enable true masonry layout - items can be different heights fillGaps: true, horizontal: false, alignRight: false, alignBottom: false, rounding: false, - // Set fixed width and let height be determined by content - // This creates a Google Keep-like masonry layout - itemPositioning: { - onLayout: true, - onResize: true, - onInit: true, - }, - }, - dragSort: true, - dragSortInterval: 50, - // Enable drag and drop with proper drag handling - dragSortHeuristics: { - sortInterval: 0, // Zero interval for immediate sorting - minDragDistance: 5, - minBounceBackAngle: 1, - }, - // Grid configuration for responsive columns - visibleStyles: { - opacity: '1', - transform: 'scale(1)', - }, - hiddenStyles: { - opacity: '0', - transform: 'scale(0.5)', }, }; // Initialize pinned grid if (pinnedGridRef.current && !pinnedMuuri.current) { - // Set container width explicitly - pinnedGridRef.current.style.width = '100%'; - pinnedGridRef.current.style.position = 'relative'; - - // Get all items in the pinned grid and set their dimensions - const pinnedItems = Array.from(pinnedGridRef.current.children); - pinnedItems.forEach((item) => { - const noteId = item.getAttribute('data-id'); - const note = pinnedNotes.find(n => n.id === noteId); - if (note) { - const dims = getItemDimensions(note); - (item as HTMLElement).style.width = `${dims.width}px`; - // Don't set height - let content determine it like Google Keep - } - }); - pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions) - .on('dragEnd', () => handleDragEnd(pinnedMuuri.current)) - .on('dragStart', () => { - // Optional: visual feedback or state update - }); - - // Initial layout - requestAnimationFrame(() => { - pinnedMuuri.current?.refreshItems().layout(); - }); + .on('dragEnd', () => handleDragEnd(pinnedMuuri.current)); } // Initialize others grid if (othersGridRef.current && !othersMuuri.current) { - // Set container width explicitly - othersGridRef.current.style.width = '100%'; - othersGridRef.current.style.position = 'relative'; - - // Get all items in the others grid and set their dimensions - const othersItems = Array.from(othersGridRef.current.children); - othersItems.forEach((item) => { - const noteId = item.getAttribute('data-id'); - const note = othersNotes.find(n => n.id === noteId); - if (note) { - const dims = getItemDimensions(note); - (item as HTMLElement).style.width = `${dims.width}px`; - // Don't set height - let content determine it like Google Keep - } - }); - othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions) .on('dragEnd', () => handleDragEnd(othersMuuri.current)); - - // Initial layout - requestAnimationFrame(() => { - othersMuuri.current?.refreshItems().layout(); - }); } }; @@ -346,16 +226,14 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Synchronize items when notes change (e.g. searching, adding, removing) + // Container ref for ResizeObserver + const containerRef = useRef(null); + + // Synchronize items when notes change (e.g. searching, adding) useEffect(() => { - const syncGridItems = ( - grid: any, - gridRef: React.RefObject, - notesArray: Note[] - ) => { + const syncGridItems = (grid: any, gridRef: React.RefObject, notesArray: Note[]) => { if (!grid || !gridRef.current) return; - // Get container width for dimension calculation const containerWidth = containerRef.current?.getBoundingClientRect().width || window.innerWidth - 32; const columns = calculateColumns(containerWidth); const itemWidth = calculateItemWidth(containerWidth, columns); @@ -363,94 +241,80 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { // Get current DOM elements and Muuri items const domElements = Array.from(gridRef.current.children) as HTMLElement[]; const muuriItems = grid.getItems(); + + // Map Muuri items to their elements for comparison const muuriElements = muuriItems.map((item: any) => item.getElement()); // Find new elements to add (in DOM but not in Muuri) const newElements = domElements.filter(el => !muuriElements.includes(el)); - // Find removed items (in Muuri but not in DOM) + // Find elements to remove (in Muuri but not in DOM) const removedItems = muuriItems.filter((item: any) => !domElements.includes(item.getElement()) ); - // Remove old items from Muuri + // Remove old items if (removedItems.length > 0) { - console.log(`[Masonry Sync] Removing ${removedItems.length} items`); grid.remove(removedItems, { layout: false }); } - // Add new items to Muuri with correct width + // Add new items with correct width if (newElements.length > 0) { - console.log(`[Masonry Sync] Adding ${newElements.length} new items`); newElements.forEach(el => { el.style.width = `${itemWidth}px`; }); grid.add(newElements, { layout: false }); } - // Update all existing item widths + // Update all item widths to ensure consistency domElements.forEach(el => { el.style.width = `${itemWidth}px`; }); - // Refresh and layout - CRITICAL: Always refresh items to catch content size changes - // Use requestAnimationFrame to ensure DOM has painted - requestAnimationFrame(() => { - grid.refreshItems().layout(); - }); + // Refresh and layout + grid.refreshItems().layout(); }; - // Use setTimeout to ensure React has finished rendering DOM elements - const timeoutId = setTimeout(() => { + // Use requestAnimationFrame to ensure DOM is updated before syncing + requestAnimationFrame(() => { syncGridItems(pinnedMuuri.current, pinnedGridRef, pinnedNotes); syncGridItems(othersMuuri.current, othersGridRef, othersNotes); - }, 50); // Increased timeout slightly to ensure DOM stability + }); + }, [pinnedNotes, othersNotes]); // Re-run when notes change - return () => clearTimeout(timeoutId); - }, [pinnedNotes, othersNotes]); - - // Handle container resize with ResizeObserver for responsive layout + // Handle container resize to update responsive layout useEffect(() => { - if (!containerRef.current) return; + if (!containerRef.current || (!pinnedMuuri.current && !othersMuuri.current)) return; let resizeTimeout: NodeJS.Timeout; const handleResize = (entries: ResizeObserverEntry[]) => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { + // Get precise width from ResizeObserver const containerWidth = entries[0]?.contentRect.width || window.innerWidth - 32; const columns = calculateColumns(containerWidth); console.log(`[Masonry Resize] Width: ${containerWidth}px, Columns: ${columns}`); - // Apply dimensions to both grids using centralized function + // Apply dimensions to both grids applyItemDimensions(pinnedMuuri.current, containerWidth); applyItemDimensions(othersMuuri.current, containerWidth); - // Refresh both grids with new layout + // Refresh layouts requestAnimationFrame(() => { - if (pinnedMuuri.current) { - pinnedMuuri.current.refreshItems().layout(); - } - if (othersMuuri.current) { - othersMuuri.current.refreshItems().layout(); - } + pinnedMuuri.current?.refreshItems().layout(); + othersMuuri.current?.refreshItems().layout(); }); - }, 150); + }, 150); // Debounce }; const observer = new ResizeObserver(handleResize); observer.observe(containerRef.current); - // Initial layout calculation + // Initial layout check if (containerRef.current) { - const initialWidth = containerRef.current.getBoundingClientRect().width || window.innerWidth - 32; - applyItemDimensions(pinnedMuuri.current, initialWidth); - applyItemDimensions(othersMuuri.current, initialWidth); - requestAnimationFrame(() => { - pinnedMuuri.current?.refreshItems().layout(); - othersMuuri.current?.refreshItems().layout(); - }); + handleResize([{ contentRect: containerRef.current.getBoundingClientRect() } as ResizeObserverEntry]); } return () => { @@ -473,6 +337,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { onResize={refreshLayout} onDragStart={startDrag} onDragEnd={endDrag} + isDragging={draggedNoteId === note.id} /> ))} @@ -493,6 +358,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) { onResize={refreshLayout} onDragStart={startDrag} onDragEnd={endDrag} + isDragging={draggedNoteId === note.id} /> ))} diff --git a/keep-notes/components/note-card.tsx b/keep-notes/components/note-card.tsx index be0c867..63cabbd 100644 --- a/keep-notes/components/note-card.tsx +++ b/keep-notes/components/note-card.tsx @@ -102,7 +102,7 @@ function getInitials(name: string): string { // Helper function to get avatar color based on name hash function getAvatarColor(name: string): string { const colors = [ - 'bg-blue-500', + 'bg-primary', 'bg-purple-500', 'bg-green-500', 'bg-orange-500', @@ -288,7 +288,8 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on colorClasses.bg, colorClasses.card, colorClasses.hover, - isDragging && 'opacity-60 scale-105 shadow-2xl rotate-1' + colorClasses.hover, + isDragging && 'shadow-2xl' // Removed opacity, scale, and rotation for clean drag )} onClick={(e) => { // Only trigger edit if not clicking on buttons @@ -315,7 +316,7 @@ export function NoteCard({ note, onEdit, isDragging, isDragOver, onDragStart, on