Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions app/ycode/components/CenterCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,94 @@ const CenterCanvas = React.memo(function CenterCanvas({
}
}, [viewportMode, autofit]);

// Scroll canvas to selected element if it's off-screen
const prevCanvasLayerIdRef = useRef<string | null>(null);
const isInitialScrollRef = useRef(true);

const scrollCanvasToLayer = useCallback((layerId: string, smooth: boolean, force = false) => {
const scrollEl = scrollContainerRef.current;
if (!canvasIframeElement || !scrollEl) return;

const iframeDoc = canvasIframeElement.contentDocument;
if (!iframeDoc) return;

const el = iframeDoc.querySelector(`[data-layer-id="${layerId}"]`) as HTMLElement;
if (!el) return;

const elRect = el.getBoundingClientRect();
const iframeRect = canvasIframeElement.getBoundingClientRect();
const zoomScale = zoom / 100;
const elTopInScroll = iframeRect.top - scrollEl.getBoundingClientRect().top + scrollEl.scrollTop + elRect.top * zoomScale;
const elBottomInScroll = elTopInScroll + elRect.height * zoomScale;
const viewTop = scrollEl.scrollTop;
const viewBottom = scrollEl.scrollTop + scrollEl.clientHeight;

if (!force && elTopInScroll >= viewTop && elBottomInScroll <= viewBottom) return;

const elScaledHeight = elRect.height * zoomScale;
const fitsInView = elScaledHeight <= scrollEl.clientHeight;
const targetScroll = fitsInView
? elTopInScroll - (scrollEl.clientHeight / 2) + (elScaledHeight / 2)
: elTopInScroll;
scrollEl.scrollTo({ top: Math.max(0, targetScroll), behavior: smooth ? 'smooth' : 'auto' });
}, [canvasIframeElement, zoom]);

useEffect(() => {
if (!selectedLayerId) {
prevCanvasLayerIdRef.current = null;
return;
}

if (!canvasIframeElement || !isCanvasReady) return;

if (prevCanvasLayerIdRef.current === selectedLayerId) return;
prevCanvasLayerIdRef.current = selectedLayerId;

const isInitial = isInitialScrollRef.current;
isInitialScrollRef.current = false;

let attempts = 0;
const maxAttempts = 20;
const delay = isInitial ? 200 : 50;

const tryScroll = () => {
const iframeDoc = canvasIframeElement.contentDocument;
const el = iframeDoc?.querySelector(`[data-layer-id="${selectedLayerId}"]`) as HTMLElement | null;
if (!el) {
attempts++;
if (attempts < maxAttempts) {
timeoutId = window.setTimeout(tryScroll, 100);
}
return;
}
scrollCanvasToLayer(selectedLayerId, !isInitial);
};

let timeoutId = window.setTimeout(tryScroll, delay);

return () => clearTimeout(timeoutId);
}, [selectedLayerId, canvasIframeElement, isCanvasReady, scrollCanvasToLayer]);

// Re-scroll when content height changes during initial load (images loading shifts layout)
const canvasReadyTimeRef = useRef<number | null>(null);
useEffect(() => {
if (isCanvasReady && !canvasReadyTimeRef.current) {
canvasReadyTimeRef.current = Date.now();
}
}, [isCanvasReady]);

useEffect(() => {
if (!selectedLayerId || !canvasIframeElement || !isCanvasReady || !reportedContentHeight) return;

const readyTime = canvasReadyTimeRef.current;
if (!readyTime || Date.now() - readyTime > 5000) return;

const timeout = setTimeout(() => {
scrollCanvasToLayer(selectedLayerId, false, true);
}, 100);
return () => clearTimeout(timeout);
}, [reportedContentHeight, selectedLayerId, canvasIframeElement, isCanvasReady, scrollCanvasToLayer]);

