Compare commits
1 Commits
llm_test/a
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3edb068a01 |
@@ -1,7 +1,8 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { FileBar } from "./components/FileBar";
|
import { FileBar } from "./components/FileBar";
|
||||||
|
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
|
||||||
import {
|
import {
|
||||||
get_documentation_model,
|
get_documentation_model,
|
||||||
get_model,
|
get_model,
|
||||||
@@ -10,6 +11,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
createNewModel,
|
createNewModel,
|
||||||
deleteSelectedModel,
|
deleteSelectedModel,
|
||||||
|
getModelsMetadata,
|
||||||
|
getSelectedUuid,
|
||||||
loadModelFromStorageOrCreate,
|
loadModelFromStorageOrCreate,
|
||||||
saveModelToStorage,
|
saveModelToStorage,
|
||||||
saveSelectedModelInStorage,
|
saveSelectedModelInStorage,
|
||||||
@@ -21,6 +24,14 @@ import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [model, setModel] = useState<Model | null>(null);
|
const [model, setModel] = useState<Model | null>(null);
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [modelsMetadata, setModelsMetadata] = useState(getModelsMetadata());
|
||||||
|
const [selectedUuid, setSelectedUuid] = useState(getSelectedUuid());
|
||||||
|
|
||||||
|
const refreshModelsData = useCallback(() => {
|
||||||
|
setModelsMetadata(getModelsMetadata());
|
||||||
|
setSelectedUuid(getSelectedUuid());
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function start() {
|
async function start() {
|
||||||
@@ -38,6 +49,7 @@ function App() {
|
|||||||
const importedModel = Model.from_bytes(model_bytes);
|
const importedModel = Model.from_bytes(model_bytes);
|
||||||
localStorage.removeItem("selected");
|
localStorage.removeItem("selected");
|
||||||
setModel(importedModel);
|
setModel(importedModel);
|
||||||
|
refreshModelsData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Model not found, or failed to load");
|
alert("Model not found, or failed to load");
|
||||||
}
|
}
|
||||||
@@ -47,6 +59,7 @@ function App() {
|
|||||||
const importedModel = Model.from_bytes(model_bytes);
|
const importedModel = Model.from_bytes(model_bytes);
|
||||||
localStorage.removeItem("selected");
|
localStorage.removeItem("selected");
|
||||||
setModel(importedModel);
|
setModel(importedModel);
|
||||||
|
refreshModelsData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Example file not found, or failed to load");
|
alert("Example file not found, or failed to load");
|
||||||
}
|
}
|
||||||
@@ -54,10 +67,11 @@ function App() {
|
|||||||
// try to load from local storage
|
// try to load from local storage
|
||||||
const newModel = loadModelFromStorageOrCreate();
|
const newModel = loadModelFromStorageOrCreate();
|
||||||
setModel(newModel);
|
setModel(newModel);
|
||||||
|
refreshModelsData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start();
|
start();
|
||||||
}, []);
|
}, [refreshModelsData]);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return (
|
return (
|
||||||
@@ -79,48 +93,80 @@ function App() {
|
|||||||
// We could use context for model, but the problem is that it should initialized to null.
|
// 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.
|
// Passing the property down makes sure it is always defined.
|
||||||
|
|
||||||
|
// Handlers for model changes that also update our models state
|
||||||
|
const handleNewModel = () => {
|
||||||
|
const newModel = createNewModel();
|
||||||
|
setModel(newModel);
|
||||||
|
refreshModelsData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetModel = (uuid: string) => {
|
||||||
|
const newModel = selectModelFromStorage(uuid);
|
||||||
|
if (newModel) {
|
||||||
|
setModel(newModel);
|
||||||
|
refreshModelsData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteModel = () => {
|
||||||
|
const newModel = deleteSelectedModel();
|
||||||
|
if (newModel) {
|
||||||
|
setModel(newModel);
|
||||||
|
refreshModelsData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<AppContainer>
|
||||||
<FileBar
|
<LeftDrawer
|
||||||
model={model}
|
open={isDrawerOpen}
|
||||||
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
|
onClose={() => setIsDrawerOpen(false)}
|
||||||
const blob = await uploadFile(arrayBuffer, fileName);
|
newModel={handleNewModel}
|
||||||
|
setModel={handleSetModel}
|
||||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
models={modelsMetadata}
|
||||||
const newModel = Model.from_bytes(bytes);
|
selectedUuid={selectedUuid}
|
||||||
saveModelToStorage(newModel);
|
setDeleteDialogOpen={() => {}}
|
||||||
|
|
||||||
setModel(newModel);
|
|
||||||
}}
|
|
||||||
newModel={() => {
|
|
||||||
setModel(createNewModel());
|
|
||||||
}}
|
|
||||||
setModel={(uuid: string) => {
|
|
||||||
const newModel = selectModelFromStorage(uuid);
|
|
||||||
if (newModel) {
|
|
||||||
setModel(newModel);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDelete={() => {
|
|
||||||
const newModel = deleteSelectedModel();
|
|
||||||
if (newModel) {
|
|
||||||
setModel(newModel);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IronCalc model={model} />
|
|
||||||
</Wrapper>
|
<MainContent isDrawerOpen={isDrawerOpen}>
|
||||||
|
<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);
|
||||||
|
refreshModelsData();
|
||||||
|
}}
|
||||||
|
newModel={handleNewModel}
|
||||||
|
setModel={handleSetModel}
|
||||||
|
onDelete={handleDeleteModel}
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
setIsDrawerOpen={setIsDrawerOpen}
|
||||||
|
refreshModelsData={refreshModelsData}
|
||||||
|
/>
|
||||||
|
<IronCalc model={model} />
|
||||||
|
</MainContent>
|
||||||
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled("div")`
|
const AppContainer = styled("div")`
|
||||||
margin: 0px;
|
display: flex;
|
||||||
padding: 0px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
||||||
|
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : "-264px")};
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
width: ${({ isDrawerOpen }) =>
|
||||||
|
isDrawerOpen ? "calc(100% - 264px)" : "100%"};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Loading = styled("div")`
|
const Loading = styled("div")`
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import type { Model } from "@ironcalc/workbook";
|
import type { Model } from "@ironcalc/workbook";
|
||||||
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
import { Button, IconButton } from "@mui/material";
|
||||||
|
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import { FileMenu } from "./FileMenu";
|
import { DesktopMenu, MobileMenu } from "./FileMenu";
|
||||||
import { ShareButton } from "./ShareButton";
|
import { ShareButton } from "./ShareButton";
|
||||||
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
||||||
import { WorkbookTitle } from "./WorkbookTitle";
|
import { WorkbookTitle } from "./WorkbookTitle";
|
||||||
@@ -29,11 +30,15 @@ export function FileBar(properties: {
|
|||||||
setModel: (key: string) => void;
|
setModel: (key: string) => void;
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
setIsDrawerOpen: (open: boolean) => void;
|
||||||
|
refreshModelsData: () => void; // Add this new prop
|
||||||
}) {
|
}) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const spacerRef = useRef<HTMLDivElement>(null);
|
const spacerRef = useRef<HTMLDivElement>(null);
|
||||||
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
|
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
|
||||||
const width = useWindowWidth();
|
const width = useWindowWidth();
|
||||||
|
const fileButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -44,34 +49,54 @@ export function FileBar(properties: {
|
|||||||
}
|
}
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
|
// Common handler functions for both menu types
|
||||||
|
const handleDownload = async () => {
|
||||||
|
const model = properties.model;
|
||||||
|
const bytes = model.toBytes();
|
||||||
|
const fileName = model.getName();
|
||||||
|
await downloadModel(bytes, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileBarWrapper>
|
<FileBarWrapper>
|
||||||
<StyledDesktopLogo />
|
<DrawerButton
|
||||||
<StyledIronCalcIcon />
|
onClick={() => properties.setIsDrawerOpen(!properties.isDrawerOpen)}
|
||||||
<Divider />
|
disableRipple
|
||||||
<FileMenu
|
|
||||||
newModel={properties.newModel}
|
|
||||||
setModel={properties.setModel}
|
|
||||||
onModelUpload={properties.onModelUpload}
|
|
||||||
onDownload={async () => {
|
|
||||||
const model = properties.model;
|
|
||||||
const bytes = model.toBytes();
|
|
||||||
const fileName = model.getName();
|
|
||||||
await downloadModel(bytes, fileName);
|
|
||||||
}}
|
|
||||||
onDelete={properties.onDelete}
|
|
||||||
/>
|
|
||||||
<HelpButton
|
|
||||||
onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
|
|
||||||
>
|
>
|
||||||
Help
|
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||||
</HelpButton>
|
</DrawerButton>
|
||||||
|
<DesktopButtonsWrapper>
|
||||||
|
<DesktopMenu
|
||||||
|
newModel={properties.newModel}
|
||||||
|
setModel={properties.setModel}
|
||||||
|
onModelUpload={properties.onModelUpload}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onDelete={properties.onDelete}
|
||||||
|
/>
|
||||||
|
<FileBarButton
|
||||||
|
disableRipple
|
||||||
|
onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</FileBarButton>
|
||||||
|
</DesktopButtonsWrapper>
|
||||||
|
<MobileButtonsWrapper>
|
||||||
|
<MobileMenu
|
||||||
|
newModel={properties.newModel}
|
||||||
|
setModel={properties.setModel}
|
||||||
|
onModelUpload={properties.onModelUpload}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onDelete={properties.onDelete}
|
||||||
|
/>
|
||||||
|
</MobileButtonsWrapper>
|
||||||
|
<Spacer ref={spacerRef} />
|
||||||
<WorkbookTitleWrapper>
|
<WorkbookTitleWrapper>
|
||||||
<WorkbookTitle
|
<WorkbookTitle
|
||||||
name={properties.model.getName()}
|
name={properties.model.getName()}
|
||||||
onNameChange={(name) => {
|
onNameChange={(name) => {
|
||||||
properties.model.setName(name);
|
properties.model.setName(name);
|
||||||
updateNameSelectedWorkbook(properties.model, name);
|
updateNameSelectedWorkbook(properties.model, name);
|
||||||
|
properties.refreshModelsData();
|
||||||
}}
|
}}
|
||||||
maxWidth={maxTitleWidth}
|
maxWidth={maxTitleWidth}
|
||||||
/>
|
/>
|
||||||
@@ -91,12 +116,8 @@ export function FileBar(properties: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the workbook title to be exactly an the center of the page,
|
|
||||||
// so we need an absolute position
|
|
||||||
const WorkbookTitleWrapper = styled("div")`
|
const WorkbookTitleWrapper = styled("div")`
|
||||||
position: absolute;
|
position: relative;
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The "Spacer" component occupies as much space as possible between the menu and the share button
|
// The "Spacer" component occupies as much space as possible between the menu and the share button
|
||||||
@@ -104,51 +125,83 @@ const Spacer = styled("div")`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDesktopLogo = styled(IronCalcLogo)`
|
const DrawerButton = styled(IconButton)`
|
||||||
width: 120px;
|
margin-left: 8px;
|
||||||
margin-left: 12px;
|
height: 32px;
|
||||||
@media (max-width: 769px) {
|
width: 32px;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledIronCalcIcon = styled(IronCalcIcon)`
|
|
||||||
width: 36px;
|
|
||||||
margin-left: 10px;
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HelpButton = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: Inter;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
svg {
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: #757575;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
`;
|
&:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
const Divider = styled("div")`
|
}
|
||||||
margin: 0px 8px 0px 16px;
|
|
||||||
height: 12px;
|
|
||||||
border-left: 1px solid #e0e0e0;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The container must be relative positioned so we can position the title absolutely
|
// The container must be relative positioned so we can position the title absolutely
|
||||||
const FileBarWrapper = styled("div")`
|
const FileBarWrapper = styled("div")`
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
min-height: 60px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DesktopButtonsWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MobileButtonsWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
@media (min-width: 601px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FileBarButtonContainer = styled("div")`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FileBarButton = styled(Button)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
min-width: 0px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: #333333;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DialogContainer = styled("div")`
|
const DialogContainer = styled("div")`
|
||||||
|
|||||||
@@ -1,76 +1,165 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { Menu, MenuItem, Modal } from "@mui/material";
|
import { Button, IconButton, Menu, MenuItem, Modal } from "@mui/material";
|
||||||
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
EllipsisVertical,
|
||||||
|
FileDown,
|
||||||
|
FileUp,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
|
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
|
||||||
import UploadFileDialog from "./UploadFileDialog";
|
import UploadFileDialog from "./UploadFileDialog";
|
||||||
import { getModelsMetadata, getSelectedUuid } from "./storage";
|
import { getModelsMetadata, getSelectedUuid } from "./storage";
|
||||||
|
|
||||||
|
export function DesktopMenu(props: {
|
||||||
|
newModel: () => void;
|
||||||
|
setModel: (key: string) => void;
|
||||||
|
onDownload: () => void;
|
||||||
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
|
onDelete: () => void;
|
||||||
|
}) {
|
||||||
|
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
|
||||||
|
const anchorElement = useRef<HTMLButtonElement>(
|
||||||
|
null as unknown as HTMLButtonElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FileBarButton
|
||||||
|
onClick={(): void => setFileMenuOpen(!isFileMenuOpen)}
|
||||||
|
ref={anchorElement}
|
||||||
|
disableRipple
|
||||||
|
isOpen={isFileMenuOpen}
|
||||||
|
>
|
||||||
|
File
|
||||||
|
</FileBarButton>
|
||||||
|
<FileMenu
|
||||||
|
newModel={props.newModel}
|
||||||
|
setModel={props.setModel}
|
||||||
|
onDownload={props.onDownload}
|
||||||
|
onModelUpload={props.onModelUpload}
|
||||||
|
onDelete={props.onDelete}
|
||||||
|
isFileMenuOpen={isFileMenuOpen}
|
||||||
|
setFileMenuOpen={setFileMenuOpen}
|
||||||
|
setMobileMenuOpen={() => {}}
|
||||||
|
anchorElement={anchorElement}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileMenu(props: {
|
||||||
|
newModel: () => void;
|
||||||
|
setModel: (key: string) => void;
|
||||||
|
onDownload: () => void;
|
||||||
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
|
onDelete: () => void;
|
||||||
|
}) {
|
||||||
|
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
|
||||||
|
const anchorElement = useRef<HTMLButtonElement>(
|
||||||
|
null as unknown as HTMLButtonElement,
|
||||||
|
);
|
||||||
|
const [fileMenuAnchorEl, setFileMenuAnchorEl] = useState<HTMLElement | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuButton
|
||||||
|
onClick={(): void => setMobileMenuOpen(true)}
|
||||||
|
ref={anchorElement}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</MenuButton>
|
||||||
|
<StyledMenu
|
||||||
|
open={isMobileMenuOpen}
|
||||||
|
onClose={(): void => setMobileMenuOpen(false)}
|
||||||
|
anchorEl={anchorElement.current}
|
||||||
|
>
|
||||||
|
<MenuItemWrapper
|
||||||
|
onClick={(event) => {
|
||||||
|
setFileMenuOpen(true);
|
||||||
|
setFileMenuAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<MenuItemText>File</MenuItemText>
|
||||||
|
<ChevronRight />
|
||||||
|
</MenuItemWrapper>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItemWrapper
|
||||||
|
onClick={() => {
|
||||||
|
window.open("https://docs.ironcalc.com", "_blank");
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<MenuItemText>Help</MenuItemText>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</StyledMenu>
|
||||||
|
<FileMenu
|
||||||
|
newModel={props.newModel}
|
||||||
|
setModel={props.setModel}
|
||||||
|
onDownload={props.onDownload}
|
||||||
|
onModelUpload={props.onModelUpload}
|
||||||
|
onDelete={props.onDelete}
|
||||||
|
isFileMenuOpen={isFileMenuOpen}
|
||||||
|
setFileMenuOpen={setFileMenuOpen}
|
||||||
|
setMobileMenuOpen={setMobileMenuOpen}
|
||||||
|
anchorElement={anchorElement}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function FileMenu(props: {
|
export function FileMenu(props: {
|
||||||
newModel: () => void;
|
newModel: () => void;
|
||||||
setModel: (key: string) => void;
|
setModel: (key: string) => void;
|
||||||
onDownload: () => void;
|
onDownload: () => void;
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
isFileMenuOpen: boolean;
|
||||||
|
setFileMenuOpen: (open: boolean) => void;
|
||||||
|
setMobileMenuOpen: (open: boolean) => void;
|
||||||
|
anchorElement: React.RefObject<HTMLButtonElement>;
|
||||||
}) {
|
}) {
|
||||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
|
||||||
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
|
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
|
||||||
const anchorElement = useRef<HTMLDivElement>(null);
|
|
||||||
const models = getModelsMetadata();
|
const models = getModelsMetadata();
|
||||||
const uuids = Object.keys(models);
|
|
||||||
const selectedUuid = getSelectedUuid();
|
const selectedUuid = getSelectedUuid();
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const elements = [];
|
|
||||||
for (const uuid of uuids) {
|
|
||||||
elements.push(
|
|
||||||
<MenuItemWrapper
|
|
||||||
key={uuid}
|
|
||||||
onClick={() => {
|
|
||||||
props.setModel(uuid);
|
|
||||||
setMenuOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIndicator>
|
|
||||||
{uuid === selectedUuid ? <StyledCheck /> : ""}
|
|
||||||
</CheckIndicator>
|
|
||||||
<MenuItemText
|
|
||||||
style={{
|
|
||||||
maxWidth: "240px",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{models[uuid]}
|
|
||||||
</MenuItemText>
|
|
||||||
</MenuItemWrapper>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileMenuWrapper
|
<StyledMenu
|
||||||
onClick={(): void => setMenuOpen(true)}
|
open={props.isFileMenuOpen}
|
||||||
ref={anchorElement}
|
onClose={(): void => props.setFileMenuOpen(false)}
|
||||||
>
|
anchorEl={props.anchorElement.current}
|
||||||
File
|
anchorOrigin={{
|
||||||
</FileMenuWrapper>
|
vertical: "bottom",
|
||||||
<Menu
|
horizontal: "left",
|
||||||
open={isMenuOpen}
|
}}
|
||||||
onClose={(): void => setMenuOpen(false)}
|
transformOrigin={{
|
||||||
anchorEl={anchorElement.current}
|
vertical: "top",
|
||||||
sx={{
|
horizontal: "left",
|
||||||
"& .MuiPaper-root": { borderRadius: "8px", padding: "4px 0px" },
|
}}
|
||||||
"& .MuiList-root": { padding: "0" },
|
// To prevent closing parent menu when interacting with submenu
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (!isImportMenuOpen && !isDeleteDialogOpen) {
|
||||||
|
props.setFileMenuOpen(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// anchorOrigin={properties.anchorOrigin}
|
|
||||||
>
|
>
|
||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.newModel();
|
props.newModel();
|
||||||
setMenuOpen(false);
|
props.setFileMenuOpen(false);
|
||||||
|
props.setMobileMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
|
disableRipple
|
||||||
>
|
>
|
||||||
<StyledPlus />
|
<StyledPlus />
|
||||||
<MenuItemText>New</MenuItemText>
|
<MenuItemText>New</MenuItemText>
|
||||||
@@ -78,30 +167,37 @@ export function FileMenu(props: {
|
|||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImportMenuOpen(true);
|
setImportMenuOpen(true);
|
||||||
setMenuOpen(false);
|
props.setFileMenuOpen(false);
|
||||||
|
props.setMobileMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
|
disableRipple
|
||||||
>
|
>
|
||||||
<StyledFileUp />
|
<StyledFileUp />
|
||||||
<MenuItemText>Import</MenuItemText>
|
<MenuItemText>Import</MenuItemText>
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
<MenuItemWrapper>
|
<MenuItemWrapper
|
||||||
|
onClick={() => {
|
||||||
|
props.onDownload();
|
||||||
|
props.setMobileMenuOpen(false);
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
<StyledFileDown />
|
<StyledFileDown />
|
||||||
<MenuItemText onClick={props.onDownload}>
|
<MenuItemText>Download (.xlsx)</MenuItemText>
|
||||||
Download (.xlsx)
|
|
||||||
</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
|
<MenuDivider />
|
||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
setMenuOpen(false);
|
props.setFileMenuOpen(false);
|
||||||
|
props.setMobileMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
|
disableRipple
|
||||||
>
|
>
|
||||||
<StyledTrash />
|
<StyledTrash />
|
||||||
<MenuItemText>Delete workbook</MenuItemText>
|
<MenuItemText>Delete workbook</MenuItemText>
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
<MenuDivider />
|
</StyledMenu>
|
||||||
{elements}
|
|
||||||
</Menu>
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isImportMenuOpen}
|
open={isImportMenuOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -133,6 +229,46 @@ export function FileMenu(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MenuButton = styled(IconButton)`
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
svg {
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: #757575;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FileBarButton = styled(Button)<{ isOpen: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
min-width: 0px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: #333333;
|
||||||
|
background-color: ${({ isOpen }) => (isOpen ? "#f2f2f2" : "none")};
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledPlus = styled(Plus)`
|
const StyledPlus = styled(Plus)`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -161,13 +297,6 @@ const StyledTrash = styled(Trash2)`
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCheck = styled(Check)`
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: #333333;
|
|
||||||
padding-right: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuDivider = styled("div")`
|
const MenuDivider = styled("div")`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -179,6 +308,7 @@ const MenuDivider = styled("div")`
|
|||||||
const MenuItemText = styled("div")`
|
const MenuItemText = styled("div")`
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MenuItemWrapper = styled(MenuItem)`
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
@@ -191,23 +321,19 @@ const MenuItemWrapper = styled(MenuItem)`
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
`;
|
min-height: 32px;
|
||||||
|
svg {
|
||||||
const FileMenuWrapper = styled("div")`
|
width: 16px;
|
||||||
display: flex;
|
height: 16px;
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: Inter;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CheckIndicator = styled("span")`
|
const StyledMenu = styled(Menu)`
|
||||||
display: flex;
|
.MuiPaper-root {
|
||||||
justify-content: center;
|
border-radius: 8px;
|
||||||
min-width: 26px;
|
padding: 4px 0px;
|
||||||
|
},
|
||||||
|
.MuiList-root {
|
||||||
|
padding: 0;
|
||||||
|
},
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import { IronCalcLogo } from "@ironcalc/workbook";
|
||||||
|
import { Avatar, Drawer, IconButton, Menu, MenuItem } from "@mui/material";
|
||||||
|
import {
|
||||||
|
EllipsisVertical,
|
||||||
|
FileDown,
|
||||||
|
FileSpreadsheet,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import UserMenu from "../UserMenu";
|
||||||
|
|
||||||
|
interface LeftDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
newModel: () => void;
|
||||||
|
setModel: (key: string) => void;
|
||||||
|
models: { [key: string]: string };
|
||||||
|
selectedUuid: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeftDrawer: React.FC<LeftDrawerProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
newModel,
|
||||||
|
setModel,
|
||||||
|
models,
|
||||||
|
selectedUuid,
|
||||||
|
}) => {
|
||||||
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [selectedWorkbookUuid, setSelectedWorkbookUuid] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMenuOpen = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
uuid: string,
|
||||||
|
) => {
|
||||||
|
console.log("Menu open", uuid);
|
||||||
|
event.stopPropagation();
|
||||||
|
setSelectedWorkbookUuid(uuid);
|
||||||
|
setMenuAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setMenuAnchorEl(null);
|
||||||
|
setSelectedWorkbookUuid(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setUserMenuAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserMenuClose = () => {
|
||||||
|
setUserMenuAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = Object.keys(models)
|
||||||
|
.reverse()
|
||||||
|
.map((uuid) => {
|
||||||
|
const isMenuOpen = menuAnchorEl !== null && selectedWorkbookUuid === uuid;
|
||||||
|
return (
|
||||||
|
<WorkbookListItem
|
||||||
|
key={uuid}
|
||||||
|
onClick={() => {
|
||||||
|
setModel(uuid);
|
||||||
|
}}
|
||||||
|
selected={uuid === selectedUuid}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<StorageIndicator>
|
||||||
|
<FileSpreadsheet />
|
||||||
|
</StorageIndicator>
|
||||||
|
<WorkbookListText>{models[uuid]}</WorkbookListText>
|
||||||
|
<EllipsisButton
|
||||||
|
onClick={(e) => handleMenuOpen(e, uuid)}
|
||||||
|
disableRipple
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</EllipsisButton>
|
||||||
|
|
||||||
|
<StyledMenu
|
||||||
|
anchorEl={menuAnchorEl}
|
||||||
|
open={Boolean(menuAnchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
MenuListProps={{
|
||||||
|
dense: true,
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
<MenuItemWrapper
|
||||||
|
onClick={() => {
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<FileDown />
|
||||||
|
Download (.xlsx)
|
||||||
|
</MenuItemWrapper>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItemWrapper
|
||||||
|
selected={false}
|
||||||
|
onClick={() => {
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
Delete workbook
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</StyledMenu>
|
||||||
|
</WorkbookListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerWrapper
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<DrawerHeader>
|
||||||
|
<StyledDesktopLogo />
|
||||||
|
<AddButton
|
||||||
|
onClick={() => {
|
||||||
|
newModel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</AddButton>
|
||||||
|
</DrawerHeader>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerContentTitle>Your workbooks</DrawerContentTitle>
|
||||||
|
{elements}
|
||||||
|
</DrawerContent>
|
||||||
|
<DrawerFooter>
|
||||||
|
<UserWrapper
|
||||||
|
disableRipple
|
||||||
|
onClick={handleUserMenuOpen}
|
||||||
|
selected={Boolean(userMenuAnchorEl)}
|
||||||
|
>
|
||||||
|
<StyledAvatar
|
||||||
|
alt="Nikola Tesla"
|
||||||
|
src="/path/to/avatar.jpg"
|
||||||
|
sx={{ bgcolor: "#f2994a", width: 24, height: 24 }}
|
||||||
|
/>
|
||||||
|
<Username>Nikola Tesla</Username>
|
||||||
|
</UserWrapper>
|
||||||
|
<UserMenu
|
||||||
|
anchorEl={userMenuAnchorEl}
|
||||||
|
onClose={handleUserMenuClose}
|
||||||
|
onPreferences={() => {
|
||||||
|
console.log("Preferences clicked");
|
||||||
|
handleUserMenuClose();
|
||||||
|
}}
|
||||||
|
onLogout={() => {
|
||||||
|
console.log("Logout clicked");
|
||||||
|
handleUserMenuClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawerWrapper = styled(Drawer)`
|
||||||
|
width: 264px;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
|
||||||
|
.MuiDrawer-paper {
|
||||||
|
width: 264px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DrawerHeader = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 8px 12px 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-height: 60px;
|
||||||
|
min-height: 60px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDesktopLogo = styled(IronCalcLogo)`
|
||||||
|
width: 120px;
|
||||||
|
height: 28px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AddButton = styled(IconButton)`
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #333333;
|
||||||
|
stroke-width: 2px;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PlusIcon = styled(Plus)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DrawerContent = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DrawerContentTitle = styled("div")`
|
||||||
|
font-weight: 600;
|
||||||
|
color: #9e9e9e;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 0px 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StorageIndicator = styled("div")`
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
svg {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
stroke: #9e9e9e;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EllipsisButton = styled(IconButton)<{ isOpen: boolean }>`
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #333333;
|
||||||
|
stroke-width: 2px;
|
||||||
|
background-color: ${({ isOpen }) => (isOpen ? "#E0E0E0" : "none")};
|
||||||
|
opacity: ${({ isOpen }) => (isOpen ? "1" : "0.5")};
|
||||||
|
transition: opacity 0.3s, background-color 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background: #bdbdbd;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WorkbookListItem = styled(MenuItem)<{ selected: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 172px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 4px 8px 8px;
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
transition: gap 0.5s;
|
||||||
|
background-color: ${({ selected }) =>
|
||||||
|
selected ? "#e0e0e0 !important" : "transparent"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WorkbookListText = styled("div")`
|
||||||
|
color: #000;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMenu = styled(Menu)`
|
||||||
|
.MuiPaper-root {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.01);
|
||||||
|
},
|
||||||
|
.MuiList-root {
|
||||||
|
padding: 0;
|
||||||
|
},
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuDivider = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-top: 1px solid #eeeeee;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 12px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-width: 140px;
|
||||||
|
margin: 0px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
gap: 8px;
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DrawerFooter = styled("div")`
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-height: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserWrapper = styled(MenuItem)<{ selected: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: ${({ selected }) =>
|
||||||
|
selected ? "#e0e0e0 !important" : "transparent"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAvatar = styled(Avatar)`
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Username = styled("div")`
|
||||||
|
font-size: 12px;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default LeftDrawer;
|
||||||
90
webapp/app.ironcalc.com/frontend/src/components/UserMenu.tsx
Normal file
90
webapp/app.ironcalc.com/frontend/src/components/UserMenu.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import { Menu, MenuItem } from "@mui/material";
|
||||||
|
import { LogOut, Settings } from "lucide-react";
|
||||||
|
|
||||||
|
interface UserMenuProps {
|
||||||
|
anchorEl: null | HTMLElement;
|
||||||
|
onClose: () => void;
|
||||||
|
onPreferences: () => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserMenu: React.FC<UserMenuProps> = ({
|
||||||
|
anchorEl,
|
||||||
|
onClose,
|
||||||
|
onPreferences,
|
||||||
|
onLogout,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledMenu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={onClose}
|
||||||
|
MenuListProps={{
|
||||||
|
dense: true,
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItemWrapper onClick={onPreferences} sx={{ gap: 1, fontSize: 14 }}>
|
||||||
|
<Settings size={16} />
|
||||||
|
<MenuItemText>Preferences</MenuItemText>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItemWrapper onClick={onLogout} sx={{ gap: 1, fontSize: 14 }}>
|
||||||
|
<LogOut size={16} />
|
||||||
|
<MenuItemText>Log out</MenuItemText>
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</StyledMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledMenu = styled(Menu)`
|
||||||
|
& .MuiPaper-root {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
& .MuiList-root {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemText = styled("div")`
|
||||||
|
color: #000;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-width: 172px;
|
||||||
|
margin: 0px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuDivider = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-top: 1px solid #eeeeee;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default UserMenu;
|
||||||
@@ -72,10 +72,10 @@ export function WorkbookTitle(properties: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled("div")`
|
const Container = styled("div")`
|
||||||
text-align: center;
|
text-align: left;
|
||||||
padding: 8px;
|
padding: 6px 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ const TitleInput = styled("input")`
|
|||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid grey;
|
outline: 1px solid grey;
|
||||||
}
|
}
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
Reference in New Issue
Block a user