update: Add a left drawer to improve workbook management (#453)

* update: add leftbar to app

* style: a few cosmetic changes

* update: allow pinning workbooks

* style: show ellipsis button only on hover

* update: add basic responsiveness

* style: use active state when file and help menus are open

* style: increase transition time

* update: allow duplication of workbooks

* chore: standardize menus
This commit is contained in:
Daniel González-Albo
2025-10-19 10:20:31 +02:00
committed by GitHub
parent dd4467f95d
commit f2da24326b
14 changed files with 1007 additions and 212 deletions

View File

@@ -2,6 +2,7 @@ import "./App.css";
import styled from "@emotion/styled";
import { useEffect, useState } from "react";
import { FileBar } from "./components/FileBar";
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog";
import {
get_documentation_model,
@@ -10,6 +11,7 @@ import {
} from "./components/rpc";
import {
createNewModel,
deleteModelByUuid,
deleteSelectedModel,
isStorageEmpty,
loadSelectedModelFromStorage,
@@ -27,6 +29,7 @@ function App() {
const [model, setModel] = useState<Model | null>(null);
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
useEffect(() => {
async function start() {
@@ -88,43 +91,71 @@ function App() {
}
}, 1000);
// Handlers for model changes that also update our models state
const handleNewModel = () => {
const newModel = createNewModel();
setModel(newModel);
};
const handleSetModel = (uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
};
const handleDeleteModel = () => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
}
};
const handleDeleteModelByUuid = (uuid: string) => {
const newModel = deleteModelByUuid(uuid);
if (newModel) {
setModel(newModel);
}
};
// We could use context for model, but the problem is that it should initialized to null.
// Passing the property down makes sure it is always defined.
return (
<Wrapper>
<FileBar
model={model}
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
const blob = await uploadFile(arrayBuffer, fileName);
const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel);
setModel(newModel);
}}
newModel={() => {
const createdModel = createNewModel();
setModel(createdModel);
}}
newModelFromTemplate={() => {
setTemplatesDialogOpen(true);
}}
setModel={(uuid: string) => {
const newModel = selectModelFromStorage(uuid);
if (newModel) {
setModel(newModel);
}
}}
onDelete={() => {
const newModel = deleteSelectedModel();
if (newModel) {
setModel(newModel);
}
}}
<LeftDrawer
open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
newModel={handleNewModel}
setModel={handleSetModel}
onDelete={handleDeleteModelByUuid}
/>
<IronCalc model={model} />
<MainContent isDrawerOpen={isDrawerOpen}>
{isDrawerOpen && (
<MobileOverlay onClick={() => setIsDrawerOpen(false)} />
)}
<FileBar
model={model}
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
const blob = await uploadFile(arrayBuffer, fileName);
const bytes = new Uint8Array(await blob.arrayBuffer());
const newModel = Model.from_bytes(bytes);
saveModelToStorage(newModel);
setModel(newModel);
}}
newModel={handleNewModel}
newModelFromTemplate={() => {
setTemplatesDialogOpen(true);
}}
setModel={handleSetModel}
onDelete={handleDeleteModel}
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
/>
<IronCalc model={model} />
</MainContent>
{showWelcomeDialog && (
<WelcomeDialog
onClose={() => {
@@ -175,13 +206,44 @@ function App() {
}
const Wrapper = styled("div")`
margin: 0px;
padding: 0px;
display: flex;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
`;
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : "-264px")};
transition: margin-left 0.2s;
width: ${({ isDrawerOpen }) =>
isDrawerOpen ? "calc(100% - 264px)" : "100%"};
display: flex;
flex-direction: column;
position: relative;
@media (max-width: 440px) {
${({ isDrawerOpen }) =>
isDrawerOpen &&
`
min-width: 440px;
`}
`;
const MobileOverlay = styled("div")`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
cursor: pointer;
@media (min-width: 441px) {
display: none;
}
`;
const Loading = styled("div")`