update: add actions, allow drawer resize

This commit is contained in:
Daniel Gonzalez Albo
2025-11-08 18:22:22 +01:00
committed by Nicolás Hatcher Andrés
parent e44a2e8c3e
commit f8bd03d92c
4 changed files with 181 additions and 26 deletions

View File

@@ -106,7 +106,6 @@ const EditNamedRange: React.FC<EditNamedRangeProps> = ({
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
/> />
</FieldWrapper> </FieldWrapper>
<FieldWrapper></FieldWrapper>
</StyledBox> </StyledBox>
</ContentArea> </ContentArea>
<Footer> <Footer>
@@ -200,9 +199,9 @@ const StyledBox = styled(Box)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 12px; gap: 16px;
width: auto; width: auto;
padding: 12px; padding: 16px 12px;
@media (max-width: 600px) { @media (max-width: 600px) {
padding: 12px; padding: 12px;
@@ -228,7 +227,7 @@ const FieldWrapper = styled(Box)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
gap: 4px; gap: 6px;
`; `;
const StyledLabel = styled("label")` const StyledLabel = styled("label")`
@@ -240,9 +239,9 @@ const StyledLabel = styled("label")`
`; `;
const StyledHelperText = styled("p")` const StyledHelperText = styled("p")`
font-size: 10px; font-size: 12px;
font-family: "Inter"; font-family: "Inter";
color: ${theme.palette.grey[400]}; color: ${theme.palette.grey[500]};
margin: 0; margin: 0;
line-height: 1.25; line-height: 1.25;
`; `;

View File

@@ -1,11 +1,16 @@
import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm"; import type { DefinedName, WorksheetProperties } from "@ironcalc/wasm";
import { Button, Tooltip, styled } from "@mui/material"; import { Button, Tooltip, styled } from "@mui/material";
import { t } from "i18next"; import { t } from "i18next";
import { BookOpen, Plus } from "lucide-react"; import { BookOpen, PencilLine, Plus, Trash2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { theme } from "../../../theme"; import { theme } from "../../../theme";
import EditNamedRange from "./EditNamedRange"; import EditNamedRange from "./EditNamedRange";
// Normalize range strings for comparison (remove quotes, handle case, etc.)
const normalizeRangeString = (range: string): string => {
return range.trim().replace(/['"]/g, "");
};
interface NamedRangesProps { interface NamedRangesProps {
title?: string; title?: string;
definedNameList?: DefinedName[]; definedNameList?: DefinedName[];
@@ -22,6 +27,7 @@ interface NamedRangesProps {
scope: number | undefined, scope: number | undefined,
formula: string, formula: string,
) => void; ) => void;
deleteDefinedName?: (name: string, scope: number | undefined) => void;
selectedArea?: () => string; selectedArea?: () => string;
} }
@@ -30,6 +36,7 @@ const NamedRanges: React.FC<NamedRangesProps> = ({
worksheets = [], worksheets = [],
updateDefinedName, updateDefinedName,
newDefinedName, newDefinedName,
deleteDefinedName,
selectedArea, selectedArea,
}) => { }) => {
const [editingDefinedName, setEditingDefinedName] = const [editingDefinedName, setEditingDefinedName] =
@@ -123,6 +130,8 @@ const NamedRanges: React.FC<NamedRangesProps> = ({
} }
// Show list view // Show list view
const currentSelectedArea = selectedArea ? selectedArea() : null;
return ( return (
<Container> <Container>
<Content> <Content>
@@ -133,17 +142,50 @@ const NamedRanges: React.FC<NamedRangesProps> = ({
definedName.scope !== undefined definedName.scope !== undefined
? worksheets[definedName.scope]?.name || "[unknown]" ? worksheets[definedName.scope]?.name || "[unknown]"
: "[global]"; : "[global]";
// Check if this named range matches the currently selected area
const isSelected =
currentSelectedArea !== null &&
normalizeRangeString(definedName.formula) ===
normalizeRangeString(currentSelectedArea);
return ( return (
<ListItem <ListItem
key={`${definedName.name}-${definedName.scope}`} key={`${definedName.name}-${definedName.scope}`}
onClick={() => handleListItemClick(definedName)}
tabIndex={0} tabIndex={0}
$isSelected={isSelected}
> >
<ListItemText> <ListItemText>
<NameText>{definedName.name}</NameText> <NameText>{definedName.name}</NameText>
<ScopeText>{scopeName}</ScopeText> <ScopeText>{scopeName}</ScopeText>
<FormulaText>{definedName.formula}</FormulaText> <FormulaText>{definedName.formula}</FormulaText>
</ListItemText> </ListItemText>
<IconsWrapper>
<Tooltip title={t("name_manager_dialog.edit")}>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleListItemClick(definedName);
}}
>
<PencilLine size={16} />
</IconButton>
</Tooltip>
<Tooltip title={t("name_manager_dialog.delete")}>
<IconButton
onClick={(e) => {
e.stopPropagation();
if (deleteDefinedName) {
deleteDefinedName(
definedName.name,
definedName.scope,
);
}
}}
>
<Trash2 size={16} />
</IconButton>
</Tooltip>
</IconsWrapper>
</ListItem> </ListItem>
); );
})} })}
@@ -198,19 +240,26 @@ const ListContainer = styled("div")({
flexDirection: "column", flexDirection: "column",
}); });
const ListItem = styled("div")({ const ListItem = styled("div")<{ $isSelected?: boolean }>(
({ $isSelected }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "flex-start",
justifyContent: "space-between", justifyContent: "space-between",
padding: "8px 12px", padding: "8px 12px",
minHeight: "40px", minHeight: "40px",
cursor: "pointer",
boxSizing: "border-box", boxSizing: "border-box",
borderBottom: `1px solid ${theme.palette.grey[200]}`, borderBottom: `1px solid ${theme.palette.grey[200]}`,
paddingLeft: $isSelected ? "20px" : "12px",
transition: "all 0.2s ease-in-out",
borderLeft: $isSelected
? `3px solid ${theme.palette.primary.main}`
: "3px solid transparent",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grey[50], backgroundColor: theme.palette.grey[50],
paddingLeft: "20px",
}, },
}); }),
);
const ListItemText = styled("div")({ const ListItemText = styled("div")({
fontSize: "12px", fontSize: "12px",
@@ -239,6 +288,26 @@ const NameText = styled("span")({
fontWeight: 600, fontWeight: 600,
}); });
const IconsWrapper = styled("div")({
display: "flex",
alignItems: "center",
gap: "2px",
});
const IconButton = styled("div")({
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "24px",
height: "24px",
borderRadius: "4px",
backgroundColor: "transparent",
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.grey[200],
},
});
export const Footer = styled("div")` export const Footer = styled("div")`
padding: 8px; padding: 8px;
display: flex; display: flex;

View File

@@ -5,17 +5,21 @@ import Tooltip from "@mui/material/Tooltip";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { t } from "i18next"; import { t } from "i18next";
import { X } from "lucide-react"; import { X } from "lucide-react";
import type { ReactNode } from "react"; import type { MouseEvent as ReactMouseEvent, ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { theme } from "../../theme"; import { theme } from "../../theme";
import { TOOLBAR_HEIGHT } from "../constants"; import { TOOLBAR_HEIGHT } from "../constants";
import NamedRanges from "./NamedRanges/NamedRanges"; import NamedRanges from "./NamedRanges/NamedRanges";
const DEFAULT_DRAWER_WIDTH = 300; const DEFAULT_DRAWER_WIDTH = 360;
const MIN_DRAWER_WIDTH = 300;
const MAX_DRAWER_WIDTH = 500;
interface RightDrawerProps { interface RightDrawerProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
width?: number; width?: number;
onWidthChange?: (width: number) => void;
children?: ReactNode; children?: ReactNode;
showCloseButton?: boolean; showCloseButton?: boolean;
backgroundColor?: string; backgroundColor?: string;
@@ -34,6 +38,7 @@ interface RightDrawerProps {
scope: number | undefined, scope: number | undefined,
formula: string, formula: string,
) => void; ) => void;
deleteDefinedName?: (name: string, scope: number | undefined) => void;
selectedArea?: () => string; selectedArea?: () => string;
} }
@@ -41,6 +46,7 @@ const RightDrawer = ({
isOpen, isOpen,
onClose, onClose,
width = DEFAULT_DRAWER_WIDTH, width = DEFAULT_DRAWER_WIDTH,
onWidthChange,
children, children,
showCloseButton = true, showCloseButton = true,
title = "Named Ranges", title = "Named Ranges",
@@ -48,12 +54,67 @@ const RightDrawer = ({
worksheets, worksheets,
updateDefinedName, updateDefinedName,
newDefinedName, newDefinedName,
deleteDefinedName,
selectedArea, selectedArea,
}: RightDrawerProps) => { }: RightDrawerProps) => {
const [drawerWidth, setDrawerWidth] = useState(width);
const [isResizing, setIsResizing] = useState(false);
const resizeHandleRef = useRef<HTMLDivElement>(null);
// Update local width when prop changes
useEffect(() => {
setDrawerWidth(width);
}, [width]);
const handleMouseDown = useCallback((e: ReactMouseEvent) => {
e.preventDefault();
setIsResizing(true);
}, []);
useEffect(() => {
if (!isResizing) return;
// Prevent text selection during resize
document.body.style.userSelect = "none";
document.body.style.cursor = "col-resize";
const handleMouseMove = (e: MouseEvent) => {
const newWidth = window.innerWidth - e.clientX;
const clampedWidth = Math.max(
MIN_DRAWER_WIDTH,
Math.min(MAX_DRAWER_WIDTH, newWidth),
);
setDrawerWidth(clampedWidth);
onWidthChange?.(clampedWidth);
};
const handleMouseUp = () => {
setIsResizing(false);
document.body.style.userSelect = "";
document.body.style.cursor = "";
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.userSelect = "";
document.body.style.cursor = "";
};
}, [isResizing, onWidthChange]);
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<DrawerContainer $drawerWidth={width}> <DrawerContainer $drawerWidth={drawerWidth}>
<ResizeHandle
ref={resizeHandleRef}
onMouseDown={handleMouseDown}
$isResizing={isResizing}
aria-label="Resize drawer"
/>
{showCloseButton && ( {showCloseButton && (
<Header> <Header>
<HeaderTitle> <HeaderTitle>
@@ -100,6 +161,7 @@ const RightDrawer = ({
worksheets={worksheets} worksheets={worksheets}
updateDefinedName={updateDefinedName} updateDefinedName={updateDefinedName}
newDefinedName={newDefinedName} newDefinedName={newDefinedName}
deleteDefinedName={deleteDefinedName}
selectedArea={selectedArea} selectedArea={selectedArea}
/> />
</DrawerContent> </DrawerContent>
@@ -178,5 +240,23 @@ const DrawerContent = styled("div")({
height: "100%", height: "100%",
}); });
const ResizeHandle = styled("div")<{ $isResizing: boolean }>(
({ $isResizing }) => ({
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: "4px",
cursor: "col-resize",
backgroundColor: $isResizing ? theme.palette.primary.main : "transparent",
zIndex: 10,
"&:hover": {
backgroundColor: theme.palette.primary.main,
opacity: 0.5,
},
transition: $isResizing ? "none" : "background-color 0.2s ease",
}),
);
export default RightDrawer; export default RightDrawer;
export { DEFAULT_DRAWER_WIDTH }; export { DEFAULT_DRAWER_WIDTH, MIN_DRAWER_WIDTH, MAX_DRAWER_WIDTH };

View File

@@ -44,6 +44,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const setRedrawId = useState(0)[1]; const setRedrawId = useState(0)[1];
const [isDrawerOpen, setDrawerOpen] = useState(false); const [isDrawerOpen, setDrawerOpen] = useState(false);
const [drawerWidth, setDrawerWidth] = useState(DEFAULT_DRAWER_WIDTH);
const worksheets = model.getWorksheetsProperties(); const worksheets = model.getWorksheetsProperties();
const info = worksheets.map( const info = worksheets.map(
@@ -700,7 +701,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
setDrawerOpen(true); setDrawerOpen(true);
}} }}
/> />
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? DEFAULT_DRAWER_WIDTH : 0}> <WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
<FormulaBar <FormulaBar
cellAddress={cellAddress()} cellAddress={cellAddress()}
formulaValue={formulaValue()} formulaValue={formulaValue()}
@@ -771,6 +772,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
<RightDrawer <RightDrawer
isOpen={isDrawerOpen} isOpen={isDrawerOpen}
onClose={() => setDrawerOpen(false)} onClose={() => setDrawerOpen(false)}
width={drawerWidth}
onWidthChange={setDrawerWidth}
definedNameList={model.getDefinedNameList()} definedNameList={model.getDefinedNameList()}
worksheets={worksheets} worksheets={worksheets}
updateDefinedName={( updateDefinedName={(
@@ -791,6 +794,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.newDefinedName(name, scope, formula); model.newDefinedName(name, scope, formula);
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
}} }}
deleteDefinedName={(name: string, scope: number | undefined) => {
model.deleteDefinedName(name, scope);
setRedrawId((id) => id + 1);
}}
selectedArea={() => { selectedArea={() => {
const worksheetNames = worksheets.map((s) => s.name); const worksheetNames = worksheets.map((s) => s.name);
const selectedView = model.getSelectedView(); const selectedView = model.getSelectedView();