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; } }