diff --git a/src/taskpane/components/App.tsx b/src/taskpane/components/App.tsx index f0d17fc1..de13c589 100644 --- a/src/taskpane/components/App.tsx +++ b/src/taskpane/components/App.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { useEffect, useState } from "react"; import MatchSizes from "./MatchSizes"; +import MatchProperties from "./MatchProperties"; import { makeStyles, Text, Subtitle1, tokens, Theme } from "@fluentui/react-components"; interface AppProps { @@ -60,6 +61,8 @@ const App: React.FC = () => { Shape Tools +
+
diff --git a/src/taskpane/components/MatchProperties.tsx b/src/taskpane/components/MatchProperties.tsx new file mode 100644 index 00000000..991293e3 --- /dev/null +++ b/src/taskpane/components/MatchProperties.tsx @@ -0,0 +1,222 @@ +import * as React from "react"; +import { + Button, + makeStyles, + Text, + Spinner, + tokens +} from "@fluentui/react-components"; +import { + ColorRegular, + SquareRegular, + ShapeUnionRegular, + InfoRegular +} from "@fluentui/react-icons"; + +const useStyles = makeStyles({ + container: { + display: "flex", + flexDirection: "column", + width: "100%", + }, + buttonGroup: { + display: "flex", + flexDirection: "column", + gap: "8px", + marginBottom: "2px", + }, + actionButton: { + justifyContent: "flex-start", + transitionProperty: "all", + transitionDuration: "200ms", + transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)", + }, + statusContainer: { + marginTop: "4px", + padding: "8px 12px", + fontSize: "13px", + borderRadius: "6px", + backgroundColor: "#f3f2f1", // Light gray background + transition: "all 0.3s ease", + }, + successStatus: { + backgroundColor: "#DFF6DD", // Light green background + color: "#107C10", // Green text + }, + warningStatus: { + backgroundColor: "#FFF4CE", // Light yellow background + color: "#797673", // Dark gray text + }, + errorStatus: { + backgroundColor: "#FDE7E9", // Light red background + color: "#A80000", // Red text + }, + statusIcon: { + marginRight: "8px", + }, + statusText: { + display: "flex", + alignItems: "center", + }, +}); + +export const MatchProperties: React.FC = () => { + const styles = useStyles(); + const [statusMessage, setStatusMessage] = React.useState(""); + const [statusType, setStatusType] = React.useState<"info" | "success" | "warning" | "error">("info"); + const [isProcessing, setIsProcessing] = React.useState(false); + + const matchPropertiesToFirstSelected = async () => { + setIsProcessing(true); + try { + 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 there is more than one shape selected + if (shapes.items.length === 1) { + setStatusMessage("Please select multiple shapes to copy properties."); + setStatusType("warning"); + return; + } + + // Get the first shape to use as template + const firstShape = shapes.items[0]; + + // Load all properties we need from the first shape + firstShape.load([ + "lineFormat/weight", + "lineFormat/dashStyle", + "fill/transparency" + ]); + + await context.sync(); + + // Loop through remaining shapes and apply properties + for (let i = 1; i < shapes.items.length; i++) { + const targetShape = shapes.items[i]; + + // Copy line properties that are available in the Office JS API + try { + // Line weight (width) + targetShape.lineFormat.weight = firstShape.lineFormat.weight; + + // Line style (dash type) + targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle; + + // Fill transparency + targetShape.fill.transparency = firstShape.fill.transparency; + + } catch (err) { + console.error("Error copying basic properties:", err); + } + + // Check if first shape has text frame + try { + // Get text ranges to copy font properties + const sourceTextRange = firstShape.textFrame.textRange; + const targetTextRange = targetShape.textFrame.textRange; + + // Load font properties from source + sourceTextRange.load([ + "font/name", + "font/size", + "font/bold", + "font/italic", + "font/underline" + ]); + + await context.sync(); + + // Copy font properties + targetTextRange.font.name = sourceTextRange.font.name; + targetTextRange.font.size = sourceTextRange.font.size; + targetTextRange.font.bold = sourceTextRange.font.bold; + targetTextRange.font.italic = sourceTextRange.font.italic; + targetTextRange.font.underline = sourceTextRange.font.underline; + + } catch (err) { + // Silently fail if text properties can't be copied + // This could happen if shape doesn't have text frame + console.error("Error copying text properties:", err); + } + } + + await context.sync(); + setStatusMessage(`Copied properties from the first shape to ${shapes.items.length - 1} other shapes.`); + setStatusType("success"); + + // Auto-clear success message after 5 seconds + setTimeout(() => { + if (statusType === "success") { + setStatusMessage(""); + } + }, 5000); + }); + } catch (error) { + setStatusMessage(`Error: ${error.message}`); + setStatusType("error"); + console.error(error); + } finally { + setIsProcessing(false); + } + }; + + const getStatusIcon = () => { + switch (statusType) { + case "success": + return ; + case "warning": + return ; + case "error": + return ; + default: + return null; + } + }; + + return ( +
+
+ +
+ + {isProcessing && ( +
+
+ + Applying properties... +
+
+ )} + + {!isProcessing && statusMessage && ( +
+
+ {getStatusIcon()} + {statusMessage} +
+
+ )} +
+ ); +}; + +export default MatchProperties; \ No newline at end of file diff --git a/src/taskpane/components/MatchSizes.tsx b/src/taskpane/components/MatchSizes.tsx index ff467bdc..8323a5ef 100644 --- a/src/taskpane/components/MatchSizes.tsx +++ b/src/taskpane/components/MatchSizes.tsx @@ -28,7 +28,7 @@ const useStyles = makeStyles({ display: "flex", flexDirection: "column", gap: "8px", - marginBottom: "12px", + marginBottom: "2px", }, actionButton: { justifyContent: "flex-start", @@ -37,7 +37,7 @@ const useStyles = makeStyles({ transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)", }, statusContainer: { - marginTop: "12px", + marginTop: "4px", padding: "8px 12px", fontSize: "13px", borderRadius: "6px",