// Recalculate zoom when content height becomes ready in preview mode
const hasRecalculatedForContent = useRef(false);
useEffect(() => {
Expand Down
70 changes: 45 additions & 25 deletions app/ycode/components/LayersTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ interface LayerRowProps {
selectedLayerId: string | null;
liveLayerUpdates?: UseLiveLayerUpdatesReturn | null;
liveComponentUpdates?: UseLiveComponentUpdatesReturn | null;
scrollToSelected?: boolean;
activeBreakpoint: Breakpoint;
isRenaming: boolean;
onRenameStart: (id: string) => void;
Expand Down Expand Up @@ -172,7 +171,6 @@ const LayerRow = React.memo(function LayerRow({
selectedLayerId,
liveLayerUpdates,
liveComponentUpdates,
scrollToSelected,
activeBreakpoint,
isRenaming,
onRenameStart,
Expand Down Expand Up @@ -203,8 +201,6 @@ const LayerRow = React.memo(function LayerRow({
disabled: isRenaming,
});

// Ref for scrolling to this element
const rowRef = React.useRef<HTMLDivElement>(null);
const renameInputRef = React.useRef<HTMLInputElement>(null);
const renameReadyRef = React.useRef(false);

Expand Down Expand Up @@ -235,20 +231,8 @@ const LayerRow = React.memo(function LayerRow({
const setRefs = (element: HTMLDivElement | null) => {
setDragRef(element);
setDropRef(element);
rowRef.current = element;
};

// Auto-scroll to this row when it becomes selected (from canvas click)
React.useEffect(() => {
if (isSelected && scrollToSelected && rowRef.current) {
rowRef.current.scrollIntoView({
behavior: 'auto', // Instant jump for immediate feedback
block: 'center', // Center in viewport to avoid sticky header
inline: 'nearest',
});
}
}, [isSelected, scrollToSelected]);

const hasChildren = node.layer.children && node.layer.children.length > 0;
const isCollapsed = node.collapsed || false;

Expand Down Expand Up @@ -646,7 +630,7 @@ const LayerRow = React.memo(function LayerRow({
: cn(
'opacity-0 group-hover:opacity-40',
isSelected ? 'group-hover:opacity-60' : '',
'hover:!opacity-100'
'hover:opacity-100!'
),
)}
aria-label={node.layer.settings?.hidden ? 'Show element' : 'Hide element'}
Expand Down Expand Up @@ -1216,22 +1200,59 @@ export default function LayersTree({
}, []);

const ROW_HEIGHT = 32;

const virtualizer = useVirtualizer({
count: flattenedNodes.length,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 20,
});

// Scroll to selected layer using the virtualizer
// Scroll to selected layer only if not already visible
useEffect(() => {
if (shouldScrollToSelected && selectedLayerId) {
const idx = flattenedNodes.findIndex(n => n.id === selectedLayerId);
if (idx >= 0) {
virtualizer.scrollToIndex(idx, { align: 'center' });
if (!shouldScrollToSelected || !selectedLayerId) return;

const idx = flattenedNodes.findIndex(n => n.id === selectedLayerId);
if (idx < 0) return;

const scrollEl = scrollContainerRef.current;
if (!scrollEl) {
virtualizer.scrollToIndex(idx, { align: 'center', behavior: 'smooth' });
return;
}

const SCROLL_MARGIN = 64;
const virtualItems = virtualizer.getVirtualItems();
const item = virtualItems.find(v => v.index === idx);

if (item) {
const wrapperTop = wrapperRef.current?.getBoundingClientRect().top ?? 0;
const scrollTop = scrollEl.getBoundingClientRect().top;
const itemScreenTop = wrapperTop + item.start;
const viewTop = scrollTop + SCROLL_MARGIN;
const viewBottom = scrollTop + scrollEl.clientHeight - SCROLL_MARGIN;

if (itemScreenTop >= viewTop && itemScreenTop + ROW_HEIGHT <= viewBottom) {
return;
}
}

// Jump to item first so virtualizer renders it, then center manually
const isAbove = idx * ROW_HEIGHT < scrollEl.scrollTop;
virtualizer.scrollToIndex(idx, { align: isAbove ? 'start' : 'end' });

const timeout = setTimeout(() => {
const wrapperEl = wrapperRef.current;
if (!wrapperEl || !scrollEl) return;

const wrapperRect = wrapperEl.getBoundingClientRect();
const scrollRect = scrollEl.getBoundingClientRect();
const wrapperOffset = wrapperRect.top - scrollRect.top + scrollEl.scrollTop;
const itemTop = wrapperOffset + idx * ROW_HEIGHT;
const targetScroll = itemTop - (scrollEl.clientHeight / 2) + (ROW_HEIGHT / 2);
scrollEl.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
}, 100);

return () => clearTimeout(timeout);
}, [shouldScrollToSelected, selectedLayerId, flattenedNodes, virtualizer]);

// Pull hover state management from editor store
Expand Down Expand Up @@ -1878,7 +1899,7 @@ export default function LayersTree({
});

return result;
}, [flattenedNodes, selectedLayerIds, selectedLayerId, collapsedIds, storeActiveSublayerIndex, storeActiveTextStyleKey]);
}, [flattenedNodes, selectedLayerIds, selectedLayerId, collapsedIds, storeActiveSublayerIndex, storeActiveTextStyleKey, storeActiveListItemIndex]);

return (
<DndContext
Expand Down Expand Up @@ -1925,7 +1946,6 @@ export default function LayersTree({
selectedLayerId={selectedLayerId}
liveLayerUpdates={liveLayerUpdates}
liveComponentUpdates={liveComponentUpdates}
scrollToSelected={shouldScrollToSelected}
activeBreakpoint={activeBreakpoint}
isRenaming={renamingLayerId === node.id}
onRenameStart={handleRenameStart}
Expand Down