Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
@@ -1,18 +0,0 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!-- Office JavaScript API -->
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
</head>
<body>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
/* global Office */
Office.onReady(() => {
// If needed, Office.js is ready to be called.
});
/**
* Shows a notification when the add-in command is executed.
* @param event
*/
function action(event: Office.AddinCommands.Event) {
const message: Office.NotificationMessageDetails = {
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
message: "Performed action.",
icon: "Icon.80x80",
persistent: true,
};
// Show a notification message.
Office.context.mailbox.item?.notificationMessages.replaceAsync(
"ActionPerformanceNotification",
message
);
// Be sure to indicate when the add-in command function is complete.
event.completed();
}
// Register the function with Office.
Office.actions.associate("action", action);
+72
View File
@@ -0,0 +1,72 @@
import * as React from "react";
import { useEffect, useState } from "react";
import ShapeResizer from "./ShapeResizer";
import { makeStyles, Text, Subtitle1, tokens, Theme } from "@fluentui/react-components";
interface AppProps {
title: string;
}
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
height: "100%",
padding: "16px",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
backgroundColor: tokens.colorNeutralBackground1,
overflow: "auto",
},
section: {
marginBottom: "24px",
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,
textAlign: "center",
marginTop: "auto",
padding: "8px 0",
},
});
const App: React.FC<AppProps> = () => {
const styles = useStyles();
const [theme, setTheme] = useState<string>("light");
// Check if macOS is in dark mode (would need to be expanded in production)
useEffect(() => {
// In a real implementation, we would listen to Office theme changes
const prefersDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
setTheme(prefersDarkMode ? "dark" : "light");
}, []);
return (
<div className={styles.root} data-theme={theme}>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Shape Tools
</Subtitle1>
<ShapeResizer />
</div>
<div className={styles.footer}>
<Text size={100}>Edison v1.0.0</Text>
</div>
</div>
);
};
export default App;
+38
View File
@@ -0,0 +1,38 @@
import * as React from "react";
import { Image, tokens, makeStyles } from "@fluentui/react-components";
export interface HeaderProps {
title: string;
logo: string;
message: string;
}
const useStyles = makeStyles({
welcome__header: {
display: "flex",
flexDirection: "column",
alignItems: "center",
paddingBottom: "30px",
paddingTop: "100px",
backgroundColor: tokens.colorNeutralBackground3,
},
message: {
fontSize: tokens.fontSizeHero900,
fontWeight: tokens.fontWeightRegular,
fontColor: tokens.colorNeutralBackgroundStatic,
},
});
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { title, logo, message } = props;
const styles = useStyles();
return (
<section className={styles.welcome__header}>
<Image width="90" height="90" src={logo} alt={title} />
<h1 className={styles.message}>{message}</h1>
</section>
);
};
export default Header;
+62
View File
@@ -0,0 +1,62 @@
import * as React from "react";
import { tokens, makeStyles } from "@fluentui/react-components";
export interface HeroListItem {
icon: React.JSX.Element;
primaryText: string;
}
export interface HeroListProps {
message: string;
items: HeroListItem[];
}
const useStyles = makeStyles({
list: {
marginTop: "20px",
},
listItem: {
paddingBottom: "20px",
display: "flex",
},
icon: {
marginRight: "10px",
},
itemText: {
fontSize: tokens.fontSizeBase300,
fontColor: tokens.colorNeutralBackgroundStatic,
},
welcome__main: {
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
},
message: {
fontSize: tokens.fontSizeBase500,
fontColor: tokens.colorNeutralBackgroundStatic,
fontWeight: tokens.fontWeightRegular,
paddingLeft: "10px",
paddingRight: "10px",
},
});
const HeroList: React.FC<HeroListProps> = (props: HeroListProps) => {
const { items, message } = props;
const styles = useStyles();
const listItems = items.map((item, index) => (
<li className={styles.listItem} key={index}>
<i className={styles.icon}>{item.icon}</i>
<span className={styles.itemText}>{item.primaryText}</span>
</li>
));
return (
<div className={styles.welcome__main}>
<h2 className={styles.message}>{message}</h2>
<ul className={styles.list}>{listItems}</ul>
</div>
);
};
export default HeroList;
+228
View File
@@ -0,0 +1,228 @@
import * as React from "react";
import {
Button,
makeStyles,
Text,
Tooltip,
InfoLabel,
Spinner,
tokens,
Card,
Body1
} from "@fluentui/react-components";
import {
ArrowFitInRegular,
ArrowSortDownRegular,
ArrowSortUpRegular,
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: "12px",
},
actionButton: {
justifyContent: "flex-start",
transitionProperty: "all",
transitionDuration: "200ms",
transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)",
},
statusContainer: {
marginTop: "12px",
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
},
infoCard: {
marginTop: "12px",
padding: "8px",
backgroundColor: "#f3f2f1", // Light gray background
borderRadius: "6px",
borderLeft: "3px solid #0078d4", // Brand blue color
},
infoIcon: {
marginRight: "8px",
color: "#0078d4", // Brand blue color
},
infoText: {
fontSize: "12px",
color: "#605e5c", // Medium gray text
},
statusIcon: {
marginRight: "8px",
},
statusText: {
display: "flex",
alignItems: "center",
},
hotkey: {
display: "inline-block",
padding: "2px 5px",
borderRadius: "4px",
background: "#f0f0f0", // Light gray background
fontSize: "11px",
color: "#605e5c", // Medium gray text
marginLeft: "4px",
}
});
export const ShapeResizer: 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 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();
// 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;
}
// 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;
}
await context.sync();
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
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);
}
};
// Implement keyboard shortcut for shape resizing (Command+Shift+M)
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.metaKey && e.shiftKey && e.key === "M") {
e.preventDefault(); // Prevent default browser behavior
matchSizeToFirstSelected();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const getStatusIcon = () => {
switch (statusType) {
case "success":
return <ShapeUnionRegular className={styles.statusIcon} />;
case "warning":
return <SquareRegular className={styles.statusIcon} />;
case "error":
return <InfoRegular className={styles.statusIcon} />;
default:
return null;
}
};
return (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Tooltip content="Command+Shift+M" relationship="label">
<Button
appearance="primary"
className={styles.actionButton}
onClick={matchSizeToFirstSelected}
icon={<ArrowFitInRegular />}
disabled={isProcessing}
>
Match Size to First Shape
</Button>
</Tooltip>
</div>
{isProcessing && (
<div className={styles.statusContainer}>
<div className={styles.statusText}>
<Spinner size="tiny" style={{ marginRight: "8px" }} />
Resizing shapes...
</div>
</div>
)}
{!isProcessing && statusMessage && (
<div className={`${styles.statusContainer} ${styles[`${statusType}Status`]}`}>
<div className={styles.statusText}>
{getStatusIcon()}
{statusMessage}
</div>
</div>
)}
<div className={styles.infoCard}>
<div style={{ display: "flex", alignItems: "center" }}>
<InfoRegular className={styles.infoIcon} />
<Body1 className={styles.infoText}>
Select multiple shapes, then click the button to resize all shapes to match the first one.
Shortcut: <span className={styles.hotkey}>++M</span>
</Body1>
</div>
</div>
</div>
);
};
export default ShapeResizer;
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import { createRoot } from "react-dom/client";
import App from "./components/App";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
/* global document, Office, module, require, HTMLElement */
const title = "Edison for PowerPoint";
const rootElement: HTMLElement | null = document.getElementById("container");
const root = rootElement ? createRoot(rootElement) : undefined;
/* Render application after Office initializes */
Office.onReady(() => {
root?.render(
<FluentProvider theme={webLightTheme}>
<App title={title} />
</FluentProvider>
);
});
if ((module as any).hot) {
(module as any).hot.accept("./components/App", () => {
const NextApp = require("./components/App").default;
root?.render(NextApp);
});
}
+158
View File
@@ -0,0 +1,158 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -->
<!-- See LICENSE in the project root for license information -->
<!doctype html>
<html lang="en" data-framework="typescript">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Edison for PowerPoint</title>
<!-- Office JavaScript API -->
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
<style>
:root {
--system-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: var(--system-font);
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
#container {
width: 100%;
height: 100%;
overflow: auto;
}
/* Custom scrollbar for macOS style */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
}
#tridentmessage {
display: none;
padding: 16px;
margin: 20px;
border-radius: 8px;
background-color: #FEF0F1;
color: #D13438;
font-family: var(--system-font);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
line-height: 1.5;
}
/* Loading animation */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 120, 212, 0.2);
border-radius: 50%;
border-top-color: #0078D4;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<!-- Loading spinner shown until React loads -->
<div id="loading" class="loading-container">
<div class="spinner"></div>
</div>
<div id="container"></div>
<!--
Fluent UI React v. 9 uses modern JavaScript syntax that is not supported in
Trident (Internet Explorer) or EdgeHTML (Edge Legacy), so this add-in won't
work in Office versions that use these webviews. The script below makes the
following div display when an unsupported webview is in use, and hides the
React container div.
-->
<div id="tridentmessage">
This add-in requires a newer version of Office. Please upgrade to Office 2021 or later,
or use a Microsoft 365 subscription.
</div>
<script>
// Check browser compatibility
if ((navigator.userAgent.indexOf("Trident") !== -1) || (navigator.userAgent.indexOf("Edge") !== -1)) {
document.getElementById("tridentmessage").style.display = "block";
document.getElementById("container").style.display = "none";
document.getElementById("loading").style.display = "none";
}
// Hide loading spinner when the app is loaded
window.addEventListener('DOMContentLoaded', function() {
// The React app will remove the loading element when it's ready
setTimeout(function() {
var loadingEl = document.getElementById("loading");
if (loadingEl) {
loadingEl.style.opacity = "0";
loadingEl.style.transition = "opacity 0.3s ease";
setTimeout(function() {
if (loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
}, 300);
}
}, 500);
});
// Add this to enable keyboard shortcuts throughout the app
document.addEventListener('keydown', function(e) {
// Allow keyboard shortcuts to work even when focus is not on interactive elements
if (e.metaKey) {
// Don't prevent default for all Command shortcuts - the React components
// will handle the specific ones they need
}
});
</script>
</body>
</html>