Merge pull request #570 from ironcalc/dani/app/localstorage-warning

update: add data-storage warnings to the app
This commit is contained in:
Daniel González-Albo
2025-11-23 13:12:35 +01:00
committed by GitHub
3 changed files with 202 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook"; import type { Model } from "@ironcalc/workbook";
import { IconButton, Tooltip } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { CloudOff, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useLayoutEffect, useRef, useState } from "react"; import { useLayoutEffect, useRef, useState } from "react";
import { FileMenu } from "./FileMenu"; import { FileMenu } from "./FileMenu";
import { HelpMenu } from "./HelpMenu"; import { HelpMenu } from "./HelpMenu";
@@ -41,6 +41,9 @@ export function FileBar(properties: {
const [maxTitleWidth, setMaxTitleWidth] = useState(0); const [maxTitleWidth, setMaxTitleWidth] = useState(0);
const width = useWindowWidth(); const width = useWindowWidth();
const cloudWarningText1 = `This workbook is stored only in your browser. To keep it safe, download it as .xlsx.`;
const cloudWarningText2 = ` Future versions may be incompatible.`;
// 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(() => {
const el = spacerRef.current; const el = spacerRef.current;
@@ -100,6 +103,49 @@ export function FileBar(properties: {
}} }}
maxWidth={maxTitleWidth} maxWidth={maxTitleWidth}
/> />
<Tooltip
title={
<div
style={{ display: "flex", flexDirection: "column", gap: "4px" }}
>
<div>{cloudWarningText1}</div>
<div style={{ fontWeight: "bold" }}>{cloudWarningText2}</div>
</div>
}
placement="bottom-start"
enterDelay={500}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
},
tooltip: {
sx: {
maxWidth: "240px",
fontSize: "11px",
padding: "8px",
backgroundColor: "#fff",
color: "#333333",
borderRadius: "8px",
border: "1px solid #e0e0e0",
boxShadow: "0px 1px 3px 0px #0000001A",
fontFamily: "Inter",
fontWeight: "400",
lineHeight: "16px",
},
},
}}
>
<CloudButton>
<CloudOff />
</CloudButton>
</Tooltip>
</WorkbookTitleWrapper> </WorkbookTitleWrapper>
<Spacer ref={spacerRef} /> <Spacer ref={spacerRef} />
<DialogContainer> <DialogContainer>
@@ -120,10 +166,35 @@ export function FileBar(properties: {
// so we need an absolute position // so we need an absolute position
const WorkbookTitleWrapper = styled("div")` const WorkbookTitleWrapper = styled("div")`
position: absolute; position: absolute;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
`; `;
const CloudButton = styled("div")`
display: flex;
align-items: center;
justify-content: center;
cursor: default;
background-color: transparent;
border-radius: 4px;
padding: 8px;
&:hover {
background-color: #f2f2f2;
}
&:active {
background-color: #e0e0e0;
}
svg {
width: 16px;
height: 16px;
color: #bdbdbd;
}
`;
// 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
const Spacer = styled("div")` const Spacer = styled("div")`
flex-grow: 1; flex-grow: 1;

View File

@@ -1,4 +1,5 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import LocalStorageAlert from "./LocalStorageAlert";
import WorkbookList from "./WorkbookList"; import WorkbookList from "./WorkbookList";
interface DrawerContentProps { interface DrawerContentProps {
@@ -10,9 +11,14 @@ function DrawerContent(props: DrawerContentProps) {
const { setModel, onDelete } = props; const { setModel, onDelete } = props;
return ( return (
<ContentContainer> <>
<WorkbookList setModel={setModel} onDelete={onDelete} /> <ContentContainer>
</ContentContainer> <WorkbookList setModel={setModel} onDelete={onDelete} />
</ContentContainer>
<LocalStorageAlertWrapper>
<LocalStorageAlert />
</LocalStorageAlertWrapper>
</>
); );
} }
@@ -22,8 +28,17 @@ const ContentContainer = styled("div")`
gap: 4px; gap: 4px;
padding: 16px 12px; padding: 16px 12px;
height: 100%; height: 100%;
overflow: scroll; overflow-y: auto;
overflow-x: hidden;
font-size: 12px; font-size: 12px;
`; `;
const LocalStorageAlertWrapper = styled("div")`
position: absolute;
bottom: 56px;
left: 0;
right: 0;
padding: 12px;
`;
export default DrawerContent; export default DrawerContent;

View File

@@ -0,0 +1,111 @@
import styled from "@emotion/styled";
import { Alert } from "@mui/material";
import { CircleAlert, X } from "lucide-react";
import { useState } from "react";
const ALERT_DISMISSED_KEY = "localStorageAlertDismissed";
function LocalStorageAlert() {
const [isAlertVisible, setIsAlertVisible] = useState(
() => localStorage.getItem(ALERT_DISMISSED_KEY) !== "true",
);
const handleClose = () => {
setIsAlertVisible(false);
localStorage.setItem(ALERT_DISMISSED_KEY, "true");
};
if (!isAlertVisible) {
return null;
}
return (
<AlertWrapper
icon={<CircleAlert />}
action={
<CloseButton onClick={handleClose}>
<X />
</CloseButton>
}
sx={{
padding: 0,
borderRadius: "8px",
backgroundColor: "rgba(255, 255, 255, 0.4)",
backdropFilter: "blur(10px)",
border: "1px solid #e0e0e0",
boxShadow: "0px 1px 3px 0px #0000001A",
fontFamily: "Inter",
fontWeight: "400",
lineHeight: "16px",
zIndex: 1,
}}
>
<AlertTitle>Heads up!</AlertTitle>
<AlertBody>
IronCalc stores your data only in your browser's local storage.
</AlertBody>
<AlertBody style={{ marginTop: "6px" }}>
<strong>Download your XLSX often</strong> future versions may not open
current workbooks, even if shared.
</AlertBody>
</AlertWrapper>
);
}
const AlertWrapper = styled(Alert)`
margin: 0;
.MuiAlert-message {
font-size: 11px;
padding: 12px 12px 12px 6px;
color: #333333;
}
.MuiAlert-icon {
height: 12px;
width: 12px;
color: #f2994a;
margin: 2px 0px 0px 8px;
padding: 6px 0px;
}
`;
const AlertTitle = styled("h2")`
font-size: 11px;
font-weight: 600;
line-height: 16px;
color: #333333;
margin: 0px 0px 4px 0px;
`;
const AlertBody = styled("p")`
font-weight: 400;
line-height: 16px;
margin: 0;
`;
const CloseButton = styled("button")`
position: absolute;
right: 4px;
top: 4px;
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
color: #666666;
border-radius: 4px;
transition: background-color 0.2s;
svg {
width: 12px;
height: 12px;
}
&:hover {
background-color: #e0e0e0;
}
&:active {
background-color: #9e9e9e;
}
`;
export default LocalStorageAlert;