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 ProgressBarButtons from "./ProgressBarButtons";
|
||||||
import AlignmentButtons from "./AlignmentButtons";
|
import AlignmentButtons from "./AlignmentButtons";
|
||||||
import GridGuidelineManager from "./GridGuidelineManager";
|
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";
|
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
@@ -79,22 +80,6 @@ const useStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
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: {
|
footer: {
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
color: tokens.colorNeutralForeground3,
|
color: tokens.colorNeutralForeground3,
|
||||||
@@ -156,10 +141,7 @@ const App: React.FC<AppProps> = () => {
|
|||||||
setIsProcessing
|
setIsProcessing
|
||||||
}}>
|
}}>
|
||||||
<div className={styles.root} data-theme={theme}>
|
<div className={styles.root} data-theme={theme}>
|
||||||
<div className={styles.section}>
|
<Section title="Content">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Content
|
|
||||||
</Subtitle1>
|
|
||||||
<MatchProperties />
|
<MatchProperties />
|
||||||
<div style={{ marginTop: "8px" }}></div>
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
<MatchSizes />
|
<MatchSizes />
|
||||||
@@ -169,42 +151,27 @@ const App: React.FC<AppProps> = () => {
|
|||||||
<InsertTitles />
|
<InsertTitles />
|
||||||
<div style={{ marginTop: "8px" }}></div>
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
<RoundImage />
|
<RoundImage />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Progress Bar">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Progress Bar
|
|
||||||
</Subtitle1>
|
|
||||||
<ProgressBarButtons />
|
<ProgressBarButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Confidential Marking">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Confidential Marking
|
|
||||||
</Subtitle1>
|
|
||||||
<ConfidentialButtons />
|
<ConfidentialButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Draft Watermark">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Draft Watermark
|
|
||||||
</Subtitle1>
|
|
||||||
<DraftButtons />
|
<DraftButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Alignment">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Alignment
|
|
||||||
</Subtitle1>
|
|
||||||
<AlignmentButtons />
|
<AlignmentButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Layout">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Layout
|
|
||||||
</Subtitle1>
|
|
||||||
<GridGuidelineManager />
|
<GridGuidelineManager />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
{/* Status message area at the bottom */}
|
{/* Status message area at the bottom */}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
|
|||||||
@@ -1,89 +1,59 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import { ArrowFitInRegular } from "@fluentui/react-icons";
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
InfoLabel,
|
|
||||||
Card
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
ArrowFitInRegular,
|
|
||||||
ArrowSortDownRegular,
|
|
||||||
ArrowSortUpRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
|
||||||
export const MatchSizes: React.FC = () => {
|
export const MatchSizes: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
|
||||||
isProcessing, setIsProcessing
|
|
||||||
} = useStatusContext();
|
|
||||||
|
|
||||||
const matchSizeToFirstSelected = async () => {
|
const matchSizeToFirstSelected = async () => {
|
||||||
setIsProcessing(true);
|
await PowerPoint.run(async (context) => {
|
||||||
try {
|
// Get the selected shapes
|
||||||
await PowerPoint.run(async (context) => {
|
const shapes = context.presentation.getSelectedShapes();
|
||||||
// Get the selected shapes
|
shapes.load("items");
|
||||||
const shapes = context.presentation.getSelectedShapes();
|
await context.sync();
|
||||||
shapes.load("items");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
// Check if shapes are selected
|
// Check if shapes are selected
|
||||||
if (shapes.items.length === 0) {
|
if (shapes.items.length === 0) {
|
||||||
setStatusMessage("No shapes are selected. Please select shapes first.");
|
setStatusMessage("No shapes are selected. Please select shapes first.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is more than one shape selected
|
// Check if there is more than one shape selected
|
||||||
if (shapes.items.length === 1) {
|
if (shapes.items.length === 1) {
|
||||||
setStatusMessage("Please select multiple shapes to resize.");
|
setStatusMessage("Please select multiple shapes to resize.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first shape's dimensions
|
// Get the first shape's dimensions
|
||||||
const firstShape = shapes.items[0];
|
const firstShape = shapes.items[0];
|
||||||
firstShape.load("width,height");
|
firstShape.load("width,height");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Loop through the remaining shapes and resize them
|
// Loop through the remaining shapes and resize them
|
||||||
for (let i = 1; i < shapes.items.length; i++) {
|
for (let i = 1; i < shapes.items.length; i++) {
|
||||||
shapes.items[i].width = firstShape.width;
|
shapes.items[i].width = firstShape.width;
|
||||||
shapes.items[i].height = firstShape.height;
|
shapes.items[i].height = firstShape.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
||||||
setStatusType("success");
|
setStatusType("success");
|
||||||
|
});
|
||||||
// Timeout is handled in App.tsx now
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
|
||||||
setStatusType("error");
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Match Sizes"
|
||||||
className={styles.actionButton}
|
icon={<ArrowFitInRegular />}
|
||||||
onClick={matchSizeToFirstSelected}
|
onClick={matchSizeToFirstSelected}
|
||||||
icon={<ArrowFitInRegular />}
|
/>
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
Match Sizes
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</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 * as React from "react";
|
||||||
import {
|
import { ArrowSwapRegular } from "@fluentui/react-icons";
|
||||||
Button
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
ArrowSwapRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
|
||||||
export const SwapPositions: React.FC = () => {
|
export const SwapPositions: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
|
||||||
isProcessing, setIsProcessing
|
|
||||||
} = useStatusContext();
|
|
||||||
|
|
||||||
const swapPositionsOfTwoSelectedObjects = async () => {
|
const swapPositionsOfTwoSelectedObjects = async () => {
|
||||||
setIsProcessing(true);
|
await PowerPoint.run(async (context) => {
|
||||||
try {
|
// Get the selected shapes
|
||||||
await PowerPoint.run(async (context) => {
|
const shapes = context.presentation.getSelectedShapes();
|
||||||
// Get the selected shapes
|
shapes.load("items/count");
|
||||||
const shapes = context.presentation.getSelectedShapes();
|
await context.sync();
|
||||||
shapes.load("items/count");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
// Check if exactly two shapes are selected
|
// Check if exactly two shapes are selected
|
||||||
if (shapes.items.length !== 2) {
|
if (shapes.items.length !== 2) {
|
||||||
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the two shapes
|
// Get the two shapes
|
||||||
const shapeObj1 = shapes.items[0];
|
const shapeObj1 = shapes.items[0];
|
||||||
const shapeObj2 = shapes.items[1];
|
const shapeObj2 = shapes.items[1];
|
||||||
|
|
||||||
// Load position properties
|
// Load position properties
|
||||||
shapeObj1.load("left,top");
|
shapeObj1.load("left,top");
|
||||||
shapeObj2.load("left,top");
|
shapeObj2.load("left,top");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Store the position of the first shape
|
// Store the position of the first shape
|
||||||
const tempLeft = shapeObj1.left;
|
const tempLeft = shapeObj1.left;
|
||||||
const tempTop = shapeObj1.top;
|
const tempTop = shapeObj1.top;
|
||||||
|
|
||||||
// Swap positions
|
// Swap positions
|
||||||
shapeObj1.left = shapeObj2.left;
|
shapeObj1.left = shapeObj2.left;
|
||||||
shapeObj1.top = shapeObj2.top;
|
shapeObj1.top = shapeObj2.top;
|
||||||
shapeObj2.left = tempLeft;
|
shapeObj2.left = tempLeft;
|
||||||
shapeObj2.top = tempTop;
|
shapeObj2.top = tempTop;
|
||||||
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
||||||
setStatusType("success");
|
setStatusType("success");
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
|
||||||
setStatusType("error");
|
|
||||||
console.error("Swap positions error:", error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Swap Positions"
|
||||||
className={styles.actionButton}
|
|
||||||
onClick={swapPositionsOfTwoSelectedObjects}
|
|
||||||
icon={<ArrowSwapRegular />}
|
icon={<ArrowSwapRegular />}
|
||||||
disabled={isProcessing}
|
onClick={swapPositionsOfTwoSelectedObjects}
|
||||||
>
|
/>
|
||||||
Swap Positions
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user