From d09dec47065904c20cbc21eb497381ea66ad5813 Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Fri, 14 Mar 2025 23:50:19 +0100 Subject: [PATCH] Refactor UI components for better reusability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created reusable ActionButton component to handle common button behavior - Created reusable Section component for consistent section styling - Refactored App, MatchSizes, and SwapPositions to use new components - Fixed scrolling issues in the main container - Improved code organization and reduced duplication 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/taskpane/components/ActionButton.tsx | 63 +++++++++++++ src/taskpane/components/App.tsx | 61 +++---------- src/taskpane/components/MatchSizes.tsx | 106 ++++++++-------------- src/taskpane/components/Section.tsx | 44 +++++++++ src/taskpane/components/SwapPositions.tsx | 105 +++++++++------------ 5 files changed, 201 insertions(+), 178 deletions(-) create mode 100644 src/taskpane/components/ActionButton.tsx create mode 100644 src/taskpane/components/Section.tsx diff --git a/src/taskpane/components/ActionButton.tsx b/src/taskpane/components/ActionButton.tsx new file mode 100644 index 00000000..1f1b98ab --- /dev/null +++ b/src/taskpane/components/ActionButton.tsx @@ -0,0 +1,63 @@ +import * as React from "react"; +import { Button, ButtonProps } from "@fluentui/react-components"; +import { useStatusContext } from "./App"; +import { useCommonStyles } from "./commonStyles"; + +export interface ActionButtonProps { + icon: ButtonProps["icon"]; + onClick: () => Promise; + disabled?: boolean; + title: string; + appearance?: "primary" | "secondary" | "outline" | "subtle"; + className?: string; +} + +/** + * Reusable action button component with consistent styling and error handling + */ +export const ActionButton: React.FC = ({ + icon, + onClick, + disabled = false, + title, + appearance = "primary", + className = "", +}) => { + const styles = useCommonStyles(); + const { + setStatusMessage, + setStatusType, + isProcessing, + setIsProcessing + } = useStatusContext(); + + const handleClick = async () => { + if (isProcessing) return; + + setIsProcessing(true); + try { + await onClick(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + setStatusMessage(`Error: ${errorMessage}`); + setStatusType("error"); + console.error(`Error in ${title} action:`, error); + } finally { + setIsProcessing(false); + } + }; + + return ( + + ); +}; + +export default ActionButton; \ No newline at end of file diff --git a/src/taskpane/components/App.tsx b/src/taskpane/components/App.tsx index b63a98be..e65525dc 100644 --- a/src/taskpane/components/App.tsx +++ b/src/taskpane/components/App.tsx @@ -10,7 +10,8 @@ import DraftButtons from "./DraftButtons"; import ProgressBarButtons from "./ProgressBarButtons"; import AlignmentButtons from "./AlignmentButtons"; import GridGuidelineManager from "./GridGuidelineManager"; -import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components"; +import Section from "./Section"; +import { makeStyles, Text, tokens, Theme, Spinner } from "@fluentui/react-components"; import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons"; interface AppProps { @@ -79,22 +80,6 @@ const useStyles = makeStyles({ display: "flex", alignItems: "center", }, - section: { - marginBottom: "6px", - padding: "12px", - borderRadius: "8px", - backgroundColor: tokens.colorNeutralBackground2, - boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)", - transition: "all 0.2s ease", - ":hover": { - boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)", - }, - }, - sectionTitle: { - marginBottom: "12px", - fontWeight: tokens.fontWeightSemibold, - color: tokens.colorBrandForeground1, - }, footer: { fontSize: "12px", color: tokens.colorNeutralForeground3, @@ -156,10 +141,7 @@ const App: React.FC = () => { setIsProcessing }}>
-
- - Content - +
@@ -169,42 +151,27 @@ const App: React.FC = () => {
-
+ -
- - Progress Bar - +
-
+ -
- - Confidential Marking - +
-
+ -
- - Draft Watermark - +
-
+ -
- - Alignment - +
-
+ -
- - Layout - +
-
+ {/* Status message area at the bottom */} {isProcessing && ( diff --git a/src/taskpane/components/MatchSizes.tsx b/src/taskpane/components/MatchSizes.tsx index 96f02764..5f687f97 100644 --- a/src/taskpane/components/MatchSizes.tsx +++ b/src/taskpane/components/MatchSizes.tsx @@ -1,89 +1,59 @@ import * as React from "react"; -import { - Button, - Text, - Tooltip, - InfoLabel, - Card -} from "@fluentui/react-components"; -import { - ArrowFitInRegular, - ArrowSortDownRegular, - ArrowSortUpRegular -} from "@fluentui/react-icons"; +import { ArrowFitInRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +import ActionButton from "./ActionButton"; export const MatchSizes: React.FC = () => { const styles = useCommonStyles(); - const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing - } = useStatusContext(); + const { setStatusMessage, setStatusType } = useStatusContext(); const matchSizeToFirstSelected = async () => { - setIsProcessing(true); - try { - await PowerPoint.run(async (context) => { - // Get the selected shapes - const shapes = context.presentation.getSelectedShapes(); - shapes.load("items"); - await context.sync(); + await PowerPoint.run(async (context) => { + // Get the selected shapes + const shapes = context.presentation.getSelectedShapes(); + shapes.load("items"); + await context.sync(); - // Check if shapes are selected - if (shapes.items.length === 0) { - setStatusMessage("No shapes are selected. Please select shapes first."); - setStatusType("warning"); - return; - } + // Check if shapes are selected + if (shapes.items.length === 0) { + setStatusMessage("No shapes are selected. Please select shapes first."); + setStatusType("warning"); + return; + } - // Check if there is more than one shape selected - if (shapes.items.length === 1) { - setStatusMessage("Please select multiple shapes to resize."); - setStatusType("warning"); - return; - } + // Check if there is more than one shape selected + if (shapes.items.length === 1) { + setStatusMessage("Please select multiple shapes to resize."); + setStatusType("warning"); + return; + } - // Get the first shape's dimensions - const firstShape = shapes.items[0]; - firstShape.load("width,height"); - await context.sync(); + // Get the first shape's dimensions + const firstShape = shapes.items[0]; + firstShape.load("width,height"); + await context.sync(); - // Loop through the remaining shapes and resize them - for (let i = 1; i < shapes.items.length; i++) { - shapes.items[i].width = firstShape.width; - shapes.items[i].height = firstShape.height; - } + // Loop through the remaining shapes and resize them + for (let i = 1; i < shapes.items.length; i++) { + shapes.items[i].width = firstShape.width; + shapes.items[i].height = firstShape.height; + } - await context.sync(); - setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`); - setStatusType("success"); - - // Timeout is handled in App.tsx now - }); - } catch (error) { - setStatusMessage(`Error: ${error.message}`); - setStatusType("error"); - console.error(error); - } finally { - setIsProcessing(false); - } + await context.sync(); + setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`); + setStatusType("success"); + }); }; - return (
- + } + onClick={matchSizeToFirstSelected} + />
); diff --git a/src/taskpane/components/Section.tsx b/src/taskpane/components/Section.tsx new file mode 100644 index 00000000..df9a2dba --- /dev/null +++ b/src/taskpane/components/Section.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { Subtitle1, makeStyles, tokens } from "@fluentui/react-components"; + +interface SectionProps { + title: string; + children: React.ReactNode; +} + +const useStyles = makeStyles({ + section: { + marginBottom: "6px", + padding: "12px", + borderRadius: "8px", + backgroundColor: tokens.colorNeutralBackground2, + boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)", + transition: "all 0.2s ease", + ":hover": { + boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)", + }, + }, + sectionTitle: { + marginBottom: "12px", + fontWeight: tokens.fontWeightSemibold, + color: tokens.colorBrandForeground1, + }, +}); + +/** + * Reusable section component with consistent styling + */ +export const Section: React.FC = ({ title, children }) => { + const styles = useStyles(); + + return ( +
+ + {title} + + {children} +
+ ); +}; + +export default Section; \ No newline at end of file diff --git a/src/taskpane/components/SwapPositions.tsx b/src/taskpane/components/SwapPositions.tsx index dbe412e2..d249fc80 100644 --- a/src/taskpane/components/SwapPositions.tsx +++ b/src/taskpane/components/SwapPositions.tsx @@ -1,82 +1,61 @@ import * as React from "react"; -import { - Button -} from "@fluentui/react-components"; -import { - ArrowSwapRegular -} from "@fluentui/react-icons"; +import { ArrowSwapRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +import ActionButton from "./ActionButton"; export const SwapPositions: React.FC = () => { const styles = useCommonStyles(); - const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing - } = useStatusContext(); + const { setStatusMessage, setStatusType } = useStatusContext(); const swapPositionsOfTwoSelectedObjects = async () => { - setIsProcessing(true); - try { - await PowerPoint.run(async (context) => { - // Get the selected shapes - const shapes = context.presentation.getSelectedShapes(); - shapes.load("items/count"); - await context.sync(); + await PowerPoint.run(async (context) => { + // Get the selected shapes + const shapes = context.presentation.getSelectedShapes(); + shapes.load("items/count"); + await context.sync(); - // Check if exactly two shapes are selected - if (shapes.items.length !== 2) { - setStatusMessage("Please select exactly two shapes to swap their positions."); - setStatusType("warning"); - return; - } + // Check if exactly two shapes are selected + if (shapes.items.length !== 2) { + setStatusMessage("Please select exactly two shapes to swap their positions."); + setStatusType("warning"); + return; + } - // Get the two shapes - const shapeObj1 = shapes.items[0]; - const shapeObj2 = shapes.items[1]; - - // Load position properties - shapeObj1.load("left,top"); - shapeObj2.load("left,top"); - await context.sync(); - - // Store the position of the first shape - const tempLeft = shapeObj1.left; - const tempTop = shapeObj1.top; - - // Swap positions - shapeObj1.left = shapeObj2.left; - shapeObj1.top = shapeObj2.top; - shapeObj2.left = tempLeft; - shapeObj2.top = tempTop; - - await context.sync(); - - setStatusMessage("Positions of the two shapes have been swapped successfully."); - setStatusType("success"); - }); - } catch (error) { - setStatusMessage(`Error: ${error.message}`); - setStatusType("error"); - console.error("Swap positions error:", error); - } finally { - setIsProcessing(false); - } + // Get the two shapes + const shapeObj1 = shapes.items[0]; + const shapeObj2 = shapes.items[1]; + + // Load position properties + shapeObj1.load("left,top"); + shapeObj2.load("left,top"); + await context.sync(); + + // Store the position of the first shape + const tempLeft = shapeObj1.left; + const tempTop = shapeObj1.top; + + // Swap positions + shapeObj1.left = shapeObj2.left; + shapeObj1.top = shapeObj2.top; + shapeObj2.left = tempLeft; + shapeObj2.top = tempTop; + + await context.sync(); + + setStatusMessage("Positions of the two shapes have been swapped successfully."); + setStatusType("success"); + }); }; return (
- + onClick={swapPositionsOfTwoSelectedObjects} + />
);