├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── log-publish.yml │ ├── test.yml │ ├── track-publishes.yml │ └── update-importmaps.yml ├── .gitignore ├── .hlxignore ├── .husky └── pre-commit ├── .renovaterc.json ├── .stylelintignore ├── .stylelintrc.json ├── 404.html ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── blocks ├── accordion │ ├── accordion.css │ └── accordion.js ├── author-box │ ├── author-box.css │ └── author-box.js ├── availability │ ├── availability.css │ ├── availability.js │ └── map.svg ├── card-list │ ├── README.md │ ├── card-list.css │ └── card-list.js ├── cards │ ├── cards.css │ └── cards.js ├── columns │ ├── README.md │ ├── columns.css │ └── columns.js ├── copy │ ├── copy.css │ └── copy.js ├── deprecation │ ├── deprecation.css │ └── deprecation.js ├── doc-search │ ├── doc-search.css │ └── doc-search.js ├── embed │ ├── embed.css │ └── embed.js ├── faq │ ├── faq.css │ └── faq.js ├── feature-list │ ├── feature-list.css │ └── feature-list.js ├── feed │ ├── feed.css │ └── feed.js ├── footer │ ├── README.md │ ├── footer.css │ └── footer.js ├── footnotes │ ├── footnotes.css │ └── footnotes.js ├── form │ ├── checkbox-checked.svg │ ├── checkbox.svg │ ├── form.css │ └── form.js ├── fragment │ ├── fragment.css │ └── fragment.js ├── get-started │ ├── get-started.css │ └── get-started.js ├── header │ ├── README.md │ ├── adobe-franklin-logo.svg │ ├── adobe-logo.svg │ ├── cta-placeholder.png │ ├── gnav-profile.js │ ├── gnav-search.js │ ├── header.css │ └── header.js ├── hero │ ├── README.md │ ├── hero.css │ └── hero.js ├── labs │ ├── labs.css │ └── labs.js ├── logo-wall │ ├── README.md │ ├── logo-wall.css │ └── logo-wall.js ├── marquee │ ├── README.md │ ├── marquee.css │ └── marquee.js ├── pagination │ ├── README.md │ ├── pagination.css │ └── pagination.js ├── publication-time │ ├── publication-time.css │ └── publication-time.js ├── questionnaire │ ├── questionnaire.css │ └── questionnaire.js ├── releases │ ├── releases.css │ └── releases.js ├── roi-calculator │ ├── README.md │ ├── roi-calculator.css │ └── roi-calculator.js ├── service-status │ ├── service-status.css │ └── service-status.js ├── side-navigation │ ├── README.md │ ├── side-navigation.css │ └── side-navigation.js ├── sidekick-generator │ ├── sidekick-generator.css │ └── sidekick-generator.js ├── sidekick-library │ ├── sidekick-library.css │ └── sidekick-library.js ├── svg-doctor │ ├── svg-doctor.css │ ├── svg-doctor.js │ └── w3color.js ├── table │ ├── table.css │ └── table.js ├── tabs │ ├── README.md │ ├── tabs.css │ └── tabs.js ├── testimonials │ ├── README.md │ ├── testimonials.css │ └── testimonials.js ├── venn │ ├── venn.css │ └── venn.js ├── video │ ├── video.css │ └── video.js ├── videotext │ ├── videotext.css │ └── videotext.js └── z-pattern │ ├── README.md │ ├── z-pattern.css │ └── z-pattern.js ├── default-meta-image.jpg ├── default-meta-image.png ├── docs ├── admin-preview.html └── admin.html ├── fonts ├── adobe-clean-300-italic.woff2 ├── adobe-clean-300.woff2 ├── adobe-clean-400-italic.woff2 ├── adobe-clean-400.woff2 ├── adobe-clean-700.woff2 ├── adobe-clean-900.woff2 ├── fonts.css ├── source-code-pro-300-italic.otf.woff2 ├── source-code-pro-300.otf.woff2 ├── source-code-pro-400-italic.otf.woff2 ├── source-code-pro-400.otf.woff2 ├── source-code-pro-700.otf.woff2 └── source-code-pro-900.otf.woff2 ├── head.html ├── helix-query.yaml ├── icons ├── icon-admin-html.svg ├── icon-aec.svg ├── icon-arrow.svg ├── icon-author.svg ├── icon-back-arrow.svg ├── icon-bin.svg ├── icon-browser.svg ├── icon-build-colored.svg ├── icon-build.svg ├── icon-caret-down.svg ├── icon-caret-right.svg ├── icon-caret-up.svg ├── icon-code.svg ├── icon-composability.svg ├── icon-content.svg ├── icon-developer.svg ├── icon-document.svg ├── icon-faq.svg ├── icon-fast-deliver.svg ├── icon-fast-publish.svg ├── icon-github.svg ├── icon-heart.svg ├── icon-img.svg ├── icon-lamp.svg ├── icon-linkedin.svg ├── icon-magicstick.svg ├── icon-people.svg ├── icon-person.svg ├── icon-scale.svg ├── icon-sidekick.svg ├── icon-speed.svg ├── icon-star.svg ├── icon-status-live.svg ├── icon-webvitals.svg ├── icon-work-colored.svg ├── icon-work.svg ├── icon-world-colored.svg ├── icon-world.svg └── safari.svg ├── img ├── bg-docs-1080.png ├── bg-docs-assets │ └── bg-docs.png ├── bg-docs.png ├── chrome-web-store.svg ├── chrome.svg ├── colorful-bg.jpg ├── colorful-bg.png ├── franklin-favicon.png ├── icon-admin-html.png ├── icon-aec.png ├── icon-build-colored.png ├── icon-faq.png ├── icon-sidekick.png ├── icon-status-live.png ├── icon-work-colored.png └── icon-world-colored.png ├── libs └── highlight │ ├── atom-one-dark.min.css │ ├── highlight.min.js │ └── rainbow.min.css ├── llms.txt ├── package-lock.json ├── package.json ├── plugins ├── experimentation │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .stylelintrc.json │ ├── CODE_OF_CONDUCT.md │ ├── LICENSE.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── index.js │ │ ├── preview.css │ │ ├── preview.js │ │ └── ued.js ├── performance.js └── time-decorator.js ├── scripts ├── delayed.js ├── indexing-test.js ├── lib-franklin.js ├── redirects.js └── scripts.js ├── styles ├── lazy-styles.css └── styles.css ├── tests ├── blocks │ ├── fragment │ │ ├── fragment.mock.html │ │ └── fragment.test.js │ ├── publication-time │ │ ├── publication-time.mock.html │ │ └── publication-time.test.js │ ├── sidekick-generator │ │ ├── sidekick-generator.mock.html │ │ ├── sidekick-generator.mock.json │ │ └── sidekick-generator.test.js │ └── z-pattern │ │ ├── z-pattern.mock.html │ │ └── z-pattern.test.js └── scripts │ ├── block.mock.plain.html │ ├── body.html │ ├── config.html │ ├── dummy.html │ ├── head.html │ ├── lib-franklin.test.js │ ├── media_mock.png │ ├── mock.svg │ ├── redirects.test.js │ └── test.css ├── tools ├── imports.sh ├── oversight │ ├── charts │ │ ├── barchart.js │ │ ├── chart.js │ │ ├── cwvperf.js │ │ ├── piechart.js │ │ ├── sankey.js │ │ └── skyline.js │ ├── colors.css │ ├── cwvperf.html │ ├── elements │ │ ├── conversion-tracker.js │ │ ├── daterange-picker.js │ │ ├── facetsidebar.js │ │ ├── incognito-checkbox.js │ │ ├── link-facet.js │ │ ├── list-facet.js │ │ ├── literal-facet.js │ │ ├── number-format.js │ │ ├── thumbnail-facet.js │ │ ├── url-selector.js │ │ └── vitals-facet.js │ ├── explorer.html │ ├── flow.html │ ├── list.html │ ├── loader.js │ ├── package-lock.json │ ├── package.json │ ├── rum-slicer.css │ ├── share.html │ ├── single.html │ ├── slicer.js │ ├── test │ │ └── utils.test.js │ ├── utils.js │ └── website.svg ├── patches.sh ├── rum │ ├── admin │ │ ├── orgs.css │ │ ├── orgs.html │ │ ├── orgs.js │ │ └── store.js │ ├── charts │ │ ├── chart.js │ │ └── skyline.js │ ├── colors.css │ ├── elements │ │ ├── daterange-picker.js │ │ ├── facetsidebar.js │ │ ├── file-facet.js │ │ ├── incognito-checkbox.js │ │ ├── link-facet.js │ │ ├── list-facet.js │ │ ├── literal-facet.js │ │ ├── thumbnail-facet.js │ │ ├── url-selector.js │ │ └── vitals-facet.js │ ├── explorer.html │ ├── loader.js │ ├── package-lock.json │ ├── package.json │ ├── rum-slicer.css │ ├── slicer.js │ ├── test │ │ └── utils.test.js │ ├── utils.js │ └── website.svg └── sidekick │ ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── it │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── ko-kr │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── pt-br │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── zh-cn │ │ └── messages.json │ ├── zh-tw │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_TW │ │ └── messages.json │ ├── app.css │ ├── app.js │ ├── config.schema.json │ ├── de.css │ ├── fr.css │ ├── lib │ └── process-queue.js │ ├── library │ ├── events │ │ └── events.js │ ├── focus-visible.js │ ├── focus-visible.js.map │ ├── index.js │ ├── index.js.map │ ├── locales │ │ └── en │ │ │ └── messages.json │ ├── overlay.js │ ├── overlay.js.map │ ├── plugins │ │ └── blocks │ │ │ ├── blocks.css │ │ │ ├── blocks.js │ │ │ └── utils.js │ └── utils │ │ ├── dom.js │ │ └── rum.js │ ├── module.js │ ├── rum.js │ └── view │ ├── json.js │ └── json │ ├── json.css │ ├── json.html │ └── json.js ├── utils ├── console.js ├── env.js ├── helpers.js ├── property.js └── tag.js └── web-components └── relative-time.js /.eslintignore: -------------------------------------------------------------------------------- 1 | tools/sidekick 2 | libs 3 | plugins/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'airbnb-base', 4 | env: { 5 | browser: true, 6 | }, 7 | parser: '@babel/eslint-parser', 8 | parserOptions: { 9 | allowImportExportEverywhere: true, 10 | sourceType: 'module', 11 | requireConfigFile: false, 12 | }, 13 | rules: { 14 | // allow reassigning param 15 | 'no-param-reassign': [2, { props: false }], 16 | 'linebreak-style': ['error', 'unix'], 17 | 'import/extensions': ['error', { 18 | js: 'always', 19 | }], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/log-publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | repository_dispatch: 3 | types: 4 | - resource-published-native 5 | jobs: 6 | print: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: | 10 | echo "Status: ${{ github.event.client_payload.status }}" 11 | echo "Path: ${{ github.event.client_payload.path }}" 12 | slack: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Format path 16 | id: format 17 | uses: frabert/replace-string-action@v2 18 | with: 19 | pattern: '\.md' 20 | string: ${{ github.event.client_payload.path }} 21 | replace-with: '' 22 | - name: Notify Slack (JSON) 23 | if: ${{ endsWith(github.event.client_payload.path, '.json') }} 24 | uses: slackapi/slack-github-action@v2.1.0 25 | env: 26 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 27 | with: 28 | # For posting a rich message using Block Kit 29 | payload: | 30 | { 31 | "text": "Just published: `${{ github.event.client_payload.path }}`" 32 | } 33 | webhook-type: webhook-trigger 34 | webhook: ${{ secrets.SLACK_WEBHOOK_URL }} 35 | - name: Notify Slack (Markdown) 36 | if: ${{ endsWith(github.event.client_payload.path, '.md') }} 37 | uses: slackapi/slack-github-action@v2.1.0 38 | env: 39 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 40 | with: 41 | # For posting a rich message using Block Kit 42 | payload: | 43 | { 44 | "text": "Just published: https://www.aem.live${{ steps.format.outputs.replaced }}" 45 | } 46 | webhook-type: webhook-trigger 47 | webhook: ${{ secrets.SLACK_WEBHOOK_URL }} 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and linting 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Use Node.js 20 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | 14 | - name: Install XVFB 15 | run: sudo apt-get install xvfb 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Run the tests 21 | run: xvfb-run -a npm test 22 | 23 | - name: run the linters 24 | run: npm run lint 25 | -------------------------------------------------------------------------------- /.github/workflows/update-importmaps.yml: -------------------------------------------------------------------------------- 1 | name: Update import maps 2 | on: 3 | pull_request: 4 | branches: 5 | - 'main' 6 | 7 | jobs: 8 | build: 9 | if: startsWith(github.head_ref, 'r-') 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | fetch-depth: 0 19 | ref: ${{ github.head_ref }} 20 | - name: Configure Git 21 | run: | 22 | git config user.name 'github-actions[bot]' 23 | git config user.email 'github-actions[bot]@users.noreply.github.com' 24 | - name: Install dependencies 25 | run: npm install 26 | - name: Update import maps 27 | run: tools/imports.sh 28 | - name: Commit changes 29 | run: | 30 | if git diff --quiet; then 31 | echo "No changes to commit" 32 | else 33 | git add -A 34 | git commit -m "chore(oversight): update import maps" 35 | git push origin HEAD:${{ github.head_ref }} 36 | fi 37 | - name: Update PR comment to include aem.live test URL 38 | env: 39 | GH_TOKEN: ${{ github.token }} 40 | run: | 41 | OLD_BODY=$(gh pr view --json body --jq ".body") 42 | BRANCH_NAME=$(echo ${{ github.head_ref }}) 43 | TEST_URL="https://$BRANCH_NAME--helix-website--adobe.aem.live/tools/oversight/explorer.html?domain=emigrationbrewing.com&view=month&domainkey=open" 44 | printf -v NEW_BODY "%s\n\nTest at: %s" "$OLD_BODY" "$TEST_URL" 45 | gh pr edit ${{ github.event.number }} --body "$NEW_BODY" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # OS Files 19 | .DS_Store 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | lcov.info 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | *.crt 110 | *.key 111 | .hlx 112 | 113 | # Test results 114 | junit.xml 115 | # jetbrains files 116 | .idea/ 117 | -------------------------------------------------------------------------------- /.hlxignore: -------------------------------------------------------------------------------- 1 | .* 2 | CODE_OF_CONDUCT.md 3 | CONTRIBUTING.md 4 | LICENSE 5 | package-lock.json 6 | package.json 7 | README.md 8 | test/ 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "enabled": true, 4 | "baseBranches": ["main"], 5 | "branchNameStrict": true, 6 | "branchName": "r_{{{packageName}}}_{{version}}", 7 | "packageRules": [{ 8 | "matchDepTypes": ["devDependencies"], 9 | "labels": ["ignore-psi-check"], 10 | "automerge": true 11 | },{ 12 | "matchManagers": ["github-actions"], 13 | "labels": ["ignore-psi-check"], 14 | "automerge": true 15 | },{ 16 | "matchDepNames": ["@adobe/rum-distiller"], 17 | "labels": ["rum-distiller"], 18 | "automerge": false, 19 | "automergeType": "pr", 20 | "fileMatch": [ 21 | "package.json" 22 | ] 23 | }, 24 | { 25 | "matchDepNames": ["chartjs", "chartjs-adapter-luxon"], 26 | "enabled": false 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /libs/highlight/*.css 2 | /tools/sidekick 3 | plugins/ -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"] 3 | } -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | Page not found 11 | 15 | 16 | 17 | 18 | 34 | 40 | 41 | 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 | 404 65 | 66 |

Page Not Found

67 |

68 | Go home 69 |

70 |
71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Helix Website 2 | [www.aem.live](https://www.aem.live) 3 | 4 | [![codecov](https://codecov.io/gh/adobe/helix-website/branch/main/graph/badge.svg?token=If90y6KMqx)](https://codecov.io/gh/adobe/helix-website) 5 | ## Introduction 6 | This repo is what powers the helix website. 7 | 8 | ## Developing 9 | Install the Helix CLI: 10 | 11 | sudo npm install -g @adobe/aem-cli 12 | 13 | Run it in this repo: 14 | 15 | aem up 16 | 17 | ## Testing 18 | ### Install dependencies: 19 | 20 | npm install 21 | ### Run tests: 22 | 23 | npm test 24 | ### Watch tests: 25 | 26 | npm test:watch 27 | 28 | This will run through all your tests, but then only run against any changed tests. 29 | 30 | 31 | -------------------------------------------------------------------------------- /blocks/accordion/accordion.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | background: #efefef; 3 | border: 1px solid #ddd; 4 | border-radius: 6px; 5 | } 6 | 7 | .item-title { 8 | cursor: pointer; 9 | padding: 12px; 10 | border-bottom: 1px solid #ddd; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | } 15 | 16 | .item-title::after { 17 | width: 10px; 18 | height: 10px; 19 | border-bottom: 2px solid #a7a7a7; 20 | border-right: 2px solid #a7a7a7; 21 | transform-origin: 75% 75%; 22 | transform: rotate(-45deg) translateY(-50%); 23 | transition: transform 0.1s ease; 24 | content: ""; 25 | } 26 | 27 | .item-title.open { 28 | background: #ddd; 29 | } 30 | 31 | .item-title.open::after { 32 | transform: rotate(-135deg) translateX(50%); 33 | } 34 | 35 | .item-content { 36 | display: none; 37 | grid-template-columns: 1fr 1fr; 38 | gap: 12px; 39 | padding: 12px; 40 | border-bottom: 1px solid #ddd; 41 | } 42 | 43 | .item-title.open + .item-content { 44 | display: grid; 45 | } 46 | -------------------------------------------------------------------------------- /blocks/accordion/accordion.js: -------------------------------------------------------------------------------- 1 | export default function init(el) { 2 | const titles = el.querySelectorAll(':scope > div:nth-child(odd)'); 3 | titles.forEach((title) => { 4 | // Add a class to the title container 5 | title.classList.add('item-title'); 6 | // Remove the empty div 7 | title.querySelector(':scope > div:last-of-type').remove(); 8 | // Add a class to the content 9 | title.nextElementSibling.classList.add('item-content'); 10 | // Add a click handler to open the content 11 | title.addEventListener('click', () => { 12 | title.classList.toggle('open'); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /blocks/card-list/README.md: -------------------------------------------------------------------------------- 1 | ## Card List 2 | 3 | Notes: This block closely resembles the layout of the cards block in the helix boilerplate. 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | input link only in doc & render `cards` block based on content | 9 | | .image-card-listing | decorate the card list using content including image & text | 10 | 11 | 12 | #### Example: 13 | 14 | See Live output (Link TBD) 15 | 16 | #### Content Structure: 17 | 18 | [See Content in Document](https://docs.google.com/document/d/1k2UqwSl9x2QcDWRR2juTLE60EX963N82sWz50ThtFsA/edit) 19 | 20 | #### Code: 21 | Card List is styled in the block CSS code. 22 | 23 | There is Javascript code for decoration purposes and apply any link used on the card to the entire card itself. 24 | 25 | [Decoration Code](card-list.js) 26 | 27 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 28 | 29 | [Styling Code](card-list.css) -------------------------------------------------------------------------------- /blocks/card-list/card-list.css: -------------------------------------------------------------------------------- 1 | .card-list > ul { 2 | list-style: none; 3 | margin: 0; 4 | padding: 24px 0; 5 | display: grid; 6 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 7 | grid-gap: 16px; 8 | } 9 | 10 | .card-list > ul > li { 11 | border: 1px solid var(--highlight-background-color); 12 | background-color: var(--background-color); 13 | } 14 | 15 | .card-list > ul > li > a { 16 | text-decoration: none; 17 | display: block; 18 | } 19 | 20 | .card-list .cards-card-body { 21 | font-size: var(--body-font-size-s); 22 | color: var(--color-gray-100); 23 | } 24 | 25 | .card-list .cards-card-image { 26 | width: 100%; 27 | aspect-ratio: 4 / 3; 28 | line-height: 0; 29 | border-radius: var(--image-border-radius-l); 30 | overflow: hidden; 31 | 32 | /* fix for safari overflow with border-radius not working */ 33 | isolation: isolate; 34 | } 35 | 36 | .card-list.cards-card-body > *:first-child { 37 | margin-top: 0; 38 | } 39 | 40 | .card-list > ul > li img { 41 | width: 100%; 42 | height: 100%; 43 | object-fit: cover; 44 | transition: var(--transition-ease-in-out); 45 | } 46 | 47 | .card-list .card-wrapper:hover img { 48 | transform: scale(1.07); 49 | } 50 | 51 | .card-list .cards-card-body h3 { 52 | font-size: var(--type-heading-m-size); 53 | line-height: var(--type-heading-m-lh); 54 | margin-top: var(--spacing-s); 55 | margin-bottom: var(--spacing-xs); 56 | } 57 | 58 | @media screen and (width >= 768px) { 59 | .card-list .cards-card-body h3 { 60 | font-size: var(--type-heading-l-size); 61 | line-height: var(--type-heading-l-lh); 62 | margin-top: var(--spacing-m); 63 | } 64 | } 65 | 66 | @media screen and (width >= 900px) { 67 | .card-list > ul { 68 | grid-gap: var(--spacing-s); 69 | } 70 | } 71 | 72 | /* block party card list style */ 73 | .card-list-wrapper .cards .cards-card-details p.description:not(.noextra) { 74 | cursor: pointer; 75 | } 76 | 77 | .card-list-wrapper .cards .cards-card-details p.description:not([aria-expanded="true"], .noextra)::after { 78 | content: "..."; 79 | } 80 | 81 | .card-list-wrapper .cards .cards-card-details span.extra:not([aria-expanded="true"]) { 82 | display: none; 83 | } 84 | 85 | .card-list-wrapper .cards .cards-card-details span.extra[aria-expanded="true"] { 86 | display: contents; 87 | color: inherit; 88 | } 89 | 90 | .card-list-wrapper .cards .cards-card-details { 91 | overflow-x: hidden; 92 | } 93 | 94 | /* card cateogry filtering */ 95 | .cards-wrapper .category-filter-wrapper { 96 | display: grid; 97 | grid-template-columns: auto 1fr; 98 | gap: 1rem; 99 | justify-content: start; 100 | align-items: center; 101 | } 102 | 103 | .cards-wrapper .category-filter-wrapper select { 104 | font-size: var(--type-body-l-size); 105 | max-width: 20rem; 106 | } 107 | -------------------------------------------------------------------------------- /blocks/cards/cards.js: -------------------------------------------------------------------------------- 1 | export default function decorate(block) { 2 | const observer = new IntersectionObserver((entries) => { 3 | entries.forEach((entry) => { 4 | if (entry.isIntersecting) { 5 | entry.target.classList.add('enter'); 6 | } 7 | }); 8 | }); 9 | 10 | const classes = ['one', 'two', 'three', 'four', 'five']; 11 | const row = block.children[0]; 12 | if (row) { 13 | if (classes[row.children.length - 1]) block.classList.add(classes[row.children.length - 1]); 14 | } 15 | block.querySelectorAll(':scope > div > div').forEach((cell) => { 16 | if (cell.firstChild) { 17 | const details = document.createElement('div'); 18 | details.classList.add('cards-card-details'); 19 | cell.classList.add('cards-card'); 20 | while (cell.firstChild) details.append(cell.firstChild); 21 | const picture = details.querySelector('picture'); 22 | if (picture) { 23 | cell.prepend(picture); 24 | } else if (details.querySelector('h3')) { 25 | cell.classList.add('cards-card-highlight'); 26 | } 27 | // highlight styling 28 | const links = details.querySelectorAll('a'); 29 | links.forEach((link) => { 30 | link.className = 'link-highlight-colorful-effect-hover-wrapper'; 31 | link.innerHTML = `${link.textContent}`; 32 | }); 33 | cell.append(details); 34 | observer.observe(cell); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /blocks/columns/README.md: -------------------------------------------------------------------------------- 1 | ## Columns 2 | 3 | Notes: Builds upon the Columns block in the Helix Boilerplate project. See details [here](https://www.hlx.live/developer/block-collection/columns). 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | two, three, four | The number of columns expected | 9 | | icon-sm | Layout for a small icon (max height 50px) | 10 | | tall | Layout for tall columns with an image in layout | 11 | | center | center the text & .icon in the column cell | 12 | | colored-icon | come from global class, add colored icon to element with `.icon` class (svg) 13 | 14 | 15 | -------------------------------------------------------------------------------- /blocks/copy/copy.css: -------------------------------------------------------------------------------- 1 | .copy { 2 | background: var(--highlight-gradient); 3 | border-radius: 25px; 4 | padding: 20px 30px; 5 | color: white; 6 | margin: 64px 0; 7 | } 8 | 9 | .copy:empty { 10 | display: none; 11 | } 12 | 13 | .copy p { 14 | margin: 0; 15 | line-height: 1em; 16 | font-size: var(--type-body-xxs-size); 17 | } 18 | 19 | .copy > div { 20 | display: flex; 21 | flex-direction: column; 22 | margin: 30px 0; 23 | } 24 | 25 | .copy > div > span { 26 | font-size: var(--type-body-xs-size); 27 | } 28 | 29 | .copy > div > input { 30 | border-radius: 5px; 31 | padding: 5px 10px; 32 | border: 0; 33 | margin: 0 0 10px; 34 | } 35 | 36 | .copy > div > button { 37 | color: white; 38 | border-radius: 20px; 39 | background-color: unset; 40 | border: 2px solid white; 41 | padding: 5px 20px; 42 | } 43 | 44 | @media (width >= 900px) { 45 | .copy > div { 46 | flex-direction: unset; 47 | justify-content: stretch; 48 | margin: 30px 0; 49 | } 50 | 51 | .copy > div > span { 52 | flex: 1 1 200px; 53 | } 54 | 55 | .copy > div > input { 56 | flex: 1 1 100%; 57 | margin: unset; 58 | } 59 | 60 | .copy > div > button { 61 | margin-left: 40px; 62 | flex: 1 1 60px; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /blocks/copy/copy.js: -------------------------------------------------------------------------------- 1 | import { readBlockConfig } from '../../scripts/lib-franklin.js'; 2 | 3 | export default function decorate(block) { 4 | const config = readBlockConfig(block); 5 | const usp = new URLSearchParams(window.location.search); 6 | const keys = Object.keys(config); 7 | block.textContent = ''; 8 | if (keys.every((key) => usp.get(key))) { 9 | block.innerHTML = '

For your conveniece find the values for this screen below to copy/paste

'; 10 | keys.forEach((key) => { 11 | const div = document.createElement('div'); 12 | div.innerHTML = `${config[key]}`; 13 | const input = document.createElement('input'); 14 | input.value = usp.get(key); 15 | div.append(input); 16 | const button = document.createElement('button'); 17 | button.textContent = 'copy'; 18 | button.addEventListener('click', () => { 19 | input.select(); 20 | document.execCommand('copy'); 21 | }); 22 | div.append(button); 23 | block.append(div); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blocks/deprecation/deprecation.css: -------------------------------------------------------------------------------- 1 | .deprecation { 2 | padding: 10px 16px; 3 | margin-bottom: 16px; 4 | background-color: #d31510; 5 | color: #fff; 6 | border-radius: var(--image-border-radius-l); 7 | font-size: var(--type-body-s-size); 8 | line-height: var(--type-body-s-lh); 9 | } 10 | 11 | .deprecation .header { 12 | font-weight: 700; 13 | font-size: var(--type-body-m-size); 14 | text-transform: uppercase; 15 | } 16 | 17 | .deprecation > div > div > p, 18 | .deprecation a:any-link, 19 | .deprecation code { 20 | color: #fff; 21 | } 22 | 23 | .deprecation code { 24 | background-color: rgb(255 255 255 / 20%); 25 | border: none; 26 | } 27 | -------------------------------------------------------------------------------- /blocks/deprecation/deprecation.js: -------------------------------------------------------------------------------- 1 | export default function decorate(block) { 2 | const h3 = block.querySelector('h3'); 3 | let title = ''; 4 | if (h3) { 5 | title = h3.textContent; 6 | h3.remove(); 7 | } else { 8 | title = 'Deprecation notice'; 9 | } 10 | const header = document.createElement('div'); 11 | header.className = 'header'; 12 | header.textContent = title; 13 | block.prepend(header); 14 | } 15 | -------------------------------------------------------------------------------- /blocks/embed/embed.css: -------------------------------------------------------------------------------- 1 | .gist .gist-file.gist-file { 2 | margin: 12px 0; 3 | } 4 | 5 | .youtube-wrapper { 6 | width: 100%; 7 | height: 0; 8 | position: relative; 9 | padding-bottom: 56.25%; 10 | } 11 | 12 | .gist .gist-meta a { 13 | font-size: 12px; 14 | } 15 | 16 | .embed.block.google-form > div > div { 17 | min-height: 1700px; 18 | } -------------------------------------------------------------------------------- /blocks/embed/embed.js: -------------------------------------------------------------------------------- 1 | import { loadCSS } from '../../scripts/lib-franklin.js'; 2 | 3 | const jsonpGist = (url, callback) => { 4 | // Setup a unique name that cane be called & destroyed 5 | const callbackName = `jsonp_${Math.round(100000 * Math.random())}`; 6 | 7 | // Create the script tag 8 | const script = document.createElement('script'); 9 | script.src = `${url}${(url.indexOf('?') >= 0 ? '&' : '?')}callback=${callbackName}`; 10 | 11 | // Define the function that the script will call 12 | window[callbackName] = (data) => { 13 | delete window[callbackName]; 14 | document.body.removeChild(script); 15 | callback(data); 16 | }; 17 | 18 | // Append to the document 19 | document.body.appendChild(script); 20 | }; 21 | 22 | const gist = (element) => { 23 | const { href } = element; 24 | const url = href.slice(-2) === 'js' ? `${href}on` : `${href}.json`; 25 | 26 | jsonpGist(url, (data) => { 27 | loadCSS(data.stylesheet); 28 | element.insertAdjacentHTML('afterend', data.div); 29 | element.remove(); 30 | }); 31 | }; 32 | 33 | const youtube = (element) => { 34 | const url = new URL(element.href); 35 | const vid = url.searchParams.get('v'); 36 | const html = `
37 | 38 |
`; 39 | element.parentElement.insertAdjacentHTML('afterend', html); 40 | element.parentElement.remove(); 41 | }; 42 | 43 | const iframe = (element) => { 44 | const url = new URL(element.href); 45 | const html = `
46 | 55 |
`; 56 | element.parentElement.insertAdjacentHTML('afterend', html); 57 | element.parentElement.remove(); 58 | }; 59 | 60 | const initObserver = (callback) => new IntersectionObserver((entries) => { 61 | entries.forEach((entry) => { 62 | if (entry.isIntersecting) { 63 | callback(entry.target); 64 | } 65 | }); 66 | }, { root: null, rootMargin: '200px', threshold: 0 }); 67 | 68 | export default function decorate(block) { 69 | const a = block.querySelector('a'); 70 | const { hostname } = new URL(a.href); 71 | if (hostname.includes('youtu')) { 72 | initObserver(() => { 73 | youtube(a); 74 | }).observe(a); 75 | } else if (hostname.includes('gist')) { 76 | initObserver(() => { 77 | gist(a); 78 | }).observe(a); 79 | } else { 80 | initObserver(() => { 81 | iframe(a); 82 | }).observe(a); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /blocks/faq/faq.css: -------------------------------------------------------------------------------- 1 | main .faq dt { 2 | margin-top: var(--spacing-m); 3 | font-weight: 700; 4 | margin-bottom: var(--spacing-xxs); 5 | scroll-margin: calc(var(--nav-height) + var(--breadcrumb-height)); 6 | } 7 | 8 | main .faq dt > a { 9 | font-size: var(--type-heading-s-size); 10 | line-height: var(--type-heading-s-lh); 11 | } 12 | 13 | main .faq .category { 14 | font-size: var(--type-body-xs-size); 15 | font-weight: 700; 16 | line-height: var(--type-body-xs-lh); 17 | margin: 0 var(--spacing-xxxs); 18 | padding: var(--spacing-xxxs) var(--spacing-xxs); 19 | border-radius: 12px; 20 | background-color: var(--bg-color-grey-2); 21 | color: currentcolor; 22 | } 23 | 24 | main .faq .category.style-1 { 25 | background-color: var(--brand-color-dark-purple); 26 | color: var(--color-white); 27 | } 28 | 29 | main .faq .category.style-2 { 30 | background-color: var(--color-accent-pink-content); 31 | color: var(--color-white); 32 | } 33 | 34 | main .faq .category.style-3 { 35 | background-color: var(--color-accent-pink-bg); 36 | } 37 | 38 | main .faq .category.style-4 { 39 | background-color: var(--color-accent-lightgreen-bg); 40 | } 41 | 42 | main .faq dd { 43 | font-size: var(--type-body-s-size); 44 | line-height: var(--type-body-s-lh); 45 | } 46 | 47 | body.guides-template main .faq dt a.anchor-link:any-link::before { 48 | top: 0; 49 | } 50 | 51 | main .faq .filter-controls { 52 | display: flex; 53 | font-size: var(--type-body-xs-size); 54 | background-color: var(--bg-color-lightgrey); 55 | padding: var(--spacing-xs) var(--spacing-s); 56 | border: solid 1px var(--bg-color-grey-2); 57 | border-radius: var(--image-border-radius-l); 58 | } 59 | 60 | main .faq .filter-control label { 61 | cursor: pointer; 62 | user-select: none; 63 | } 64 | 65 | main .faq .filter-control input[type="checkbox"] { 66 | display: none; 67 | } 68 | 69 | main .faq .filter-control input[type="checkbox"]:not(:checked) + label { 70 | background-color: var(--color-bg); 71 | color: currentcolor; 72 | } 73 | 74 | main .faq .hidden-by-filter { 75 | display: none; 76 | } 77 | -------------------------------------------------------------------------------- /blocks/feature-list/feature-list.css: -------------------------------------------------------------------------------- 1 | .feature-list { 2 | background: #f5f5f5; 3 | padding: var(--spacing-xxl) 0; 4 | color: rgb(63 63 63); 5 | } 6 | 7 | .feature-list.contained > div { 8 | max-width: var(--grid-mobile-container-width); 9 | margin: 0 auto; 10 | } 11 | 12 | .feature-list > div:last-of-type { 13 | display: grid; 14 | grid-template-columns: 1fr; 15 | grid-template-areas: 16 | "image" 17 | "copy"; 18 | grid-gap: var(--spacing-s); 19 | align-items: center; 20 | } 21 | 22 | .feature-list h2 { 23 | text-align: center; 24 | margin-bottom: var(--spacing-s); 25 | } 26 | 27 | .feature-list > div:last-of-type > div:first-of-type { 28 | grid-area: copy; 29 | } 30 | 31 | .feature-list > div:last-of-type > div:last-of-type { 32 | grid-area: image; 33 | } 34 | 35 | .feature-list ul { 36 | list-style: none; 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | .feature-list li { 42 | padding-bottom: var(--spacing-xs); 43 | } 44 | 45 | .feature-list li:last-of-type { 46 | padding-bottom: 0; 47 | } 48 | 49 | .feature-list img { 50 | max-width: 100%; 51 | } 52 | 53 | @media screen and (width >= 1200px) { 54 | .feature-list > div:last-of-type { 55 | grid-template-columns: 1fr 1fr; 56 | grid-template-areas: "copy image"; 57 | grid-gap: var(--spacing-xxl); 58 | } 59 | } 60 | 61 | @media screen and (width >= 1440px) { 62 | .feature-list.contained > div { 63 | max-width: var(--grid-desktop-container-width); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /blocks/feature-list/feature-list.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/blocks/feature-list/feature-list.js -------------------------------------------------------------------------------- /blocks/footer/README.md: -------------------------------------------------------------------------------- 1 | ## Footer 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default Footer | 9 | 10 | #### Example: 11 | 12 | See Live output: 13 | https://redesign-new-footer--helix-website--adobe.hlx.page/drafts/redesign/new-home 14 | 15 | #### Content Structure: 16 | 17 | See Content in Document (Link): 18 | https://docs.google.com/document/d/1Qr26pZVperklcaRuErsE1qZ1ebl1lL6tNodMN28SdEU/edit 19 | 20 | - the new footer styles are now shown based on the path in the url. When migrate, we'll need to update the code and release the logic to global 21 | - the styles are handled separately in mobile vs desktop, where there's one div for mobile version, and another div for desktop version. 22 | 23 | top section (wrapped by
in document): 24 | - h3 -> heading link 25 | - ul -> sublinks under the heading link 26 | - link in bolded -> cta button, which only shows on desktop only 27 | 28 | bottom section (wrapped by
in document: 29 | - p -> copyright text, separate lines on mobile, same line on desktop 30 | 31 | Animation styles 32 | - from global class: `.link-underline-effect` 33 | 34 | [Decoration Code](footer.js) 35 | 36 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 37 | 38 | [Styling Code](footer.css) -------------------------------------------------------------------------------- /blocks/footnotes/footnotes.css: -------------------------------------------------------------------------------- 1 | .footnotes { 2 | margin: var(--spacing-ml) 0; 3 | } 4 | 5 | .footnotes > div { 6 | display: flex; 7 | } 8 | 9 | .footnotes > div, 10 | .footnotes .footnote-text > a { 11 | font-size: var(--type-body-xs-size); 12 | line-height: var(--type-body-xs-lh); 13 | } 14 | 15 | .footnotes .footnote-num { 16 | text-align: right; 17 | padding-right: 20px; 18 | } 19 | -------------------------------------------------------------------------------- /blocks/footnotes/footnotes.js: -------------------------------------------------------------------------------- 1 | export default async function decorate($block) { 2 | // enumerate all footnote references 3 | [...document.querySelectorAll('main sup')] 4 | .forEach((sup, i) => { 5 | if (sup.textContent === '#') { 6 | sup.textContent = i + 1; 7 | } 8 | }); 9 | 10 | // enumerate footnotes 11 | document.querySelector('.section.content').append($block); 12 | [...$block.children].forEach(($footnote, i) => { 13 | $footnote.firstElementChild.classList.add('footnote-text'); 14 | const $num = document.createElement('div'); 15 | $num.classList.add('footnote-num'); 16 | $num.innerHTML = `${i + 1}`; 17 | $footnote.prepend($num); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /blocks/form/checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /blocks/form/checkbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blocks/fragment/fragment.css: -------------------------------------------------------------------------------- 1 | /* nothing here */ 2 | -------------------------------------------------------------------------------- /blocks/fragment/fragment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Fragment Block 3 | * Include content from one Helix page in another. 4 | * https://www.hlx.live/developer/block-collection/fragment 5 | */ 6 | 7 | import { decorateMain } from '../../scripts/scripts.js'; 8 | import { loadBlocks } from '../../scripts/lib-franklin.js'; 9 | 10 | /** 11 | * Loads a fragment. 12 | * @param {string} path The path to the fragment 13 | * @returns {HTMLElement} The root element of the fragment 14 | */ 15 | async function loadFragment(path) { 16 | if (path && path.startsWith('/')) { 17 | const resp = await fetch(`${path}.plain.html`); 18 | if (resp.ok) { 19 | const main = document.createElement('main'); 20 | main.innerHTML = await resp.text(); 21 | decorateMain(main); 22 | await loadBlocks(main); 23 | return main; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | export default async function decorate(block) { 30 | const link = block.querySelector('a'); 31 | const path = link ? link.getAttribute('href') : block.textContent.trim(); 32 | const fragment = await loadFragment(path); 33 | if (fragment) { 34 | const fragmentSection = fragment.querySelector(':scope .section'); 35 | if (fragmentSection) { 36 | block.closest('.fragment-wrapper').replaceWith(...fragmentSection.childNodes); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /blocks/get-started/get-started.css: -------------------------------------------------------------------------------- 1 | .get-started { 2 | background: linear-gradient(45deg, rgb(0 0 0 / 100%) 0%, rgb(38 18 57 / 100%) 42%, rgb(154 26 10 / 100%) 100%); 3 | color: white; 4 | padding: var(--spacing-xxl) 0; 5 | text-align: center; 6 | } 7 | 8 | .get-started > div { 9 | max-width: 83.4%; 10 | margin: 0 auto; 11 | } 12 | 13 | .get-started > div + div > div:not(:last-of-type) { 14 | margin: var(--spacing-l) 0; 15 | } 16 | 17 | .get-started h2 { 18 | margin-bottom: var(--spacing-l); 19 | } 20 | 21 | .get-started h3 { 22 | margin-bottom: var(--spacing-s); 23 | } 24 | 25 | .get-started p { 26 | margin: 0 0 6px; 27 | } 28 | 29 | .get-started a { 30 | color: white; 31 | } 32 | 33 | .get-started img, 34 | .get-started svg { 35 | max-width: 128px; 36 | } 37 | 38 | @media (width >= 900px) { 39 | .get-started > div:last-of-type { 40 | display: grid; 41 | grid-template-columns: 1fr 1fr; 42 | } 43 | 44 | .get-started > div + div > div:not(:last-of-type) { 45 | margin: 0; 46 | } 47 | } 48 | 49 | @media (width >= 1440px) { 50 | .get-started > div { 51 | max-width: 1200px; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blocks/get-started/get-started.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/blocks/get-started/get-started.js -------------------------------------------------------------------------------- /blocks/header/README.md: -------------------------------------------------------------------------------- 1 | # GlobalNav 2 | ## Content Contract 3 | ``` 4 | | Gnav Brand | 5 | | ------------------------------------ | 6 | | [Adobe Blog](https://blog.adobe.com) | 7 | --- 8 | ## [News](https://www.adobe.com/news) 9 | --- 10 | ## Basics 11 | 12 | * Nav Item 13 | * Nav Item 14 | * Nav Item 15 | --- 16 | ## Insights 17 | 18 | * Nav Item 19 | * Nav Item 20 | * Nav Item 21 | 22 | | Gnav Promo | 23 | | ------------- | 24 | | Awesomeness | 25 | --- 26 | | Search | 27 | | ------------- | 28 | | Search | 29 | 30 | | Profile | 31 | | ------------- | 32 | | Sign In | 33 | | ------------- | 34 | | View Account | 35 | | Sign Out | 36 | 37 | | Adobe Logo | 38 | | ------------------------------ | 39 | | [Adobe](https://www.adobe.com) | 40 | 41 | ``` 42 | ### Optional Content 43 | * Nav Items 44 | * Gnav Promo 45 | * Search 46 | * Profile 47 | 48 | ## Features 49 | * Allows for full IMS integration 50 | * Support small, medium, large menu sizing 51 | * UX generally aligns with [Consonant Navigation](https://consonant.adobe.com/1975e5ba1/p/48adfd-navigation). 52 | 53 | ## Behaviors 54 | ### Mobile (0-1199px) 55 | 1. Hamburger will toggle curtain, search, and nav menu. 56 | 2. Scroll is permitted. 57 | 3. Re-size will hide curtain, search, and nav menu. 58 | 59 | ### Desktop (1200-∞) 60 | 1. Nav menus will close on scroll and on document click. 61 | 1. Only one nav menu can be open at a time. 62 | 1. Opening search will close any open nav menu. 63 | 1. Opening a menu will close the search bar. 64 | 1. Search will not close on scroll. -------------------------------------------------------------------------------- /blocks/header/adobe-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /blocks/header/cta-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/blocks/header/cta-placeholder.png -------------------------------------------------------------------------------- /blocks/hero/README.md: -------------------------------------------------------------------------------- 1 | ## Hero 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default Home Hero: text in center, image at bottom, colorful checkerboard pattern background | 9 | | side-by-side | 50% detail, 50% image in same row | 10 | | square-image | image will become 1:1 in aspect ratio | 11 | | mutiple-cta | decorate multiple cta buttons with black border styles (ul a) | 12 | | page-not-found | page not found ui | 13 | 14 | #### Example: 15 | 16 | See Live output (Link): 17 | https://redesign-hero-and-logo-wall--helix-website--adobe.hlx.page/drafts/redesign/blocks/hero 18 | TBC: https://website-redesign--helix-website--adobe.hlx.page/drafts/redesign/blocks/hero 19 | 20 | #### Content Structure: 21 | 22 | See Content in Document (Link): 23 | https://docs.google.com/document/d/1LfT3loAme82XIWhWAUOaWK1aPP42XzZ1GVjwHYtk0Zc/edit# 24 | 25 | #### Code: 26 | - Background Image: display image from 1st table row as cover. If no image provided, it will fallback to default grey checkerboard background 27 | - Text Content from 2nd table row 28 | - h1 -> heading 29 | - a -> cta-button 30 | - p -> description 31 | - ul list with multiple elements -> cta button list (need to use with .multple-cta on block) 32 | - Content Image from 3rd table row 33 | 34 | [Decoration Code](hero.js) 35 | 36 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 37 | 38 | [Styling Code](hero.css) -------------------------------------------------------------------------------- /blocks/hero/hero.js: -------------------------------------------------------------------------------- 1 | import createTag from '../../utils/tag.js'; 2 | 3 | export default function decorate(block) { 4 | const backgroundImageWrapper = block.children[0].querySelector('div'); 5 | const backgroundImage = backgroundImageWrapper.querySelector('img'); 6 | backgroundImageWrapper.setAttribute('class', 'background-image-wrapper'); 7 | 8 | const innerContent = block.children[1].querySelector('div'); 9 | innerContent.setAttribute('class', 'inner-content'); 10 | 11 | if (innerContent) { 12 | const eyebrow = innerContent.querySelector('h3'); 13 | if (eyebrow && eyebrow.querySelector('.icon')) { 14 | eyebrow.classList.add('icon-eyebrow'); 15 | } 16 | } 17 | 18 | if (!block.classList.contains('multiple-cta')) { 19 | const ctaButton = innerContent.querySelector('a'); 20 | if (ctaButton) { 21 | ctaButton.classList.add('button', 'large'); 22 | ctaButton.closest('p').replaceWith(ctaButton); 23 | } 24 | } else { 25 | const ctaButtonList = innerContent.querySelector('ul'); 26 | ctaButtonList.classList.add('cta-button-list'); 27 | const ctaButtons = ctaButtonList.querySelectorAll('ul a'); 28 | ctaButtons.forEach((btn) => { 29 | btn.classList.add('button', 'large', 'black-border'); 30 | }); 31 | } 32 | 33 | const imageWrapper = block.children[2].querySelector('div'); 34 | imageWrapper.setAttribute('class', 'image-wrapper'); 35 | 36 | block.innerHTML = ''; 37 | if (backgroundImage) { 38 | backgroundImageWrapper.classList.add('with-bg-img'); 39 | block.style.background = 'transparent'; 40 | block.append(backgroundImageWrapper); 41 | } else { 42 | block.classList.add('colorful-bg'); 43 | const checkerBoardGuide = createTag('div', { 44 | class: 'checker-board-guide', 45 | }); 46 | block.append(checkerBoardGuide); 47 | } 48 | 49 | if (block.classList.contains('side-by-side')) { 50 | // for hero.side-by-side 51 | const containedWrapper = createTag('div', { 52 | class: 'contained-wrapper', 53 | }); 54 | containedWrapper.append(innerContent, imageWrapper); 55 | block.append(containedWrapper); 56 | } else { 57 | // default hero 58 | block.append(innerContent, imageWrapper); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /blocks/labs/labs.css: -------------------------------------------------------------------------------- 1 | .labs { 2 | padding: 10px 16px; 3 | margin-bottom: 16px; 4 | background-color: #e6118a; 5 | color: #fff; 6 | border-radius: var(--image-border-radius-l); 7 | font-size: var(--type-body-s-size); 8 | line-height: var(--type-body-s-lh); 9 | } 10 | 11 | .labs .header { 12 | font-weight: 700; 13 | font-size: var(--type-heading-m-size); 14 | text-transform: uppercase; 15 | } 16 | 17 | .labs a { 18 | color: #fff; 19 | text-decoration: underline; 20 | font-weight: 700; 21 | } 22 | 23 | .home-template .labs { 24 | margin-left: 25%; 25 | margin-right: 25%; 26 | margin-top: -2em; 27 | z-index: 10; 28 | position: relative; 29 | text-align: center; 30 | } 31 | -------------------------------------------------------------------------------- /blocks/labs/labs.js: -------------------------------------------------------------------------------- 1 | export default function decorate(block) { 2 | const header = document.createElement('div'); 3 | header.className = 'header'; 4 | let parts; 5 | if (block.querySelector('h1, h2, h3, h4, h5, h6')) { 6 | parts = [block.querySelector('h1, h2, h3, h4, h5, h6').innerText]; 7 | block.querySelector('h1, h2, h3, h4, h5, h6').remove(); 8 | parts.push(block.innerHTML); 9 | } else { 10 | parts = block.innerText.split(/:/) 11 | .map((p) => p.trim()); 12 | } 13 | if (parts.length === 1) { 14 | parts.unshift('Early-access technology'); 15 | parts[1] = `Ask us about this feature from the ${parts[1]} labs on your Slack channel!`; 16 | } 17 | [header.textContent, block.innerHTML] = parts; 18 | block.prepend(header); 19 | } 20 | -------------------------------------------------------------------------------- /blocks/logo-wall/README.md: -------------------------------------------------------------------------------- 1 | ## Logo Wall 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default Logo Wall Layout | 9 | 10 | #### Example: 11 | 12 | See Live output (Link): 13 | https://redesign-hero-and-logo-wall--helix-website--adobe.hlx.page/drafts/redesign/blocks/logo-wall 14 | TBC: https://website-redesign--helix-website--adobe.hlx.page/drafts/redesign/blocks/logo-wall 15 | 16 | #### Content Structure: 17 | 18 | See Content in Document (Link): 19 | https://docs.google.com/document/d/1LuMFbPFHBkKc09WN8HCHbkqhCLM6kyiMrJKk7GAtPVg/edit# 20 | 21 | #### Code: 22 | - Heading: h1/h2/h3/h4/h5/h6 in the first row 23 | - Logo item: image(.png) + link in each row -> direct to linked url in a new page 24 | 25 | [Decoration Code](logo-wall.js) 26 | 27 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 28 | 29 | [Styling Code](logo-wall.css) -------------------------------------------------------------------------------- /blocks/logo-wall/logo-wall.css: -------------------------------------------------------------------------------- 1 | .logo-wall { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | padding: var(--spacing-xl) 0; 8 | } 9 | 10 | .logo-wall .logo-wall-title { 11 | max-width: 75vw; 12 | text-align: center; 13 | padding-bottom: var(--spacing-s); 14 | } 15 | 16 | .logo-wall .logo-wall-list { 17 | display: flex; 18 | flex-wrap: wrap; 19 | justify-content: center; 20 | gap: var(--spacing-s); 21 | width: 100%; 22 | max-width: 740px; 23 | padding-inline-start: 0; 24 | } 25 | 26 | .logo-wall .logo-wall-list-item { 27 | list-style-type: none; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | align-self: center; 32 | height: 100%; 33 | } 34 | 35 | .logo-wall .logo-wall-list-item a { 36 | transition: opacity 0.3s ease; 37 | } 38 | 39 | .logo-wall .logo-wall-list-item a:hover { 40 | opacity: 0.7; 41 | } 42 | 43 | .logo-wall .logo-wall-list-item img { 44 | margin: 0 auto; 45 | display: block; 46 | width: 100%; 47 | height: 100%; 48 | max-width: 150px; 49 | max-height: 54px; 50 | object-fit: contain; 51 | } 52 | 53 | .logo-wall.meriative-height-adjustment .logo-wall-list-item img { 54 | max-height: 80px; 55 | } 56 | 57 | @media (width >= 900px) { 58 | .logo-wall { 59 | padding: var(--spacing-xxl) 0; 60 | } 61 | 62 | .logo-wall .logo-wall-title { 63 | padding-bottom: var(--spacing-xl); 64 | } 65 | 66 | .logo-wall .logo-wall-list { 67 | gap: var(--spacing-l) var(--spacing-xl); 68 | max-width: 1200px; 69 | } 70 | 71 | .logo-wall.technology .logo-wall-list { 72 | max-width: 970px; 73 | } 74 | 75 | .logo-wall .logo-wall-list-item img { 76 | max-width: clamp(180px, 20vw, 280px); 77 | max-height: 63px; 78 | } 79 | 80 | .logo-wall.meriative-height-adjustment .logo-wall-list-item img { 81 | max-height: 96px; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /blocks/logo-wall/logo-wall.js: -------------------------------------------------------------------------------- 1 | import createTag from '../../utils/tag.js'; 2 | import { addInViewAnimationToMultipleElements } from '../../utils/helpers.js'; 3 | 4 | const animationConfig = { 5 | staggerTime: 0.4, 6 | items: [ 7 | { 8 | selector: '.logo-wall-title', 9 | animatedClass: 'slide-reveal-up', 10 | }, 11 | { 12 | selector: '.logo-wall-list', 13 | animatedClass: 'fade-up', 14 | }, 15 | ], 16 | }; 17 | 18 | export default function decorate(block) { 19 | block.classList.add('contained'); 20 | 21 | const logoWallList = document.createElement('ul'); 22 | logoWallList.setAttribute('class', 'logo-wall-list'); 23 | 24 | [...block.children].forEach((row) => { 25 | [...row.children].forEach((div) => { 26 | const title = div.querySelector('h1,h2,h3,h4,h5,h6'); 27 | const picture = div.querySelector('picture'); 28 | const svg = div.querySelector('a[href$=".svg"]'); 29 | const listItem = document.createElement('li'); 30 | 31 | if (title) { 32 | title.setAttribute('class', 'logo-wall-title'); 33 | row.replaceWith(title); 34 | } else if (svg) { 35 | const svgHref = new URL(svg.href).pathname; 36 | const linkEl = div.querySelectorAll('a')[1]; 37 | listItem.setAttribute('class', 'logo-wall-list-item'); 38 | 39 | const svgEl = createTag('img', { 40 | src: svgHref, 41 | alt: linkEl.textContent, 42 | class: 'logo-wall-item-svg', 43 | }); 44 | 45 | if (linkEl) { 46 | const svgLink = createTag('a', { 47 | href: linkEl.href, 48 | title: linkEl.title, 49 | target: '_blank', 50 | class: 'logo-wall-item-link', 51 | 'aria-label': linkEl.textContent, 52 | }, svgEl); 53 | 54 | listItem.append(svgLink); 55 | logoWallList.append(listItem); 56 | row.remove(); 57 | } 58 | } else if (picture) { 59 | listItem.setAttribute('class', 'logo-wall-list-item'); 60 | 61 | const linkEl = div.querySelector('a'); 62 | if (linkEl) { 63 | const pictureLink = createTag('a', { 64 | href: linkEl.href, 65 | title: linkEl.title, 66 | target: '_blank', 67 | class: 'logo-wall-item-link', 68 | 'aria-label': linkEl.textContent, 69 | }, picture); 70 | 71 | listItem.append(pictureLink); 72 | } 73 | 74 | logoWallList.append(listItem); 75 | row.remove(); 76 | } 77 | }); 78 | }); 79 | block.append(logoWallList); 80 | 81 | if (block.classList.contains('inview-animation')) { 82 | addInViewAnimationToMultipleElements(animationConfig.items, block, animationConfig.staggerTime); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /blocks/marquee/README.md: -------------------------------------------------------------------------------- 1 | ## Marquee 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default Marquee | 9 | | landscape-image | Turn image in image-side into 4:3 | 10 | | testimonial | Turn Marquee into testimonial layout | 11 | 12 | #### Example: 13 | 14 | See Live output: 15 | 16 | Marquee (testimonial) Link: 17 | https://redesign-tabs-and-marquee--helix-website--adobe.hlx.page/drafts/redesign/blocks/customer-stories-tabs-marquee 18 | TBC: https://website-redesign--helix-website--adobe.hlx.page/drafts/redesign/blocks/customer-stories-tabs-marquee 19 | 20 | #### Content Structure: 21 | 22 | See Content in Document (Link): 23 | https://docs.google.com/document/d/1_I1mwiaSk0CG-wikoknyjKPqEIjGmJMWINdJ8omKQBE/edit#heading=h.9p0k02gwynj6 24 | 25 | #### Code For Marquee in Testimonial format: 26 | - Background Image: 1st row from table 27 | - Side Image: any column in 2nd row without headings 28 | - Detail: any column in 2nd row with headings 29 | - h4 -> testimonial description 30 | - image -> avatar icon 31 | - h5 -> name + position (italic) 32 | - link -> button 33 | - nested unordered list -> for statistics 34 | 35 | [Decoration Code](marquee.js) 36 | 37 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 38 | 39 | [Styling Code](marquee.css) -------------------------------------------------------------------------------- /blocks/marquee/marquee.js: -------------------------------------------------------------------------------- 1 | import createTag from '../../utils/tag.js'; 2 | import { returnLinkTarget } from '../../utils/helpers.js'; 3 | 4 | const extractCustomerInfo = (detailContainer) => { 5 | const customerInfo = createTag('div', { 6 | class: 'customer-info', 7 | }, ''); 8 | 9 | const icon = detailContainer.querySelector('img'); 10 | if (icon) { 11 | customerInfo.append(icon); 12 | } 13 | 14 | const titles = detailContainer.querySelectorAll('h5'); 15 | if (titles) { 16 | let titlesHTML = ''; 17 | titles.forEach((title) => { 18 | titlesHTML += `
${title.innerHTML}
`; 19 | }); 20 | const titlesDiv = createTag('div', { 21 | class: 'titles', 22 | }, titlesHTML); 23 | customerInfo.append(titlesDiv); 24 | } 25 | 26 | const cta = detailContainer.querySelector('a'); 27 | if (cta) { 28 | cta.classList.add('button', 'secondary'); 29 | cta.setAttribute('target', returnLinkTarget(cta.href)); 30 | customerInfo.append(cta); 31 | 32 | const titleContainer = customerInfo.querySelector('.titles'); 33 | if (!titleContainer || titleContainer.innerHTML.trim() === '') { 34 | cta.classList.add('align-desktop-right'); 35 | } 36 | } 37 | 38 | return customerInfo; 39 | }; 40 | 41 | export default function decorate(block) { 42 | [...block.children].forEach((row) => { 43 | [...row.children].forEach((div) => { 44 | const picture = div.querySelector('picture'); 45 | const hasTitle = div.querySelector('h1,h2,h3,h4,h5,h6'); 46 | 47 | if (hasTitle) { 48 | div.classList.add('info-side'); 49 | // restyle for marquee.testimonial 50 | if (block.classList.contains('testimonial')) { 51 | const description = div.querySelectorAll('h4'); 52 | const intro = description[0]; 53 | const quote = description[1]; 54 | 55 | const customerInfo = extractCustomerInfo(div); 56 | const statistics = div.querySelector('ul'); 57 | 58 | const testimonial = createTag('div', { 59 | class: 'testimonial-info', 60 | }, ''); 61 | if (intro) { testimonial.append(intro); } 62 | if (quote) { testimonial.append(quote); } 63 | if (customerInfo) { testimonial.append(customerInfo); } 64 | if (statistics) { testimonial.append(statistics); } 65 | div.replaceWith(testimonial); 66 | } 67 | } else if (picture) { 68 | div.classList.add('image-side'); 69 | } 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /blocks/pagination/README.md: -------------------------------------------------------------------------------- 1 | ## Pagination 2 | 3 | Notes: The Pagination is a block meant to be used on the documentation pages below the main content. It provides the opportunity to link users directly to the next or previous piece of documentation. 4 | 5 | 6 | ##### Custom Classes 7 | | Class | Function | 8 | |--------|------------| 9 | | - | - | 10 | 11 | 12 | #### Example: 13 | 14 | See Live output (Link TBD) 15 | 16 | #### Content Structure: 17 | 18 | [See Content in Document](https://docs.google.com/document/d/1nf9oSOtMKsjk8m6zwQeqbU3mTC7XGIC6z0THkd9GykI/edit) 19 | 20 | #### Code: 21 | Side Navigation is styled in the block CSS code. 22 | 23 | There is Javascript code for decoration purposes. 24 | 25 | [Decoration Code](pagination.js) 26 | 27 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 28 | 29 | [Styling Code](pagination.css) -------------------------------------------------------------------------------- /blocks/pagination/pagination.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | max-width: 800px; 3 | margin: var(--spacing-xl) auto 0; 4 | } 5 | 6 | .block.pagination { 7 | max-width: none; 8 | } 9 | 10 | .pagination > div { 11 | display: grid; 12 | grid-template-columns: 1fr 1fr; 13 | column-gap: 8px; 14 | } 15 | 16 | .pagination .column { 17 | background-color: var(--bg-color-lightgrey); 18 | border-radius: 16px; 19 | border: solid 1px var(--bg-color-grey); 20 | padding: 12px 16px; 21 | transition: all 0.3s ease-in-out; 22 | } 23 | 24 | .pagination .column:hover { 25 | cursor: pointer; 26 | box-shadow: 0 0 15px var(--bg-color-grey); 27 | } 28 | 29 | .pagination .column svg { 30 | color: var(--spectrum-blue); 31 | } 32 | 33 | .pagination .column.left p > span { 34 | margin-right: 8px; 35 | } 36 | 37 | .pagination .column.right p > span { 38 | margin-left: 8px; 39 | } 40 | 41 | .pagination .column.right p > span svg { 42 | transform: rotate(180deg); 43 | } 44 | 45 | .pagination .column p { 46 | font-size: var(--type-body-xs-size); 47 | margin: 0 0 var(--spacing-xxs); 48 | } 49 | 50 | .pagination .column p:first-of-type { 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .pagination .column p:first-of-type span { 56 | display: flex; 57 | } 58 | 59 | .pagination .column p:last-of-type span { 60 | display: flex; 61 | margin-left: 8px; 62 | } 63 | 64 | .pagination .column h3 { 65 | margin: 0; 66 | line-height: 1; 67 | } 68 | 69 | .pagination .column h3 a { 70 | font-size: var(--type-heading-s-size); 71 | font-weight: 900; 72 | line-height: 1; 73 | letter-spacing: -0.04em; 74 | color: black; 75 | text-decoration: none; 76 | } 77 | 78 | @media (width >= 600px) { 79 | .block.pagination { 80 | max-width: none; 81 | } 82 | 83 | .pagination .column p { 84 | font-size: var(--type-body-s-size); 85 | margin: 0 0 var(--spacing-xs); 86 | } 87 | 88 | .pagination > div { 89 | column-gap: 16px; 90 | } 91 | 92 | .pagination .column h3 a { 93 | font-size: var(--type-heading-m-size); 94 | } 95 | } 96 | 97 | @media (width >= 900px) { 98 | .pagination { 99 | margin: var(--spacing-xxxl) auto 0; 100 | } 101 | 102 | .block.pagination { 103 | max-width: none; 104 | } 105 | 106 | .pagination > div { 107 | column-gap: var(--spacing-m); 108 | } 109 | 110 | .pagination .column { 111 | padding: var(--spacing-s) var(--spacing-m); 112 | } 113 | 114 | .pagination .column p a { 115 | font-size: var(--type-body-l-size); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /blocks/pagination/pagination.js: -------------------------------------------------------------------------------- 1 | export default function decorate(block) { 2 | block.querySelectorAll(':scope > div > div').forEach((column, i) => { 3 | column.classList.add('column'); 4 | column.classList.add(i % 2 ? 'right' : 'left'); 5 | const linkElement = column.querySelector('a'); 6 | 7 | if (linkElement) { 8 | column.classList.add('link-highlight-colorful-effect-hover-wrapper'); 9 | linkElement.classList.add('link-highlight-colorful-effect'); 10 | 11 | column.addEventListener('click', () => { 12 | linkElement.click(); 13 | }); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /blocks/publication-time/publication-time.css: -------------------------------------------------------------------------------- 1 | .highlight { 2 | margin-left: 1em; 3 | width: max-content; 4 | background-color: yellow; 5 | } 6 | -------------------------------------------------------------------------------- /blocks/publication-time/publication-time.js: -------------------------------------------------------------------------------- 1 | import '../../web-components/relative-time.js'; 2 | 3 | export default function decorate($block) { 4 | // First row of the block defines the text prefix 5 | const prefix = $block.querySelector(':scope > div > div ')?.textContent?.trim(); 6 | 7 | // Output the element as specified, 8 | // https://github.com/github/relative-time-element 9 | if (!document.lastModified) { 10 | // eslint-disable-next-line no-console 11 | console.error('document.lastModified returns null'); 12 | } else { 13 | const lastMod = new Date(document.lastModified); 14 | const p = document.createElement('p'); 15 | const rt = document.createElement('relative-time'); 16 | rt.setAttribute('datetime', lastMod.toISOString()); 17 | rt.textContent = lastMod.toLocaleDateString(); 18 | p.textContent = `${prefix} `; 19 | p.appendChild(rt); 20 | $block.replaceChildren(p); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blocks/releases/releases.css: -------------------------------------------------------------------------------- 1 | .releases .releases-controls input[type="checkbox"] { 2 | pointer-events: none; 3 | } 4 | 5 | .releases .release-aem-certificate-rotation { 6 | background-color: #d4e8ff; 7 | } 8 | 9 | .releases .release-helix-admin { 10 | background-color: #ffd4f1; 11 | } 12 | 13 | .releases .release-helix-pipeline-service { 14 | background-color: #d1ffed; 15 | } 16 | 17 | .releases .release-helix-importer-ui { 18 | background-color: #e8e8e8; 19 | } 20 | 21 | .releases .release-aem-sidekick, 22 | .releases .release-helix-sidekick-extension { 23 | background-color: #e4e8bf; 24 | } 25 | 26 | .releases .release-franklin-sidekick-library { 27 | background-color: #f4f8cf; 28 | } 29 | 30 | .releases .release-helix-cli { 31 | background-color: #ffebd4; 32 | } 33 | 34 | .releases .release-aem-lib { 35 | background-color: #cfebf8; 36 | } 37 | 38 | .releases .release-helix-config-service { 39 | background-color: #edd1ff; 40 | } 41 | 42 | .releases .release { 43 | padding: 16px 32px 32px; 44 | margin: 16px 0; 45 | border-radius: 16px; 46 | display: none; 47 | } 48 | 49 | .releases .releases-controls { 50 | display: flex; 51 | gap: 8px; 52 | flex-wrap: wrap; 53 | } 54 | 55 | .releases .releases-control { 56 | padding: 4px 16px; 57 | border-radius: 8px; 58 | font-size: 14px; 59 | display: flex; 60 | align-items: start; 61 | } 62 | 63 | .releases-date { 64 | font-weight: 700; 65 | margin-bottom: 0; 66 | } 67 | 68 | .releases .release h2 { 69 | margin-top: 0; 70 | } 71 | 72 | .releases .releases-results[aria-hidden="true"] { 73 | display: none; 74 | } 75 | 76 | .releases .helix-admin .release-helix-admin, 77 | .releases .helix-pipeline-service .release-helix-pipeline-service, 78 | .releases .helix-importer-ui .release-helix-importer-ui, 79 | .releases .helix-sidekick-extension .release-helix-sidekick-extension, 80 | .releases .aem-sidekick .release-aem-sidekick, 81 | .releases .helix-cli .release-helix-cli, 82 | .releases .helix-config-service .release-helix-config-service, 83 | .releases .aem-lib .release-aem-lib, 84 | .releases .franklin-sidekick-library .release-franklin-sidekick-library, 85 | .releases .aem-certificate-rotation .release-aem-certificate-rotation { 86 | display: block; 87 | } 88 | -------------------------------------------------------------------------------- /blocks/roi-calculator/README.md: -------------------------------------------------------------------------------- 1 | ## ROI Calculator 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default ROI Calculator Layout | 9 | 10 | #### Example: 11 | 12 | See Live output (Link): 13 | https://redesign-polish--helix-website--adobe.hlx.page/drafts/redesign/blocks/roi-calculator 14 | TBC: https://website-redesign--helix-website--adobe.hlx.page/drafts/redesign/blocks/roi-calculator 15 | 16 | #### Content Structure: 17 | 18 | See Content in Document (Link): 19 | https://docs.google.com/document/d/1LADlPd4SPHwKKuK61WHMYvfTwFdVEg-JuNFiXZTgToo/edit 20 | 21 | #### Code: 22 | Default uses colorful background (block.colorful-bg + div.checker-board-guide) 23 | First Row Content 24 | - Heading: h2 25 | - CTA Button: link 26 | 27 | Second Row Content 28 | - can be used to input info for ROI form & calulator 29 | - TBC development from Adobe team 30 | 31 | [Decoration Code](roi-calculator.js) 32 | 33 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 34 | 35 | [Styling Code](roi-calculator.css) -------------------------------------------------------------------------------- /blocks/roi-calculator/roi-calculator.css: -------------------------------------------------------------------------------- 1 | .section.roi-calulator-outer-wrapper { 2 | padding: 0; 3 | } 4 | 5 | .roi-calculator { 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | padding: var(--spacing-xxl) 0; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .roi-calculator .inner-content { 16 | position: relative; 17 | margin: 0 auto; 18 | width: 100%; 19 | text-align: center; 20 | z-index: 2; 21 | } 22 | 23 | .roi-calculator .upper-content { 24 | margin: 0 auto; 25 | width: 100%; 26 | max-width: 600px; 27 | text-align: center; 28 | z-index: 2; 29 | } 30 | 31 | .roi-calculator .upper-content h2 { 32 | font-size: var(--type-heading-xxl-size); 33 | line-height: var(--type-heading-xxxl-lh); 34 | text-align: center; 35 | font-weight: 700; 36 | margin: 0 auto var(--spacing-s); 37 | max-width: 300px; 38 | } 39 | 40 | .roi-calculator .inner-content a.button:any-link { 41 | margin-bottom: 0; 42 | margin-top: 0; 43 | } 44 | 45 | @media screen and (width >= 900px) { 46 | .section.roi-calulator-outer-wrapper { 47 | padding-top: var(--spacing-ml); 48 | } 49 | 50 | .roi-calculator { 51 | min-height: 480px; 52 | } 53 | 54 | .roi-calculator .upper-content h2 { 55 | font-size: var(--type-heading-xxxl-size); 56 | margin-bottom: var(--spacing-ml); 57 | max-width: 560px; 58 | } 59 | 60 | .roi-calculator .inner-content a.button:any-link { 61 | margin-bottom: 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /blocks/roi-calculator/roi-calculator.js: -------------------------------------------------------------------------------- 1 | import createTag from '../../utils/tag.js'; 2 | 3 | export default function decorate(block) { 4 | const outerSectionWrapper = block.closest('.section'); 5 | outerSectionWrapper.classList.add('roi-calulator-outer-wrapper'); 6 | 7 | const upperContent = block.children[0].querySelector('div'); 8 | upperContent.setAttribute('class', 'upper-content'); 9 | const ctaButton = upperContent.querySelector('a'); 10 | if (ctaButton) { 11 | ctaButton.classList.add('button', 'large'); 12 | ctaButton.closest('p').replaceWith(ctaButton); 13 | } 14 | 15 | const innerContent = createTag('div', { class: 'inner-content contained-wrapper' }); 16 | innerContent.appendChild(upperContent); 17 | 18 | block.innerHTML = ''; 19 | 20 | // colorful background setting 21 | block.classList.add('colorful-bg'); 22 | const checkerBoardGuide = createTag('div', { 23 | class: 'checker-board-guide', 24 | }); 25 | block.append(checkerBoardGuide); 26 | 27 | // TODO: append more content like form & ROI calculator here 28 | // const calculatorContent = block.children[1].querySelector('div'); 29 | 30 | block.append(innerContent); 31 | } 32 | -------------------------------------------------------------------------------- /blocks/service-status/service-status.css: -------------------------------------------------------------------------------- 1 | .service-status { 2 | background: aliceblue; 3 | padding: var(--spacing-xl) 0; 4 | text-align: center; 5 | } 6 | 7 | .service-status statuspage-widget { 8 | margin: var(--spacing-s) 0; 9 | } 10 | -------------------------------------------------------------------------------- /blocks/service-status/service-status.js: -------------------------------------------------------------------------------- 1 | import { loadScript } from '../../scripts/lib-franklin.js'; 2 | import createTag from '../../utils/tag.js'; 3 | 4 | async function loadWidget(el) { 5 | await loadScript('https://unpkg.com/@statuspage/status-widget/dist/index.js'); 6 | const status = createTag('statuspage-widget', { src: 'https://status.project-helix.io', appearance: 'badge' }); 7 | el.append(status); 8 | } 9 | 10 | export default async function init(el) { 11 | setTimeout(() => { 12 | loadWidget(el); 13 | }, 3000); 14 | } 15 | -------------------------------------------------------------------------------- /blocks/side-navigation/README.md: -------------------------------------------------------------------------------- 1 | ## Side Navigation 2 | 3 | Notes: The Side Navigation block is built automatically into all pages with the Page metadata template set to `guides`. The search functionality queiries the dopages-index sheet in the root of the project content folder. 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | - | - | 9 | 10 | 11 | #### Example: 12 | 13 | See Live output (Link TBD) 14 | 15 | #### Content Structure: 16 | 17 | [See Content in Document](https://docs.google.com/document/d/1ST15vxa7XD9vVYJM-CH3fgJwtn8ERogEjZdSwcMkrLI/edit) 18 | 19 | #### Code: 20 | Side Navigation is styled in the block CSS code. 21 | 22 | There is Javascript code for decoration purposes and to set up a target click handler for list items that require a toggle to reveal additional list items. 23 | 24 | [Decoration Code](side-navigation.js) 25 | 26 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 27 | 28 | [Styling Code](side-navigation.css) -------------------------------------------------------------------------------- /blocks/sidekick-generator/sidekick-generator.css: -------------------------------------------------------------------------------- 1 | .sidekick-generator form { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .sidekick-generator form label { 7 | width: 100%; 8 | font-weight: 700; 9 | } 10 | 11 | .sidekick-generator a.back-link::before { 12 | content: "< Back to "; 13 | padding-right: 8px; 14 | display: inline-block; 15 | } 16 | 17 | .sidekick-generator input[type="text"], 18 | .sidekick-generator input[type="password"] { 19 | display: block; 20 | width: 100%; 21 | line-height: 30px; 22 | border: 1px solid; 23 | border-color: rgb(202 202 202); 24 | border-radius: 4px; 25 | padding: 0 8px; 26 | font-size: 14px; 27 | font-family: adobe-clean, sans-serif; 28 | outline: none !important; 29 | appearance: none; 30 | box-sizing: border-box; 31 | transition: border-color 130ms ease-in-out, box-shadow 130ms ease-in-out; 32 | flex: 1 1 0%; 33 | } 34 | 35 | .sidekick-generator input[type="checkbox"] { 36 | border: 1px solid; 37 | border-color: rgb(202 202 202); 38 | border-radius: 4px; 39 | transition: border-color 130ms ease-in-out, box-shadow 130ms ease-in-out; 40 | } 41 | 42 | .sidekick-generator input[type="text"]::placeholder { 43 | font-weight: 400; 44 | color: #8e8e8e; 45 | transition: color 130ms ease-in-out; 46 | } 47 | 48 | .sidekick-generator input[type="text"]:focus::placeholder, 49 | .sidekick-generator input[type="text"]:hover::placeholder { 50 | color: #2c2c2c; 51 | } 52 | 53 | .sidekick-generator input[type="text"]:focus, 54 | .sidekick-generator input[type="password"]:focus { 55 | border-color: rgb(42 124 223); 56 | } 57 | 58 | .sidekick-generator button { 59 | line-height: 32px; 60 | background: rgb(20 115 230); 61 | color: white; 62 | border-radius: 16px; 63 | font-size: 14px; 64 | font-weight: 600; 65 | padding: 0 16px; 66 | border: none; 67 | font-family: adobe-clean, "Trebuchet MS", sans-serif; 68 | flex: 0 1 auto; 69 | } 70 | 71 | .sidekick-generator p > .icon svg { 72 | box-sizing: border-box; 73 | height: 24px; 74 | margin-bottom: -5px; 75 | margin-right: 4px; 76 | } 77 | -------------------------------------------------------------------------------- /blocks/sidekick-library/sidekick-library.css: -------------------------------------------------------------------------------- 1 | body.docs-template main .section:has(.sidekick-library-wrapper) { 2 | padding: 0; 3 | max-width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /blocks/sidekick-library/sidekick-library.js: -------------------------------------------------------------------------------- 1 | import '../../tools/sidekick/library/index.js'; 2 | 3 | export default function decorate(block) { 4 | const library = document.createElement('sidekick-library'); 5 | block.replaceChildren(library); 6 | } 7 | -------------------------------------------------------------------------------- /blocks/table/table.css: -------------------------------------------------------------------------------- 1 | .table { 2 | max-width: 100%; 3 | overflow-x: auto; 4 | width: fit-content; 5 | } 6 | 7 | /* temp fix for table rendering weirdly */ 8 | .content > .table-wrapper > .table { 9 | border: 1px solid var(--bg-color-grey); 10 | padding: var(--spacing-xxs) var(--spacing-xxs) var(--spacing-xs); 11 | margin-bottom: var(--spacing-s); 12 | } 13 | 14 | .table tbody { 15 | width: 100%; 16 | } 17 | 18 | .table td, 19 | body main .table td p { 20 | font-size: var(--type-body-s-size); 21 | line-height: var(--type-body-s-lh); 22 | color: var(--color-light-grey-600); 23 | } 24 | 25 | .table td { 26 | padding: var(--spacing-xxs) var(--spacing-xxs) 0 var(--spacing-xxs); 27 | min-width: 180px; 28 | } 29 | 30 | body main .table td p { 31 | padding-left: 0; 32 | } 33 | 34 | .table tr:first-of-type { 35 | border-bottom: 1px solid var(--bg-color-grey); 36 | } 37 | 38 | .table td:first-child { 39 | color: inherit; 40 | } 41 | 42 | .table tr:first-of-type td { 43 | text-transform: uppercase; 44 | padding-top: var(--spacing-xxs); 45 | padding-bottom: var(--spacing-xxs); 46 | color: var(--color-black); 47 | } 48 | 49 | .table thead + tbody tr:first-of-type td { 50 | text-transform: none; 51 | } 52 | -------------------------------------------------------------------------------- /blocks/table/table.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Table Block 3 | * Recreate a table 4 | * https://www.hlx.live/developer/block-collection/table 5 | */ 6 | 7 | function buildCell(rowIndex) { 8 | const cell = rowIndex ? document.createElement('td') : document.createElement('th'); 9 | if (!rowIndex) cell.setAttribute('scope', 'col'); 10 | return cell; 11 | } 12 | 13 | export default async function decorate(block) { 14 | if (block.querySelector('table')) return; 15 | const table = document.createElement('table'); 16 | const thead = document.createElement('thead'); 17 | const tbody = document.createElement('tbody'); 18 | 19 | const header = !block.classList.contains('no-header'); 20 | if (header) table.append(thead); 21 | table.append(tbody); 22 | 23 | [...block.children].forEach((child, i) => { 24 | const row = document.createElement('tr'); 25 | if (header && i === 0) thead.append(row); 26 | else tbody.append(row); 27 | [...child.children].forEach((col) => { 28 | const cell = buildCell(header ? i : i + 1); 29 | cell.innerHTML = col.innerHTML; 30 | row.append(cell); 31 | }); 32 | }); 33 | block.innerHTML = ''; 34 | block.append(table); 35 | } 36 | -------------------------------------------------------------------------------- /blocks/tabs/README.md: -------------------------------------------------------------------------------- 1 | ## Tabs 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default tabs | 9 | | image-based | Support image in top tablist | 10 | 11 | #### Example: 12 | 13 | See Live output: 14 | Tabs (.image-based) Link: 15 | https://redesign-tabs-and-marquee--helix-website--adobe.hlx.page/drafts/redesign/blocks/customer-stories-tabs-marquee 16 | TBC: https://website-redesign--helix-website--adobe.hlx.page/drafts/redesign/blocks/customer-stories-tabs-marquee 17 | 18 | #### Content Structure: 19 | 20 | See Content in Document (Link): 21 | https://docs.google.com/document/d/1_I1mwiaSk0CG-wikoknyjKPqEIjGmJMWINdJ8omKQBE/edit#heading=h.9p0k02gwynj6 22 | 23 | #### Code For Marquee in Testimonial format: 24 | - Image in unordered list in google doc -> Image in tab list 25 | - Tabs content are grouped using `---` and `section-metadata` block where `style` in `section-metadata` will need to be `Tab ${index}` 26 | 27 | [Decoration Code](tabs.js) 28 | 29 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 30 | 31 | [Styling Code](tabs.css) -------------------------------------------------------------------------------- /blocks/testimonials/README.md: -------------------------------------------------------------------------------- 1 | ## Testimonials 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | N/A | Default testimonials | 9 | 10 | #### Example: 11 | 12 | See Live output: 13 | Testimonials Link: 14 | https://redesign-testimonial-simplify--helix-website--adobe.hlx.page/drafts/redesign/blocks/testimonials 15 | 16 | #### Content Structure: 17 | 18 | See Content in Document (Link): 19 | https://docs.google.com/document/d/10gnxPdUyIMT2Du_gimFjL-6VMX7yojCyb881ZQSb1SU/edit 20 | 21 | #### Code For Testimonials format: 22 | - first column: image + text for tabs rendering 23 | - second column: image in tabcontent testimonial section 24 | - third column: text content for testimonial 25 | - start with " ' " will add quote class 26 | - img -> icon 27 | - text after image -> customer titles 28 | - link -> cta 29 | - ul list -> stats info, will separate string into 2 part, 1st part with stat, 2nd part with info 30 | 31 | [Decoration Code](testimonials.js) 32 | 33 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 34 | 35 | [Styling Code](testimonials.css) -------------------------------------------------------------------------------- /blocks/video/video.css: -------------------------------------------------------------------------------- 1 | .video-wrapper { 2 | width: 100%; 3 | overflow: hidden; 4 | } 5 | 6 | .video { 7 | margin: 32px 0; 8 | } 9 | 10 | .video video { 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /blocks/video/video.js: -------------------------------------------------------------------------------- 1 | import { sampleRUM } from '../../scripts/lib-franklin.js'; 2 | 3 | function decorateVideoBlock($block, videoURL) { 4 | if (videoURL.endsWith('.mp4')) { 5 | let attrs = ''; 6 | attrs = 'playsinline controls'; 7 | if ($block.classList.contains('autoplay')) attrs = 'playsinline controls muted autoplay loop'; 8 | $block.innerHTML = /* html */` 9 |
10 | 11 |
12 | `; 13 | $block.querySelector('video').addEventListener('play', (e) => { 14 | sampleRUM('play', { 15 | source: e.target.currentSrc, 16 | }); 17 | }); 18 | } 19 | } 20 | 21 | export default function decorate($block) { 22 | const $a = $block.querySelector('a'); 23 | const videoURL = $a.href; 24 | const observer = new IntersectionObserver((entries) => { 25 | entries.forEach((entry) => { 26 | if (entry.isIntersecting) { 27 | decorateVideoBlock($block, videoURL); 28 | } 29 | }); 30 | }); 31 | observer.observe($block); 32 | } 33 | -------------------------------------------------------------------------------- /blocks/videotext/videotext.css: -------------------------------------------------------------------------------- 1 | /* default rule for mobile */ 2 | .videotext > div { 3 | display: grid; 4 | padding-top: var(--spacing-xxs); 5 | } 6 | 7 | .videocontent video { 8 | width: 100%; 9 | } 10 | 11 | .videotext .text { 12 | font-size: var(--type-body-xs-size); 13 | padding-top: var(--spacing-xxs); 14 | } 15 | -------------------------------------------------------------------------------- /blocks/videotext/videotext.js: -------------------------------------------------------------------------------- 1 | export default function decorate(block) { 2 | [...block.children].forEach((row) => { 3 | const videoEl = [...row.children][0]; 4 | videoEl.classList.add('videocontent'); 5 | const videoURL = videoEl.innerHTML; 6 | videoEl.innerHTML = ``; 7 | const textEl = [...row.children][1]; 8 | textEl.classList.add('text'); 9 | textEl.querySelectorAll('a').forEach((a) => { 10 | a.target = '_blank'; 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /blocks/z-pattern/README.md: -------------------------------------------------------------------------------- 1 | ## Z Pattern 2 | 3 | Notes: 4 | 5 | ##### Custom Classes 6 | | Class | Function | 7 | |--------|------------| 8 | | wide-text | Used for a more prominent text split. | 9 | | spacing-lg | Used to increase the spacing between rows. | 10 | | value-props | Used to add global classes `.icon-eyebrow` and `.main-headline`, and `colored-tag` with color class for props styling. | 11 | | right-first | Default is left-side content first, with this class applied the pattern should begin with right-side content first | 12 | 13 | #### Example: 14 | 15 | See Live output (Link) 16 | 17 | #### Content Structure: 18 | 19 | See Content in Document (Link) 20 | 21 | #### Code: 22 | Z Patten is styled in the block CSS code. 23 | 24 | There is Javascript code for decoration purposes primarily to alternate the layout of even and odd rows. 25 | 26 | - Left Side: Image 27 | - Right Side: Content 28 | 29 | z-pattern.value-props: 30 | - Left Side: Image 31 | - Right Side: Content 32 | - p (first p tag) -> .icon-eyebrow 33 | - h3 -> .main-headline 34 | - ul -> li 35 | - bolded text -> .colored-tag with colors 36 | - other text in li -> .colored-tag-description 37 | 38 | [Decoration Code](z-pattern.js) 39 | 40 | The CSS Styling is very project specific and gets adjusted as needed for a project or block by block. 41 | 42 | [Styling Code](z-pattern.css) 43 | -------------------------------------------------------------------------------- /default-meta-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/default-meta-image.jpg -------------------------------------------------------------------------------- /default-meta-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/default-meta-image.png -------------------------------------------------------------------------------- /fonts/adobe-clean-300-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-300-italic.woff2 -------------------------------------------------------------------------------- /fonts/adobe-clean-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-300.woff2 -------------------------------------------------------------------------------- /fonts/adobe-clean-400-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-400-italic.woff2 -------------------------------------------------------------------------------- /fonts/adobe-clean-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-400.woff2 -------------------------------------------------------------------------------- /fonts/adobe-clean-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-700.woff2 -------------------------------------------------------------------------------- /fonts/adobe-clean-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/adobe-clean-900.woff2 -------------------------------------------------------------------------------- /fonts/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: adobe-clean; 3 | src:url("/fonts/adobe-clean-400.woff2") format("woff2"); 4 | font-display:optional;font-style:normal;font-weight:400; 5 | } 6 | 7 | @font-face { 8 | font-family: adobe-clean; 9 | src:url("/fonts/adobe-clean-400-italic.woff2") format("woff2"); 10 | font-display:optional;font-style:italic;font-weight:400; 11 | } 12 | 13 | @font-face { 14 | font-family: adobe-clean; 15 | src:url("/fonts/adobe-clean-700.woff2") format("woff2"); 16 | font-display:optional;font-style:normal;font-weight:700; 17 | } 18 | 19 | @font-face { 20 | font-family: adobe-clean; 21 | src:url("/fonts/adobe-clean-900.woff2") format("woff2"); 22 | font-display:optional;font-style:normal;font-weight:900; 23 | } 24 | -------------------------------------------------------------------------------- /fonts/source-code-pro-300-italic.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-300-italic.otf.woff2 -------------------------------------------------------------------------------- /fonts/source-code-pro-300.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-300.otf.woff2 -------------------------------------------------------------------------------- /fonts/source-code-pro-400-italic.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-400-italic.otf.woff2 -------------------------------------------------------------------------------- /fonts/source-code-pro-400.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-400.otf.woff2 -------------------------------------------------------------------------------- /fonts/source-code-pro-700.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-700.otf.woff2 -------------------------------------------------------------------------------- /fonts/source-code-pro-900.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/fonts/source-code-pro-900.otf.woff2 -------------------------------------------------------------------------------- /head.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /helix-query.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | indices: 4 | default: 5 | target: /query-index 6 | properties: 7 | title: 8 | select: head > meta[property="og:title"] 9 | value: | 10 | attribute(el, 'content') 11 | image: 12 | select: head > meta[property="og:image"] 13 | value: | 14 | match(attribute(el, 'content'), 'https:\/\/[^/]+(\/.*)') 15 | description: 16 | select: head > meta[name="description"] 17 | value: | 18 | attribute(el, 'content') 19 | lastModified: 20 | select: none 21 | value: | 22 | parseTimestamp(headers['last-modified'], 'ddd, DD MMM YYYY hh:mm:ss GMT') 23 | robots: 24 | select: head > meta[name="robots"] 25 | value: | 26 | attribute(el, 'content') 27 | publicationDate: 28 | select: head > meta[name="publication-date"] 29 | value: | 30 | attribute(el, 'content') 31 | deprecation: 32 | select: head > meta[name="deprecation"] 33 | value: | 34 | attribute(el, 'content') 35 | labs: 36 | select: head > meta[name="labs"] 37 | value: | 38 | attribute(el, 'content') 39 | docpages: 40 | target: /docpages-index 41 | include: 42 | - /docs/** 43 | - /developer/** 44 | properties: 45 | title: 46 | select: head > meta[property="og:title"] 47 | value: | 48 | attribute(el, 'content') 49 | image: 50 | select: head > meta[property="og:image"] 51 | value: | 52 | match(attribute(el, 'content'), 'https:\/\/[^/]+(\/.*)') 53 | description: 54 | select: head > meta[name="description"] 55 | value: | 56 | attribute(el, 'content') 57 | labs: 58 | select: head > meta[name="labs"] 59 | value: | 60 | attribute(el, 'content') 61 | lastModified: 62 | select: none 63 | value: | 64 | parseTimestamp(headers['last-modified'], 'ddd, DD MMM YYYY hh:mm:ss GMT') 65 | robots: 66 | select: head > meta[name="robots"] 67 | value: | 68 | attribute(el, 'content') 69 | content: 70 | select: main 71 | value: | 72 | textContent(el) 73 | -------------------------------------------------------------------------------- /icons/icon-aec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-author.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-back-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-bin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/icon-build-colored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-build.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-caret-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-caret-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/icon-caret-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-composability.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-developer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-fast-deliver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/icon-fast-publish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/icon-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /icons/icon-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-lamp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-people.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-scale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-speed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-work-colored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-work.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-world-colored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/icon-world.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/bg-docs-1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/bg-docs-1080.png -------------------------------------------------------------------------------- /img/bg-docs-assets/bg-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/bg-docs-assets/bg-docs.png -------------------------------------------------------------------------------- /img/bg-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/bg-docs.png -------------------------------------------------------------------------------- /img/chrome-web-store.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /img/colorful-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/colorful-bg.jpg -------------------------------------------------------------------------------- /img/colorful-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/colorful-bg.png -------------------------------------------------------------------------------- /img/franklin-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/franklin-favicon.png -------------------------------------------------------------------------------- /img/icon-admin-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-admin-html.png -------------------------------------------------------------------------------- /img/icon-aec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-aec.png -------------------------------------------------------------------------------- /img/icon-build-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-build-colored.png -------------------------------------------------------------------------------- /img/icon-faq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-faq.png -------------------------------------------------------------------------------- /img/icon-sidekick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-sidekick.png -------------------------------------------------------------------------------- /img/icon-status-live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-status-live.png -------------------------------------------------------------------------------- /img/icon-work-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-work-colored.png -------------------------------------------------------------------------------- /img/icon-world-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/img/icon-world-colored.png -------------------------------------------------------------------------------- /libs/highlight/atom-one-dark.min.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} -------------------------------------------------------------------------------- /libs/highlight/rainbow.min.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#474949;color:#d1d9e1}.hljs-comment,.hljs-quote{color:#969896;font-style:italic}.hljs-addition,.hljs-keyword,.hljs-literal,.hljs-selector-tag,.hljs-type{color:#c9c}.hljs-number,.hljs-selector-attr,.hljs-selector-pseudo{color:#f99157}.hljs-doctag,.hljs-regexp,.hljs-string{color:#8abeb7}.hljs-built_in,.hljs-name,.hljs-section,.hljs-title{color:#b5bd68}.hljs-class .hljs-title,.hljs-selector-id,.hljs-template-variable,.hljs-title.class_,.hljs-variable{color:#fc6}.hljs-name,.hljs-section,.hljs-strong{font-weight:700}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-subst,.hljs-symbol{color:#f99157}.hljs-deletion{color:#dc322f}.hljs-formula{background:#eee8d5}.hljs-attr,.hljs-attribute{color:#81a2be}.hljs-emphasis{font-style:italic} -------------------------------------------------------------------------------- /llms.txt: -------------------------------------------------------------------------------- 1 | # Adobe Experience Manager 2 | 3 | > Edge Delivery Services for Adobe Experience Manager Sites 4 | 5 | Build blazingly fast websites using tools content creators and developers already know. 6 | 7 | ## Docs 8 | 9 | - [Getting Started - Developer Tutorial](https://www.aem.live/developer/tutorial.md) 10 | - [Anatomy of a Project](https://www.aem.live/developer/anatomy-of-a-project.md) 11 | - [Block Collection](https://www.aem.live/developer/block-collection.md) 12 | - [Spreadsheets](https://www.aem.live/developer/spreadsheets.md) 13 | - [Indexing](https://www.aem.live/developer/indexing.md) 14 | - [Web Performance](https://www.aem.live/developer/keeping-it-100.md) 15 | - [Markup - Sections](https://www.aem.live/developer/markup-sections-blocks.md) 16 | - [Favicon](https://www.aem.live/developer/favicon.md) 17 | - [Custom Headers](https://www.aem.live/docs/custom-headers.md) 18 | - [Best Practices for Developers](https://www.aem.live/docs/dev-collab-and-good-practices.md) 19 | - [Where to Author](https://www.aem.live/docs/authoring-guide.md) 20 | - [Authoring](https://www.aem.live/docs/authoring.md) 21 | - [Authoring with AEM](https://www.aem.live/docs/aem-authoring.md) 22 | - [Bulk Metadata](https://www.aem.live/docs/bulk-metadata.md) 23 | - [Placeholders](https://www.aem.live/docs/placeholders.md) 24 | - [Sitemap](https://www.aem.live/developer/sitemap.md) 25 | - [SharePoint Setup](https://www.aem.live/docs/setup-customer-sharepoint.md) 26 | - [Go Live Checklist](https://www.aem.live/docs/go-live-checklist.md) 27 | - [Push Invalidation](https://www.aem.live/docs/setup-byo-cdn-push-invalidation.md) 28 | - [Cloudflare Worker Setup](https://www.aem.live/docs/byo-cdn-cloudflare-worker-setup.md) 29 | - [Akamai Setup](https://www.aem.live/docs/byo-cdn-akamai-setup.md) 30 | - [Fastly Setup](https://www.aem.live/docs/byo-cdn-fastly-setup.md) 31 | - [Redirects](https://www.aem.live/docs/redirects.md) 32 | - [Audit Log](https://www.aem.live/docs/auditlog.md) 33 | - [Using Sidekick](https://www.aem.live/docs/sidekick.md) 34 | - [Sidekick Security](https://www.aem.live/docs/sidekick-security.md) 35 | - [Sidekick for Developers](https://www.aem.live/developer/sidekick-development.md) 36 | - [Sidekick Library](https://www.aem.live/docs/sidekick-library.md) 37 | - [FAQ](https://www.aem.live/docs/faq.md) 38 | 39 | ## API 40 | 41 | - [Admin API](https://www.aem.live/docs/admin.html) 42 | - [aem CLI Reference](https://www.aem.live/developer/cli-reference.md) 43 | 44 | ## Optional 45 | 46 | - [Sidekick Chrome Extension](https://chromewebstore.google.com/detail/aem-sidekick/igkmdomcgoebiipaifhmpfjhbjccggml) 47 | - [Github Bot](https://github.com/apps/aem-code-sync) 48 | - [AEM Tools](https://tools.aem.live/) 49 | - [AEM Labs](https://labs.aem.live/) 50 | - [AEM Status](https://status.adobe.com/products/503489) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helix-website", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "scripts.js", 6 | "scripts": { 7 | "test": "wtr \"./tests/**/*.test.js\" --node-resolve --port=2000 --coverage", 8 | "test:watch": "npm test -- --watch", 9 | "lint:js": "eslint .", 10 | "lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css'", 11 | "lint": "npm run lint:js && npm run lint:css", 12 | "lint:fix": "eslint --fix --ext .js,.jsx . && npm run lint -- --fix" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/adobe/helix-website.git" 17 | }, 18 | "author": "", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/adobe/helix-website/issues" 22 | }, 23 | "homepage": "https://github.com/adobe/helix-website#readme", 24 | "devDependencies": { 25 | "@adobe/rum-distiller": "1.16.3", 26 | "@babel/core": "7.27.4", 27 | "@babel/eslint-parser": "7.27.5", 28 | "@esm-bundle/chai": "4.3.4-fix.0", 29 | "@web/test-runner": "0.20.2", 30 | "@web/test-runner-commands": "0.9.0", 31 | "eslint": "8.57.1", 32 | "eslint-config-airbnb-base": "15.0.0", 33 | "eslint-plugin-import": "2.31.0", 34 | "husky": "9.1.7", 35 | "lint-staged": "16.1.0", 36 | "sinon": "20.0.0", 37 | "stylelint": "16.20.0", 38 | "stylelint-config-standard": "38.0.0" 39 | }, 40 | "lint-staged": { 41 | "*.js": "eslint", 42 | "*.cjs": "eslint", 43 | "*.css": "stylelint" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugins/experimentation/.eslintignore: -------------------------------------------------------------------------------- 1 | src/ued.js -------------------------------------------------------------------------------- /plugins/experimentation/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'airbnb-base', 4 | env: { 5 | browser: true, 6 | }, 7 | parser: '@babel/eslint-parser', 8 | parserOptions: { 9 | allowImportExportEverywhere: true, 10 | sourceType: 'module', 11 | requireConfigFile: false, 12 | }, 13 | rules: { 14 | // allow reassigning param 15 | 'no-param-reassign': [2, { props: false }], 16 | 'linebreak-style': ['error', 'unix'], 17 | 'import/extensions': ['error', { 18 | js: 'always', 19 | }], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /plugins/experimentation/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard" 4 | ], 5 | "rules": { 6 | "declaration-block-no-redundant-longhand-properties": null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugins/experimentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-experimentation", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "lint:js": "eslint src", 7 | "lint:css": "stylelint src/**/*.css", 8 | "lint": "npm run lint:js && npm run lint:css" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/adobe/aem-experimentation.git" 13 | }, 14 | "author": "Adobe Inc.", 15 | "license": "Apache-2.0", 16 | "keywords": [ 17 | "aem", 18 | "experimentation", 19 | "experience", 20 | "decisioning", 21 | "plugin", 22 | "campaigns", 23 | "audiences" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/adobe/aem-experimentation/issues" 27 | }, 28 | "homepage": "https://github.com/adobe/aem-experimentation#readme", 29 | "devDependencies": { 30 | "@babel/eslint-parser": "7.27.5", 31 | "eslint": "8.57.1", 32 | "eslint-config-airbnb-base": "15.0.0", 33 | "eslint-plugin-import": "2.31.0", 34 | "stylelint": "16.20.0", 35 | "stylelint-config-standard": "38.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins/performance.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lighthouse performance instrumentation helper 3 | * (needs a refactor) 4 | */ 5 | 6 | function stamp(message, time = new Date() - performance.timing.navigationStart, type = '') { 7 | if (window.name.includes('performance')) { 8 | // eslint-disable-next-line no-console 9 | const colors = { 10 | general: '#888', 11 | cls: '#c50', 12 | lcp: 'green', 13 | tbt: 'red', 14 | }; 15 | const color = colors[type] || '#888'; 16 | // eslint-disable-next-line no-console 17 | console.log( 18 | `%c${Math.round(time).toString().padStart(5, ' ')}%c %c${type}%c ${message}`, 19 | 'background-color: #444; padding: 3px; border-radius: 3px;', 20 | '', 21 | `background-color: ${color}; padding: 3px 5px; border-radius: 3px;`, 22 | '', 23 | ); 24 | } 25 | } 26 | 27 | function registerPerformanceLogger() { 28 | try { 29 | const polcp = new PerformanceObserver((entryList) => { 30 | const entries = entryList.getEntries(); 31 | entries.forEach((entry) => { 32 | stamp(JSON.stringify(entry), entry.startTime, 'lcp'); 33 | // eslint-disable-next-line no-console 34 | console.log(entry.element); 35 | }); 36 | }); 37 | polcp.observe({ type: 'largest-contentful-paint', buffered: true }); 38 | 39 | const pols = new PerformanceObserver((entryList) => { 40 | const entries = entryList.getEntries(); 41 | entries.forEach((entry) => { 42 | const to = entry.sources[0].currentRect; 43 | const from = entry.sources[0].previousRect; 44 | stamp(`${Math.round(entry.value * 100000) / 100000} 45 | from: ${from.top} ${from.right} ${from.bottom} ${from.left} 46 | to: ${to.top} ${to.right} ${to.bottom} ${to.left}`, entry.startTime, 'cls'); 47 | // eslint-disable-next-line no-console 48 | console.log(entry.sources[0].node); 49 | }); 50 | }); 51 | pols.observe({ type: 'layout-shift', buffered: true }); 52 | 53 | const polt = new PerformanceObserver((list) => { 54 | list.getEntries().forEach((entry) => { 55 | // Log the entry and all associated details. 56 | stamp(JSON.stringify(entry), 'tbt', entry.startTime); 57 | }); 58 | }); 59 | 60 | // Start listening for `longtask` entries to be dispatched. 61 | polt.observe({ type: 'longtask', buffered: true }); 62 | 63 | const pores = new PerformanceObserver((entryList) => { 64 | const entries = entryList.getEntries(); 65 | entries.forEach((entry) => { 66 | stamp(`${entry.name} loaded`, Math.round(entry.startTime + entry.duration)); 67 | }); 68 | }); 69 | 70 | pores.observe({ type: 'resource', buffered: true }); 71 | } catch (e) { 72 | // no output 73 | } 74 | } 75 | 76 | export default function performanceLogger() { 77 | stamp('helix performance logging started'); 78 | registerPerformanceLogger(); 79 | } 80 | -------------------------------------------------------------------------------- /scripts/delayed.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-cycle 2 | import { sampleRUM } from './lib-franklin.js'; 3 | 4 | // Core Web Vitals RUM collection 5 | sampleRUM('cwv'); 6 | 7 | // add more delayed functionality here 8 | 9 | const preloaded = new Set(); 10 | 11 | async function preloadPage(href) { 12 | const hrefURL = new URL(href); 13 | if (hrefURL.origin === window.location.origin) { 14 | // use speculation rules to preload the page 15 | const script = document.createElement('script'); 16 | script.type = 'speculationrules'; 17 | script.textContent = JSON.stringify({ 18 | prerender: [ 19 | { 20 | urls: [`${hrefURL.pathname}${hrefURL.search}`], 21 | }, 22 | ], 23 | }); 24 | document.head.appendChild(script); 25 | } 26 | } 27 | 28 | const HPADDING = 50; // extend horizontal trigger area by 50px around the link 29 | const VPADDING = 2; // extend vertical trigger area by 10px around the link 30 | 31 | function getIsMouseOverForElement(el) { 32 | const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; 33 | const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; 34 | 35 | const rect = el.getBoundingClientRect(); 36 | 37 | const top = rect.top + scrollTop - VPADDING; 38 | const left = rect.left + scrollLeft - HPADDING; 39 | const bottom = rect.bottom + scrollTop + VPADDING; 40 | const right = rect.right + HPADDING; 41 | 42 | return (x, y) => { 43 | const st = document.documentElement.scrollTop || document.body.scrollTop; 44 | const sl = document.documentElement.scrollLeft || document.body.scrollLeft; 45 | 46 | const xs = x + sl; 47 | const ys = y + st; 48 | 49 | if (xs >= left 50 | && xs <= right 51 | && ys >= top 52 | && ys <= bottom) { 53 | return el.href; 54 | } 55 | return null; 56 | }; 57 | } 58 | 59 | const linksIsMouseOver = []; 60 | 61 | document.querySelectorAll('.side-navigation a[href]').forEach((a) => { 62 | const isMouseOver = getIsMouseOverForElement(a); 63 | linksIsMouseOver.push(isMouseOver); 64 | }); 65 | 66 | document.addEventListener('mousemove', (e) => { 67 | const x = e.clientX; 68 | const y = e.clientY; 69 | 70 | linksIsMouseOver.forEach((isMouseOver) => { 71 | const href = isMouseOver(x, y); 72 | if (href && !preloaded.has(href)) { 73 | preloadPage(href); 74 | preloaded.add(href); 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /scripts/indexing-test.js: -------------------------------------------------------------------------------- 1 | document.documentElement.classList.add('indexing-test4'); 2 | const meta = document.createElement('meta'); 3 | meta.name = 'indexing-test'; 4 | meta.content = 'indexing-test4'; 5 | document.head.appendChild(meta); 6 | -------------------------------------------------------------------------------- /scripts/redirects.js: -------------------------------------------------------------------------------- 1 | function globToRegex(glob) { 2 | return new RegExp(`^${glob.replace(/\*/g, '(.*)').replace(/\?/g, '(.)').replace(/\//g, '\\/')}$`); 3 | } 4 | 5 | export function activateRedirects(data) { 6 | return data.map((o) => Object.entries(o) 7 | .reduce((acc, [k, v]) => { 8 | if (k.toLowerCase() === 'from') { 9 | acc.from = globToRegex(v); 10 | } else if (k.toLowerCase() === 'to') { 11 | acc.to = (...replacements) => { 12 | replacements.shift(); 13 | const result = v.replace(/(\$\d+|\*)/g, (matched) => { 14 | if (matched.startsWith('$')) { 15 | return replacements[matched.slice(1) - 1]; 16 | } 17 | if (matched === '*') { 18 | return replacements.shift(); 19 | } 20 | return matched; 21 | }); 22 | return result; 23 | }; 24 | } else if (k.toLowerCase() === 'start') { 25 | acc.start = new Date( 26 | Date.UTC(1899, 11, 30, 0, 0, 0) 27 | + (v - Math.floor(v)) * 86400000 + Math.floor(v) * 86400000, 28 | ); 29 | } 30 | return acc; 31 | }, {})); 32 | } 33 | export async function fetchRedirects(path = '/smart-redirects.json') { 34 | const response = await fetch(path); 35 | const redirects = await response.json(); 36 | if (redirects.data) { 37 | return activateRedirects(redirects.data); 38 | } 39 | return []; 40 | } 41 | 42 | export async function getRedirect(redirects, path, currentURL) { 43 | const redirect = (await redirects) 44 | .filter((r) => typeof r.start === 'undefined' || r.start.getTime() <= Date.now()) 45 | .find((r) => r.from.test(path)); 46 | if (redirect) { 47 | const target = redirect.to(path, ...redirect.from.exec(path).slice(1)); 48 | const targetURL = new URL(target, currentURL); 49 | // Copy all URL parameters from currentURL to targetURL 50 | currentURL.searchParams.forEach((value, key) => { 51 | targetURL.searchParams.set(key, value); 52 | }); 53 | 54 | targetURL.searchParams.set('redirect_from', path); 55 | return targetURL.toString(); 56 | } 57 | return null; 58 | } 59 | 60 | export async function applyRedirects( 61 | redirects = fetchRedirects(), 62 | path = window.location.pathname, 63 | ) { 64 | const redirect = await getRedirect(redirects, path, new URL(window.location.href)); 65 | if (redirect) { 66 | window.location.replace(redirect); 67 | } 68 | return path; 69 | } 70 | -------------------------------------------------------------------------------- /styles/lazy-styles.css: -------------------------------------------------------------------------------- 1 | /* adobe clean */ 2 | @font-face { 3 | font-family: adobe-clean; 4 | src: url("/fonts/adobe-clean-400.woff2") format("woff2"); 5 | font-display: swap; 6 | font-style: normal; 7 | font-weight: 400; 8 | } 9 | 10 | @font-face { 11 | font-family: adobe-clean; 12 | src: url("/fonts/adobe-clean-400-italic.woff2") format("woff2"); 13 | font-display: swap; 14 | font-style: italic; 15 | font-weight: 400; 16 | } 17 | 18 | @font-face { 19 | font-family: adobe-clean; 20 | src: url("/fonts/adobe-clean-700.woff2") format("woff2"); 21 | font-display: swap; 22 | font-style: normal; 23 | font-weight: 700; 24 | } 25 | 26 | @font-face { 27 | font-family: adobe-clean; 28 | src: url("/fonts/adobe-clean-900.woff2") format("woff2"); 29 | font-display: swap; 30 | font-style: normal; 31 | font-weight: 900; 32 | } 33 | 34 | /* source code pro */ 35 | @font-face { 36 | font-family: source-code-pro; 37 | src: url("/fonts/source-code-pro-900.otf.woff2") format("woff2"); 38 | font-display: swap; 39 | font-style: normal; 40 | font-weight: 900; 41 | } 42 | 43 | @font-face { 44 | font-family: source-code-pro; 45 | src: url("/fonts/source-code-pro-400.otf.woff2") format("woff2"); 46 | font-display: swap; 47 | font-style: normal; 48 | font-weight: 400; 49 | } 50 | 51 | @font-face { 52 | font-family: source-code-pro; 53 | src: url("/fonts/source-code-pro-400-italic.otf.woff2") format("woff2"); 54 | font-display: swap; 55 | font-style: italic; 56 | font-weight: 400; 57 | } 58 | 59 | @font-face { 60 | font-family: source-code-pro; 61 | src: url("/fonts/source-code-pro-700.otf.woff2") format("woff2"); 62 | font-display: swap; 63 | font-style: normal; 64 | font-weight: 700; 65 | } 66 | 67 | @font-face { 68 | font-family: source-code-pro; 69 | src: url("/fonts/source-code-pro-900.otf.woff2") format("woff2"); 70 | font-display: swap; 71 | font-style: normal; 72 | font-weight: 900; 73 | } 74 | -------------------------------------------------------------------------------- /tests/blocks/fragment/fragment.mock.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
/tests/scripts/block.mock
4 |
5 |
6 | 7 |
8 |
9 |
/tests/scripts/nope.mock
10 |
11 |
12 | -------------------------------------------------------------------------------- /tests/blocks/fragment/fragment.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* global describe it */ 3 | 4 | import { readFile } from '@web/test-runner-commands'; 5 | import { expect } from '@esm-bundle/chai'; 6 | import setFragment from '../../../blocks/fragment/fragment.js'; 7 | 8 | const mock = await readFile({ path: './fragment.mock.html' }); 9 | document.body.innerHTML = mock; 10 | 11 | describe('Fragment loading', async () => { 12 | it('fragment is loaded', async () => { 13 | const fragment = document.querySelector('.fragment'); 14 | await setFragment(fragment); 15 | const heading = document.querySelector('.h1-from-fragment'); 16 | expect(heading).to.exist; 17 | }); 18 | 19 | it('fragment is not loaded', async () => { 20 | const el = document.querySelector('.fragment.nope'); 21 | await setFragment(el); 22 | const a = document.querySelector('.nope a'); 23 | // fragment not found, not replaced 24 | expect(a).to.exist; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/blocks/publication-time/publication-time.mock.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
prefix for relative time
6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /tests/blocks/publication-time/publication-time.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* global describe it */ 3 | 4 | import { readFile } from '@web/test-runner-commands'; 5 | import { expect } from '@esm-bundle/chai'; 6 | import decorate from '../../../blocks/publication-time/publication-time.js'; 7 | 8 | const mock = await readFile({ path: './publication-time.mock.html' }); 9 | document.body.innerHTML = mock; 10 | 11 | describe('Publication time block', async () => { 12 | it('has a relative-time element', async () => { 13 | const block = document.querySelector('.block'); 14 | decorate(block); 15 | const rt = document.querySelector('relative-time'); 16 | const lastMod = new Date(document.lastModified); 17 | const shortDate = lastMod.toLocaleDateString(); 18 | expect(rt.getAttribute('datetime')).to.equal(lastMod.toISOString()); 19 | expect(rt.textContent).to.equal(shortDate); 20 | expect(block.textContent).to.equal(`prefix for relative time ${shortDate}`); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/blocks/sidekick-generator/sidekick-generator.mock.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Sidekick Configurator

5 |

6 | Form: /tests/blocks/sidekick-generator/sidekick-generator.mock 7 |

8 |

9 | Go 10 |

11 |
12 |
13 |
14 |
15 |

Installation

16 |

Drag the button below to your browser's bookmark bar.

17 |

18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /tests/blocks/sidekick-generator/sidekick-generator.mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 3, 3 | "offset": 0, 4 | "limit": 3, 5 | "data": [ 6 | { 7 | "label": "Repository URL", 8 | "type": "text", 9 | "required": "x", 10 | "name": "giturl", 11 | "placeholder": "https://github.com/..." 12 | }, 13 | { 14 | "label": "Project Name", 15 | "type": "text", 16 | "required": "", 17 | "name": "project" 18 | }, 19 | { 20 | "label": "Shared Key", 21 | "type": "password", 22 | "required": "", 23 | "name": "token" 24 | }, 25 | { 26 | "label": "Helix 3", 27 | "type": "checkbox", 28 | "required": "", 29 | "name": "hlx3", 30 | "placeholder": "", 31 | "checked": "x" 32 | } 33 | ], 34 | ":type": "sheet" 35 | } -------------------------------------------------------------------------------- /tests/blocks/z-pattern/z-pattern.mock.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

How it works

5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |

Create content in Microsoft Word or Google Docs. 14 |

15 |

Create a new folder in Google Drive or Microsoft SharePoint. Then create a new document and put some 16 | content in it.

17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |

Share your folder with Project Helix.

25 |

After you create a page, head back to your folder and share it with Helix.

26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |

Give your site some features.

34 |

Create a new repository in Github. Add styles and scripts to make your site completely bespoke to your 35 | own needs.

36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |

See your site in action.

44 |

After you have added some content, shared with Project Helix, built a repo, and tied your repo to your 45 | conent, you can see your site in action.

46 |
47 |
48 |
49 |
Start the tutorial
50 |
51 |
52 |
-------------------------------------------------------------------------------- /tests/blocks/z-pattern/z-pattern.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* global describe it */ 3 | 4 | import { readFile } from '@web/test-runner-commands'; 5 | import { expect } from '@esm-bundle/chai'; 6 | import init from '../../../blocks/z-pattern/z-pattern.js'; 7 | 8 | const mock = await readFile({ path: './z-pattern.mock.html' }); 9 | document.body.innerHTML = mock; 10 | 11 | describe('Z Pattern', () => { 12 | it('has a heading', () => { 13 | const zPattern = document.querySelector('.z-pattern'); 14 | init(zPattern); 15 | const heading = zPattern.querySelector('.z-pattern-heading'); 16 | expect(heading).to.exist; 17 | }); 18 | 19 | it('has a trailing cta', () => { 20 | const zPattern = document.querySelector('.z-pattern'); 21 | init(zPattern); 22 | const cta = zPattern.querySelector('.z-pattern-cta'); 23 | expect(cta).to.exist; 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/scripts/block.mock.plain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Include me!

5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/scripts/config.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Prop 0
4 |
Plain text
5 |
6 |
7 |
Prop 1
8 |

Paragraph

9 |
10 |
11 |
Prop 2
12 |

First paragraph

Second paragraph

13 |
14 |
15 |
Prop 3
16 |
Link
17 |
18 |
19 |
Prop 4
20 |
21 | First link 22 | Second link 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /tests/scripts/dummy.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | -------------------------------------------------------------------------------- /tests/scripts/head.html: -------------------------------------------------------------------------------- 1 | Foo 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/scripts/media_mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/helix-website/16924d81dfe58d90eebd89f670534173c13aa6e2/tests/scripts/media_mock.png -------------------------------------------------------------------------------- /tests/scripts/mock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/scripts/redirects.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { readFile } from '@web/test-runner-commands'; 3 | import { activateRedirects, getRedirect } from '../../scripts/redirects.js'; 4 | /* eslint-env mocha */ 5 | 6 | document.body.innerHTML = await readFile({ path: './dummy.html' }); 7 | document.head.innerHTML = await readFile({ path: './head.html' }); 8 | 9 | describe.only('Redirects', () => { 10 | it('loads redirects', async () => { 11 | const emptyRedirects = []; 12 | const redirects = await activateRedirects(emptyRedirects); 13 | expect(redirects).to.deep.equal(emptyRedirects); 14 | }); 15 | 16 | it('applies applies a simple redirect', async () => { 17 | const exampleRedirects = [ 18 | { 19 | from: '/foo', 20 | to: '/bar', 21 | start: 1, 22 | }, 23 | ]; 24 | const redirects = await activateRedirects(exampleRedirects); 25 | const currentURL = new URL('https://example.com/foo'); 26 | const redirect = await getRedirect(redirects, '/foo', currentURL); 27 | expect(redirect).to.equal('https://example.com/bar?redirect_from=%2Ffoo'); 28 | }); 29 | 30 | it('applies applies a redirect with a parameter', async () => { 31 | const exampleRedirects = [ 32 | { 33 | from: '/foo/*', 34 | to: '/bar/*', 35 | start: 1, 36 | }, 37 | ]; 38 | const redirects = await activateRedirects(exampleRedirects); 39 | const currentURL = new URL('https://example.com/foo/baz'); 40 | const redirect = await getRedirect(redirects, '/foo/baz', currentURL); 41 | expect(redirect).to.equal('https://example.com/bar/baz?redirect_from=%2Ffoo%2Fbaz'); 42 | }); 43 | 44 | it('applies applies a redirect with multiple parameters', async () => { 45 | const exampleRedirects = [ 46 | { 47 | from: '/foo/*/*', 48 | to: '/bar/*/baz/*', 49 | start: 1, 50 | }, 51 | ]; 52 | const redirects = await activateRedirects(exampleRedirects); 53 | const currentURL = new URL('https://example.com/foo/fifi/qux'); 54 | const redirect = await getRedirect(redirects, '/foo/fifi/qux', currentURL); 55 | expect(redirect).to.equal('https://example.com/bar/fifi/baz/qux?redirect_from=%2Ffoo%2Ffifi%2Fqux'); 56 | }); 57 | 58 | it('applies applies a redirect with multiple parameters and changed order', async () => { 59 | const exampleRedirects = [ 60 | { 61 | from: '/foo/*/*', 62 | to: '/bar/$2/baz/$1', 63 | start: 1, 64 | }, 65 | ]; 66 | const redirects = await activateRedirects(exampleRedirects); 67 | const currentURL = new URL('https://example.com/foo/fifi/qux'); 68 | const redirect = await getRedirect(redirects, '/foo/fifi/qux', currentURL); 69 | expect(redirect).to.equal('https://example.com/bar/qux/baz/fifi?redirect_from=%2Ffoo%2Ffifi%2Fqux'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/scripts/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tools/imports.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | DEPENDENCY="@adobe/rum-distiller" 4 | VERSION_FROM_PACKAGE_JSON=$(node -e "console.log(require('./package.json').devDependencies['@adobe/rum-distiller'])") 5 | 6 | # find all import maps in the project, and replace the import named $1 with the value of $2 7 | 8 | find ./tools/oversight ./tools/rum -name "*.html" | while read file; do 9 | echo "Processing $file upgrading $DEPENDENCY to $VERSION_FROM_PACKAGE_JSON" 10 | # Use sed to replace the import map entry with the new version 11 | sed -i.bak "s|\"@adobe/rum-distiller\": \"https://esm.sh/@adobe/rum-distiller@[0-9.]*\"|\"@adobe/rum-distiller\": \"https://esm.sh/@adobe/rum-distiller@$VERSION_FROM_PACKAGE_JSON\"|g" "$file" 12 | rm "$file.bak" 13 | done -------------------------------------------------------------------------------- /tools/oversight/charts/chart.js: -------------------------------------------------------------------------------- 1 | export default class AbstractChart { 2 | constructor(dataChunks, elems) { 3 | this.chartConfig = {}; 4 | this.dataChunks = dataChunks; 5 | this.elems = elems; 6 | this.chart = {}; 7 | } 8 | 9 | set config(config) { 10 | this.chartConfig = config; 11 | } 12 | 13 | get config() { 14 | return this.chartConfig || {}; 15 | } 16 | 17 | render() { 18 | throw new Error('render method must be implemented', this); 19 | } 20 | 21 | async draw() { 22 | throw new Error('draw method must be implemented', this); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tools/oversight/elements/literal-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | /** 5 | * A custom HTML element to display a list of facets with literal 6 | * values. If a placeholder has been provided, then the explanation 7 | * will be shown after the literal value. 8 | * 9 | * Media Source 10 | * 11 | */ 12 | export default class LiteralFacet extends ListFacet { 13 | // eslint-disable-next-line class-methods-use-this 14 | createLabelHTML(labelText) { 15 | if (this.placeholders && this.placeholders[labelText]) { 16 | return `${labelText}${this.placeholders[labelText]}`; 17 | } 18 | return `${escapeHTML(labelText)}`; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/oversight/elements/number-format.js: -------------------------------------------------------------------------------- 1 | import { stats } from '@adobe/rum-distiller'; 2 | import { findNearestVulgarFraction } from '../utils.js'; 3 | 4 | const { roundToConfidenceInterval, samplingError } = stats; 5 | 6 | export default class NumberFormat extends HTMLElement { 7 | constructor() { 8 | super(); 9 | this.mutationObserver = null; 10 | } 11 | 12 | connectedCallback() { 13 | this.mutationObserver = new MutationObserver(() => { 14 | const fv = this.querySelector('span.formatted-value'); 15 | if (!fv) this.updateState(); 16 | }); 17 | this.updateState(); 18 | this.mutationObserver.observe(this, { childList: true }); 19 | } 20 | 21 | updateState() { 22 | const isPureNumber = !!this.textContent.trim().match(/^\d+(\.\d+)?$/); 23 | const titleValue = parseFloat((this.getAttribute('title') || '').replace(/ .*/g, ''), 10); 24 | const contentValue = parseFloat(this.textContent, 10); 25 | const number = isPureNumber 26 | ? contentValue 27 | : titleValue || contentValue; 28 | 29 | const sampleSize = parseInt(this.getAttribute('sample-size'), 10); 30 | const total = parseInt(this.getAttribute('total'), 10); 31 | const precision = parseInt(this.getAttribute('precision'), 10); 32 | const fuzzy = this.getAttribute('fuzzy') !== 'false'; 33 | const fv = document.createElement('span'); 34 | fv.classList.add('formatted-value'); 35 | if (Number.isNaN(number)) { 36 | fv.textContent = '-'; 37 | this.setAttribute('title', 'no data available'); 38 | this.replaceChildren(fv); 39 | } else { 40 | fv.textContent = roundToConfidenceInterval( 41 | number, 42 | Number.isNaN(sampleSize) ? 0 : sampleSize, 43 | precision, 44 | fuzzy, 45 | ); 46 | this.replaceChildren(fv); 47 | 48 | // set the title to the original number 49 | this.title = number; 50 | if (!Number.isNaN(sampleSize)) { 51 | this.title = `${number} ±${samplingError(number, sampleSize)}`; 52 | } 53 | // remove all classes ending with '-of-total' 54 | Array.from(this.classList) 55 | .filter((cls) => cls.endsWith('-of-total')) 56 | .forEach((cls) => { 57 | this.classList.remove(cls); 58 | }); 59 | // if the total is given, add the percentage class 60 | if (!Number.isNaN(total) && total > 0) { 61 | const fraction = number / total; 62 | this.classList.add(`${findNearestVulgarFraction(fraction)}-of-total`); 63 | } 64 | } 65 | if (this.getAttribute('trend')) { 66 | this.title += ` and ${this.getAttribute('trend')}`; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tools/oversight/elements/thumbnail-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | /** 5 | * A custom HTML element to display a list of facets with thumbnails. 6 | * 7 | * User Agent 8 | *
9 | *
desktop
10 | *
Chrome 90.0.4430.93 (Windows 10)
11 | *
12 | *
13 | */ 14 | export default class ThumbnailFacet extends ListFacet { 15 | // eslint-disable-next-line class-methods-use-this 16 | createLabelHTML(labelText) { 17 | const fileName = labelText.split('/').pop().replace(/\?.*/, ''); 18 | if (labelText.startsWith('https://') && labelText.includes('media_')) { 19 | return `${escapeHTML(fileName)}`; 20 | } if (labelText.startsWith('http') && labelText.match(/\.(jpeg|jpg|gif|png|svg|webp)$/)) { 21 | return `${escapeHTML(fileName)}`; 22 | } 23 | return escapeHTML(labelText); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/oversight/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-rum-explorer", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@adobe/aem-rum-explorer", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "@adobe/rum-distiller": "1.16.3" 12 | } 13 | }, 14 | "node_modules/@adobe/rum-distiller": { 15 | "version": "1.16.3", 16 | "resolved": "https://registry.npmjs.org/@adobe/rum-distiller/-/rum-distiller-1.16.3.tgz", 17 | "integrity": "sha512-cV0VMcIEpveJAlGcJmFmxXwuhHIthx8lABM6e/195XLf6H7UE2V2Bdc5Cby0G32k4Jgk9o10a76gipMOY94PZQ==", 18 | "license": "Apache-2.0" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tools/oversight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-rum-explorer", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "test": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=../../lcov.info --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=../../junit.xml" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "@adobe/rum-distiller": "1.16.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tools/oversight/test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import assert from 'node:assert/strict'; 3 | import { 4 | truncate, escapeHTML, 5 | } from '../utils.js'; 6 | 7 | describe('truncate', () => { 8 | it('truncates to the beginning of the hour', () => { 9 | const time = new Date('2021-01-01T01:30:00Z'); 10 | assert.strictEqual(truncate(time, 'hour'), '2021-01-01T02:00:00+01:00'); 11 | }); 12 | 13 | it('truncates to the beginning of the day', () => { 14 | const time = new Date('2021-01-01T01:30:00Z'); 15 | assert.strictEqual(truncate(time, 'day'), '2021-01-01T00:00:00+01:00'); 16 | }); 17 | 18 | it('truncates to the beginning of the week', () => { 19 | const time = new Date('2021-01-01T01:30:00Z'); 20 | assert.strictEqual(truncate(time, 'week'), '2020-12-27T00:00:00+01:00'); 21 | }); 22 | 23 | it('truncates to the beginning of the week (May 11th)', () => { 24 | const time = new Date('2024-05-11T00:00:00+02:00'); 25 | assert.strictEqual(truncate(time, 'week'), '2024-05-05T00:00:00+02:00'); 26 | }); 27 | 28 | it('truncates to the beginning of the week (May 12th)', () => { 29 | const time = new Date('2024-05-12T00:00:00+02:00'); 30 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 31 | }); 32 | 33 | it('truncates to the beginning of the week (May 13th)', () => { 34 | const time = new Date('2024-05-13T00:00:00+02:00'); 35 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 36 | }); 37 | 38 | it('truncates to the beginning of the week (May 14th)', () => { 39 | const time = new Date('2024-05-14T00:00:00+02:00'); 40 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 41 | }); 42 | }); 43 | 44 | describe('escapeHTML', () => { 45 | it('escapes HTML entities', () => { 46 | assert.strictEqual(escapeHTML(''), '<script>alert("xss")</script>'); 47 | assert.strictEqual(escapeHTML(""), '<script>alert('xss')</script>'); 48 | assert.strictEqual(escapeHTML('
hello
'), '<div>hello</div>'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tools/oversight/website.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 18 | 22 | 26 | 32 | -------------------------------------------------------------------------------- /tools/patches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## do the following to set up the list of patch files 4 | 5 | # OLDBRANCH=$1 6 | # NEWBRANCH=$2 7 | 8 | # git checkout $OLDBRANCH 9 | # git format-patch main 10 | # git checkout main 11 | # git checkout -b $NEWBRANCH 12 | # git checkout $NEWBRANCH 13 | 14 | ## you will now have a directory of files called 0001-your-commit-message.patch 15 | ## that you could, under normal circumstances just apply using `git am`. But 16 | ## we need to change the patch path from tools/rum to tools/oversight so we do 17 | ## that with `sed` 18 | 19 | for patch in 0*-*.patch; do 20 | cat $patch | sed -e "s|tools/rum/|tools/oversight/|g" | git am --3way && rm $patch 21 | done 22 | 23 | ## If any of these patches fails, the script will abort and leave your working copy 24 | ## in a slightly dirty stage. The right way to get out of this is to: 25 | ## fix the merge conflict in your favorite editor 26 | # git add tools/oversight/conflicted.js # tell git that the conflict has been resolved 27 | # git am --continue # reuse the original commit message, author, etc 28 | # rm 0003-the-patch-that-failed.patch # remove the patch file, so that we don't try this again next 29 | # sh patches.sh # apply remaining patches, hope for fewer conflicts 30 | 31 | ## The wrong way to get out of this, but that will still leave you with a clean working copy is 32 | # rm 0*-*.patch # remove all patch files 33 | # git am --abort # forget about this whole `git am` thing -------------------------------------------------------------------------------- /tools/rum/admin/orgs.css: -------------------------------------------------------------------------------- 1 | #rumadmin { 2 | display: flex; 3 | flex-direction: column; 4 | margin: auto; 5 | max-width: 1440px; 6 | } 7 | 8 | #rumadmin h1.title { 9 | margin: 20px 0; 10 | font-size: 2em; 11 | } 12 | 13 | #rumadmin .topbar { 14 | display: flex; 15 | justify-content: space-between; 16 | } 17 | 18 | #rumadmin .topbar #org-selected { 19 | width: 250px; 20 | } 21 | 22 | #rumadmin .topbar #org-actions { 23 | display: none; 24 | } 25 | 26 | #rumadmin .topbar #org-actions .orgkey { 27 | width: 300px; 28 | } 29 | 30 | #rumadmin #org-details { 31 | margin: 20px 0; 32 | } 33 | 34 | #rumadmin #org-details .empty-message { 35 | text-align: center; 36 | font-style: italic; 37 | } 38 | 39 | 40 | #rumadmin #org-details .domain-row { 41 | display: flex; 42 | flex-direction: row; 43 | padding: 8px; 44 | } 45 | 46 | #rumadmin #org-details .domain-row:nth-child(even) { 47 | background-color: #f9f9f9 48 | } 49 | 50 | #org-details .domain-row .cell-checkbox { 51 | align-content: center; 52 | margin: 0 12px 0 0; 53 | } 54 | 55 | #org-details .domain-row .cell-domain { 56 | flex-grow: 1; 57 | } 58 | 59 | #org-details .domain-row .cell-domain-actions { 60 | align-content: center; 61 | } 62 | 63 | #rumadmin .modal { 64 | position: absolute; 65 | left: 0; 66 | top: 0; 67 | bottom: 0; 68 | right: 0; 69 | z-index: 9999; 70 | background: #00000070; 71 | } 72 | 73 | #rumadmin .modal .modal-content { 74 | display: flex; 75 | flex-direction: column; 76 | min-width: 500px; 77 | width: 30%; 78 | margin: clamp(1rem, 12%, min(15vw, 15rem)) auto; 79 | background-color: #fff; 80 | padding: 0 20px 20px; 81 | } 82 | 83 | #rumadmin .modal .modal-content span.cancel { 84 | align-self: flex-end; 85 | padding: 0 8px; 86 | margin: 8px -8px 0; 87 | cursor: pointer; 88 | } 89 | 90 | #rumadmin .modal .modal-content h2.modal-title { 91 | font-size: 1.5em; 92 | margin: -20px 0 20px; 93 | pointer-events: none; 94 | } 95 | 96 | #rumadmin .modal .modal-content .content { 97 | display: flex; 98 | flex-direction: column; 99 | gap: 8px; 100 | } 101 | 102 | #rumadmin .modal .modal-content .modal-actions { 103 | margin-top: 20px; 104 | } -------------------------------------------------------------------------------- /tools/rum/admin/orgs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RUM Admin | Orgs | AEM Live 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Manage Orgs

19 |
20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

No org selected

36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tools/rum/charts/chart.js: -------------------------------------------------------------------------------- 1 | export default class AbstractChart { 2 | constructor(dataChunks, elems) { 3 | this.chartConfig = {}; 4 | this.dataChunks = dataChunks; 5 | this.elems = elems; 6 | this.chart = {}; 7 | } 8 | 9 | set config(config) { 10 | this.chartConfig = config; 11 | } 12 | 13 | render() { 14 | throw new Error('render method must be implemented', this); 15 | } 16 | 17 | async draw() { 18 | throw new Error('draw method must be implemented', this); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/rum/elements/facetsidebar.js: -------------------------------------------------------------------------------- 1 | export default class FacetSidebar extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.eventListeners = {}; 5 | this.elems = {}; 6 | this.predefinedFacets = []; 7 | } 8 | 9 | set data(data) { 10 | this.dataChunks = data; 11 | } 12 | 13 | connectedCallback() { 14 | this.initDOM(); 15 | } 16 | 17 | initDOM() { 18 | const predefinedFacets = Array.from(this.querySelectorAll(':scope > *')); 19 | //
20 | //
21 | // 22 | //
23 | // 25 | //
; 26 | const filters = document.createElement('div'); 27 | filters.className = 'filters'; 28 | const quickFilter = document.createElement('div'); 29 | quickFilter.className = 'quick-filter'; 30 | const filterInput = document.createElement('input'); 31 | filterInput.type = 'text'; 32 | filterInput.id = 'filter'; 33 | filterInput.placeholder = 'Type to filter...'; 34 | quickFilter.append(filterInput); 35 | this.append(quickFilter); 36 | const facetsElement = document.createElement('aside'); 37 | facetsElement.id = 'facets'; 38 | this.append(facetsElement); 39 | 40 | this.elems.filterInput = filterInput; 41 | this.elems.facetsElement = facetsElement; 42 | 43 | predefinedFacets.forEach((facet) => { 44 | this.elems.facetsElement.append(facet); 45 | }); 46 | } 47 | 48 | updateFacets(mode) { 49 | const filterTags = document.querySelector('.filter-tags'); 50 | filterTags.textContent = ''; 51 | const addFilterTag = (name, value) => { 52 | const tag = document.createElement('span'); 53 | if (value) tag.textContent = `${name}: ${value}`; 54 | else tag.textContent = `${name}`; 55 | tag.classList.add(`filter-tag-${name}`); 56 | filterTags.append(tag); 57 | }; 58 | 59 | if (this.elems.filterInput.value) addFilterTag('text', this.elems.filterInput.value); 60 | 61 | const existingFacetElements = Array.from(this.elems.facetsElement.children); 62 | existingFacetElements.forEach((facet) => { 63 | facet.setAttribute('mode', 'hidden'); 64 | }); 65 | 66 | const keys = Object.keys(this.dataChunks.facets) 67 | // only show facets that have no decorators or are not hidden 68 | .filter((key) => key !== 'filter'); 69 | keys.forEach((facetName) => { 70 | const facetEl = this.querySelector(`[facet="${facetName}"]`); 71 | // eslint-disable-next-line no-console 72 | console.assert(facetEl, `Facet ${facetName} not found in provided UI elements.`); 73 | 74 | if (facetEl) facetEl.setAttribute('mode', mode || 'default'); 75 | if (facetEl) this.elems.facetsElement.append(facetEl); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tools/rum/elements/file-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | /** 5 | * A custom HTML element to display a list of facets with literal 6 | * values. If a placeholder has been provided, then the explanation 7 | * will be shown after the literal value. 8 | * 9 | * Media Source 10 | * 11 | */ 12 | export default class FileFacet extends ListFacet { 13 | // eslint-disable-next-line class-methods-use-this 14 | createLabelHTML(labelText) { 15 | return `${escapeHTML(labelText)}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tools/rum/elements/link-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | function labelURLParts(url) { 5 | const u = new URL(url); 6 | return ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash'] 7 | .reduce((acc, part) => `${acc}${u[part]}`, ''); 8 | } 9 | 10 | /** 11 | * A custom HTML element to display a list of facets with links. 12 | * 13 | * Referrer 14 | * 15 | */ 16 | export default class LinkFacet extends ListFacet { 17 | // eslint-disable-next-line class-methods-use-this 18 | createLabelHTML(labelText) { 19 | const thumbnailAtt = this.getAttribute('thumbnail') === 'true'; 20 | const faviconAtt = this.getAttribute('favicon') === 'true'; 21 | if (thumbnailAtt && labelText.startsWith('https://')) { 22 | const u = new URL('https://www.aem.live/tools/rum/_ogimage'); 23 | u.searchParams.set('proxyurl', labelText); 24 | return `${labelURLParts(labelText)}`; 25 | } 26 | if (thumbnailAtt && (labelText.startsWith('http://') || labelText.startsWith('https://') || labelText.startsWith('android-app://'))) { 27 | const u = new URL('https://www.aem.live/tools/rum/_ogimage'); 28 | u.searchParams.set('proxyurl', labelText); 29 | return ` 30 | thumbnail image for ${labelText} 31 | ${labelURLParts(labelText)}`; 32 | } 33 | if (labelText.startsWith('https://') || labelText.startsWith('http://')) { 34 | return `${labelText}`; 35 | } 36 | if (labelText.startsWith('referrer:')) { 37 | return `${labelText.replace('referrer:', '')}`; 38 | } 39 | const currentURL = new URL(window.location.href); 40 | const domain = currentURL.searchParams.get('domain'); 41 | if (labelText.startsWith('navigate:')) { 42 | return `navigate from ${labelText.replace('navigate:', '')}`; 43 | } 44 | if (this.placeholders && this.placeholders[labelText]) { 45 | return (`${this.placeholders[labelText]} [${labelText}]`); 46 | } 47 | if (domain.endsWith(':all')) { 48 | currentURL.searchParams.set('domain', labelText); 49 | currentURL.searchParams.delete('domainkey'); 50 | return `${labelText}`; 51 | } 52 | return escapeHTML(labelText); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tools/rum/elements/literal-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | /** 5 | * A custom HTML element to display a list of facets with literal 6 | * values. If a placeholder has been provided, then the explanation 7 | * will be shown after the literal value. 8 | * 9 | * Media Source 10 | * 11 | */ 12 | export default class LiteralFacet extends ListFacet { 13 | // eslint-disable-next-line class-methods-use-this 14 | createLabelHTML(labelText) { 15 | if (this.placeholders && this.placeholders[labelText]) { 16 | return `${labelText}${this.placeholders[labelText]}`; 17 | } 18 | return `${escapeHTML(labelText)}`; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/rum/elements/thumbnail-facet.js: -------------------------------------------------------------------------------- 1 | import { escapeHTML } from '../utils.js'; 2 | import ListFacet from './list-facet.js'; 3 | 4 | /** 5 | * A custom HTML element to display a list of facets with thumbnails. 6 | * 7 | * User Agent 8 | *
9 | *
desktop
10 | *
Chrome 90.0.4430.93 (Windows 10)
11 | *
12 | *
13 | */ 14 | export default class ThumbnailFacet extends ListFacet { 15 | // eslint-disable-next-line class-methods-use-this 16 | createLabelHTML(labelText) { 17 | const fileName = labelText.split('/').pop().replace(/\?.*/, ''); 18 | const isMediaStr = labelText.startsWith('media_'); 19 | if ((labelText.startsWith('https://') && labelText.includes('media_')) || isMediaStr) { 20 | let src = labelText; 21 | if (isMediaStr) { 22 | const domain = new URL(window.location.href).searchParams.get('domain'); 23 | // cannot work in all cases (context path...), but good enough for now 24 | src = `https://${domain}/${labelText}`; 25 | } 26 | return `${escapeHTML(fileName)}`; 27 | } if (labelText.startsWith('http') && labelText.match(/\.(jpeg|jpg|gif|png|svg|webp)$/)) { 28 | return `${escapeHTML(fileName)}`; 29 | } 30 | return escapeHTML(labelText); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools/rum/elements/vitals-facet.js: -------------------------------------------------------------------------------- 1 | import { toHumanReadable } from '../utils.js'; 2 | 3 | export default class VitalsFacet extends HTMLElement { 4 | constructor() { 5 | super(); 6 | this.placeholders = {}; 7 | } 8 | 9 | get dataChunks() { 10 | return this.parentElement.parentElement.dataChunks; 11 | } 12 | 13 | connectedCallback() { 14 | if (this.dataChunks) this.update(); 15 | } 16 | 17 | update() { 18 | const facetName = this.getAttribute('facet'); 19 | 20 | const u = new URL(window.location.href); 21 | const { searchParams } = u; 22 | 23 | const facetEntries = this.dataChunks.facets[facetName]; 24 | 25 | const fieldSet = this.querySelector('fieldset') || document.createElement('fieldset'); 26 | const legendText = this.querySelector('legend')?.textContent || facetName; 27 | 28 | fieldSet.textContent = ''; 29 | 30 | facetEntries.forEach(({ name, value, weight }) => { 31 | const div = document.createElement('div'); 32 | div.classList.add('facet-entry'); 33 | div.dataset.name = name; 34 | div.dataset.value = value; 35 | div.dataset.weight = weight; 36 | const label = document.createElement('label'); 37 | label.textContent = `${value.replace(/(good|poor|ni)(.+)$/, (_, m, metric) => { 38 | if (m === 'ni') return `${metric} ni `; 39 | return `${metric} ${m}`; 40 | })}: ${toHumanReadable(weight)}`; 41 | label.htmlFor = `${name}=${value}`; 42 | const input = document.createElement('input'); 43 | input.type = 'checkbox'; 44 | input.id = `${name}=${value}`; 45 | input.value = value; 46 | input.checked = searchParams.has(facetName) && searchParams.getAll(facetName).includes(value); 47 | 48 | input.addEventListener('change', (evt) => { 49 | evt.stopPropagation(); 50 | this.parentElement.parentElement.dispatchEvent(new Event('facetchange')); 51 | }); 52 | 53 | div.append(input, label); 54 | 55 | fieldSet.append(div); 56 | }); 57 | 58 | const legend = this.querySelector('legend') || document.createElement('legend'); 59 | legend.textContent = legendText; 60 | fieldSet.append(legend); 61 | // append fieldset 62 | this.append(fieldSet); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tools/rum/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-rum-explorer", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@adobe/aem-rum-explorer", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "@adobe/rum-distiller": "1.16.3" 12 | } 13 | }, 14 | "node_modules/@adobe/rum-distiller": { 15 | "version": "1.16.3", 16 | "resolved": "https://registry.npmjs.org/@adobe/rum-distiller/-/rum-distiller-1.16.3.tgz", 17 | "integrity": "sha512-cV0VMcIEpveJAlGcJmFmxXwuhHIthx8lABM6e/195XLf6H7UE2V2Bdc5Cby0G32k4Jgk9o10a76gipMOY94PZQ==", 18 | "license": "Apache-2.0" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tools/rum/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-rum-explorer", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "test": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=../../lcov.info --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=../../junit.xml" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "@adobe/rum-distiller": "1.16.3" 10 | } 11 | } -------------------------------------------------------------------------------- /tools/rum/test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | import assert from 'node:assert/strict'; 3 | import { truncate, escapeHTML } from '../utils.js'; 4 | 5 | describe('truncate', () => { 6 | it('truncates to the beginning of the hour', () => { 7 | const time = new Date('2021-01-01T01:30:00Z'); 8 | assert.strictEqual(truncate(time, 'hour'), '2021-01-01T02:00:00+01:00'); 9 | }); 10 | 11 | it('truncates to the beginning of the day', () => { 12 | const time = new Date('2021-01-01T01:30:00Z'); 13 | assert.strictEqual(truncate(time, 'day'), '2021-01-01T00:00:00+01:00'); 14 | }); 15 | 16 | it('truncates to the beginning of the week', () => { 17 | const time = new Date('2021-01-01T01:30:00Z'); 18 | assert.strictEqual(truncate(time, 'week'), '2020-12-27T00:00:00+01:00'); 19 | }); 20 | 21 | it('truncates to the beginning of the week (May 11th)', () => { 22 | const time = new Date('2024-05-11T00:00:00+02:00'); 23 | assert.strictEqual(truncate(time, 'week'), '2024-05-05T00:00:00+02:00'); 24 | }); 25 | 26 | it('truncates to the beginning of the week (May 12th)', () => { 27 | const time = new Date('2024-05-12T00:00:00+02:00'); 28 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 29 | }); 30 | 31 | it('truncates to the beginning of the week (May 13th)', () => { 32 | const time = new Date('2024-05-13T00:00:00+02:00'); 33 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 34 | }); 35 | 36 | it('truncates to the beginning of the week (May 14th)', () => { 37 | const time = new Date('2024-05-14T00:00:00+02:00'); 38 | assert.strictEqual(truncate(time, 'week'), '2024-05-12T00:00:00+02:00'); 39 | }); 40 | }); 41 | 42 | describe('escapeHTML', () => { 43 | it('escapes HTML entities', () => { 44 | assert.strictEqual(escapeHTML(''), '<script>alert("xss")</script>'); 45 | assert.strictEqual(escapeHTML(""), '<script>alert('xss')</script>'); 46 | assert.strictEqual(escapeHTML('
hello
'), '<div>hello</div>'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tools/rum/website.svg: -------------------------------------------------------------------------------- 1 | 8 | 14 | 18 | 22 | 26 | 32 | -------------------------------------------------------------------------------- /tools/sidekick/lib/process-queue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | // Copy of: https://github.com/adobe/helix-shared/blob/main/packages/helix-shared-process-queue/src/process-queue.js 14 | 15 | /** 16 | * Simple dequeing iterator. 17 | * @param queue 18 | * @returns {Generator<*, void, *>} 19 | */ 20 | function* dequeue(queue) { 21 | while (queue.length) { 22 | yield queue.shift(); 23 | } 24 | } 25 | 26 | /** 27 | * Processes the given queue concurrently. The handler functions can add more items to the queue 28 | * if needed. 29 | * 30 | * @param {Iterable|Array} queue A list of tasks 31 | * @param {ProcessQueueHandler} fn A handler function `fn(task:any, queue:array, results:array)` 32 | * @param {number} [maxConcurrent = 8] Concurrency level 33 | * @returns {Promise<[]>} the results 34 | */ 35 | export async function processQueue(queue, fn, maxConcurrent = 8) { 36 | if (typeof queue !== 'object') { 37 | throw Error('invalid queue argument: iterable expected'); 38 | } 39 | 40 | const running = []; 41 | const results = []; 42 | 43 | const handler = (entry) => { 44 | const task = fn(entry, queue, results); 45 | if (task?.then) { 46 | running.push(task); 47 | task 48 | .then((r) => { 49 | if (r !== undefined) { 50 | results.push(r); 51 | } 52 | }) 53 | .catch(() => {}) 54 | .finally(() => { 55 | running.splice(running.indexOf(task), 1); 56 | }); 57 | } else if (task !== undefined) { 58 | results.push(task); 59 | } 60 | }; 61 | 62 | const iter = Array.isArray(queue) 63 | ? dequeue(queue) 64 | : queue; 65 | if (!iter || !('next' in iter)) { 66 | throw Error('invalid queue argument: iterable expected'); 67 | } 68 | 69 | for await (const value of iter) { 70 | while (running.length >= maxConcurrent) { 71 | // eslint-disable-next-line no-await-in-loop 72 | await Promise.race(running); 73 | } 74 | handler(value); 75 | } 76 | // wait until remaining tasks have completed 77 | await Promise.all(running); 78 | return results; 79 | } 80 | -------------------------------------------------------------------------------- /tools/sidekick/library/events/events.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | export const APP_EVENTS = { 14 | TOAST: 'Toast', 15 | LIBRARY_LOADED: 'LibraryLoaded', 16 | PLUGIN_LOADED: 'PluginLoaded', 17 | PLUGIN_UNLOADED: 'PluginUnloaded', 18 | LOCALE_SET: 'LocaleSet', 19 | SEARCH_UPDATED: 'SearchUpdated', 20 | ON_ACTION: 'OnAction', 21 | }; 22 | 23 | export const PLUGIN_EVENTS = { 24 | TOAST: 'Toast', 25 | SHOW_LOADER: 'ShowLoader', 26 | HIDE_LOADER: 'HideLoader', 27 | }; 28 | -------------------------------------------------------------------------------- /tools/sidekick/library/focus-visible.js: -------------------------------------------------------------------------------- 1 | var y=(s,d)=>()=>(d||s((d={exports:{}}).exports,d),d.exports);var w=y((L,b)=>{var u;u=function(){function s(o){var i=!0,a=!1,m=null,p={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function c(e){return!!(e&&e!==document&&e.nodeName!=="HTML"&&e.nodeName!=="BODY"&&"classList"in e&&"contains"in e.classList)}function v(e){e.classList.contains("focus-visible")||(e.classList.add("focus-visible"),e.setAttribute("data-focus-visible-added",""))}function r(e){i=!1}function l(){document.addEventListener("mousemove",t),document.addEventListener("mousedown",t),document.addEventListener("mouseup",t),document.addEventListener("pointermove",t),document.addEventListener("pointerdown",t),document.addEventListener("pointerup",t),document.addEventListener("touchmove",t),document.addEventListener("touchstart",t),document.addEventListener("touchend",t)}function t(e){e.target.nodeName&&e.target.nodeName.toLowerCase()==="html"||(i=!1,document.removeEventListener("mousemove",t),document.removeEventListener("mousedown",t),document.removeEventListener("mouseup",t),document.removeEventListener("pointermove",t),document.removeEventListener("pointerdown",t),document.removeEventListener("pointerup",t),document.removeEventListener("touchmove",t),document.removeEventListener("touchstart",t),document.removeEventListener("touchend",t))}document.addEventListener("keydown",function(e){e.metaKey||e.altKey||e.ctrlKey||(c(o.activeElement)&&v(o.activeElement),i=!0)},!0),document.addEventListener("mousedown",r,!0),document.addEventListener("pointerdown",r,!0),document.addEventListener("touchstart",r,!0),document.addEventListener("visibilitychange",function(e){document.visibilityState==="hidden"&&(a&&(i=!0),l())},!0),l(),o.addEventListener("focus",function(e){var n,f,E;c(e.target)&&(i||(n=e.target,f=n.type,(E=n.tagName)==="INPUT"&&p[f]&&!n.readOnly||E==="TEXTAREA"&&!n.readOnly||n.isContentEditable))&&v(e.target)},!0),o.addEventListener("blur",function(e){var n;c(e.target)&&(e.target.classList.contains("focus-visible")||e.target.hasAttribute("data-focus-visible-added"))&&(a=!0,window.clearTimeout(m),m=window.setTimeout(function(){a=!1},100),(n=e.target).hasAttribute("data-focus-visible-added")&&(n.classList.remove("focus-visible"),n.removeAttribute("data-focus-visible-added")))},!0),o.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&o.host?o.host.setAttribute("data-js-focus-visible",""):o.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window<"u"&&typeof document<"u"){var d;window.applyFocusVisiblePolyfill=s;try{d=new CustomEvent("focus-visible-polyfill-ready")}catch(o){(d=document.createEvent("CustomEvent")).initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(d)}typeof document<"u"&&s(document)},typeof L=="object"&&typeof b<"u"?u():typeof define=="function"&&define.amd?define(u):u()});export default w(); 2 | //# sourceMappingURL=focus-visible.js.map 3 | -------------------------------------------------------------------------------- /tools/sidekick/library/locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": { 3 | "message": "AEM Sidekick Library" 4 | }, 5 | "unknownPlugin": { 6 | "message": "Unknown plugin" 7 | }, 8 | "invalidConfiguration": { 9 | "message": "Invalid Configuration" 10 | }, 11 | "invalidConfigurationDescription": { 12 | "message": "The library is misconfigured" 13 | }, 14 | "errorLoadingPlugin": { 15 | "message": "Error loading plugin" 16 | }, 17 | "errorLoadingLibraryJSON": { 18 | "message": "Error loading library JSON" 19 | }, 20 | "generativeTextPrompt": { 21 | "message": "What would you like to say?" 22 | }, 23 | "generativeTextCTA": { 24 | "message": "Generate" 25 | }, 26 | "search": { 27 | "message": "Search" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tools/sidekick/library/plugins/blocks/blocks.css: -------------------------------------------------------------------------------- 1 | .message-container { 2 | width: 100%; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | margin-top: 65px; 7 | } 8 | 9 | .block-library { 10 | height: 100%; 11 | } 12 | 13 | sp-split-view { 14 | height: 100%; 15 | } 16 | 17 | .menu { 18 | height: 100%; 19 | overflow-y: hidden; 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | .menu .list-container { 25 | height: 100%; 26 | overflow-y: auto; 27 | } 28 | 29 | .search { 30 | padding: 10px; 31 | } 32 | 33 | .search sp-search { 34 | width: 100%; 35 | } 36 | 37 | .details-container { 38 | width: auto; 39 | display: flex; 40 | flex-direction: column; 41 | background-color: var(--spectrum-global-color-gray-75); 42 | overflow: hidden; 43 | } 44 | 45 | .details-container .details { 46 | height: 100%; 47 | padding: 0 16px 0 16px; 48 | overflow-y: auto; 49 | } 50 | 51 | .details-container .actions { 52 | display: flex; 53 | justify-content: flex-end; 54 | } 55 | 56 | .frame-view { 57 | outline: 1px solid transparent; 58 | margin: 0 auto; 59 | background-color: #fff; 60 | transition: 0.2s ease-in-out; 61 | width: 100%; 62 | overflow: hidden; 63 | } 64 | 65 | .frame-view block-renderer { 66 | border: 0; 67 | width: 100%; 68 | height: 100%; 69 | } 70 | 71 | 72 | .frame-view { 73 | height: 100%; 74 | } 75 | 76 | .view { 77 | display: flex; 78 | flex-direction: column; 79 | overflow-y: hidden; 80 | } 81 | 82 | .view .action-bar { 83 | display: flex; 84 | flex-direction: column; 85 | align-items: center; 86 | } 87 | 88 | .view .action-bar sp-action-group{ 89 | padding: 10px; 90 | } 91 | 92 | .details-container .action-bar { 93 | display: flex; 94 | justify-content: space-between; 95 | align-items: center; 96 | background-color: var(--spectrum-global-color-gray-75); 97 | padding: 10px 16px; 98 | } 99 | 100 | .details-container .action-bar h3 { 101 | margin: 0; 102 | } -------------------------------------------------------------------------------- /tools/sidekick/library/utils/rum.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | export function sampleRUM(checkpoint, data = {}) { 14 | sampleRUM.defer = sampleRUM.defer || []; 15 | const defer = (fnname) => { 16 | sampleRUM[fnname] = sampleRUM[fnname] 17 | || ((...args) => sampleRUM.defer.push({ fnname, args })); 18 | }; 19 | sampleRUM.drain = sampleRUM.drain 20 | || ((dfnname, fn) => { 21 | sampleRUM[dfnname] = fn; 22 | sampleRUM.defer 23 | .filter(({ fnname }) => dfnname === fnname) 24 | .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); 25 | }); 26 | sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; 27 | defer('observe'); 28 | defer('cwv'); 29 | try { 30 | window.hlx = window.hlx || {}; 31 | if (!window.hlx.rum) { 32 | const usp = new URLSearchParams(window.location.search); 33 | const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. 34 | // eslint-disable-next-line no-bitwise 35 | const hashCode = s => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); 36 | const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; 37 | const random = Math.random(); 38 | const isSelected = (random * weight < 1); 39 | // eslint-disable-next-line object-curly-newline 40 | window.hlx.rum = { weight, id, random, isSelected, sampleRUM }; 41 | } 42 | const { weight, id } = window.hlx.rum; 43 | if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { 44 | const sendPing = (pdata = data) => { 45 | // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define 46 | const body = JSON.stringify({ weight, id, referer: window.location.href, checkpoint, ...data }); 47 | const url = `https://rum.hlx.page/.rum/${weight}`; 48 | // eslint-disable-next-line no-unused-expressions 49 | navigator.sendBeacon(url, body); 50 | // eslint-disable-next-line no-console 51 | console.debug(`ping:${checkpoint}`, pdata); 52 | }; 53 | sendPing(data); 54 | if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } 55 | } 56 | } catch (error) { 57 | // something went wrong 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tools/sidekick/view/json/json.css: -------------------------------------------------------------------------------- 1 | .hlx-sk-view { 2 | background-color: var(--hlx-sk-bg); 3 | color: var(--hlx-sk-color); 4 | padding: 10px 20px; 5 | margin: 0; 6 | } 7 | 8 | .hlx-sk-view h2 { 9 | margin: 10px 0; 10 | font-size: 1.25rem; 11 | min-height: 1.25rem; 12 | } 13 | 14 | .hlx-sk-view table { 15 | width: 100%; 16 | border-collapse: collapse; 17 | text-align: left; 18 | margin-bottom: 20px; 19 | } 20 | 21 | .hlx-sk-view table tr { 22 | vertical-align: top; 23 | } 24 | 25 | 26 | .hlx-sk-view table th { 27 | background-color: var(--hlx-sk-bg); 28 | padding: 5px; 29 | } 30 | 31 | .hlx-sk-view table, 32 | .hlx-sk-view table tr, 33 | .hlx-sk-view table th, 34 | .hlx-sk-view table td { 35 | border: solid 1px var(--hlx-sk-special-view-table-border); 36 | } 37 | 38 | .hlx-sk-view table td > div { 39 | padding: 5px; 40 | min-height: 0.875rem; 41 | font-size: 0.875rem; 42 | } 43 | 44 | .hlx-sk-view table td > div.image, 45 | .hlx-sk-view table td > div.video { 46 | padding: 0; 47 | } 48 | 49 | .hlx-sk-view table td > div img, 50 | .hlx-sk-view table td > div video { 51 | width: 100%; 52 | max-width: 240px; 53 | } 54 | 55 | .hlx-sk-view table td > div ul { 56 | margin: 0; 57 | padding-inline-start: 20px; 58 | } 59 | 60 | .hlx-sk-view table td > div li { 61 | padding: 0; 62 | } 63 | 64 | .hlx-sk-view table td > div a:any-link { 65 | color: var(--hlx-sk-color); 66 | text-decoration: none; 67 | } 68 | 69 | .hlx-sk-view table td > div a:hover { 70 | text-decoration: underline; 71 | } 72 | -------------------------------------------------------------------------------- /tools/sidekick/view/json/json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /utils/console.js: -------------------------------------------------------------------------------- 1 | export default function debug(message) { 2 | const { hostname } = window.location; 3 | const usp = new URLSearchParams(window.location.search); 4 | if (usp.has('helix-debug') || hostname === 'localhost') { 5 | // eslint-disable-next-line no-console 6 | console.log(message); 7 | return message; 8 | } 9 | return null; 10 | } 11 | -------------------------------------------------------------------------------- /utils/env.js: -------------------------------------------------------------------------------- 1 | function setHelixEnv(name) { 2 | if (name) { 3 | sessionStorage.setItem('helix-env', name); 4 | } else { 5 | sessionStorage.removeItem('helix-env'); 6 | } 7 | } 8 | 9 | export async function displayEnv() { 10 | try { 11 | const usp = new URLSearchParams(window.location.search); 12 | const env = usp.get('helix-env'); 13 | if (env) { setHelixEnv(env); } 14 | } catch (e) { 15 | const { debug } = await import('./console.js'); 16 | debug(`display env failed: ${e.message}`); 17 | } 18 | } 19 | 20 | /** 21 | * Get the current Helix environment 22 | * @returns {Object} the env object 23 | */ 24 | export function getEnv() { 25 | let envName = sessionStorage.getItem('helix-env'); 26 | if (!envName) envName = 'prod'; 27 | const envs = { 28 | stage: { 29 | ims: 'stg1', 30 | adminconsole: 'stage.adminconsole.adobe.com', 31 | account: 'stage.account.adobe.com', 32 | target: false, 33 | }, 34 | prod: { 35 | ims: 'prod', 36 | adminconsole: 'adminconsole.adobe.com', 37 | account: 'account.adobe.com', 38 | target: true, 39 | }, 40 | }; 41 | const env = envs[envName]; 42 | if (env) { 43 | env.name = envName; 44 | } 45 | return env; 46 | } 47 | -------------------------------------------------------------------------------- /utils/property.js: -------------------------------------------------------------------------------- 1 | const getProperty = (object, objectPath) => { 2 | const pathArray = objectPath.split('.'); 3 | return pathArray.reduce((acc, part) => acc && acc[part], object); 4 | }; 5 | 6 | const getObjectProperty = (property, timeout) => new Promise((resolve) => { 7 | let i = 0; 8 | const interval = 10; 9 | const refreshId = setInterval(() => { 10 | const prop = getProperty(window, property); 11 | if (prop !== null && typeof prop !== 'undefined') { 12 | resolve(prop); 13 | clearInterval(refreshId); 14 | } else if (i >= timeout) { 15 | resolve(null); 16 | clearInterval(refreshId); 17 | } 18 | i += interval; 19 | }, interval); 20 | }); 21 | 22 | export default getObjectProperty; 23 | -------------------------------------------------------------------------------- /utils/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create an element with ID, class, children, and attributes 3 | * @param {String} tag the tag nav of the element 4 | * @param {Object} attributes the attributes of the tag 5 | * @param {HTMLElement} html the content of the element 6 | * @returns {HTMLElement} the element created 7 | */ 8 | export default function createTag(tag, attributes, html) { 9 | const el = document.createElement(tag); 10 | if (html) { 11 | if (html instanceof HTMLElement) { 12 | el.append(html); 13 | } else { 14 | el.insertAdjacentHTML('beforeend', html); 15 | } 16 | } 17 | if (attributes) { 18 | Object.keys(attributes).forEach((key) => { 19 | el.setAttribute(key, attributes[key]); 20 | }); 21 | } 22 | return el; 23 | } 24 | --------------------------------------------------------------------------------