Compare commits

..

4 Commits

Author SHA1 Message Date
Zimeng Xiong 8fcca43b0d chore: pre-release v0.4.4-dev 2026-02-07 11:58:09 -08:00
Zimeng Xiong f20412cdfb separate debounced autosave 2026-02-07 11:57:32 -08:00
Zimeng Xiong a366acfedc chore: pre-release v0.4.3-dev 2026-02-07 11:08:03 -08:00
Zimeng Xiong 154dcbb151 update resopnsiveness hamburger 2026-02-07 11:07:15 -08:00
5 changed files with 56 additions and 36 deletions
+1 -1
View File
@@ -1 +1 @@
0.4.2 0.4.4
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "backend", "name": "backend",
"version": "0.4.2", "version": "0.4.4",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.4.2", "version": "0.4.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 6767", "dev": "vite --port 6767",
+9 -2
View File
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Menu, X } from 'lucide-react'; import { Menu, X } from 'lucide-react';
import { Sidebar } from './Sidebar'; import { Sidebar } from './Sidebar';
import { Logo } from './Logo';
import { UploadStatus } from './UploadStatus'; import { UploadStatus } from './UploadStatus';
import type { Collection } from '../types'; import type { Collection } from '../types';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -89,16 +90,22 @@ export const Layout: React.FC<LayoutProps> = ({
{isMobile ? ( {isMobile ? (
<div className="relative h-full min-w-0"> <div className="relative h-full min-w-0">
<main className="h-full min-w-0 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm transition-colors duration-200 overflow-hidden flex flex-col"> <main className="h-full min-w-0 bg-white/40 dark:bg-neutral-900/40 backdrop-blur-sm rounded-2xl border border-white/50 dark:border-neutral-800/50 shadow-sm transition-colors duration-200 overflow-hidden flex flex-col">
<div className="px-3 pt-3 flex-shrink-0"> <div className="h-16 flex-shrink-0 flex items-center px-4 border-b border-black/5 dark:border-white/5 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-md">
<button <button
type="button" type="button"
onClick={() => setIsSidebarOpen(v => !v)} onClick={() => setIsSidebarOpen(v => !v)}
className="inline-flex items-center justify-center h-11 w-11 rounded-xl border-2 border-black dark:border-neutral-700 bg-white/90 dark:bg-neutral-900/90 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] backdrop-blur-sm text-slate-900 dark:text-neutral-200 hover:-translate-y-0.5 transition-all" className="inline-flex items-center justify-center h-11 w-11 rounded-xl border-2 border-black dark:border-neutral-700 bg-white/90 dark:bg-neutral-900/90 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] text-slate-900 dark:text-neutral-200 hover:-translate-y-0.5 transition-all active:translate-y-0 active:shadow-none"
title={isSidebarOpen ? 'Close menu' : 'Open menu'} title={isSidebarOpen ? 'Close menu' : 'Open menu'}
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'} aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'}
> >
{isSidebarOpen ? <X size={20} /> : <Menu size={20} />} {isSidebarOpen ? <X size={20} /> : <Menu size={20} />}
</button> </button>
<div className="ml-auto flex items-center gap-2">
<Logo className="w-8 h-8" />
<span className="text-xl text-slate-900 dark:text-white mt-1" style={{ fontFamily: 'Excalifont' }}>ExcaliDash</span>
<span className="text-[10px] font-bold text-red-500 mt-2" style={{ fontFamily: 'sans-serif' }}>BETA</span>
</div>
</div> </div>
<div className="flex-1 min-w-0 overflow-y-auto"> <div className="flex-1 min-w-0 overflow-y-auto">
+44 -31
View File
@@ -124,7 +124,7 @@ export const Editor: React.FC = () => {
const latestFilesRef = useRef<any>(null); const latestFilesRef = useRef<any>(null);
const lastSyncedFilesRef = useRef<Record<string, any>>({}); const lastSyncedFilesRef = useRef<Record<string, any>>({});
const latestAppStateRef = useRef<any>(null); const latestAppStateRef = useRef<any>(null);
const debouncedSaveRef = useRef<((elements: readonly any[], appState: any) => void) | null>(null); const debouncedSaveRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => void) | null>(null);
const emitFilesDeltaIfNeeded = useCallback( const emitFilesDeltaIfNeeded = useCallback(
(nextFiles: Record<string, any>) => { (nextFiles: Record<string, any>) => {
@@ -361,13 +361,13 @@ export const Editor: React.FC = () => {
const didEmit = emitFilesDeltaIfNeeded(nextFiles); const didEmit = emitFilesDeltaIfNeeded(nextFiles);
// Persist after file data becomes available so new tabs (tab3) load correctly. // Persist after file data becomes available so new tabs (tab3) load correctly.
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) { if (didEmit && id && latestAppStateRef.current && debouncedSaveRef.current) {
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current); debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
} }
}; };
} }
setIsReady(true); setIsReady(true);
}, [emitFilesDeltaIfNeeded]); }, [emitFilesDeltaIfNeeded, id]);
// Handle #addLibrary URL hash parameter for importing libraries from links // Handle #addLibrary URL hash parameter for importing libraries from links
useEffect(() => { useEffect(() => {
@@ -428,12 +428,12 @@ export const Editor: React.FC = () => {
scrollToContent: true, scrollToContent: true,
}), []); }), []);
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null); const saveDataRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => Promise<void>) | null>(null);
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null); const savePreviewRef = useRef<((drawingId: string, elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null); const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
saveDataRef.current = async (elements: readonly any[], appState: any) => { saveDataRef.current = async (drawingId: string, elements: readonly any[], appState: any) => {
if (!id) return; if (!drawingId) return;
try { try {
const persistableAppState = { const persistableAppState = {
@@ -446,27 +446,27 @@ export const Editor: React.FC = () => {
const persistableElements = Array.isArray(snapshot) ? snapshot : []; const persistableElements = Array.isArray(snapshot) ? snapshot : [];
console.log("[Editor] Saving drawing", { console.log("[Editor] Saving drawing", {
drawingId: id, drawingId,
elementCount: persistableElements.length, elementCount: persistableElements.length,
hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted), hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted),
appState: persistableAppState, appState: persistableAppState,
}); });
await api.updateDrawing(id, { await api.updateDrawing(drawingId, {
elements: persistableElements, elements: persistableElements,
appState: persistableAppState, appState: persistableAppState,
files: latestFilesRef.current || {}, files: latestFilesRef.current || {},
}); });
console.log("[Editor] Save complete", { drawingId: id }); console.log("[Editor] Save complete", { drawingId });
} catch (err) { } catch (err) {
console.error('Failed to save drawing', err); console.error('Failed to save drawing', err);
toast.error("Failed to save changes"); toast.error("Failed to save changes");
} }
}; };
savePreviewRef.current = async (elements: readonly any[], appState: any, files: any) => { savePreviewRef.current = async (drawingId: string, elements: readonly any[], appState: any, files: any) => {
if (!id) return; if (!drawingId) return;
try { try {
const currentSnapshot = latestElementsRef.current ?? elements; const currentSnapshot = latestElementsRef.current ?? elements;
@@ -484,13 +484,13 @@ export const Editor: React.FC = () => {
const preview = svg.outerHTML; const preview = svg.outerHTML;
console.log("[Editor] Saving preview", { console.log("[Editor] Saving preview", {
drawingId: id, drawingId,
elementCount: currentSnapshot.length, elementCount: currentSnapshot.length,
}); });
await api.updateDrawing(id, { preview }); await api.updateDrawing(drawingId, { preview });
console.log("[Editor] Preview save complete", { drawingId: id }); console.log("[Editor] Preview save complete", { drawingId });
} catch (err) { } catch (err) {
console.error('Failed to save preview', err); console.error('Failed to save preview', err);
} }
@@ -509,9 +509,9 @@ export const Editor: React.FC = () => {
const debouncedSave = useCallback( const debouncedSave = useCallback(
debounce((elements, appState) => { debounce((drawingId, elements, appState) => {
if (saveDataRef.current) { if (saveDataRef.current) {
saveDataRef.current(elements, appState); saveDataRef.current(drawingId, elements, appState);
} }
}, 1000), }, 1000),
[] // Empty dependency array = Stable across renders [] // Empty dependency array = Stable across renders
@@ -519,9 +519,9 @@ export const Editor: React.FC = () => {
// Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves. // Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves.
debouncedSaveRef.current = debouncedSave; debouncedSaveRef.current = debouncedSave;
const debouncedSavePreview = useCallback( const debouncedSavePreview = useCallback(
debounce((elements, appState, files) => { debounce((drawingId, elements, appState, files) => {
if (savePreviewRef.current) { if (savePreviewRef.current) {
savePreviewRef.current(elements, appState, files); savePreviewRef.current(drawingId, elements, appState, files);
} }
}, 10000), }, 10000),
[] []
@@ -536,6 +536,13 @@ export const Editor: React.FC = () => {
[] []
); );
useEffect(() => {
return () => {
debouncedSave.cancel();
debouncedSavePreview.cancel();
};
}, [debouncedSave, debouncedSavePreview]);
const broadcastChanges = useCallback( const broadcastChanges = useCallback(
throttle((elements: readonly any[], currentFiles?: Record<string, any>) => { throttle((elements: readonly any[], currentFiles?: Record<string, any>) => {
if (!socketRef.current || !id) return; if (!socketRef.current || !id) return;
@@ -670,8 +677,9 @@ export const Editor: React.FC = () => {
const files = excalidrawAPI.current.getFiles() || {}; const files = excalidrawAPI.current.getFiles() || {};
latestElementsRef.current = elements; latestElementsRef.current = elements;
latestFilesRef.current = files; latestFilesRef.current = files;
await saveDataRef.current(elements, appState); if (!id) return;
savePreviewRef.current(elements, appState, files); await saveDataRef.current(id, elements, appState);
savePreviewRef.current(id, elements, appState, files);
toast.success("Saved changes to server"); toast.success("Saved changes to server");
} }
} }
@@ -739,7 +747,9 @@ export const Editor: React.FC = () => {
elementCount: allElements.length, elementCount: allElements.length,
hasRenderableElements, hasRenderableElements,
}); });
debouncedSave(allElements, appState); if (id) {
debouncedSave(id, allElements, appState);
}
// Trigger Slow Preview Gen // Trigger Slow Preview Gen
const filesSnapshot = currentFiles; const filesSnapshot = currentFiles;
@@ -748,8 +758,10 @@ export const Editor: React.FC = () => {
drawingId: id, drawingId: id,
fileCount: Object.keys(filesSnapshot).length, fileCount: Object.keys(filesSnapshot).length,
}); });
debouncedSavePreview(allElements, appState, filesSnapshot); if (id) {
}, [debouncedSave, debouncedSavePreview, broadcastChanges]); debouncedSavePreview(id, allElements, appState, filesSnapshot);
}
}, [debouncedSave, debouncedSavePreview, broadcastChanges, id]);
// Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously) // Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously)
// are still broadcast to collaborators AND persisted to the server. // are still broadcast to collaborators AND persisted to the server.
@@ -767,7 +779,7 @@ export const Editor: React.FC = () => {
// Persist after file data becomes available (covers the "tab 3" case). // Persist after file data becomes available (covers the "tab 3" case).
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) { if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) {
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current); debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
} }
}, 1000); }, 1000);
@@ -803,6 +815,7 @@ export const Editor: React.FC = () => {
// Save drawing and generate preview before navigating // Save drawing and generate preview before navigating
try { try {
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) { if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
if (!id) return;
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted(); const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
const appState = excalidrawAPI.current.getAppState(); const appState = excalidrawAPI.current.getAppState();
const files = excalidrawAPI.current.getFiles() || {}; const files = excalidrawAPI.current.getFiles() || {};
@@ -810,8 +823,8 @@ export const Editor: React.FC = () => {
latestFilesRef.current = files; latestFilesRef.current = files;
await Promise.all([ await Promise.all([
saveDataRef.current(elements, appState), saveDataRef.current(id, elements, appState),
savePreviewRef.current(elements, appState, files) savePreviewRef.current(id, elements, appState, files)
]); ]);
console.log("[Editor] Saved on back navigation", { drawingId: id }); console.log("[Editor] Saved on back navigation", { drawingId: id });
} }
@@ -827,7 +840,7 @@ export const Editor: React.FC = () => {
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden"> <div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
<header <header
className={clsx( className={clsx(
"h-14 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10 fixed top-0 left-0 right-0 transition-transform duration-300", "h-16 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10 fixed top-0 left-0 right-0 transition-transform duration-300",
isHeaderVisible ? "translate-y-0" : "-translate-y-full" isHeaderVisible ? "translate-y-0" : "-translate-y-full"
)} )}
> >
@@ -945,8 +958,8 @@ export const Editor: React.FC = () => {
<div <div
className="flex-1 w-full relative transition-all duration-300" className="flex-1 w-full relative transition-all duration-300"
style={{ style={{
height: isHeaderVisible ? 'calc(100vh - 3.5rem)' : '100vh', height: isHeaderVisible ? 'calc(100vh - 4rem)' : '100vh',
marginTop: isHeaderVisible ? '3.5rem' : '0' marginTop: isHeaderVisible ? '4rem' : '0'
}} }}
> >
{loadError ? ( {loadError ? (