diff --git a/src/@adobe/gatsby-theme-aio/components/GlobalHeader/avatar.svg b/src/@adobe/gatsby-theme-aio/components/GlobalHeader/avatar.svg
new file mode 100644
index 000000000..75305cc07
--- /dev/null
+++ b/src/@adobe/gatsby-theme-aio/components/GlobalHeader/avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/@adobe/gatsby-theme-aio/components/GlobalHeader/index.js b/src/@adobe/gatsby-theme-aio/components/GlobalHeader/index.js
new file mode 100644
index 000000000..15d01eacd
--- /dev/null
+++ b/src/@adobe/gatsby-theme-aio/components/GlobalHeader/index.js
@@ -0,0 +1,1109 @@
+/*
+ * Copyright 2020 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import React, { Fragment, useRef, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import nextId from 'react-id-generator';
+import { withPrefix } from 'gatsby';
+import { GatsbyLink } from '@adobe/gatsby-theme-aio/src/components/GatsbyLink';
+import {
+ findSelectedTopPage,
+ findSelectedTopPageMenu,
+ rootFix,
+ rootFixPages,
+ getExternalLinkProps,
+ DESKTOP_SCREEN_WIDTH,
+ MOBILE_SCREEN_WIDTH,
+ DEFAULT_HOME,
+} from '@adobe/gatsby-theme-aio/src/utils';
+import { css } from '@emotion/react';
+import { AnchorButton } from '@adobe/gatsby-theme-aio/src/components/AnchorButton';
+import { Button } from '@adobe/gatsby-theme-aio/src/components/Button';
+import { ProgressCircle } from '@adobe/gatsby-theme-aio/src/components/ProgressCircle';
+import { Adobe, ChevronDown, Magnify, Close, TripleGripper, CheckMark } from '@adobe/gatsby-theme-aio/src/components/Icons';
+import { ActionButton, Text as ActionButtonLabel } from '@adobe/gatsby-theme-aio/src/components/ActionButton';
+import { PickerButton } from '@adobe/gatsby-theme-aio/src/components/Picker';
+import { Menu, Item as MenuItem } from '@adobe/gatsby-theme-aio/src/components/Menu';
+import { Popover } from '@adobe/gatsby-theme-aio/src/components/Popover';
+import { Image } from '@adobe/gatsby-theme-aio/src/components/Image';
+import { Link } from '@adobe/gatsby-theme-aio/src/components/Link';
+import {
+ Tabs,
+ HeaderTabItem as TabsItem,
+ Label as TabsItemLabel,
+ TabsIndicator,
+ positionIndicator,
+ animateIndicator,
+} from '@adobe/gatsby-theme-aio/src/components/Tabs';
+import '@spectrum-css/typography';
+import '@spectrum-css/assetlist';
+import { Divider } from '@adobe/gatsby-theme-aio/src/components/Divider';
+import DEFAULT_AVATAR from './avatar.svg';
+
+const getSelectedTabIndex = (location, pages) => {
+ const pathWithRootFix = rootFix(location.pathname);
+ const pagesWithRootFix = rootFixPages(pages);
+
+ let selectedIndex = pagesWithRootFix.indexOf(
+ findSelectedTopPage(pathWithRootFix, pagesWithRootFix)
+ );
+ let tempArr = pathWithRootFix.split('/');
+ let inx = tempArr.indexOf('use-cases');
+ if (selectedIndex === -1 && inx > -1) {
+ tempArr[inx + 1] = 'agreements-and-contracts';
+ tempArr[inx + 2] = 'sales-proposals-and-contracts';
+ if (tempArr[inx + 3] == undefined) {
+ tempArr.push('');
+ }
+ let tempPathName = tempArr.join('/');
+ selectedIndex = pagesWithRootFix.indexOf(findSelectedTopPage(tempPathName, pagesWithRootFix));
+ }
+ // Assume first item is selected
+ if (selectedIndex === -1) {
+ selectedIndex = 0;
+ }
+ return selectedIndex;
+};
+
+const getAvatar = async userId => {
+ try {
+ const req = await fetch(`https://cc-api-behance.adobe.io/v2/users/${userId}?api_key=SUSI2`);
+ const res = await req.json();
+ return res?.user?.images?.['138'] ?? DEFAULT_AVATAR;
+ } catch (e) {
+ console.warn(e);
+ return DEFAULT_AVATAR;
+ }
+};
+
+const GlobalHeader = ({
+ hasIMS,
+ ims,
+ isLoadingIms,
+ home,
+ versions,
+ pages,
+ docs,
+ location,
+ toggleSideNav,
+ hasSideNav,
+ hasSearch,
+ showSearch,
+ setShowSearch,
+ searchButtonId,
+}) => {
+ const [selectedTabIndex, setSelectedTabIndex] = useState(getSelectedTabIndex(location, pages));
+ const tabsRef = useRef(null);
+ const tabsContainerRef = useRef(null);
+ const selectedTabIndicatorRef = useRef(null);
+ // Don't animate the tab indicator by default
+ const [isAnimated, setIsAnimated] = useState(false);
+ const versionPopoverRef = useRef(null);
+ const profilePopoverRef = useRef(null);
+ const [openVersion, setOpenVersion] = useState(false);
+ const [openProfile, setOpenProfile] = useState(false);
+ const [openMenuIndex, setOpenMenuIndex] = useState(-1);
+ const [profile, setProfile] = useState(null);
+ const [avatar, setAvatar] = useState(DEFAULT_AVATAR);
+ const [isLoadingProfile, setIsLoadingProfile] = useState(true);
+ const [selectedMenuItem, setSelectedMenuItem] = useState({});
+
+ const POPOVER_ANIMATION_DELAY = 200;
+ const versionPopoverId = 'version ' + nextId();
+ const profilePopoverId = 'profile ' + nextId();
+ const hasHome = home?.hidden !== true;
+
+ const positionSelectedTabIndicator = index => {
+ const selectedTab = pages[index].tabRef;
+
+ if (selectedTab?.current) {
+ positionIndicator(selectedTabIndicatorRef, selectedTab);
+ }
+ };
+
+ useEffect(() => {
+ const index = getSelectedTabIndex(location, pages);
+ setSelectedTabIndex(index);
+ const pathWithRootFix = rootFix(location.pathname);
+ setSelectedMenuItem(findSelectedTopPageMenu(pathWithRootFix, pages[index]));
+ animateIndicator(selectedTabIndicatorRef, isAnimated);
+ positionSelectedTabIndicator(index);
+ }, [location.pathname]);
+
+ useEffect(() => {
+ (async () => {
+ if (ims && ims.isSignedInUser()) {
+ const profile = await ims.getProfile();
+ setProfile(profile);
+ setAvatar(await getAvatar(profile.userId));
+ setIsLoadingProfile(false);
+ } else if (!isLoadingIms) {
+ setIsLoadingProfile(false);
+ }
+ })();
+ }, [ims]);
+
+ useEffect(() => {
+ if (versionPopoverRef.current) {
+ if (openVersion) {
+ const { left } = versionPopoverRef.current.getBoundingClientRect();
+
+ versionPopoverRef.current.style.left = `calc(${left}px + var(--spectrum-global-dimension-size-160))`;
+ versionPopoverRef.current.style.position = 'fixed';
+ } else {
+ // Wait for animation to finish
+ setTimeout(() => {
+ versionPopoverRef.current.style = '';
+ }, POPOVER_ANIMATION_DELAY);
+ }
+ }
+ }, [openVersion]);
+
+ useEffect(() => {
+ if (openMenuIndex !== -1) {
+ const menuRef = pages[openMenuIndex].menuRef;
+
+ const { left } = menuRef.current.getBoundingClientRect();
+
+ menuRef.current.style.left = `${left}px`;
+ menuRef.current.style.position = 'fixed';
+ } else {
+ pages.forEach(page => {
+ const menuRef = page.menuRef;
+ if (menuRef) {
+ // Wait for animation to finish
+ setTimeout(() => {
+ menuRef.current.style = '';
+ }, POPOVER_ANIMATION_DELAY);
+ }
+ });
+ }
+ }, [openMenuIndex]);
+
+ useEffect(() => {
+ // Clicking outside of menu should close menu
+ const onClick = event => {
+ if (versionPopoverRef.current && !versionPopoverRef.current.contains(event.target)) {
+ setOpenVersion(false);
+ }
+
+ if (profilePopoverRef?.current && !profilePopoverRef.current.contains(event.target)) {
+ setOpenProfile(false);
+ }
+
+ pages.some(page => {
+ if (page?.menuRef?.current && !page.menuRef.current.contains(event.target)) {
+ setOpenMenuIndex(-1);
+ }
+ });
+ };
+
+ document.addEventListener('click', onClick);
+
+ return () => document.removeEventListener('click', onClick);
+ }, []);
+
+ useEffect(() => {
+ const onScroll = () => {
+ setOpenVersion(false);
+ setOpenMenuIndex(-1);
+ };
+
+ tabsContainerRef.current.addEventListener('scroll', onScroll, { passive: true });
+
+ return () => tabsContainerRef.current.removeEventListener('scroll', onScroll);
+ }, []);
+
+ const openDropDown = data => {
+ if (data.isOpen) {
+ setOpenMenuIndex(data.index);
+ setOpenVersion(data.isOpen);
+ if (openMenuIndex === -1 || openMenuIndex !== data.index) {
+ setTimeout(() => {
+ document.getElementById(`menuIndex${data.index}-0`).focus();
+ }, 100);
+ }
+ }
+ };
+
+ const handleCredential = () => {
+
+ const section = document.getElementById('adobe-get-credential');
+
+ if (section) {
+ section.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center',
+ });
+ }
+
+ }
+
+ return (
+
+
+
+ );
+};
+
+GlobalHeader.propTypes = {
+ ims: PropTypes.object,
+ isLoadingIms: PropTypes.bool,
+ home: PropTypes.object,
+ versions: PropTypes.array,
+ pages: PropTypes.array,
+ docs: PropTypes.object,
+ location: PropTypes.object,
+ toggleSideNav: PropTypes.func,
+ hasSideNav: PropTypes.bool,
+ setShowSearch: PropTypes.func,
+ hasSearch: PropTypes.bool,
+ showSearch: PropTypes.bool,
+ searchButtonId: PropTypes.string,
+};
+
+export { GlobalHeader };
diff --git a/src/@adobe/gatsby-theme-aio/components/Layout/index.js b/src/@adobe/gatsby-theme-aio/components/Layout/index.js
new file mode 100644
index 000000000..b004cac71
--- /dev/null
+++ b/src/@adobe/gatsby-theme-aio/components/Layout/index.js
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+// Re-export the original Layout component
+// This allows the shadowed GlobalHeader to work properly
+export { default } from '@adobe/gatsby-theme-aio/src/components/Layout';
+
diff --git a/src/pages/guides/getting_started/hello-world.md b/src/pages/guides/getting_started/hello-world.md
index b30d911f3..95249875a 100644
--- a/src/pages/guides/getting_started/hello-world.md
+++ b/src/pages/guides/getting_started/hello-world.md
@@ -36,6 +36,14 @@ This guide is **divided into two tracks**, which you can follow independently of
+
+
+
+
The [Code Playground](#code-playground) path is based on a browser sandbox that runs instantly, requires no installation, and lets you explore add-on APIs with real-time feedback directly inside Adobe Express. **If you are new to add-on development**, or prefer to tinker-to-learn, then begin in the Playground to familiarise yourself with the environment; you can always try the CLI later.
The [Command Line Interface (CLI)](#command-line-interface-cli) path will teach you to set up a local development environment, complete with a build pipeline, that allows you to build more complex add-ons that include external dependencies. This is the preferred path **for developers who want fully control**. You can always prototype rapidly in the Playground and transition to the CLI whenever project complexity calls for it.
diff --git a/src/pages/guides/learn/how_to/group_elements.md b/src/pages/guides/learn/how_to/group_elements.md
index 91d9d16d5..e139e8471 100644
--- a/src/pages/guides/learn/how_to/group_elements.md
+++ b/src/pages/guides/learn/how_to/group_elements.md
@@ -55,17 +55,17 @@ To create a Group, you can use the [`editor.createGroup()`](../../../references/
### Example
-```js
+```js{try id=createBasicGroup}
// sandbox/code.js
import { editor } from "express-document-sdk";
// Create some Text
const greeting = editor.createText("Hiya!");
-greeting.translation = { x: 100, y: 50 };
+greeting.translation = { x: 0, y: 0 };
// Create some other Text
const saluto = editor.createText("Ciao!");
-saluto.translation = { x: 100, y: 150 };
+saluto.translation = { x: 0, y: 50 };
// Create a Group 👈
const greetingsGroup = editor.createGroup();
diff --git a/src/pages/guides/learn/how_to/position_elements.md b/src/pages/guides/learn/how_to/position_elements.md
index 2b5e360f8..726f2f484 100644
--- a/src/pages/guides/learn/how_to/position_elements.md
+++ b/src/pages/guides/learn/how_to/position_elements.md
@@ -52,7 +52,7 @@ faq:
Let's use this simple Rectangle to demonstrate how to move and rotate elements in Adobe Express.
-```js
+```js{try id=createAndPositionRectangle}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -60,6 +60,9 @@ const rect = editor.createRectangle();
rect.width = 200;
rect.height = 100;
+// Move the rectangle 50px to the right and 100px down
+rect.translation = { x: 50, y: 100 };
+
editor.context.insertionParent.children.append(rect);
```
@@ -121,7 +124,7 @@ By definition, the bounds of an element (or its _bounding box_) are the smallest
Let's see how to get the bounds of a rotated rectangle in both local and parent coordinates; since the rectangle is rotated, the two bounding boxes will differ.
-```js
+```js{try id=createAndRotateRectangle}
// sandbox/code.js
import { editor } from "express-document-sdk";
diff --git a/src/pages/guides/learn/how_to/use_color.md b/src/pages/guides/learn/how_to/use_color.md
index 81f75f576..3b43c7c78 100644
--- a/src/pages/guides/learn/how_to/use_color.md
+++ b/src/pages/guides/learn/how_to/use_color.md
@@ -105,15 +105,15 @@ Colors are not directly applied, instead, to shapes; more generally, they are us
If you're confused, worry not! This is the wondrous word of object oriented programming. The following example should clarify things:
-```js
+```js{try id=applyFillAndStrokeColors}
// sandbox/code.js
import { editor, colorUtils } from "express-document-sdk";
// Create the shape
const ellipse = editor.createEllipse();
-ellipse.width = 100;
-ellipse.height = 50;
-ellipse.translation = { x: 50, y: 50 };
+ellipse.rx = 100;
+ellipse.ry = 50;
+ellipse.translation = { x: 150, y: 150 };
// Generate the needed colors
const innerColor = colorUtils.fromHex("#A38AF0");
diff --git a/src/pages/guides/learn/how_to/use_geometry.md b/src/pages/guides/learn/how_to/use_geometry.md
index e5c5a5d8b..8814be5e9 100644
--- a/src/pages/guides/learn/how_to/use_geometry.md
+++ b/src/pages/guides/learn/how_to/use_geometry.md
@@ -48,7 +48,7 @@ Adobe Express provides a set of geometric shapes that you can create and style p
### Example: Add a Rectangle
-```js
+```js{try id=createBasicRectangle}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -58,11 +58,8 @@ const rect = editor.createRectangle();
rect.width = 100;
rect.height = 100;
-// The current page, where the rectangle will be placed
-const currentPage = editor.context.currentPage;
-
-// Append the rectangle to the page.
-currentPage.artboards.first.children.append(rect);
+// Add the rectangle to the document
+editor.context.insertionParent.children.append(rect);
```
@@ -91,7 +88,7 @@ Ellipses don't have a `width` and `height` properties, but a [`rx`](../../../ref
An ellipse with a radius of 200 on the x-axis and 100 on the y-axis will result in a shape with 400 wide (`rx` times two) and a 200 tall (`ry` times two)!
-```js
+```js{try id=createBasicEllipse}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -102,18 +99,15 @@ ellipse.ry = 100; // radius y 👈
console.log(ellipse.boundsLocal);
// { x: 0, y: 0, width: 400, height: 200 } 👈 mind the actual bounds!
-// The current page, where the rectangle will be placed
-const currentPage = editor.context.currentPage;
-
-// Append the rectangle to the page.
-currentPage.artboards.first.children.append(rect);
+// Add the ellipse to the document
+editor.context.insertionParent.children.append(ellipse);
```
### Example: Style Shapes
-Shapes have `fill` and `stroke` properties that you can use to style them. The following example demonstrates how to create a rectangle with a fill and a stroke.
+Shapes have `fill` and `stroke` properties that you can use to style them. The following example demonstrates how to create an ellipse with a fill and a stroke.
-```js
+```js{try id=createEllipseWithFillAndStroke}
// sandbox/code.js
import { editor, colorUtils, constants } from "express-document-sdk";
@@ -152,7 +146,7 @@ Paths are a versatile tool to create complex shapes in Adobe Express. The [`edit
### Example: Single path
-```js
+```js{try id=createSinglePath}
// sandbox/code.js
import { editor } from "express-document-sdk";
diff --git a/upload-playground-samples.mjs b/upload-playground-samples.mjs
index 836155bcb..2b0c5cf5a 100644
--- a/upload-playground-samples.mjs
+++ b/upload-playground-samples.mjs
@@ -73,13 +73,40 @@ async function getImsServiceToken() {
}
}
+/**
+ * Comment out express-document-sdk import statements in code.
+ * The Code Playground Script mode automatically imports these modules,
+ * so we comment them out to avoid conflicts while preserving them for educational context.
+ * @param code - The code to process.
+ * @returns the code with import statements commented out.
+ */
+function commentOutExpressDocumentSDKImports(code) {
+ // Comment out import statements for express-document-sdk
+ // Handles various import formats:
+ // - import { editor } from "express-document-sdk";
+ // - import { editor, fonts } from "express-document-sdk";
+ // - import * as expressSDK from "express-document-sdk";
+ // - Single or double quotes
+ const importRegex = /^(import\s+.*\s+from\s+["']express-document-sdk["'];?\s*)$/gm;
+
+ // Replace with commented version and add helpful note
+ const processedCode = code.replace(
+ importRegex,
+ "// Note: Uncomment the import below when using in your add-on's code.js\n// $1"
+ );
+
+ return processedCode;
+}
+
/**
* Create a zip file from a code block.
* @param block - The code block to create a zip file from.
*/
async function createZipFileFromCodeBlock(block) {
const zip = new JSZip();
- zip.file("script.js", block.code);
+ // Comment out express-document-sdk imports before adding to zip
+ const processedCode = commentOutExpressDocumentSDKImports(block.code);
+ zip.file("script.js", processedCode);
return zip.generateAsync({ type: "nodebuffer" });
}
@@ -91,13 +118,29 @@ async function createZipFileFromCodeBlock(block) {
*/
async function uploadCodeBlockToFFC(codeBlock, projectId) {
try {
+ // Process the code and log it for verification
+ const processedCode = commentOutExpressDocumentSDKImports(codeBlock.code);
+
+ console.log("\n" + "=".repeat(80));
+ console.log(`Uploading code block: ${projectId}`);
+ console.log("File: " + codeBlock.filePath);
+ console.log("-".repeat(80));
+ console.log("Processed code that will be uploaded:");
+ console.log("-".repeat(80));
+ console.log(processedCode);
+ console.log("=".repeat(80) + "\n");
+
const accessToken = await getImsServiceToken();
const url = new URL(
`${FFC_PLAYGROUND_ENDPOINT}/${projectId}`,
FFC_BASE_URL
);
- const zipBuffer = await createZipFileFromCodeBlock(codeBlock);
+ // Create zip with the already-processed code
+ const zip = new JSZip();
+ zip.file("script.js", processedCode);
+ const zipBuffer = await zip.generateAsync({ type: "nodebuffer" });
+
const form = new FormData();
form.append(
"file",
@@ -105,7 +148,7 @@ async function uploadCodeBlockToFFC(codeBlock, projectId) {
`${projectId}.zip`
);
form.append("name", projectId);
-
+
const response = await fetch(url, {
method: "PUT",
headers: {
@@ -124,9 +167,11 @@ async function uploadCodeBlockToFFC(codeBlock, projectId) {
`Failed to upload code block to FFC - HTTP ${response.status}: ${text}`
);
}
+
+ console.log(`✅ Successfully uploaded: ${projectId}\n`);
return response.json();
} catch (error) {
- console.error("Failed to upload code block to FFC:", error.message);
+ console.error(`❌ Failed to upload code block to FFC (${projectId}):`, error.message);
throw error;
}
}