Refactor UI components for better reusability
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void>;
|
||||
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<ActionButtonProps> = ({
|
||||
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 (
|
||||
<Button
|
||||
appearance={appearance}
|
||||
className={`${styles.actionButton} ${className}`}
|
||||
onClick={handleClick}
|
||||
icon={icon}
|
||||
disabled={disabled || isProcessing}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -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<AppProps> = () => {
|
||||
setIsProcessing
|
||||
}}>
|
||||
<div className={styles.root} data-theme={theme}>
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Content
|
||||
</Subtitle1>
|
||||
<Section title="Content">
|
||||
<MatchProperties />
|
||||
<div style={{ marginTop: "8px" }}></div>
|
||||
<MatchSizes />
|
||||
@@ -169,42 +151,27 @@ const App: React.FC<AppProps> = () => {
|
||||
<InsertTitles />
|
||||
<div style={{ marginTop: "8px" }}></div>
|
||||
<RoundImage />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Progress Bar
|
||||
</Subtitle1>
|
||||
<Section title="Progress Bar">
|
||||
<ProgressBarButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Confidential Marking
|
||||
</Subtitle1>
|
||||
<Section title="Confidential Marking">
|
||||
<ConfidentialButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Draft Watermark
|
||||
</Subtitle1>
|
||||
<Section title="Draft Watermark">
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Alignment
|
||||
</Subtitle1>
|
||||
<Section title="Alignment">
|
||||
<AlignmentButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Layout
|
||||
</Subtitle1>
|
||||
<Section title="Layout">
|
||||
<GridGuidelineManager />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Status message area at the bottom */}
|
||||
{isProcessing && (
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={styles.actionButton}
|
||||
onClick={matchSizeToFirstSelected}
|
||||
icon={<ArrowFitInRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Match Sizes
|
||||
</Button>
|
||||
<ActionButton
|
||||
title="Match Sizes"
|
||||
icon={<ArrowFitInRegular />}
|
||||
onClick={matchSizeToFirstSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<SectionProps> = ({ title, children }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
{title}
|
||||
</Subtitle1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Section;
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={styles.actionButton}
|
||||
onClick={swapPositionsOfTwoSelectedObjects}
|
||||
<ActionButton
|
||||
title="Swap Positions"
|
||||
icon={<ArrowSwapRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Swap Positions
|
||||
</Button>
|
||||
onClick={swapPositionsOfTwoSelectedObjects}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user