diff --git a/src/@adobe/gatsby-theme-aio/components/Code/index.js b/src/@adobe/gatsby-theme-aio/components/Code/index.js
index 23c988199..1ca2510f6 100644
--- a/src/@adobe/gatsby-theme-aio/components/Code/index.js
+++ b/src/@adobe/gatsby-theme-aio/components/Code/index.js
@@ -164,7 +164,7 @@ const Code = ({ children, className = "", theme, metastring = "" }) => {
!isMobile && openCodePlayground(children, sampleId)
}
>
- Try
+ Try in playground
)}
diff --git a/src/@adobe/gatsby-theme-aio/components/Code/styles.css b/src/@adobe/gatsby-theme-aio/components/Code/styles.css
index c4543201b..8470f97b7 100644
--- a/src/@adobe/gatsby-theme-aio/components/Code/styles.css
+++ b/src/@adobe/gatsby-theme-aio/components/Code/styles.css
@@ -40,7 +40,7 @@
}
.code-copy-button.with-try {
- inset-inline-end: var(--spectrum-global-dimension-size-700);
+ inset-inline-end: calc(var(--spectrum-global-dimension-size-2000) + var(--spectrum-global-dimension-size-100));
}
.code-try-button {
@@ -67,7 +67,7 @@
}
.code-tooltip-container.with-try {
- inset-inline-end: calc(var(--spectrum-global-dimension-size-700) + var(--spectrum-global-dimension-size-600) + var(--spectrum-global-dimension-size-125));
+ inset-inline-end: calc(var(--spectrum-global-dimension-size-2000) + var(--spectrum-global-dimension-size-100) + var(--spectrum-global-dimension-size-600) + var(--spectrum-global-dimension-size-125));
}
.code-tooltip {
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..bd6d72af6
--- /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..37bf33c07 100644
--- a/src/pages/guides/learn/how_to/group_elements.md
+++ b/src/pages/guides/learn/how_to/group_elements.md
@@ -55,26 +55,26 @@ 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 š
+// Create a Group
const greetingsGroup = editor.createGroup();
greetingsGroup.translation = { x: 100, y: 100 };
-// Append the Text nodes to the Group š
+// Append the Text nodes to the Group
greetingsGroup.children.append(greeting, saluto);
-// Append the Group to the page š
+// Append the Group to the page
editor.context.insertionParent.children.append(greetingsGroup);
```
diff --git a/src/pages/guides/learn/how_to/index.md b/src/pages/guides/learn/how_to/index.md
index e7fb55014..426a760a5 100644
--- a/src/pages/guides/learn/how_to/index.md
+++ b/src/pages/guides/learn/how_to/index.md
@@ -24,9 +24,9 @@ The following guides contain a set of common use cases and accompanying code sni
-Code Playground integration is coming soon!
+Try samples in Code Playground!
-The [Code Playground](../../getting_started/code_playground.md) is the perfect tool to test each how-to snippet. We're currently working on a leaner integration between the Playground and our documentation; running the code will be even easier soon!
+Many how-to guides now include a **"Try"** button that opens the code sample directly in the [Code Playground](../../getting_started/code_playground.md). Click the button to instantly test Document API samples in Script mode without setting up a local development environment!
We're constantly adding new how-tos, so make sure to check back often. If you're looking for Tutorials that guide you in building add-ons from the ground up, please check the [Complete Projects](#complete-projects) section.
@@ -78,6 +78,9 @@ We're constantly adding new how-tos, so make sure to check back often. If you're
| | [Use PDF and PowerPoint](./use_pdf_powerpoint.md) |
| | [Group Elements](./group_elements.md) |
| | [Position Elements](./position_elements.md) |
+| | [Resize & Rescale Elements](./resize_rescale_elements.md) |
+| | [Handle Selection](./handle_selection.md) |
+| Manage Pages | [Manage Pages](./manage_pages.md) |
| Use Metadata | [Document Metadata](./document_metadata.md) |
| | [Page Metadata](./page_metadata.md) |
| | [Element Metadata](./element_metadata.md) |
diff --git a/src/pages/guides/learn/how_to/manage_pages.md b/src/pages/guides/learn/how_to/manage_pages.md
index 27f6554b5..2dbe1fe03 100644
--- a/src/pages/guides/learn/how_to/manage_pages.md
+++ b/src/pages/guides/learn/how_to/manage_pages.md
@@ -78,7 +78,7 @@ Use the [`editor.documentRoot.pages.addPage()`](../../../references/document-san
#### JavaScript
-```js
+```js{try id=addStandardPage}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -264,7 +264,7 @@ const lastPage: PageNode = allPages[allPages.length - 1];
#### JavaScript
-```js
+```js{try id=addContentToNewPage}
// sandbox/code.js
import { editor } from "express-document-sdk";
diff --git a/src/pages/guides/learn/how_to/position_elements.md b/src/pages/guides/learn/how_to/position_elements.md
index 2b5e360f8..d9eb2151f 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";
@@ -133,9 +136,9 @@ rect.translation = { x: 50, y: 100 };
rect.setRotationInParent(15, { x: 0, y: 0 });
console.log(rect.boundsLocal);
-// {x: 0, y: 0, width: 200, height: 100} š
+// {x: 0, y: 0, width: 200, height: 100}
console.log("boundsInParent", rect.boundsInParent);
-// {x: 24.2, y: 100, width: 219.0, height: 148.3} š
+// {x: 24.2, y: 100, width: 219.0, height: 148.3}
editor.context.insertionParent.children.append(rect);
```
diff --git a/src/pages/guides/learn/how_to/resize_rescale_elements.md b/src/pages/guides/learn/how_to/resize_rescale_elements.md
index 1adf47020..746512904 100644
--- a/src/pages/guides/learn/how_to/resize_rescale_elements.md
+++ b/src/pages/guides/learn/how_to/resize_rescale_elements.md
@@ -68,15 +68,17 @@ Rescaling operations maintain the aspect ratio of elements while changing their
Use `rescaleProportionalToWidth()` to adjust an element's width while maintaining its aspect ratio. The height will automatically adjust proportionally.
-```js
+```js{try id=rescaleByWidth}
// sandbox/code.js
-import { editor } from "express-document-sdk";
+import { editor, colorUtils } from "express-document-sdk";
// Create a rectangle with specific dimensions
const rect = editor.createRectangle();
rect.width = 200;
rect.height = 100;
+rect.translation = { x: 100, y: 100 };
rect.fill = editor.makeColorFill(colorUtils.fromHex("#3498db"));
+
// Add it to the page
editor.context.insertionParent.children.append(rect);
@@ -93,13 +95,14 @@ console.log(`New dimensions: ${rect.width} x ${rect.height}`);
Similarly, use `rescaleProportionalToHeight()` to adjust an element's height while maintaining its aspect ratio. The width will automatically adjust proportionally.
-```js
+```js{try id=rescaleByHeight}
// sandbox/code.js
-// import { editor } from "express-document-sdk";
+import { editor, colorUtils } from "express-document-sdk";
const ellipse = editor.createEllipse();
ellipse.rx = 100; // radius x = 100 (width = 200)
ellipse.ry = 50; // radius y = 50 (height = 100)
+ellipse.translation = { x: 150, y: 100 };
ellipse.fill = editor.makeColorFill(colorUtils.fromHex("#F0B76C"));
editor.context.insertionParent.children.append(ellipse);
@@ -107,9 +110,7 @@ editor.context.insertionParent.children.append(ellipse);
// Rescale to 150px height - width becomes 300px automatically
ellipse.rescaleProportionalToHeight(150);
-console.log(
- `New bounds: ${ellipse.boundsLocal.width} x ${ellipse.boundsLocal.height}`
-);
+console.log(`New bounds: ${ellipse.boundsLocal.width} x ${ellipse.boundsLocal.height}`);
// New bounds: 300 x 150
```
diff --git a/src/pages/guides/learn/how_to/use_color.md b/src/pages/guides/learn/how_to/use_color.md
index 81f75f576..cc8c51d1b 100644
--- a/src/pages/guides/learn/how_to/use_color.md
+++ b/src/pages/guides/learn/how_to/use_color.md
@@ -57,17 +57,23 @@ Colors in Adobe Express are created as instances of the [`Color`](../../../refer
The entrypoint for creating colors is the [`colorUtils`](../../../references/document-sandbox/document-apis/classes/ColorUtils.md) class, imported from the `"express-document-sdk"`, so we're talking about [Document APIs](../../../references/document-sandbox/document-apis/index.md) here. Especially the static [`fromRGB()`](../../../references/document-sandbox/document-apis/classes/ColorUtils.md#fromrgb) and [`fromHex()`](../../../references/document-sandbox/document-apis/classes/ColorUtils.md#fromhex) methods.
-```js
+```js{try id=createColors}
// sandbox/code.js
import { editor, colorUtils } from "express-document-sdk";
-// Alpha is optional, defaults to 1
+// Create colors from RGB (values from 0 to 1)
const red = colorUtils.fromRGB(1, 0, 0);
+
+// Create colors from HEX
const green = colorUtils.fromHex("#00FF00");
-// With alpha
-const feldgrau = colorUtils.fromRGB(0.28, 0.32, 0.39, 0.5); // 50% opacity
-const heliotrope = colorUtils.fromHex("#C768F780"); // 50% opacity
+// Create a rectangle and apply the red color
+const rect = editor.createRectangle();
+rect.width = 200;
+rect.height = 100;
+rect.translation = { x: 100, y: 100 };
+rect.fill = editor.makeColorFill(red);
+editor.context.insertionParent.children.append(rect);
```
In case you need it, you can also convert a color to a HEX string using the [`toHex()`](../../../references/document-sandbox/document-apis/classes/ColorUtils.md#tohex) method. Please note that the alpha value is always included in the output string.
@@ -83,17 +89,20 @@ You can directly set the `color` property of a Text node via [`applyCharacterSty
### Example: Text color
-```js
+```js{try id=applyTextColor}
// sandbox/code.js
import { editor, colorUtils } from "express-document-sdk";
-// Assuming the user has selected a text frame
-const textNode = editor.context.selection[0];
+// Create a text node
+const textNode = editor.createText("Hello, World!");
+const insertionParent = editor.context.insertionParent;
+textNode.translation = { x: 100, y: 100 };
+insertionParent.children.append(textNode);
-// Apply character styles to the first three letters
+// Apply character styles to the first five letters
textNode.fullContent.applyCharacterStyles(
- { color: colorUtils.fromHex("#E1A141") }, // š
- { start: 0, length: 3 }
+ { color: colorUtils.fromHex("#E1A141") },
+ { start: 0, length: 5 }
);
```
@@ -105,15 +114,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");
@@ -126,7 +135,7 @@ const outerColorStroke = editor.makeStroke({
width: 20,
});
-// š Apply the fill and stroke
+// Apply the fill and stroke
ellipse.fill = innerColorFill;
ellipse.stroke = outerColorStroke;
diff --git a/src/pages/guides/learn/how_to/use_geometry.md b/src/pages/guides/learn/how_to/use_geometry.md
index e5c5a5d8b..a4a211ab2 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,29 +88,26 @@ 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";
const ellipse = editor.createEllipse();
-ellipse.rx = 200; // radius x š
-ellipse.ry = 100; // radius y š
+ellipse.rx = 200; // radius x
+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;
+// { x: 0, y: 0, width: 400, height: 200 } - mind the actual bounds!
-// 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=createEllipseWithFillStroke}
// sandbox/code.js
import { editor, colorUtils, constants } from "express-document-sdk";
@@ -121,11 +115,12 @@ import { editor, colorUtils, constants } from "express-document-sdk";
const ellipse = editor.createEllipse();
ellipse.rx = 200;
ellipse.ry = 100;
+ellipse.translation = { x: 250, y: 150 };
-// š Apply the fill color
+// Apply the fill color
ellipse.fill = editor.makeColorFill(colorUtils.fromHex("#F3D988"));
-// š Create the stroke
+// Create the stroke
const stroke = editor.makeStroke({
color: colorUtils.fromHex("#E29E4E"),
width: 20,
@@ -133,7 +128,7 @@ const stroke = editor.makeStroke({
dashPattern: [50, 2],
});
-// š Apply the stroke
+// Apply the stroke
ellipse.stroke = stroke;
// Add the shape to the document
@@ -152,7 +147,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";
@@ -169,7 +164,7 @@ editor.context.insertionParent.children.append(p1);
Combining and grouping multiple paths, you can create complex shapes, like in the following example:
-```js
+```js{try id=createMultiplePaths}
// sandbox/code.js
import { editor } from "express-document-sdk";
diff --git a/src/pages/guides/learn/how_to/use_text.md b/src/pages/guides/learn/how_to/use_text.md
index a1a325a98..ad143b3a4 100644
--- a/src/pages/guides/learn/how_to/use_text.md
+++ b/src/pages/guides/learn/how_to/use_text.md
@@ -97,7 +97,7 @@ The `editor.createText()` method accepts a string as a parameter, and returns a
### Example: Create basic Text
-```js
+```js{try id=createBasicText}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -149,7 +149,7 @@ Although possible, it's not recommended to replace the text content of a `TextNo
#### Example: Basic Text Replacement
-```js
+```js{try id=replaceTextBasic}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -271,7 +271,7 @@ The `insertText()` method inserts new text at a specific position within the exi
#### Example: Basic Text Insertion
-```js
+```js{try id=insertTextWithColor}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -289,7 +289,7 @@ textNode.setPositionInParent(
// Add the TextNode to the document
insertionParent.children.append(textNode);
-// Insert text at position 10
+// Insert text at position 6
contentModel.insertText(
"Express ",
6,
@@ -351,7 +351,7 @@ The `appendText()` method adds new text to the end of the existing content. It a
#### Example: Append Text
-```js
+```js{try id=appendTextMultiple}
// sandbox/code.js
import { editor } from "express-document-sdk";
@@ -386,9 +386,9 @@ The `deleteText()` method removes a specific range of text from the content. It
#### Example: Delete Text Range
-```js
+```js{try id=deleteTextRange}
// sandbox/code.js
-import { editor, constants } from "express-document-sdk";
+import { editor } from "express-document-sdk";
// Create a new TextNode
const textNode = editor.createText("It's Friday, don't deploy to Production!");
@@ -404,7 +404,7 @@ textNode.setPositionInParent(
// Add the TextNode to the document
insertionParent.children.append(textNode);
-// Delete 13 characters starting at position 6
+// Delete 6 characters starting at position 13
contentModel.deleteText({ start: 13, length: 6 });
// It's Friday, don't deploy to Production!
@@ -413,7 +413,6 @@ contentModel.deleteText({ start: 13, length: 6 });
// It's Friday, deploy to Production!
// You can delete multiple ranges calling the method as many times as needed
-
contentModel.deleteText({ start: 12, length: 22 });
// It's Friday!
diff --git a/src/styles.css b/src/styles.css
index b9114fdeb..b7e78830e 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -190,3 +190,20 @@ section:has(> div > p > span > picture) {
.developers-live-announcement a:hover span {
color: #fff !important;
}
+
+.open-playground-button {
+ padding: 10px 20px;
+ border-radius: 20px;
+ font-weight: 700;
+ border: 2px solid #000000;
+ background-color: #ffffff;
+ cursor: pointer;
+ font-family: "adobe-clean" !important;
+}
+
+.playground-button-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 20px 0;
+}
diff --git a/upload-playground-samples.mjs b/upload-playground-samples.mjs
old mode 100644
new mode 100755
index 836155bcb..4abf24fec
--- a/upload-playground-samples.mjs
+++ b/upload-playground-samples.mjs
@@ -73,62 +73,127 @@ 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" });
}
/**
- * Upload a code block to FFC.
+ * Upload a code block to FFC with retry logic.
* @param block - The code block to store.
* @param projectId - The project ID corresponding to the code block.
+ * @param maxRetries - Maximum number of retry attempts (default: 3).
* @returns the response from the FFC API.
*/
-async function uploadCodeBlockToFFC(codeBlock, projectId) {
- try {
- const accessToken = await getImsServiceToken();
- const url = new URL(
- `${FFC_PLAYGROUND_ENDPOINT}/${projectId}`,
- FFC_BASE_URL
- );
-
- const zipBuffer = await createZipFileFromCodeBlock(codeBlock);
- const form = new FormData();
- form.append(
- "file",
- new Blob([zipBuffer], { type: "application/zip" }),
- `${projectId}.zip`
- );
- form.append("name", projectId);
+async function uploadCodeBlockToFFC(codeBlock, projectId, maxRetries = 3) {
+ let lastError;
+
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ // Process the code and log it for verification
+ const processedCode = commentOutExpressDocumentSDKImports(codeBlock.code);
+
+ if (attempt === 1) {
+ console.log(`\nš¤ Uploading: ${projectId} (from ${codeBlock.filePath})`);
+ } else {
+ console.log(` Retry ${attempt - 1}/${maxRetries - 1}: ${projectId}`);
+ }
- const response = await fetch(url, {
- method: "PUT",
- headers: {
- Accept: "application/vnd.adobe-ffcaddon.response+json",
- Authorization: `Bearer ${accessToken}`,
- "x-api-key": PLAYGROUND_API_KEY,
- },
- body: form,
- });
+ const accessToken = await getImsServiceToken();
+ const url = new URL(
+ `${FFC_PLAYGROUND_ENDPOINT}/${projectId}`,
+ FFC_BASE_URL
+ );
- if (!response.ok) {
- const text = await response.text();
- const requestId = response.headers.get(FFC_REQUEST_ID);
- console.log("FFC Request ID:", requestId);
- throw new Error(
- `Failed to upload code block to FFC - HTTP ${response.status}: ${text}`
+ // 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",
+ new Blob([zipBuffer], { type: "application/zip" }),
+ `${projectId}.zip`
);
+ form.append("name", projectId);
+
+ const response = await fetch(url, {
+ method: "PUT",
+ headers: {
+ Accept: "application/vnd.adobe-ffcaddon.response+json",
+ Authorization: `Bearer ${accessToken}`,
+ "x-api-key": PLAYGROUND_API_KEY,
+ },
+ body: form,
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ const requestId = response.headers.get(FFC_REQUEST_ID);
+
+ // Log request ID for debugging
+ if (requestId) {
+ console.log(` FFC Request ID: ${requestId}`);
+ }
+
+ throw new Error(
+ `Failed to upload code block to FFC - HTTP ${response.status}: ${text}`
+ );
+ }
+
+ console.log(`ā Successfully uploaded: ${projectId}`);
+ return response.json();
+
+ } catch (error) {
+ lastError = error;
+
+ // If this was the last attempt, don't retry
+ if (attempt === maxRetries) {
+ console.error(`ā Failed to upload (${projectId}) after ${maxRetries} attempts: ${error.message}`);
+ throw error;
+ }
+
+ // Exponential backoff: wait 2^attempt seconds
+ const waitTime = Math.pow(2, attempt) * 1000;
+ console.log(` ā³ Waiting ${waitTime / 1000}s before retry...`);
+ await new Promise(resolve => setTimeout(resolve, waitTime));
}
- return response.json();
- } catch (error) {
- console.error("Failed to upload code block to FFC:", error.message);
- throw error;
}
+
+ throw lastError;
}
/**
@@ -187,19 +252,43 @@ function extractCodeBlocks(content, filePath) {
* Main function to run the code block extractor.
* 1. Find all markdown files in the pages directory.
* 2. Extract code blocks from each markdown file.
- * 3. Store each code block in the backend API.
+ * 3. Store each code block in the backend API with retry logic.
*/
async function run() {
const markdownFiles = await findMarkdownFiles(PAGES_DIRECTORY);
+ let successCount = 0;
+ let failureCount = 0;
for (const filePath of markdownFiles) {
const content = await fs.readFile(filePath, "utf8");
const codeBlocks = extractCodeBlocks(content, filePath);
for (const codeBlock of codeBlocks) {
- await uploadCodeBlockToFFC(codeBlock, codeBlock.id);
+ try {
+ await uploadCodeBlockToFFC(codeBlock, codeBlock.id);
+ successCount++;
+
+ // Small delay between uploads to avoid overwhelming the backend
+ await new Promise(resolve => setTimeout(resolve, 500));
+ } catch (error) {
+ failureCount++;
+ console.error(`Skipping ${codeBlock.id} due to upload failure.`);
+ // Continue with next upload instead of failing entire script
+ }
}
}
+
+ console.log(`\n${"=".repeat(80)}`);
+ console.log(`š Upload Summary:`);
+ console.log(` ā Successful: ${successCount}`);
+ console.log(` ā Failed: ${failureCount}`);
+ console.log(` š¦ Total: ${successCount + failureCount}`);
+ console.log("=".repeat(80));
+
+ if (failureCount > 0) {
+ console.log(`\nā ļø Some uploads failed. You may need to run the script again.`);
+ process.exit(1);
+ }
}
run().catch((error) => {
diff --git a/yarn.lock b/yarn.lock
index fd612c0b5..99415f95a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8211,7 +8211,7 @@ __metadata:
dependencies:
"@adobe/gatsby-theme-aio": ^4.15.1
dotenv: 17.2.2
- fs-extra: ^11.3.2
+ fs-extra: 11.3.2
gatsby: 4.22.0
jszip: 3.10.1
markdownlint: ^0.35.0
@@ -10103,25 +10103,25 @@ __metadata:
languageName: node
linkType: hard
-"fs-extra@npm:^10.1.0":
- version: 10.1.0
- resolution: "fs-extra@npm:10.1.0"
+"fs-extra@npm:11.3.2":
+ version: 11.3.2
+ resolution: "fs-extra@npm:11.3.2"
dependencies:
graceful-fs: ^4.2.0
jsonfile: ^6.0.1
universalify: ^2.0.0
- checksum: dc94ab37096f813cc3ca12f0f1b5ad6744dfed9ed21e953d72530d103cea193c2f81584a39e9dee1bea36de5ee66805678c0dddc048e8af1427ac19c00fffc50
+ checksum: 24a7a6e09668add7f74bf6884086b860ce39c7883d94f564623d4ca5c904ff9e5e33fa6333bd3efbf3528333cdedf974e49fa0723e9debf952f0882e6553d81e
languageName: node
linkType: hard
-"fs-extra@npm:^11.3.2":
- version: 11.3.2
- resolution: "fs-extra@npm:11.3.2"
+"fs-extra@npm:^10.1.0":
+ version: 10.1.0
+ resolution: "fs-extra@npm:10.1.0"
dependencies:
graceful-fs: ^4.2.0
jsonfile: ^6.0.1
universalify: ^2.0.0
- checksum: 24a7a6e09668add7f74bf6884086b860ce39c7883d94f564623d4ca5c904ff9e5e33fa6333bd3efbf3528333cdedf974e49fa0723e9debf952f0882e6553d81e
+ checksum: dc94ab37096f813cc3ca12f0f1b5ad6744dfed9ed21e953d72530d103cea193c2f81584a39e9dee1bea36de5ee66805678c0dddc048e8af1427ac19c00fffc50
languageName: node
linkType: hard