Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 589ad162f9 | |||
| 93608bda1e | |||
| e576ca7d2d | |||
| 8630492b44 | |||
| d52c08cd10 |
+7
-61
@@ -80,10 +80,10 @@ async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|||||||
|
|
||||||
function DrawerIcon() {
|
function DrawerIcon() {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true" width="16" height="16">
|
||||||
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
|
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
||||||
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,6 @@ export function App() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const currentIdRef = useRef<string | null>(activeId);
|
const currentIdRef = useRef<string | null>(activeId);
|
||||||
const currentTitleRef = useRef(activeTitle);
|
const currentTitleRef = useRef(activeTitle);
|
||||||
@@ -107,7 +106,6 @@ export function App() {
|
|||||||
const inFlightSaveRef = useRef<Promise<void> | null>(null);
|
const inFlightSaveRef = useRef<Promise<void> | null>(null);
|
||||||
const loadVersionRef = useRef(0);
|
const loadVersionRef = useRef(0);
|
||||||
const themeSyncFrameRef = useRef<number | null>(null);
|
const themeSyncFrameRef = useRef<number | null>(null);
|
||||||
const toastTimeoutRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const activeDrawing = useMemo(
|
const activeDrawing = useMemo(
|
||||||
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
|
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
|
||||||
@@ -120,7 +118,7 @@ export function App() {
|
|||||||
return list;
|
return list;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const saveScene = useCallback(async (isManual = false) => {
|
const saveScene = useCallback(async () => {
|
||||||
const drawingId = currentIdRef.current;
|
const drawingId = currentIdRef.current;
|
||||||
const nextScene = latestSceneRef.current;
|
const nextScene = latestSceneRef.current;
|
||||||
if (!drawingId || !nextScene) {
|
if (!drawingId || !nextScene) {
|
||||||
@@ -128,37 +126,15 @@ export function App() {
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (isManual) {
|
|
||||||
setToastMessage("Saving...");
|
|
||||||
if (toastTimeoutRef.current !== null) {
|
|
||||||
clearTimeout(toastTimeoutRef.current);
|
|
||||||
toastTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
|
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(nextScene),
|
body: JSON.stringify(nextScene),
|
||||||
})
|
})
|
||||||
.then((body) => {
|
.then((body) => {
|
||||||
setDrawings((current) => replaceMeta(current, body.drawing));
|
setDrawings((current) => replaceMeta(current, body.drawing));
|
||||||
if (isManual) {
|
|
||||||
setToastMessage("Saved");
|
|
||||||
if (toastTimeoutRef.current !== null) {
|
|
||||||
clearTimeout(toastTimeoutRef.current);
|
|
||||||
}
|
|
||||||
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), 2000);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((saveError: unknown) => {
|
.catch((saveError: unknown) => {
|
||||||
setError(saveError instanceof Error ? saveError.message : "Save failed");
|
setError(saveError instanceof Error ? saveError.message : "Save failed");
|
||||||
if (isManual) {
|
|
||||||
setToastMessage("Save failed");
|
|
||||||
if (toastTimeoutRef.current !== null) {
|
|
||||||
clearTimeout(toastTimeoutRef.current);
|
|
||||||
}
|
|
||||||
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), 2000);
|
|
||||||
}
|
|
||||||
throw saveError;
|
throw saveError;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -182,17 +158,6 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}, [saveScene]);
|
}, [saveScene]);
|
||||||
|
|
||||||
const triggerManualSave = useCallback(async () => {
|
|
||||||
if (timeoutRef.current !== null) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
timeoutRef.current = null;
|
|
||||||
}
|
|
||||||
if (inFlightSaveRef.current) {
|
|
||||||
await inFlightSaveRef.current;
|
|
||||||
}
|
|
||||||
await saveScene(true);
|
|
||||||
}, [saveScene]);
|
|
||||||
|
|
||||||
const scheduleSave = useCallback(() => {
|
const scheduleSave = useCallback(() => {
|
||||||
if (timeoutRef.current !== null) {
|
if (timeoutRef.current !== null) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
@@ -201,7 +166,7 @@ export function App() {
|
|||||||
timeoutRef.current = window.setTimeout(() => {
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
timeoutRef.current = null;
|
timeoutRef.current = null;
|
||||||
void saveScene();
|
void saveScene();
|
||||||
}, 30000);
|
}, 1500);
|
||||||
}, [saveScene]);
|
}, [saveScene]);
|
||||||
|
|
||||||
const loadDrawing = useCallback(
|
const loadDrawing = useCallback(
|
||||||
@@ -364,20 +329,6 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}, [syncThemeTokens]);
|
}, [syncThemeTokens]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void triggerManualSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
||||||
};
|
|
||||||
}, [triggerManualSave]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPathId = pathToId();
|
const currentPathId = pathToId();
|
||||||
if (currentPathId) {
|
if (currentPathId) {
|
||||||
@@ -459,16 +410,11 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell" ref={appShellRef}>
|
<div className="app-shell" ref={appShellRef}>
|
||||||
{toastMessage && (
|
|
||||||
<div className="toast-overlay" aria-live="polite">
|
|
||||||
{toastMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<main className="editor-shell">
|
<main className="editor-shell">
|
||||||
<div className="app-actions">
|
<div className="app-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary-button app-actions-toggle"
|
className="app-actions-toggle"
|
||||||
onClick={openSidebar}
|
onClick={openSidebar}
|
||||||
aria-label="Open drawings"
|
aria-label="Open drawings"
|
||||||
>
|
>
|
||||||
|
|||||||
+11
-28
@@ -169,22 +169,21 @@ input {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: var(--lg-button-size, 2.25rem);
|
width: 2.25rem;
|
||||||
height: var(--lg-button-size, 2.25rem);
|
height: 2.25rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-lg, 12px);
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 0 1px var(--app-surface-lowest);
|
box-shadow: none;
|
||||||
background: var(--app-surface-low);
|
background: var(--app-surface-low);
|
||||||
color: var(--app-text-color);
|
color: var(--app-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 120ms ease, box-shadow 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-actions-toggle svg {
|
.app-actions-toggle svg {
|
||||||
width: 1rem;
|
width: 16px;
|
||||||
height: 1rem;
|
height: 16px;
|
||||||
overflow: visible;
|
|
||||||
transform: translateY(0.5px) scale(1.08);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
@@ -253,22 +252,6 @@ input {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
z-index: 100;
|
|
||||||
background: var(--app-overlay-bg);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
color: var(--app-text-color);
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: var(--app-sidebar-font-size);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid var(--app-sidebar-border);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-frame,
|
.editor-frame,
|
||||||
.editor-loading {
|
.editor-loading {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -281,12 +264,12 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-actions-toggle:hover {
|
.app-actions-toggle:hover {
|
||||||
background: var(--app-button-hover-bg);
|
background-color: var(--app-button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-actions-toggle:active {
|
.app-actions-toggle:active {
|
||||||
box-shadow: 0 0 0 1px var(--app-button-active-border);
|
background-color: var(--app-button-active-bg);
|
||||||
background: var(--app-button-active-bg);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
|
|||||||
Reference in New Issue
Block a user