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
25 changes: 9 additions & 16 deletions app/ycode/components/CenterCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,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);
Expand Down Expand Up @@ -705,7 +706,7 @@ const CenterCanvas = React.memo(function CenterCanvas({
const referencedItems = useCollectionLayerStore((state) => state.referencedItems);
const fetchReferencedCollectionItems = useCollectionLayerStore((state) => state.fetchReferencedCollectionItems);

const { routeType, urlState, navigateToLayers, navigateToPage, navigateToPageEdit, updateQueryParams } = useEditorUrl();
const { urlState, navigateToLayers, navigateToPage, navigateToPageEdit, updateQueryParams } = useEditorUrl();
const components = useComponentsStore((state) => state.components);
const componentDrafts = useComponentsStore((state) => state.componentDrafts);
const [collectionItems, setCollectionItems] = useState<Array<{ id: string; label: string }>>([]);
Expand Down Expand Up @@ -1696,28 +1697,20 @@ const CenterCanvas = React.memo(function CenterCanvas({

// Handle page selection
const handlePageSelect = useCallback((pageId: string) => {
// Clear selection FIRST to release locks on the current page's channel
// before switching to the new page's channel
setSelectedLayerId(null);
if (pageId === currentPageId) return;

// Set the page ID immediately for responsive UI
// The URL effect in YCodeBuilderMain uses a ref to track when we're navigating
// to prevent reverting to the old page before the URL updates
// Set to body directly so the layer sync effect won't trigger a second URL update
setSelectedLayerId('body');
setCurrentPageId(pageId);

// Navigate to the same route type but with the new page ID
// IMPORTANT: Explicitly pass 'body' as the layer to avoid carrying over invalid layer IDs from the old page
if (routeType === 'layers') {
navigateToLayers(pageId, undefined, undefined, 'body');
} else if (routeType === 'page' && urlState.isEditing) {
if (urlState.isEditing) {
navigateToPageEdit(pageId);
} else if (routeType === 'page') {
} else if (activeSidebarTab === 'pages') {
navigateToPage(pageId, undefined, undefined, 'body');
} else {
// Default to layers if no route type
navigateToLayers(pageId, undefined, undefined, 'body');
}
}, [setSelectedLayerId, setCurrentPageId, routeType, urlState.isEditing, navigateToLayers, navigateToPage, navigateToPageEdit]);
}, [currentPageId, setSelectedLayerId, setCurrentPageId, activeSidebarTab, urlState.isEditing, navigateToLayers, navigateToPage, navigateToPageEdit]);

// Fetch referenced collection items recursively when layers with reference fields are detected
useEffect(() => {
Expand Down Expand Up @@ -2203,7 +2196,7 @@ const CenterCanvas = React.memo(function CenterCanvas({
)}

{/* Selection overlay - renders outlines on top of the iframe */}
{!isPreviewMode && canvasIframeElement && (
{!isPreviewMode && activeSidebarTab !== 'pages' && canvasIframeElement && (
<SelectionOverlay
iframeElement={canvasIframeElement}
containerElement={scrollContainerRef.current}
Expand Down
1 change: 1 addition & 0 deletions app/ycode/components/ComponentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function ComponentCard({
width={640}
height={262}
unoptimized
loading="eager"
className="object-contain w-full h-full rounded pointer-events-none"
/>
</button>
Expand Down
55 changes: 38 additions & 17 deletions app/ycode/components/ElementLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ function ElementButton({
interface ElementLibraryProps {
isOpen: boolean;
onClose: () => void;
defaultTab?: 'elements' | 'layouts' | 'components';
liveLayerUpdates?: UseLiveLayerUpdatesReturn | null;
}

Expand Down Expand Up @@ -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();
Expand All @@ -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<Record<string, HTMLDivElement | null>>({});

const circularComponentIds = useMemo(() => {
if (!editingComponentId) return new Set<string>();
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 (
<div className="fixed left-64 top-14 bottom-0 w-64 bg-background border-r z-50 flex flex-col">
<div
className={cn(
'fixed left-64 top-14 bottom-0 w-64 bg-background border-r z-50 flex flex-col',
!isOpen && 'hidden'
)}
>
{/* Tabs */}
<Tabs
value={activeTab} onValueChange={(value) => 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"
>
<div className="flex flex-col shrink-0 gap-2">
Expand All @@ -1391,7 +1404,10 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements
<hr className="mt-2 mb-0 mx-4 shrink-0" />
</div>

<TabsContent value="elements" className="flex flex-col divide-y overflow-y-auto flex-1 px-4 pb-4 no-scrollbar">
<TabsContent
value="elements" forceMount
ref={(el) => { 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]) => (
<div key={categoryName} className="flex flex-col pb-5">
<header className="py-5">
Expand All @@ -1414,7 +1430,10 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements
))}
</TabsContent>

<TabsContent value="layouts" className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar">
<TabsContent
value="layouts" forceMount
ref={(el) => { tabRefs.current.layouts = el; }} className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar"
>
{getAllLayoutKeys().length === 0 ? (
<Empty>
<EmptyTitle>No layouts available</EmptyTitle>
Expand All @@ -1440,8 +1459,7 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements
/>
<Label className="cursor-pointer">{category}</Label>
</header>
{!isCollapsed && (
<div className="grid grid-cols-1 gap-1.5 pb-5">
<div className={cn('grid grid-cols-1 gap-1.5 pb-5', isCollapsed && 'hidden')}>
{layoutKeys.map((layoutKey) => {
const previewImage = getLayoutPreviewImage(layoutKey);

Expand All @@ -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"
/>
)}
Expand Down Expand Up @@ -1496,15 +1515,17 @@ export default function ElementLibrary({ isOpen, onClose, defaultTab = 'elements
);
})}
</div>
)}
</div>
);
})}
</div>
)}
</TabsContent>

<TabsContent value="components" className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar">
<TabsContent
value="components" forceMount
ref={(el) => { tabRefs.current.components = el; }} className="flex flex-col overflow-y-auto flex-1 px-4 pb-4 no-scrollbar"
>
{components.length === 0 ? (
<Empty>
<EmptyTitle>No components yet</EmptyTitle>
Expand Down
84 changes: 25 additions & 59 deletions app/ycode/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { usePagesStore } from '@/stores/usePagesStore';
import { resetBindingsAfterMove } from '@/lib/layer-utils';

// 5.5 Hooks
import { useEditorUrl, useEditorActions } from '@/hooks/use-editor-url';
import { useEditorUrl } from '@/hooks/use-editor-url';
import type { EditorTab } from '@/hooks/use-editor-url';
import { useLayerLocks } from '@/hooks/use-layer-locks';

Expand Down Expand Up @@ -51,10 +51,8 @@ const LeftSidebar = React.memo(function LeftSidebar({
liveLayerUpdates,
liveComponentUpdates,
}: LeftSidebarProps) {
const { sidebarTab, urlState } = useEditorUrl();
const { navigateToLayers, navigateToPage } = useEditorActions();
const { sidebarTab } = useEditorUrl();
const [showElementLibrary, setShowElementLibrary] = useState(false);
const [elementLibraryTab, setElementLibraryTab] = useState<'elements' | 'layouts' | 'components'>('elements');
const [assetMessage, setAssetMessage] = useState<string | null>(null);

// Optimize store subscriptions - scoped to current page only
Expand All @@ -71,25 +69,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<EditorTab>(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);
Expand All @@ -101,9 +92,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;

Expand All @@ -114,10 +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
setLocalActiveTab('layers');
setActiveSidebarTab('layers');
} else {
setShowElementLibrary((prev) => !prev);
Expand Down Expand Up @@ -307,34 +292,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!"
>
<TabsList className="w-full shrink-0">
<TabsTrigger value="layers">Layers</TabsTrigger>
Expand All @@ -348,7 +317,7 @@ const LeftSidebar = React.memo(function LeftSidebar({
value="layers" className="flex flex-col min-h-0 overflow-y-auto no-scrollbar"
forceMount
>
<header className="py-5 flex justify-between shrink-0 sticky top-0 bg-gradient-to-b from-background to-transparent z-20">
<header className="py-5 flex justify-between shrink-0 sticky top-0 bg-linear-to-b from-background to-transparent z-20">
<span className="font-medium">{editingComponentId ? 'Layers' : 'Layers'}</span>
<div className="-my-1">
<Button
Expand Down Expand Up @@ -400,17 +369,14 @@ const LeftSidebar = React.memo(function LeftSidebar({
</div>
</div>

{/* Element Library Slide-Out (lazy loaded) */}
{showElementLibrary && (
<Suspense fallback={null}>
<ElementLibrary
isOpen={showElementLibrary}
onClose={() => setShowElementLibrary(false)}
defaultTab={elementLibraryTab}
liveLayerUpdates={liveLayerUpdates}
/>
</Suspense>
)}
{/* Element Library Slide-Out (lazy loaded, always mounted to preserve state) */}
<Suspense fallback={null}>
<ElementLibrary
isOpen={showElementLibrary}
onClose={() => setShowElementLibrary(false)}
liveLayerUpdates={liveLayerUpdates}
/>
</Suspense>
</>
);
});
Expand Down
Loading