diff --git a/src/App.tsx b/src/App.tsx index c1971a3..ac3d29d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -87,6 +87,16 @@ async function requestJson(url: string, init?: RequestInit): Promise { return body; } +function DrawerIcon() { + return ( + + ); +} + export function App() { const [drawings, setDrawings] = useState([]); const [activeId, setActiveId] = useState(pathToId()); @@ -94,6 +104,7 @@ export function App() { const [scene, setScene] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const currentIdRef = useRef(activeId); const currentTitleRef = useRef(activeTitle); @@ -370,13 +381,104 @@ export function App() { }; }, []); + useEffect(() => { + if (!isSidebarOpen) { + return; + } + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsSidebarOpen(false); + } + }; + + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [isSidebarOpen]); + + const openSidebar = useCallback(() => { + setIsSidebarOpen(true); + }, []); + + const closeSidebar = useCallback(() => { + setIsSidebarOpen(false); + }, []); + + const handleSelectDrawing = useCallback( + async (drawingId: string) => { + setIsSidebarOpen(false); + await navigateToDrawing(drawingId); + }, + [navigateToDrawing], + ); + + const handleCreateDrawing = useCallback(async () => { + setIsSidebarOpen(false); + await createDrawing(); + }, [createDrawing]); + return (
- - -
-
- - -
- - {loading || !scene ? ( -
{error ?? "Loading..."}
- ) : ( -
- { - const nextScene = sceneFromEditor(elements, appState, files); - latestSceneRef.current = nextScene; - - if (ignoreChangeRef.current) { - ignoreChangeRef.current = false; - return; - } - - scheduleSave(); - }} - /> -
- )} -
); } diff --git a/src/styles.css b/src/styles.css index b6a593a..4f3c81c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -32,17 +32,29 @@ input { } .app-shell { - display: flex; height: 100%; } .sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 40; display: flex; - width: 260px; - min-width: 260px; + height: 100%; + width: min(340px, calc(100vw - 32px)); flex-direction: column; border-right: 1px solid #e4e4e7; background: #ffffff; + box-shadow: 0 24px 80px rgba(24, 24, 27, 0.22); + transform: translateX(calc(-100% - 24px)); + transition: + transform 180ms ease, + box-shadow 180ms ease; +} + +.sidebar-open { + transform: translateX(0); } .sidebar-header { @@ -60,7 +72,14 @@ input { font-weight: 600; } +.sidebar-actions { + display: flex; + align-items: center; + gap: 8px; +} + .drawing-list { + flex: 1; overflow: auto; padding: 8px; } @@ -104,18 +123,41 @@ input { .editor-shell { position: relative; - min-width: 0; - flex: 1; + height: 100%; } -.toolbar { - position: absolute; - top: 16px; - right: 16px; +.app-actions { + position: fixed; + right: max(16px, calc(env(safe-area-inset-right) + 16px)); + bottom: calc(max(16px, env(safe-area-inset-bottom)) + 56px); z-index: 20; display: flex; +} + +.app-actions-toggle { + display: inline-flex; align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + padding: 0; + border: none; + box-shadow: 0 0 0 1px #1e1e24; + background: #2f2f37; + color: #f4f4f5; +} + +.app-actions-toggle svg { + width: 0.95rem; + height: 0.95rem; +} + +.sidebar-toolbar { + display: flex; + flex-wrap: wrap; gap: 8px; + border-bottom: 1px solid #e4e4e7; + padding: 12px 16px; } .primary-button, @@ -143,6 +185,21 @@ input { cursor: pointer; } +.sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 30; + background: rgba(24, 24, 27, 0.38); + opacity: 0; + pointer-events: none; + transition: opacity 180ms ease; +} + +.sidebar-backdrop-open { + opacity: 1; + pointer-events: auto; +} + .editor-frame, .editor-loading { height: 100%; @@ -154,16 +211,34 @@ input { color: #52525b; } +.app-actions-toggle:hover { + background: #3b3b45; +} + +.app-actions-toggle:active { + box-shadow: 0 0 0 1px #5b8def; + background: #444450; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + @media (max-width: 900px) { .sidebar { - width: 220px; - min-width: 220px; + width: min(300px, calc(100vw - 20px)); } - .toolbar { - left: 12px; - right: 12px; - flex-wrap: wrap; - justify-content: flex-end; + .app-actions { + right: max(16px, calc(env(safe-area-inset-right) + 16px)); + bottom: calc(max(16px, env(safe-area-inset-bottom)) + 64px); } }