Compare commits
21 Commits
feature/da
...
right-draw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
443ff6808d | ||
|
|
ed64716f0f | ||
|
|
dd29287c5a | ||
|
|
7841abe2d2 | ||
|
|
49c3d1e03a | ||
|
|
b709041f9d | ||
|
|
b177a33815 | ||
|
|
b506ccf908 | ||
|
|
eb3e92ffd8 | ||
|
|
0b925a4d6a | ||
|
|
6a3e37f4c1 | ||
|
|
2496227344 | ||
|
|
72355a5201 | ||
|
|
81901ec717 | ||
|
|
aa664a95a1 | ||
|
|
c1aa743763 | ||
|
|
6321030ac8 | ||
|
|
c2c5751ee3 | ||
|
|
6c27ae1355 | ||
|
|
7bcd978998 | ||
|
|
3f083d9882 |
@@ -84,7 +84,7 @@ And then use this code in `main.rs`:
|
||||
|
||||
```rust
|
||||
use ironcalc::{
|
||||
base::{expressions::utils::number_to_column, model::Model},
|
||||
base::{expressions::utils::number_to_column, Model},
|
||||
export::save_to_xlsx,
|
||||
};
|
||||
|
||||
|
||||
@@ -99,10 +99,9 @@ const FormulaSymbolButton = styled(StyledButton)`
|
||||
|
||||
const Divider = styled("div")`
|
||||
background-color: ${theme.palette.grey["300"]};
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
min-width: 1px;
|
||||
height: 16px;
|
||||
margin: 0px 16px;
|
||||
`;
|
||||
|
||||
const FormulaContainer = styled("div")`
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
HorizontalAlignment,
|
||||
VerticalAlignment,
|
||||
} from "@ironcalc/wasm";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import type {} from "@mui/system";
|
||||
import {
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
ArrowUpToLine,
|
||||
Bold,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
DecimalsArrowLeft,
|
||||
DecimalsArrowRight,
|
||||
Euro,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
Grid2x2Check,
|
||||
Grid2x2X,
|
||||
ImageDown,
|
||||
Inbox,
|
||||
Italic,
|
||||
Minus,
|
||||
PaintBucket,
|
||||
@@ -36,7 +40,7 @@ import {
|
||||
Undo2,
|
||||
WrapText,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowMiddleFromLine } from "../../icons";
|
||||
import { theme } from "../../theme";
|
||||
@@ -87,6 +91,7 @@ type ToolbarProperties = {
|
||||
showGridLines: boolean;
|
||||
onToggleShowGridLines: (show: boolean) => void;
|
||||
nameManagerProperties: NameManagerProperties;
|
||||
openDrawer: () => void;
|
||||
};
|
||||
|
||||
function Toolbar(properties: ToolbarProperties) {
|
||||
@@ -94,45 +99,119 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false);
|
||||
const [borderPickerOpen, setBorderPickerOpen] = useState(false);
|
||||
const [nameManagerDialogOpen, setNameManagerDialogOpen] = useState(false);
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||
|
||||
const fontColorButton = useRef(null);
|
||||
const fillColorButton = useRef(null);
|
||||
const borderButton = useRef(null);
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { canEdit } = properties;
|
||||
|
||||
const scrollLeft = () =>
|
||||
toolbarRef.current?.scrollBy({ left: -200, behavior: "smooth" });
|
||||
const scrollRight = () =>
|
||||
toolbarRef.current?.scrollBy({ left: 200, behavior: "smooth" });
|
||||
|
||||
const updateArrows = useCallback(() => {
|
||||
if (!toolbarRef.current) return;
|
||||
const { scrollLeft, scrollWidth, clientWidth } = toolbarRef.current;
|
||||
setShowLeftArrow(scrollLeft > 0);
|
||||
setShowRightArrow(scrollLeft < scrollWidth - clientWidth);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const toolbar = toolbarRef.current;
|
||||
if (!toolbar) return;
|
||||
|
||||
updateArrows();
|
||||
toolbar.addEventListener("scroll", updateArrows);
|
||||
return () => toolbar.removeEventListener("scroll", updateArrows);
|
||||
}, [updateArrows]);
|
||||
|
||||
return (
|
||||
<ToolbarContainer>
|
||||
<ToolbarWrapper>
|
||||
{showLeftArrow && (
|
||||
<Tooltip
|
||||
title={t("toolbar.scroll_left")}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ScrollArrow $direction="left" onClick={scrollLeft}>
|
||||
<ChevronLeft />
|
||||
</ScrollArrow>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ToolbarContainer ref={toolbarRef}>
|
||||
{/* History/Edit Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.undo")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onUndo}
|
||||
disabled={!properties.canUndo}
|
||||
title={t("toolbar.undo")}
|
||||
>
|
||||
<Undo2 />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.redo")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onRedo}
|
||||
disabled={!properties.canRedo}
|
||||
title={t("toolbar.redo")}
|
||||
>
|
||||
<Redo2 />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Format Tools Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.copy_styles")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onCopyStyles}
|
||||
title={t("toolbar.copy_styles")}
|
||||
>
|
||||
<PaintRoller />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.clear_formatting")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => {
|
||||
properties.onClearFormatting();
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<RemoveFormatting />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Number Format Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.euro")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -140,10 +219,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
properties.onNumberFormatPicked(NumberFormats.CURRENCY_EUR);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.euro")}
|
||||
>
|
||||
<Euro />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.percentage")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -151,10 +231,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
properties.onNumberFormatPicked(NumberFormats.PERCENTAGE);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.percentage")}
|
||||
>
|
||||
<Percent />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.decimal_places_decrease")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -164,10 +245,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.decimal_places_decrease")}
|
||||
>
|
||||
<DecimalsArrowLeft />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.decimal_places_increase")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -177,10 +259,10 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.decimal_places_increase")}
|
||||
>
|
||||
<DecimalsArrowRight />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<FormatMenu
|
||||
numFmt={properties.numFmt}
|
||||
onChange={(numberFmt): void => {
|
||||
@@ -192,11 +274,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
vertical: "bottom",
|
||||
}}
|
||||
>
|
||||
<Tooltip title={t("toolbar.format_number")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.format_number")}
|
||||
sx={{
|
||||
width: "40px", // Keep in sync with anchorOrigin in FormatMenu above
|
||||
padding: "0px 4px",
|
||||
@@ -205,8 +287,15 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
{"123"}
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</FormatMenu>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Font Size Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.decrease_font_size")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -214,11 +303,12 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
onClick={() => {
|
||||
properties.onIncreaseFontSize(-1);
|
||||
}}
|
||||
title={t("toolbar.decrease_font_size")}
|
||||
>
|
||||
<Minus />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<FontSizeBox>{properties.fontSize}</FontSizeBox>
|
||||
<Tooltip title={t("toolbar.increase_font_size")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -226,81 +316,106 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
onClick={() => {
|
||||
properties.onIncreaseFontSize(1);
|
||||
}}
|
||||
title={t("toolbar.increase_font_size")}
|
||||
>
|
||||
<Plus />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Text Style Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.bold")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.bold}
|
||||
onClick={() => properties.onToggleBold(!properties.bold)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.bold")}
|
||||
>
|
||||
<Bold />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.italic")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.italic}
|
||||
onClick={() => properties.onToggleItalic(!properties.italic)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.italic")}
|
||||
>
|
||||
<Italic />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.underline")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.underline}
|
||||
onClick={() => properties.onToggleUnderline(!properties.underline)}
|
||||
onClick={() =>
|
||||
properties.onToggleUnderline(!properties.underline)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.underline")}
|
||||
>
|
||||
<Underline />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.strike_through")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.strike}
|
||||
onClick={() => properties.onToggleStrike(!properties.strike)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.strike_through")}
|
||||
>
|
||||
<Strikethrough />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Color & Border Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.font_color")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.font_color")}
|
||||
ref={fontColorButton}
|
||||
onClick={() => setFontColorPickerOpen(true)}
|
||||
>
|
||||
<Type />
|
||||
<ColorLine color={properties.fontColor} />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.fill_color")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.fill_color")}
|
||||
ref={fillColorButton}
|
||||
onClick={() => setFillColorPickerOpen(true)}
|
||||
>
|
||||
<PaintBucket />
|
||||
<ColorLine color={properties.fillColor} />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.borders.title")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => setBorderPickerOpen(true)}
|
||||
ref={borderButton}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.borders.title")}
|
||||
>
|
||||
<Grid2X2 />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Alignment Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.align_left")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "left"}
|
||||
@@ -310,23 +425,27 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_left")}
|
||||
>
|
||||
<AlignLeft />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.align_center")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "center"}
|
||||
onClick={() =>
|
||||
properties.onToggleHorizontalAlign(
|
||||
properties.horizontalAlign === "center" ? "general" : "center",
|
||||
properties.horizontalAlign === "center"
|
||||
? "general"
|
||||
: "center",
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_center")}
|
||||
>
|
||||
<AlignCenter />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.align_right")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "right"}
|
||||
@@ -336,37 +455,41 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_right")}
|
||||
>
|
||||
<AlignRight />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.vertical_align_top")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "top"}
|
||||
onClick={() => properties.onToggleVerticalAlign("top")}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_top")}
|
||||
>
|
||||
<ArrowUpToLine />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.vertical_align_middle")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "center"}
|
||||
onClick={() => properties.onToggleVerticalAlign("center")}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_middle")}
|
||||
>
|
||||
<ArrowMiddleFromLine />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.vertical_align_bottom")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "bottom"}
|
||||
onClick={() => properties.onToggleVerticalAlign("bottom")}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_bottom")}
|
||||
>
|
||||
<ArrowDownToLine />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.wrap_text")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.wrapText === true}
|
||||
@@ -374,12 +497,17 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
properties.onToggleWrapText(!properties.wrapText);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.wrap_text")}
|
||||
>
|
||||
<WrapText />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* View & Tools Group */}
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t("toolbar.show_hide_grid_lines")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -387,11 +515,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
properties.onToggleShowGridLines(!properties.showGridLines)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.show_hide_grid_lines")}
|
||||
>
|
||||
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.name_manager")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -399,23 +527,11 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
setNameManagerDialogOpen(true);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.name_manager")}
|
||||
>
|
||||
<Tags />
|
||||
</StyledButton>
|
||||
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => {
|
||||
properties.onClearFormatting();
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.clear_formatting")}
|
||||
>
|
||||
<RemoveFormatting />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("toolbar.selected_png")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
@@ -423,10 +539,24 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
properties.onDownloadPNG();
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.selected_png")}
|
||||
>
|
||||
<ImageDown />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t("toolbar.open_drawer")}>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => {
|
||||
properties.openDrawer();
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<Inbox />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<ColorPicker
|
||||
color={properties.fontColor}
|
||||
@@ -480,23 +610,52 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
model={properties.nameManagerProperties}
|
||||
/>
|
||||
</ToolbarContainer>
|
||||
{showRightArrow && (
|
||||
<Tooltip
|
||||
title={t("toolbar.scroll_right")}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ScrollArrow $direction="right" onClick={scrollRight}>
|
||||
<ChevronRight />
|
||||
</ScrollArrow>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ToolbarWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const ToolbarContainer = styled("div")`
|
||||
const ToolbarWrapper = styled("div")`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.palette.background.paper};
|
||||
height: ${TOOLBAR_HEIGHT}px;
|
||||
line-height: ${TOOLBAR_HEIGHT}px;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.palette.grey["300"]};
|
||||
font-family: Inter;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
`;
|
||||
|
||||
const ToolbarContainer = styled("div")`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
padding: 0px 12px;
|
||||
gap: 4px;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
type TypeButtonProperties = { $pressed: boolean };
|
||||
@@ -559,10 +718,10 @@ const ColorLine = styled("div")<{ color: string }>(({ color }) => ({
|
||||
}));
|
||||
|
||||
const Divider = styled("div")({
|
||||
width: "0px",
|
||||
height: "12px",
|
||||
borderLeft: `1px solid ${theme.palette.grey["300"]}`,
|
||||
margin: "0px 12px",
|
||||
minWidth: "1px",
|
||||
height: "16px",
|
||||
backgroundColor: theme.palette.grey["300"],
|
||||
margin: "0px 8px",
|
||||
});
|
||||
|
||||
const FontSizeBox = styled("div")({
|
||||
@@ -577,4 +736,39 @@ const FontSizeBox = styled("div")({
|
||||
minWidth: "24px",
|
||||
});
|
||||
|
||||
const ButtonGroup = styled("div")({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
});
|
||||
|
||||
type ScrollArrowProps = { $direction: "left" | "right" };
|
||||
const ScrollArrow = styled("button", {
|
||||
shouldForwardProp: (prop) => prop !== "$direction",
|
||||
})<ScrollArrowProps>(({ $direction }) => ({
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
[$direction]: "0px",
|
||||
zIndex: 10,
|
||||
width: "24px",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "white",
|
||||
border:
|
||||
$direction === "left"
|
||||
? `none; border-right: 1px solid ${theme.palette.grey["300"]};`
|
||||
: `none; border-left: 1px solid ${theme.palette.grey["300"]};`,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.grey["100"],
|
||||
},
|
||||
svg: {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
},
|
||||
}));
|
||||
|
||||
export default Toolbar;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
||||
getNewClipboardId,
|
||||
} from "../clipboard";
|
||||
import { TOOLBAR_HEIGHT } from "../constants";
|
||||
import {
|
||||
type NavigationKey,
|
||||
getCellAddress,
|
||||
@@ -41,6 +42,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
// This is needed because `model` or `workbookState` can change without React being aware of it
|
||||
const setRedrawId = useState(0)[1];
|
||||
|
||||
const [isDrawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const worksheets = model.getWorksheetsProperties();
|
||||
const info = worksheets.map(
|
||||
({ name, color, sheet_id, state }: WorksheetProperties) => {
|
||||
@@ -692,7 +695,11 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
worksheets,
|
||||
definedNameList: model.getDefinedNameList(),
|
||||
}}
|
||||
openDrawer={() => {
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? DRAWER_WIDTH : 0}>
|
||||
<FormulaBar
|
||||
cellAddress={cellAddress()}
|
||||
formulaValue={formulaValue()}
|
||||
@@ -759,10 +766,48 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
setRedrawId((value) => value + 1);
|
||||
}}
|
||||
/>
|
||||
</WorksheetAreaLeft>
|
||||
<WorksheetAreaRight $drawerWidth={isDrawerOpen ? DRAWER_WIDTH : 0}>
|
||||
<span
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
setDrawerOpen(false);
|
||||
}
|
||||
}}
|
||||
aria-label="Close drawer"
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</WorksheetAreaRight>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const DRAWER_WIDTH = 300;
|
||||
|
||||
type WorksheetAreaLeftProps = { $drawerWidth: number };
|
||||
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
||||
({ $drawerWidth }) => ({
|
||||
position: "absolute",
|
||||
top: `${TOOLBAR_HEIGHT + 1}px`,
|
||||
width: `calc(100% - ${$drawerWidth}px)`,
|
||||
height: `calc(100% - ${TOOLBAR_HEIGHT + 1}px)`,
|
||||
}),
|
||||
);
|
||||
|
||||
const WorksheetAreaRight = styled("div")<WorksheetAreaLeftProps>(
|
||||
({ $drawerWidth }) => ({
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "red",
|
||||
right: 0,
|
||||
top: `${TOOLBAR_HEIGHT + 1}px`,
|
||||
bottom: 0,
|
||||
width: `${$drawerWidth}px`,
|
||||
}),
|
||||
);
|
||||
|
||||
const Container = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -18,11 +18,7 @@ import {
|
||||
outlineColor,
|
||||
} from "../WorksheetCanvas/constants";
|
||||
import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||
import {
|
||||
FORMULA_BAR_HEIGHT,
|
||||
NAVIGATION_HEIGHT,
|
||||
TOOLBAR_HEIGHT,
|
||||
} from "../constants";
|
||||
import { FORMULA_BAR_HEIGHT, NAVIGATION_HEIGHT } from "../constants";
|
||||
import type { Cell } from "../types";
|
||||
import type { WorkbookState } from "../workbookState";
|
||||
import CellContextMenu from "./CellContextMenu";
|
||||
@@ -459,7 +455,7 @@ const SheetContainer = styled("div")`
|
||||
const Wrapper = styled("div")({
|
||||
position: "absolute",
|
||||
overflow: "scroll",
|
||||
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
|
||||
top: FORMULA_BAR_HEIGHT + 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: NAVIGATION_HEIGHT + 1,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const TOOLBAR_HEIGHT = 48;
|
||||
export const TOOLBAR_HEIGHT = 40;
|
||||
export const FORMULA_BAR_HEIGHT = 40;
|
||||
export const NAVIGATION_HEIGHT = 40;
|
||||
|
||||
@@ -18,6 +18,7 @@ import InsertRowAboveIcon from "./insert-row-above.svg?react";
|
||||
import InsertRowBelow from "./insert-row-below.svg?react";
|
||||
|
||||
import IronCalcIcon from "./ironcalc_icon.svg?react";
|
||||
import IronCalcIconWhite from "./ironcalc_icon_white.svg?react";
|
||||
import IronCalcLogo from "./orange+black.svg?react";
|
||||
|
||||
import Fx from "./fx.svg?react";
|
||||
@@ -41,6 +42,7 @@ export {
|
||||
InsertRowAboveIcon,
|
||||
InsertRowBelow,
|
||||
IronCalcIcon,
|
||||
IronCalcIconWhite,
|
||||
IronCalcLogo,
|
||||
Fx,
|
||||
};
|
||||
|
||||
7
webapp/IronCalc/src/icons/ironcalc_icon_white.svg
Normal file
7
webapp/IronCalc/src/icons/ironcalc_icon_white.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.8" d="M9.95898 8.08594C9.60893 8.35318 9.27389 8.64313 8.95898 8.95801C7.09126 10.8257 6.042 13.3586 6.04199 16H6.04102V7.91406C6.39142 7.64662 6.72781 7.35715 7.04297 7.04199C8.90157 5.18307 9.9492 2.6648 9.95898 0.0371094V8.08594Z" fill="white"/>
|
||||
<path opacity="0.8" d="M6.04102 7.91406C4.31493 9.23162 2.19571 9.95898 0 9.95898V6.04102C1.60208 6.04102 3.13861 5.40429 4.27148 4.27148C5.40436 3.13861 6.04101 1.60213 6.04102 0L6.04102 7.91406Z" fill="white"/>
|
||||
<path opacity="0.8" d="M9.95947 8.08594C11.6856 6.76838 13.8048 6.04102 16.0005 6.04102V9.95898C14.3984 9.95898 12.8619 10.5957 11.729 11.7285C10.5961 12.8614 9.95948 14.3979 9.95947 16L9.95947 8.08594Z" fill="white"/>
|
||||
<path d="M9.95898 0C9.95898 2.64126 8.90957 5.17429 7.04199 7.04199C6.727 7.35698 6.39119 7.64674 6.04102 7.91406L6.04102 0H9.95898Z" fill="white"/>
|
||||
<path d="M6.04102 16C6.04102 13.3587 7.09042 10.8257 8.95801 8.95801C9.273 8.64302 9.60881 8.35326 9.95898 8.08594V16H6.04102Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,5 @@
|
||||
import init, { Model } from "@ironcalc/wasm";
|
||||
import IronCalc from "./IronCalc";
|
||||
import { IronCalcIcon, IronCalcLogo } from "./icons";
|
||||
import { IronCalcIcon, IronCalcIconWhite, IronCalcLogo } from "./icons";
|
||||
|
||||
export { init, Model, IronCalc, IronCalcIcon, IronCalcLogo };
|
||||
export { init, Model, IronCalc, IronCalcIcon, IronCalcIconWhite, IronCalcLogo };
|
||||
|
||||
@@ -27,13 +27,15 @@
|
||||
"vertical_align_top": "Align top",
|
||||
"selected_png": "Export Selected area as PNG",
|
||||
"wrap_text": "Wrap text",
|
||||
"scroll_left": "Scroll left",
|
||||
"scroll_right": "Scroll right",
|
||||
"format_menu": {
|
||||
"auto": "Auto",
|
||||
"number": "Number",
|
||||
"percentage": "Percentage",
|
||||
"currency_eur": "Euro (EUR)",
|
||||
"currency_usd": "Dollar (USD)",
|
||||
"currency_gbp": "British Pound (GBD)",
|
||||
"currency_gbp": "British Pound (GBP)",
|
||||
"date_short": "Short date",
|
||||
"date_long": "Long date",
|
||||
"custom": "Custom",
|
||||
|
||||
@@ -2,6 +2,7 @@ import "./App.css";
|
||||
import styled from "@emotion/styled";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FileBar } from "./components/FileBar";
|
||||
import WelcomeDialog from "./components/WelcomeDialog/WelcomeDialog";
|
||||
import {
|
||||
get_documentation_model,
|
||||
get_model,
|
||||
@@ -10,7 +11,8 @@ import {
|
||||
import {
|
||||
createNewModel,
|
||||
deleteSelectedModel,
|
||||
loadModelFromStorageOrCreate,
|
||||
isStorageEmpty,
|
||||
loadSelectedModelFromStorage,
|
||||
saveModelToStorage,
|
||||
saveSelectedModelInStorage,
|
||||
selectModelFromStorage,
|
||||
@@ -18,9 +20,13 @@ import {
|
||||
|
||||
// From IronCalc
|
||||
import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
|
||||
import { Modal } from "@mui/material";
|
||||
import TemplatesDialog from "./components/WelcomeDialog/TemplatesDialog";
|
||||
|
||||
function App() {
|
||||
const [model, setModel] = useState<Model | null>(null);
|
||||
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
|
||||
const [isTemplatesDialogOpen, setTemplatesDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function start() {
|
||||
@@ -52,10 +58,16 @@ function App() {
|
||||
}
|
||||
} else {
|
||||
// try to load from local storage
|
||||
const newModel = loadModelFromStorageOrCreate();
|
||||
const newModel = loadSelectedModelFromStorage();
|
||||
if (!newModel) {
|
||||
setShowWelcomeDialog(true);
|
||||
const createdModel = new Model("template", "en", "UTC");
|
||||
setModel(createdModel);
|
||||
} else {
|
||||
setModel(newModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
start();
|
||||
}, []);
|
||||
|
||||
@@ -93,7 +105,11 @@ function App() {
|
||||
setModel(newModel);
|
||||
}}
|
||||
newModel={() => {
|
||||
setModel(createNewModel());
|
||||
const createdModel = createNewModel();
|
||||
setModel(createdModel);
|
||||
}}
|
||||
newModelFromTemplate={() => {
|
||||
setTemplatesDialogOpen(true);
|
||||
}}
|
||||
setModel={(uuid: string) => {
|
||||
const newModel = selectModelFromStorage(uuid);
|
||||
@@ -109,6 +125,51 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
<IronCalc model={model} />
|
||||
{showWelcomeDialog && (
|
||||
<WelcomeDialog
|
||||
onClose={() => {
|
||||
if (isStorageEmpty()) {
|
||||
const createdModel = createNewModel();
|
||||
setModel(createdModel);
|
||||
}
|
||||
setShowWelcomeDialog(false);
|
||||
}}
|
||||
onSelectTemplate={async (templateId) => {
|
||||
switch (templateId) {
|
||||
case "blank": {
|
||||
const createdModel = createNewModel();
|
||||
setModel(createdModel);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const model_bytes = await get_documentation_model(templateId);
|
||||
const importedModel = Model.from_bytes(model_bytes);
|
||||
saveModelToStorage(importedModel);
|
||||
setModel(importedModel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setShowWelcomeDialog(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
open={isTemplatesDialogOpen}
|
||||
onClose={() => setTemplatesDialogOpen(false)}
|
||||
aria-labelledby="templates-dialog-title"
|
||||
aria-describedby="templates-dialog-description"
|
||||
>
|
||||
<TemplatesDialog
|
||||
onClose={() => setTemplatesDialogOpen(false)}
|
||||
onSelectTemplate={async (fileName) => {
|
||||
const model_bytes = await get_documentation_model(fileName);
|
||||
const importedModel = Model.from_bytes(model_bytes);
|
||||
saveModelToStorage(importedModel);
|
||||
setModel(importedModel);
|
||||
setTemplatesDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ function useWindowWidth() {
|
||||
export function FileBar(properties: {
|
||||
model: Model;
|
||||
newModel: () => void;
|
||||
newModelFromTemplate: () => void;
|
||||
setModel: (key: string) => void;
|
||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||
onDelete: () => void;
|
||||
@@ -52,6 +53,7 @@ export function FileBar(properties: {
|
||||
<Divider />
|
||||
<FileMenu
|
||||
newModel={properties.newModel}
|
||||
newModelFromTemplate={properties.newModelFromTemplate}
|
||||
setModel={properties.setModel}
|
||||
onModelUpload={properties.onModelUpload}
|
||||
onDownload={async () => {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { Menu, MenuItem, Modal } from "@mui/material";
|
||||
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
|
||||
import { Check, FileDown, FileUp, Plus, Table2, Trash2 } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
|
||||
import UploadFileDialog from "./UploadFileDialog";
|
||||
// import TemplatesDialog from "./WelcomeDialog/TemplatesDialog";
|
||||
import { getModelsMetadata, getSelectedUuid } from "./storage";
|
||||
|
||||
export function FileMenu(props: {
|
||||
newModel: () => void;
|
||||
newModelFromTemplate: () => void;
|
||||
setModel: (key: string) => void;
|
||||
onDownload: () => void;
|
||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||
@@ -20,7 +22,6 @@ export function FileMenu(props: {
|
||||
const uuids = Object.keys(models);
|
||||
const selectedUuid = getSelectedUuid();
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const elements = [];
|
||||
for (const uuid of uuids) {
|
||||
elements.push(
|
||||
@@ -92,7 +93,18 @@ export function FileMenu(props: {
|
||||
<StyledIcon>
|
||||
<Plus />
|
||||
</StyledIcon>
|
||||
<MenuItemText>New</MenuItemText>
|
||||
<MenuItemText>New blank workbook</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper
|
||||
onClick={() => {
|
||||
props.newModelFromTemplate();
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<StyledIcon>
|
||||
<Table2 />
|
||||
</StyledIcon>
|
||||
<MenuItemText>New from template</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper
|
||||
onClick={() => {
|
||||
@@ -105,6 +117,7 @@ export function FileMenu(props: {
|
||||
</StyledIcon>
|
||||
<MenuItemText>Import</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
<MenuDivider />
|
||||
<MenuItemWrapper onClick={props.onDownload}>
|
||||
<StyledIcon>
|
||||
<FileDown />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Dialog, styled } from "@mui/material";
|
||||
import { X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import TemplatesList, {
|
||||
Cross,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogFooterButton,
|
||||
} from "./TemplatesList";
|
||||
|
||||
function TemplatesDialog(properties: {
|
||||
onClose: () => void;
|
||||
onSelectTemplate: (templateId: string) => void;
|
||||
}) {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
|
||||
|
||||
const handleClose = () => {
|
||||
properties.onClose();
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
setSelectedTemplate(templateId);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper open={true} onClose={() => {}}>
|
||||
<DialogTemplateHeader>
|
||||
<span style={{ flexGrow: 2, marginLeft: 12 }}>Choose a template</span>
|
||||
<Cross
|
||||
style={{ marginRight: 12 }}
|
||||
onClick={handleClose}
|
||||
title="Close Dialog"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
|
||||
>
|
||||
<X />
|
||||
</Cross>
|
||||
</DialogTemplateHeader>
|
||||
<DialogContent>
|
||||
<TemplatesList
|
||||
selectedTemplate={selectedTemplate}
|
||||
handleTemplateSelect={handleTemplateSelect}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<DialogFooterButton
|
||||
onClick={() => properties.onSelectTemplate(selectedTemplate)}
|
||||
>
|
||||
Create workbook
|
||||
</DialogFooterButton>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const DialogWrapper = styled(Dialog)`
|
||||
font-family: Inter;
|
||||
.MuiDialog-paper {
|
||||
width: 440px;
|
||||
border-radius: 12px;
|
||||
margin: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.MuiBackdrop-root {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
const DialogTemplateHeader = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: Inter;
|
||||
`;
|
||||
|
||||
export default TemplatesDialog;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Dialog, styled } from "@mui/material";
|
||||
import { House, TicketsPlane } from "lucide-react";
|
||||
import TemplatesListItem from "./TemplatesListItem";
|
||||
|
||||
function TemplatesList(props: {
|
||||
selectedTemplate: string;
|
||||
handleTemplateSelect: (templateId: string) => void;
|
||||
}) {
|
||||
const { selectedTemplate, handleTemplateSelect } = props;
|
||||
return (
|
||||
<TemplatesListWrapper>
|
||||
<TemplatesListItem
|
||||
title="Mortgage calculator"
|
||||
description="Estimate payments, interest, and overall cost."
|
||||
icon={<House />}
|
||||
iconColor="#2F80ED"
|
||||
active={selectedTemplate === "mortgage_calculator"}
|
||||
onClick={() => handleTemplateSelect("mortgage_calculator")}
|
||||
/>
|
||||
<TemplatesListItem
|
||||
title="Travel expenses tracker"
|
||||
description="Track trip costs and stay on budget."
|
||||
icon={<TicketsPlane />}
|
||||
iconColor="#EB5757"
|
||||
active={selectedTemplate === "travel_expenses_tracker"}
|
||||
onClick={() => handleTemplateSelect("travel_expenses_tracker")}
|
||||
/>
|
||||
</TemplatesListWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const DialogWrapper = styled(Dialog)`
|
||||
font-family: Inter;
|
||||
.MuiDialog-paper {
|
||||
width: 440px;
|
||||
border-radius: 12px;
|
||||
margin: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.MuiBackdrop-root {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Cross = styled("div")`
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DialogContent = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
export const TemplatesListWrapper = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
export const DialogFooter = styled("div")`
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
export const DialogFooterButton = styled("button")`
|
||||
background-color: #f2994a;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
font-family: Inter;
|
||||
&:hover {
|
||||
background-color: #d68742;
|
||||
}
|
||||
&:active {
|
||||
background-color: #d68742;
|
||||
}
|
||||
`;
|
||||
|
||||
// export default TemplatesDialog;
|
||||
export default TemplatesList;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { styled } from "@mui/material";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface TemplatesListItemProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
iconColor: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function TemplatesListItem({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconColor,
|
||||
active,
|
||||
onClick,
|
||||
}: TemplatesListItemProps) {
|
||||
return (
|
||||
<ListItemWrapper active={active} iconColor={iconColor} onClick={onClick}>
|
||||
<StyledIcon iconColor={iconColor}>{icon}</StyledIcon>
|
||||
<TemplatesListItemTitle>
|
||||
<Title>{title}</Title>
|
||||
<Subtitle>{description}</Subtitle>
|
||||
</TemplatesListItemTitle>
|
||||
<RadioButton active={active}>
|
||||
<RadioButtonDot />
|
||||
</RadioButton>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const ListItemWrapper = styled("div")<{ active?: boolean; iconColor?: string }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #424242;
|
||||
border: 1px solid ${(props) => (props.active ? props.iconColor || "#424242" : "rgba(224, 224, 224, 0.60)")};
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
outline: ${(props) => (props.active ? `4px solid ${props.iconColor || "#424242"}24` : "none")};
|
||||
transition: border 0.1s ease-in-out;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
border: 1px solid ${(props) => props.iconColor};
|
||||
transition: border 0.1s ease-in-out;
|
||||
}
|
||||
`;
|
||||
|
||||
const TemplatesListItemTitle = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #424242;
|
||||
width: 100%;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
const Title = styled("div")`
|
||||
font-weight: 600;
|
||||
color: #424242;
|
||||
line-height: 16px;
|
||||
`;
|
||||
|
||||
const Subtitle = styled("div")`
|
||||
color: #757575;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled("div")<{ iconColor?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -1px;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
color: ${(props) => props.iconColor || "#424242"};
|
||||
}
|
||||
`;
|
||||
|
||||
const RadioButton = styled("div")<{ active?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 16px;
|
||||
margin-top: -4px;
|
||||
margin-right: -4px;
|
||||
background-color: ${(props) => (props.active ? "#F2994A" : "#FFFFFF")};
|
||||
border: ${(props) => (props.active ? "none" : "1px solid #E0E0E0")};
|
||||
`;
|
||||
|
||||
const RadioButtonDot = styled("div")`
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background-color: #FFF;
|
||||
`;
|
||||
|
||||
export default TemplatesListItem;
|
||||
@@ -0,0 +1,136 @@
|
||||
import { IronCalcIconWhite as IronCalcIcon } from "@ironcalc/workbook";
|
||||
import { styled } from "@mui/material";
|
||||
import { Table, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import TemplatesListItem from "./TemplatesListItem";
|
||||
|
||||
import TemplatesList, {
|
||||
Cross,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogFooterButton,
|
||||
DialogWrapper,
|
||||
TemplatesListWrapper,
|
||||
} from "./TemplatesList";
|
||||
|
||||
function WelcomeDialog(properties: {
|
||||
onClose: () => void;
|
||||
onSelectTemplate: (templateId: string) => void;
|
||||
}) {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>("blank");
|
||||
|
||||
const handleClose = () => {
|
||||
properties.onClose();
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
setSelectedTemplate(templateId);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper open={true} onClose={() => {}}>
|
||||
<DialogWelcomeHeader>
|
||||
<DialogHeaderTitleWrapper>
|
||||
<DialogHeaderLogoWrapper>
|
||||
<IronCalcIcon />
|
||||
</DialogHeaderLogoWrapper>
|
||||
<DialogHeaderTitle>Welcome to IronCalc</DialogHeaderTitle>
|
||||
<DialogHeaderTitleSubtitle>
|
||||
Start with a blank workbook or a ready-made template.
|
||||
</DialogHeaderTitleSubtitle>
|
||||
</DialogHeaderTitleWrapper>
|
||||
<Cross
|
||||
onClick={handleClose}
|
||||
title="Close Dialog"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => event.key === "Enter" && properties.onClose()}
|
||||
>
|
||||
<X />
|
||||
</Cross>
|
||||
</DialogWelcomeHeader>
|
||||
<DialogContent>
|
||||
<ListTitle>New</ListTitle>
|
||||
<TemplatesListWrapper>
|
||||
<TemplatesListItem
|
||||
title="Blank workbook"
|
||||
description="Create from scratch or upload your own file."
|
||||
icon={<Table />}
|
||||
iconColor="#F2994A"
|
||||
active={selectedTemplate === "blank"}
|
||||
onClick={() => handleTemplateSelect("blank")}
|
||||
/>
|
||||
</TemplatesListWrapper>
|
||||
<ListTitle>Templates</ListTitle>
|
||||
<TemplatesList
|
||||
selectedTemplate={selectedTemplate}
|
||||
handleTemplateSelect={handleTemplateSelect}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<DialogFooterButton
|
||||
onClick={() => properties.onSelectTemplate(selectedTemplate)}
|
||||
>
|
||||
Create workbook
|
||||
</DialogFooterButton>
|
||||
</DialogFooter>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const DialogWelcomeHeader = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 16px;
|
||||
font-family: Inter;
|
||||
`;
|
||||
|
||||
const DialogHeaderTitleWrapper = styled("span")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 4px 0px;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const DialogHeaderTitle = styled("span")`
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const DialogHeaderTitleSubtitle = styled("span")`
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
`;
|
||||
|
||||
const DialogHeaderLogoWrapper = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
background-color: #f2994a;
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: rotate(-8deg);
|
||||
user-select: none;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListTitle = styled("div")`
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #424242;
|
||||
`;
|
||||
|
||||
export default WelcomeDialog;
|
||||
@@ -60,7 +60,7 @@ export function createNewModel(): Model {
|
||||
return model;
|
||||
}
|
||||
|
||||
export function loadModelFromStorageOrCreate(): Model {
|
||||
export function loadSelectedModelFromStorage(): Model | null {
|
||||
const uuid = localStorage.getItem("selected");
|
||||
if (uuid) {
|
||||
// We try to load the selected model
|
||||
@@ -68,14 +68,22 @@ export function loadModelFromStorageOrCreate(): Model {
|
||||
if (modelBytesString) {
|
||||
return Model.from_bytes(base64ToBytes(modelBytesString));
|
||||
}
|
||||
// If it doesn't exist we create one at that uuid
|
||||
const newModel = new Model("Workbook1", "en", "UTC");
|
||||
localStorage.setItem("selected", uuid);
|
||||
localStorage.setItem(uuid, bytesToBase64(newModel.toBytes()));
|
||||
return newModel;
|
||||
}
|
||||
// If there was no selected model we create a new one
|
||||
return createNewModel();
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if storage is empty
|
||||
export function isStorageEmpty(): boolean {
|
||||
const modelsJson = localStorage.getItem("models");
|
||||
if (!modelsJson) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const models = JSON.parse(modelsJson);
|
||||
return Object.keys(models).length === 0;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSelectedModelInStorage(model: Model) {
|
||||
|
||||
Reference in New Issue
Block a user