From 270a99f76161b0758c1e6da4534d883cf0833529 Mon Sep 17 00:00:00 2001 From: Tristan Mouchet Date: Mon, 23 Mar 2026 11:49:15 +0100 Subject: [PATCH 1/4] fix: URL issues and layer selection when switching between Layers and Pages --- app/ycode/components/CenterCanvas.tsx | 3 +- app/ycode/components/LeftSidebar.tsx | 62 +++++++---------------- app/ycode/components/LeftSidebarPages.tsx | 52 +++++-------------- app/ycode/components/YCodeBuilderMain.tsx | 7 ++- hooks/use-editor-url.ts | 13 +++-- 5 files changed, 46 insertions(+), 91 deletions(-) diff --git a/app/ycode/components/CenterCanvas.tsx b/app/ycode/components/CenterCanvas.tsx index 9aabb3b3..43a6e50c 100644 --- a/app/ycode/components/CenterCanvas.tsx +++ b/app/ycode/components/CenterCanvas.tsx @@ -551,6 +551,7 @@ const CenterCanvas = React.memo(function CenterCanvas({ const hoveredLayerId = useEditorStore((state) => state.hoveredLayerId); const setHoveredLayerId = useEditorStore((state) => state.setHoveredLayerId); const isPreviewMode = useEditorStore((state) => state.isPreviewMode); + const activeSidebarTab = useEditorStore((state) => state.activeSidebarTab); const activeInteractionTriggerLayerId = useEditorStore((state) => state.activeInteractionTriggerLayerId); const richTextSheetLayerId = useEditorStore((state) => state.richTextSheetLayerId); const closeRichTextSheet = useEditorStore((state) => state.closeRichTextSheet); @@ -2245,7 +2246,7 @@ const CenterCanvas = React.memo(function CenterCanvas({ )} {/* Selection overlay - renders outlines on top of the iframe */} - {!isPreviewMode && canvasIframeElement && ( + {!isPreviewMode && activeSidebarTab !== 'pages' && canvasIframeElement && ( ('elements'); const [assetMessage, setAssetMessage] = useState(null); @@ -71,25 +70,18 @@ const LeftSidebar = React.memo(function LeftSidebar({ const editingComponentId = useEditorStore((state) => state.editingComponentId); const setActiveSidebarTab = useEditorStore((state) => state.setActiveSidebarTab); - // Local state for instant tab switching - syncs with URL but allows immediate UI feedback - const [localActiveTab, setLocalActiveTab] = useState(sidebarTab); - - // Read the store's activeSidebarTab const storeSidebarTab = useEditorStore((state) => state.activeSidebarTab); - // Sync local tab with URL when URL changes (e.g., from navigation or page load) + // Sync URL → store only on initial mount + const hasInitializedTabRef = useRef(false); useEffect(() => { - setLocalActiveTab(sidebarTab); - setActiveSidebarTab(sidebarTab); + if (!hasInitializedTabRef.current) { + hasInitializedTabRef.current = true; + setActiveSidebarTab(sidebarTab); + } }, [sidebarTab, setActiveSidebarTab]); - // Sync local tab with store when store changes (e.g., from canvas layer click) - useEffect(() => { - if (storeSidebarTab && storeSidebarTab !== localActiveTab) { - setLocalActiveTab(storeSidebarTab); - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- localActiveTab intentionally excluded to avoid sync loops - }, [storeSidebarTab]); + const activeTab = storeSidebarTab || sidebarTab; const componentDrafts = useComponentsStore((state) => state.componentDrafts); const getComponentById = useComponentsStore((state) => state.getComponentById); @@ -101,9 +93,6 @@ const LeftSidebar = React.memo(function LeftSidebar({ const layerLocksRef = useRef(layerLocks); layerLocksRef.current = layerLocks; - // Use local state for immediate tab switching - const activeTab = localActiveTab; - // Get component layers if in edit mode const editingComponent = editingComponentId ? getComponentById(editingComponentId) : null; @@ -117,7 +106,6 @@ const LeftSidebar = React.memo(function LeftSidebar({ setElementLibraryTab(tab); setShowElementLibrary(true); // Switch to Layers tab so the element library context is correct - setLocalActiveTab('layers'); setActiveSidebarTab('layers'); } else { setShowElementLibrary((prev) => !prev); @@ -307,34 +295,18 @@ const LeftSidebar = React.memo(function LeftSidebar({ onValueChange={(value) => { const newTab = value as EditorTab; - // Immediately update local state AND store for instant UI feedback - setLocalActiveTab(newTab); setActiveSidebarTab(newTab); setShowElementLibrary(false); - // Clear layer selection when switching away from Layers tab - // This releases the lock so other users can edit - if (newTab === 'pages') { - onLayerSelect(null); + // Update URL without triggering Next.js navigation to avoid re-renders + const targetPageId = currentPageId || (pages.length > 0 ? pages[0].id : null); + if (targetPageId) { + const segment = newTab === 'layers' ? 'layers' : 'pages'; + const newPath = `/ycode/${segment}/${targetPageId}${window.location.search}`; + window.history.replaceState(null, '', newPath); } - - // Defer URL navigation to avoid blocking the UI - // startTransition marks this as a low-priority update - startTransition(() => { - if (newTab === 'layers') { - const targetPageId = currentPageId || (pages.length > 0 ? pages[0].id : null); - if (targetPageId) { - navigateToLayers(targetPageId, urlState.view || undefined, urlState.rightTab || undefined, urlState.layerId || undefined); - } - } else if (newTab === 'pages') { - const targetPageId = currentPageId || (pages.length > 0 ? pages[0].id : null); - if (targetPageId) { - navigateToPage(targetPageId, urlState.view || undefined, urlState.rightTab || undefined, urlState.layerId || undefined); - } - } - }); }} - className="h-full overflow-hidden !gap-0" + className="h-full overflow-hidden gap-0!" > Layers @@ -348,7 +320,7 @@ const LeftSidebar = React.memo(function LeftSidebar({ value="layers" className="flex flex-col min-h-0 overflow-y-auto no-scrollbar" forceMount > -
+
{editingComponentId ? 'Layers' : 'Layers'}
diff --git a/app/ycode/components/ElementLibrary.tsx b/app/ycode/components/ElementLibrary.tsx index 98d136e3..fae8607a 100644 --- a/app/ycode/components/ElementLibrary.tsx +++ b/app/ycode/components/ElementLibrary.tsx @@ -147,7 +147,6 @@ function ElementButton({ interface ElementLibraryProps { isOpen: boolean; onClose: () => void; - defaultTab?: 'elements' | 'layouts' | 'components'; liveLayerUpdates?: UseLiveLayerUpdatesReturn | null; } @@ -275,7 +274,7 @@ async function restoreInlinedComponents( return newLayer; } -export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements', liveLayerUpdates }: ElementLibraryProps) { +export default function ElementLibrary({ isOpen, onClose, liveLayerUpdates }: ElementLibraryProps) { const { addLayerFromTemplate, updateLayer, setDraftLayers, draftsByPageId, pages } = usePagesStore(); const { currentPageId, selectedLayerId, setSelectedLayerId, editingComponentId, activeBreakpoint, pushComponentNavigation, startCanvasDrag, endCanvasDrag } = useEditorStore(); const { components, componentDrafts, updateComponentDraft, deleteComponent, getDeletePreview, loadComponentDraft, getComponentById, loadComponents } = useComponentsStore(); @@ -287,16 +286,16 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements const [deletePreviewInfo, setDeletePreviewInfo] = useState<{ pageCount: number; componentCount: number } | null>(null); const [isDeleting, setIsDeleting] = useState(false); const [activeTab, setActiveTab] = React.useState<'elements' | 'layouts' | 'components'>(() => { - // Try to load from sessionStorage first if (typeof window !== 'undefined') { const saved = sessionStorage.getItem('elementLibrary-activeTab'); if (saved && ['elements', 'layouts', 'components'].includes(saved)) { return saved as 'elements' | 'layouts' | 'components'; } } - return defaultTab; + return 'elements'; }); const [componentSearch, setComponentSearch] = useState(''); + const tabRefs = React.useRef>({}); const circularComponentIds = useMemo(() => { if (!editingComponentId) return new Set(); @@ -338,10 +337,17 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements return new Set(allCategories.filter(cat => cat !== 'Navigation' && cat !== 'Hero' && cat !== 'Blog header' && cat !== 'Blog posts')); }); - // Sync active tab when defaultTab prop changes (e.g., "Add layout" button, keyboard shortcut) + // Sync tab when explicitly requested (e.g., "Add layout" button) React.useEffect(() => { - setActiveTab(defaultTab); - }, [defaultTab]); + const handleToggle = (event: Event) => { + const tab = (event as CustomEvent<{ tab?: string }>).detail?.tab; + if (tab && ['elements', 'layouts', 'components'].includes(tab)) { + setActiveTab(tab as 'elements' | 'layouts' | 'components'); + } + }; + window.addEventListener('toggleElementLibrary', handleToggle); + return () => window.removeEventListener('toggleElementLibrary', handleToggle); + }, []); // Persist active tab to sessionStorage React.useEffect(() => { @@ -1370,13 +1376,20 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements const deleteConfirmDescription = `Are you sure you want to delete "${componentName}"? ${usageSuffix}`; - if (!isOpen) return null; - return ( -
+
{/* Tabs */} setActiveTab(value as 'elements' | 'layouts' | 'components')} + value={activeTab} onValueChange={(value) => { + const tab = value as 'elements' | 'layouts' | 'components'; + setActiveTab(tab); + tabRefs.current[tab]?.scrollTo(0, 0); + }} className="flex flex-col h-full overflow-hidden gap-0" >
@@ -1391,7 +1404,10 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements
- + { tabRefs.current.elements = el; }} className="flex flex-col divide-y overflow-y-auto flex-1 px-4 pb-4 no-scrollbar" + > {Object.entries(elementCategories).map(([categoryName, elements]) => (
@@ -1414,7 +1430,10 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements ))} - + { tabRefs.current.layouts = el; }} className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar" + > {getAllLayoutKeys().length === 0 ? ( No layouts available @@ -1440,8 +1459,7 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements />
- {!isCollapsed && ( -
+
{layoutKeys.map((layoutKey) => { const previewImage = getLayoutPreviewImage(layoutKey); @@ -1463,6 +1481,7 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements width={640} height={262} alt="Layout preview" + loading="eager" className="object-contain w-full h-full rounded pointer-events-none" /> )} @@ -1496,7 +1515,6 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements ); })}
- )}
); })} @@ -1504,7 +1522,10 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements )} - + { tabRefs.current.components = el; }} className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar" + > {components.length === 0 ? ( No components yet diff --git a/app/ycode/components/LeftSidebar.tsx b/app/ycode/components/LeftSidebar.tsx index 3ad64f75..aacfe38f 100644 --- a/app/ycode/components/LeftSidebar.tsx +++ b/app/ycode/components/LeftSidebar.tsx @@ -53,7 +53,6 @@ const LeftSidebar = React.memo(function LeftSidebar({ }: LeftSidebarProps) { const { sidebarTab } = useEditorUrl(); const [showElementLibrary, setShowElementLibrary] = useState(false); - const [elementLibraryTab, setElementLibraryTab] = useState<'elements' | 'layouts' | 'components'>('elements'); const [assetMessage, setAssetMessage] = useState(null); // Optimize store subscriptions - scoped to current page only @@ -103,9 +102,7 @@ const LeftSidebar = React.memo(function LeftSidebar({ const tab = customEvent.detail?.tab; if (tab) { - setElementLibraryTab(tab); setShowElementLibrary(true); - // Switch to Layers tab so the element library context is correct setActiveSidebarTab('layers'); } else { setShowElementLibrary((prev) => !prev); @@ -372,17 +369,14 @@ const LeftSidebar = React.memo(function LeftSidebar({
- {/* Element Library Slide-Out (lazy loaded) */} - {showElementLibrary && ( - - setShowElementLibrary(false)} - defaultTab={elementLibraryTab} - liveLayerUpdates={liveLayerUpdates} - /> - - )} + {/* Element Library Slide-Out (lazy loaded, always mounted to preserve state) */} + + setShowElementLibrary(false)} + liveLayerUpdates={liveLayerUpdates} + /> + ); }); diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 8e47a53f..9d4c473a 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -66,8 +66,8 @@ function TabsContent({ data-slot="tabs-content" className={cn( 'flex-1 outline-none', - // When forceMount is used, hide inactive tabs with CSS (!important to override flex) - forceMount && 'data-[state=inactive]:!hidden', + // When forceMount is used, collapse inactive tabs but keep them rendered so images preload + forceMount && 'data-[state=inactive]:flex-none! data-[state=inactive]:h-0! data-[state=inactive]:p-0! data-[state=inactive]:overflow-hidden! data-[state=inactive]:pointer-events-none!', className )} forceMount={forceMount}