├── .example.env ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .stylelintrc.json ├── README.md ├── dist ├── .vite │ └── ssr-manifest.json ├── entry-server.js ├── favicon-96x96.png ├── images │ ├── apple-touch-icon.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── favicon │ │ ├── apple-touch-icon.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── site.webmanifest │ │ ├── web-app-manifest-192x192.png │ │ └── web-app-manifest-512x512.png │ ├── logo.png │ ├── logo.svg │ ├── paint_icon.png │ ├── site.webmanifest │ ├── squiz-logo.png │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png ├── main.css ├── main.js ├── paint_icon.png └── server.js ├── dxp ├── 01_compilers │ ├── dxp-compiler.js │ ├── manifest-compiler.js │ ├── manifests.json │ └── vite-plugins.js ├── component-service │ ├── accordion │ │ ├── README.md │ │ ├── css │ │ │ └── accordion.scss │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── banner │ │ ├── README.md │ │ ├── css │ │ │ └── banner.scss │ │ ├── example-data │ │ │ ├── example-video.data.json │ │ │ └── example.data.json │ │ ├── js │ │ │ ├── frontend.js │ │ │ └── frontend.test.js │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── blockquote │ │ ├── README.md │ │ ├── css │ │ │ └── blockquote.scss │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── cards-manual │ │ ├── README.md │ │ ├── css │ │ │ └── cards-manual.scss │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── cards-matrix │ │ ├── README.md │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ ├── mock-data │ │ │ ├── mockDataWrapper.js │ │ │ └── mockResolvedData.js │ │ └── preview.html │ ├── cards-root │ │ ├── README.md │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ ├── mock-data │ │ │ ├── mockDataWrapper.js │ │ │ └── mockRootNodeData.js │ │ └── preview.html │ ├── dynamic-header │ │ ├── README.md │ │ ├── css │ │ │ └── dynamic-header.scss │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── icon-card │ │ ├── README.md │ │ ├── css │ │ │ └── icon-card.scss │ │ ├── example.data.json │ │ ├── iconMap.js │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── image-text-row │ │ ├── README.md │ │ ├── css │ │ │ └── single-column.scss │ │ ├── example-data │ │ │ ├── example-switched.data.json │ │ │ └── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ ├── key-statistics │ │ ├── README.md │ │ ├── css │ │ │ └── key-statistics.scss │ │ ├── example.data.json │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html │ └── testimonials │ │ ├── README.md │ │ ├── css │ │ └── testimonials.scss │ │ ├── example.data.json │ │ ├── js │ │ ├── frontend.js │ │ └── frontend.test.js │ │ ├── main.js │ │ ├── main.test.js │ │ ├── manifest.json │ │ └── preview.html └── utils │ ├── html.js │ └── xss.js ├── eslint.config.js ├── index.html ├── installing-git.md ├── package-lock.json ├── package.json ├── server.js ├── src ├── entry-server.js ├── global_components │ ├── content │ │ ├── article.js │ │ ├── getting-started.js │ │ └── homepage.js │ ├── footer │ │ └── footer.js │ └── navigation │ │ └── navigation.js ├── html │ ├── article-page.js │ ├── homepage.js │ ├── index.js │ ├── subpage.js │ └── wysiwyg-elements.js ├── images │ ├── favicon │ │ ├── apple-touch-icon.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── site.webmanifest │ │ ├── web-app-manifest-192x192.png │ │ └── web-app-manifest-512x512.png │ ├── logo.png │ ├── logo.svg │ ├── paint_icon.png │ └── squiz-logo.png ├── scripts │ ├── common │ │ ├── navbar.js │ │ ├── switcher.js │ │ └── tests │ │ │ ├── navbar.test.js │ │ │ └── switcher.test.js │ └── main.js └── styles │ ├── common │ ├── _variables.scss │ ├── fonts.scss │ ├── footer.scss │ ├── global-shared.scss │ ├── mixins.scss │ ├── navbar.scss │ ├── shared-themes │ │ ├── README.md │ │ ├── _shared-theme-mixin.scss │ │ ├── shared-accordion.scss │ │ ├── shared-banner.scss │ │ ├── shared-blockquote.scss │ │ ├── shared-cards.scss │ │ ├── shared-dynamic-header.scss │ │ ├── shared-global.scss │ │ ├── shared-icon-cards.scss │ │ ├── shared-image-text-row.scss │ │ ├── shared-key-statistics.scss │ │ └── shared-testimonials.scss │ └── switcher.scss │ ├── main.scss │ └── themes │ ├── black-theme │ ├── _black-variables.scss │ ├── black-accordion.scss │ ├── black-banner.scss │ ├── black-blockquote.scss │ ├── black-cards.scss │ ├── black-dynamic-header.scss │ ├── black-global.scss │ ├── black-icon-cards.scss │ ├── black-image-text-row.scss │ ├── black-key-statistics.scss │ └── black-testimonials.scss │ ├── green-theme │ ├── _green-variables.scss │ ├── green-accordion.scss │ ├── green-banner.scss │ ├── green-blockquote.scss │ ├── green-cards.scss │ ├── green-dynamic-header.scss │ ├── green-global.scss │ ├── green-icon-cards.scss │ ├── green-image-text-row.scss │ ├── green-key-statistics.scss │ └── green-testimonials.scss │ ├── violet-theme │ ├── _violet-variables.scss │ ├── violet-accordion.scss │ ├── violet-banner.scss │ ├── violet-blockquote.scss │ ├── violet-cards.scss │ ├── violet-dynamic-header.scss │ ├── violet-global.scss │ ├── violet-icon-cards.scss │ ├── violet-image-text-row.scss │ ├── violet-key-statistics.scss │ └── violet-testimonials.scss │ └── white-theme │ ├── _white-variables.scss │ ├── white-accordion.scss │ ├── white-banner.scss │ ├── white-blockquote.scss │ ├── white-cards.scss │ ├── white-dynamic-header.scss │ ├── white-global.scss │ ├── white-icon-cards.scss │ ├── white-image-text-row.scss │ ├── white-key-statistics.scss │ └── white-testimonials.scss ├── verMgmt.config.json └── vite.config.js /.example.env: -------------------------------------------------------------------------------- 1 | BASE_DOMAIN="https://cms-domain/" 2 | BASE_PATH="cms-base-path/_api/components/" 3 | API_IDENTIFIER="api-identifier" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | .dxp 12 | node_modules 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # Tests 28 | src/coverage 29 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.17.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-recommended-scss"], 3 | "rules": { 4 | "color-named": "never", 5 | "color-no-invalid-hex": true, 6 | "function-name-case": null, 7 | "font-family-no-duplicate-names": true, 8 | "declaration-block-no-duplicate-properties": true, 9 | "no-descending-specificity": null, 10 | "no-invalid-position-at-import-rule": null, 11 | "block-no-empty": true, 12 | "at-rule-empty-line-before": null, 13 | "scss/no-global-function-names": null, 14 | "scss/operator-no-unspaced": true, 15 | "selector-class-pattern": [ 16 | "^[a-z]([-]?[a-z0-9]+)*(__[a-z0-9]([-]?[a-z0-9]+)*)?(--[a-z0-9]([-]?[a-z0-9]+)*)?$", 17 | { 18 | "resolveNestedSelectors": true, 19 | "message": "Expected class selector to be in BEM format (block__element--modifier)", 20 | "severity": "warning" 21 | } 22 | ], 23 | "declaration-no-important": true, 24 | "import-notation": "string", 25 | "selector-max-id": 0, 26 | "max-nesting-depth": [ 27 | 3, 28 | { 29 | "ignore": ["pseudo-classes"] 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dist/.vite/ssr-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dxp/component-service/banner/js/frontend.js": [], 3 | "dxp/component-service/testimonials/js/frontend.js": [], 4 | "src/entry-server.js": [], 5 | "src/scripts/common/navbar.js": [], 6 | "src/scripts/common/switcher.js": [], 7 | "src/scripts/main.js": [], 8 | "src/styles/main.scss": [] 9 | } -------------------------------------------------------------------------------- /dist/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/favicon-96x96.png -------------------------------------------------------------------------------- /dist/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/apple-touch-icon.png -------------------------------------------------------------------------------- /dist/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon-96x96.png -------------------------------------------------------------------------------- /dist/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon.ico -------------------------------------------------------------------------------- /dist/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /dist/images/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /dist/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon/favicon.ico -------------------------------------------------------------------------------- /dist/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyWebSite", 3 | "short_name": "MySite", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /dist/images/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /dist/images/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /dist/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/logo.png -------------------------------------------------------------------------------- /dist/images/paint_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/paint_icon.png -------------------------------------------------------------------------------- /dist/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyWebSite", 3 | "short_name": "MySite", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /dist/images/squiz-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/squiz-logo.png -------------------------------------------------------------------------------- /dist/images/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /dist/images/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/images/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | const u=e=>{const t=e.querySelector(".banner__button"),a=e.querySelector("video");a&&t?t.addEventListener("click",()=>{const r=!a.paused;a[r?"pause":"play"](),t.classList.toggle("banner__button--play",r),t.classList.toggle("banner__button--pause",!r),t.setAttribute("aria-label",r?"Play decorative video in the banner's background":"Pause decorative video in the banner's background")}):t&&(t.style.display="none")},b=()=>{document.querySelectorAll(".banner").forEach(t=>u(t))};b();const g=e=>{const t=e.querySelector("[data-testimonials-track]"),a=e.querySelector("[data-testimonials-prev]"),r=e.querySelector("[data-testimonials-next]");if(t&&a&&r){let n=0;const i=Array.from(t.children),o=(s,c)=>{s.setAttribute("aria-disabled",c),c?s.setAttribute("disabled",""):s.removeAttribute("disabled")},l=()=>{i.forEach((s,c)=>{s.removeAttribute("aria-current"),s.setAttribute("tabindex","-1"),c===n&&(s.setAttribute("aria-current","true"),s.setAttribute("tabindex","0"),s.focus())}),o(a,n===0),o(r,n===i.length-1)};a.addEventListener("click",()=>{n>0&&(n--,l())}),r.addEventListener("click",()=>{n{s.setAttribute("tabindex","-1")}),l()}},m=()=>{document.querySelectorAll("[data-testimonials]").forEach(t=>g(t))};m();const h=e=>{console.error(`No element found for theme: ${e}`)},d=(e,t)=>{const a=document.documentElement;if(t.forEach(n=>{var i;a.classList.remove(n.getAttribute("value")),n.disabled=!1,(i=n.nextElementSibling)==null||i.classList.remove("active")}),a.classList.add(e),!document.querySelector(`.style-switcher__css[value="${e}"]`)){h(e);return}localStorage.setItem("theme",e)},v=()=>{const e=document.querySelector('[aria-controls="style-switcher"]'),t=document.querySelector("#style-switcher"),a=document.querySelectorAll(".style-switcher__css");if(e&&t&&a.length){const r=localStorage.getItem("theme");r&&d(r,a),e.addEventListener("click",()=>{const n=e.getAttribute("aria-expanded")==="true";e.setAttribute("aria-expanded",!n),t.hidden=n}),a.forEach(n=>{n.addEventListener("click",i=>{const o=i.target.getAttribute("value");d(o,a)})}),document.addEventListener("click",n=>{e.getAttribute("aria-expanded")==="true"&&!n.target.closest("#style-switcher")&&n.target!==e&&(e.setAttribute("aria-expanded","false"),t.hidden=!0)})}};document.addEventListener("DOMContentLoaded",v);document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector(".navbar__toggler"),t=document.querySelector(".collapse");e&&t&&e.addEventListener("click",()=>{t.classList.toggle("show")});const a=document.querySelectorAll(".navbar__link"),r=window.location.pathname;a.forEach(n=>{const i=new URL(n.href,window.location.origin).pathname;n.classList.toggle("active",i===r)})}); 2 | -------------------------------------------------------------------------------- /dist/paint_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/dist/paint_icon.png -------------------------------------------------------------------------------- /dist/server.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dxp/01_compilers/dxp-compiler.js: -------------------------------------------------------------------------------- 1 | import { globSync } from 'glob'; 2 | import path from 'path'; 3 | import * as esbuild from 'esbuild'; 4 | import { promises as fs } from 'fs'; 5 | 6 | // bundle components into a format for deployment 7 | (async () => { 8 | // get all component service directories 9 | const csComponents = globSync( 10 | path.posix.join('.', 'dxp', 'component-service', '*/') 11 | ); 12 | // Define our array of build promises 13 | const allBuildPromises = []; 14 | 15 | // For each component use esbuild to format to cjs 16 | for (let i = 0; i < csComponents.length; i++) { 17 | // Get the current component path 18 | const componentPath = csComponents[i]; 19 | // process component 20 | let buildPromise = await esbuild.build({ 21 | entryPoints: [path.posix.join(componentPath, 'main.js')], 22 | bundle: true, 23 | treeShaking: true, 24 | outdir: path.posix.join(componentPath), 25 | format: 'cjs', 26 | platform: 'node', 27 | target: 'node20', 28 | sourcemap: false, 29 | external: ['react', 'react-dom', 'react-dom-server'], 30 | outExtension: { 31 | '.js': '.cjs' 32 | } 33 | }); 34 | // Push our promises to the component array 35 | allBuildPromises.push(buildPromise); 36 | } 37 | 38 | // When all promises have resolved then log that we have succeeded with the build 39 | Promise.all(allBuildPromises).then(async () => { 40 | console.log('✅ build has completed successfully'); 41 | }); 42 | 43 | async function copyImages() { 44 | const images = globSync('./src/images/**/*.{png,jpg,jpeg,svg,gif}'); 45 | const outputDir = './dist/images'; 46 | 47 | await fs.mkdir(outputDir, { recursive: true }); 48 | 49 | await Promise.all( 50 | images.map(async (image) => { 51 | const fileName = path.basename(image); 52 | const destPath = path.posix.join(outputDir, fileName); 53 | await fs.copyFile(image, destPath); 54 | }) 55 | ); 56 | console.log('✅ Images copied to dist/images'); 57 | } 58 | 59 | await copyImages(); 60 | })(); 61 | -------------------------------------------------------------------------------- /dxp/01_compilers/manifest-compiler.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { globSync } from 'glob'; 3 | import fs from 'fs'; 4 | 5 | // Function to compile manifest data 6 | export default async function generateManifestRecord(file) { 7 | if (!file || !file.endsWith('manifest.json')) return; 8 | // output array of strings: ["namespace/name/version", "..."] 9 | const content = []; 10 | // get all manifest.json files 11 | const manifests = globSync( 12 | path.posix.join('.', 'dxp', 'component-service', '*', 'manifest.json') 13 | ); 14 | 15 | // Loop through each manifest.json file 16 | for (const manifestPath of manifests) { 17 | try { 18 | // Read and parse the manifest.json file 19 | const manifestData = JSON.parse( 20 | await fs.promises.readFile(manifestPath, 'utf-8') 21 | ); 22 | 23 | // Extract name, namespace, and version from the manifest file 24 | const { name, namespace, version } = manifestData; 25 | 26 | // Format it as "namespace/name/version" 27 | if (name && namespace && version) { 28 | content.push(`${namespace}/${name}/${version}`); 29 | } else { 30 | console.warn(`Missing data in manifest: ${manifestPath}`); 31 | } 32 | } catch (error) { 33 | console.error(`Error reading manifest file: ${manifestPath}`, error); 34 | } 35 | } 36 | 37 | // Write the formatted content array to a new file 38 | try { 39 | await fs.promises.writeFile( 40 | './dxp/01_compilers/manifests.json', 41 | JSON.stringify(content, null, 2), 42 | 'utf-8' 43 | ); 44 | } catch (error) { 45 | console.error('Error writing manifests.json:', error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /dxp/01_compilers/manifests.json: -------------------------------------------------------------------------------- 1 | [ 2 | "edge-dxp-comp-lib/testimonials/2.0.2", 3 | "edge-dxp-comp-lib/key-statistics/2.0.4", 4 | "edge-dxp-comp-lib/image-text-row/2.0.1", 5 | "edge-dxp-comp-lib/icon-card/2.0.3", 6 | "edge-dxp-comp-lib/dynamic-header/2.0.1", 7 | "edge-dxp-comp-lib/cards-root/2.0.2", 8 | "edge-dxp-comp-lib/cards-matrix/2.0.4", 9 | "edge-dxp-comp-lib/cards-manual/2.0.18", 10 | "edge-dxp-comp-lib/blockquote/2.0.1", 11 | "edge-dxp-comp-lib/banner/2.0.2", 12 | "edge-dxp-comp-lib/accordion/2.0.4" 13 | ] -------------------------------------------------------------------------------- /dxp/component-service/accordion/README.md: -------------------------------------------------------------------------------- 1 | # Accordion 2 | 3 | ## Description 4 | 5 | An accordion displays a list of headings with hidden related content on the page. Users can show and hide related content by clicking on the heading. 6 | 7 | ## Editing 8 | 9 | To customize and configure the component, you can pass different properties to modify its behavior or appearance. Specifically, you can change the heading and content for each accordion item. There is a minimum of 1 item and a maximum of 20 items. 10 | 11 | ## Properties Example: 12 | 13 | ``` 14 | { 15 | "accordion": [ 16 | { 17 | "heading": "Accordion One", 18 | "content": "

This is accordion one!

" 19 | }, 20 | { 21 | "heading": "Accordion Two", 22 | "content": "

This is accordion two!

" 23 | }, 24 | { 25 | "heading": "Accordion Three", 26 | "content": "

This is accordion three!

" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | ## Component Properties 33 | 34 | | Property | Property Description | Property Type | Is Required | Default | 35 | | :-------- | :-----------------------------: | :-----------: | :---------: | :-----: | 36 | | title | The section title | string | | | 37 | | accordion | The accordion items | array | ✓ | | 38 | | heading | The accordion item heading text | string | ✓ | | 39 | | content | The accordion item content | FormattedText | ✓ | | 40 | -------------------------------------------------------------------------------- /dxp/component-service/accordion/css/accordion.scss: -------------------------------------------------------------------------------- 1 | .accordion { 2 | &__item { 3 | border-bottom: 1px solid rgb(var(--color-text) 0.2); 4 | 5 | &:first-child { 6 | border-top: 1px solid rgb(var(--color-text) 0.2); 7 | } 8 | } 9 | 10 | &__heading { 11 | margin: 0; 12 | cursor: pointer; 13 | text-align: left; 14 | width: 100%; 15 | display: flex; 16 | gap: var(--spacing-sm); 17 | align-content: center; 18 | padding: var(--spacing-sm); 19 | background-color: var(--color-bg-alt); 20 | 21 | &:hover { 22 | background-color: var(--primary-darker); 23 | } 24 | 25 | &::after { 26 | content: ''; 27 | display: inline-block; 28 | min-width: var(--spacing-sm); 29 | width: var(--spacing-sm); 30 | height: var(--spacing-sm); 31 | border-right: 3px solid currentcolor; 32 | border-bottom: 3px solid currentcolor; 33 | transform: rotate(45deg); 34 | margin-left: auto; 35 | } 36 | } 37 | 38 | &__item[open] .accordion__heading::after { 39 | transform: rotate(-135deg); 40 | } 41 | 42 | // Default content state (closed) 43 | &__content { 44 | display: none; 45 | } 46 | 47 | // Open content state when details are open 48 | &__item[open] .accordion__content { 49 | display: block; 50 | padding: var(--spacing-sm); 51 | border-top: 1px solid var(--color-bg-alt); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dxp/component-service/accordion/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Accordion component", 3 | "accordion": [ 4 | { 5 | "heading": "Accordion One", 6 | "content": "

This is accordion one!

" 7 | }, 8 | { 9 | "heading": "Accordion Two", 10 | "content": "

This is accordion two!

" 11 | }, 12 | { 13 | "heading": "Accordion Three", 14 | "content": "

An exploration of contemporary themes through the lens of modern art, featuring innovative works that challenge traditional boundaries and invite viewers to see the world in new ways.

An exploration of contemporary themes through the lens of modern art, featuring innovative works that challenge traditional boundaries and invite viewers to see the world in new ways. An exploration of contemporary themes through the lens of modern art, featuring innovative works that challenge traditional boundaries and invite viewers to see the world in new ways.

" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /dxp/component-service/accordion/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | // This module takes an object with "accordion" properties as input. 6 | // The preview information comes from example.data.json and is defined as input in manifest.json 7 | export default { 8 | async main({ title, accordion }) { 9 | return html` 10 |
11 | 12 | ${title 13 | ? `

${xssSafeContent(title)}

` 14 | : ''} 15 | 16 | 17 | 18 | ${accordion 19 | .map( 20 | (item, idx) => html` 21 |
22 | 26 | ${xssSafeContent(item.heading)} 27 | 28 |
32 | ${xssSafeContent(item.content)} 33 |
34 |
35 | ` 36 | ) 37 | .join('')} 38 |
39 | `; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /dxp/component-service/accordion/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "accordion", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays a list of headings with hidden related content that users can reveal by clicking.", 6 | "displayName": "Accordion", 7 | "version": "2.0.4", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "toc", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "functions": [ 18 | { 19 | "name": "main", 20 | "entry": "main.js", 21 | "input": { 22 | "type": "object", 23 | "properties": { 24 | "title": { 25 | "type": "string", 26 | "title": "Title", 27 | "description": "The title to be displayed above the component.", 28 | "default": "Section title", 29 | "ui:metadata": { 30 | "inlineEditable": true 31 | } 32 | }, 33 | "accordion": { 34 | "title": "Accordion Items", 35 | "description": "A list of expandable accordion items to be displayed.", 36 | "type": "array", 37 | "minItems": 1, 38 | "maxItems": 20, 39 | "items": { 40 | "type": "object", 41 | "title": "Accordion Item", 42 | "required": ["heading", "content"], 43 | "properties": { 44 | "heading": { 45 | "type": "string", 46 | "title": "Heading", 47 | "description": "The heading of the accordion item.", 48 | "default": "Heading content", 49 | "ui:metadata": { 50 | "inlineEditable": true 51 | } 52 | }, 53 | "content": { 54 | "type": "FormattedText", 55 | "title": "Content", 56 | "description": "The text that appears inside the accordion panel, hidden until the item is expanded.", 57 | "ui:metadata": { 58 | "inlineEditable": true 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "required": ["accordion"] 66 | }, 67 | "output": { 68 | "responseType": "html" 69 | } 70 | } 71 | ], 72 | "staticFiles": { 73 | "locationRoot": "./" 74 | }, 75 | "previews": { 76 | "default": { 77 | "functionData": { 78 | "main": { 79 | "inputData": { 80 | "type": "file", 81 | "path": "example.data.json" 82 | }, 83 | "wrapper": { 84 | "path": "preview.html" 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /dxp/component-service/accordion/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/banner/README.md: -------------------------------------------------------------------------------- 1 | # Banner 2 | 3 | ## Description 4 | 5 | The Banner component allows users to display a banner with optional video or image content. The component also includes play/pause functionality for the video content and color background as a fallback. 6 | 7 | ## Editing 8 | 9 | Users can customize the Banner component by configuring its content and media. The content includes a heading, while the media can either be a video or an image. Depending on the `mediaType`, additional properties (like `videoSource` or `image`) are required. 10 | 11 | If a video is provided, play/pause functionality is included. 12 | 13 | ## Properties Example: 14 | 15 | ``` 16 | { 17 | "heading": "About us", 18 | "mediaType": "Video", 19 | "videoSource": { 20 | "text": "Decorative video", 21 | "url": "https://bytes.co/wp-content/uploads/2022/03/BYTES.CO-VIDEO-CARBONATE-MEDIA-1080HD-HD-1080p.m4v" 22 | } 23 | } 24 | ``` 25 | 26 | ## Component Properties 27 | 28 | | Property | Property Description | Property Type | Is Required | Default | 29 | | :---------- | :--------------------------------: | :---------------------------------------------------: | :---------: | :-----: | 30 | | heading | Text for the banner heading | string | ✓ | | 31 | | mediaType | | Type of media to display: "video", "image", or "none" | ✓ | Image | 32 | | videoSource | Video source object with video URL | SquizLink | | | 33 | | image | Image object for the banner | SquizImage | | | 34 | -------------------------------------------------------------------------------- /dxp/component-service/banner/css/banner.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/mixins'; 2 | @use '@styles/common/variables'; 3 | 4 | .banner { 5 | position: relative; 6 | width: 100%; 7 | 8 | &__media { 9 | @include mixins.center-overlay; 10 | 11 | object-fit: cover; 12 | max-height: 100%; 13 | min-height: variables.toRem(400); 14 | } 15 | 16 | &__background--default { 17 | background-color: var(--default-background-color); 18 | 19 | @include mixins.center-overlay; 20 | } 21 | 22 | &__content { 23 | padding: variables.toRem(32) variables.toRem(136); 24 | position: relative; 25 | background: var(--overlay-background); 26 | color: var(--white); 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: center; 30 | min-height: variables.toRem(400); 31 | 32 | @media screen and (max-width: variables.$breakpoint-md) { 33 | padding: variables.toRem(24) variables.toRem(16) variables.toRem(80); 34 | } 35 | } 36 | 37 | &__title { 38 | color: var(--white); 39 | margin: 0; 40 | } 41 | 42 | &__text { 43 | margin-top: variables.toRem(24); 44 | } 45 | 46 | &__button { 47 | width: variables.toRem(36); 48 | height: variables.toRem(48); 49 | position: absolute; 50 | right: variables.toRem(16); 51 | bottom: variables.toRem(16); 52 | background-size: cover; 53 | cursor: pointer; 54 | 55 | &--play { 56 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0id2hpdGUiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDkyLjIgMTIyLjg4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA5Mi4yIDEyMi44OCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO308L3N0eWxlPjxnPjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iOTIuMiw2MC45NyAwLDEyMi44OCAwLDAgOTIuMiw2MC45NyIvPjwvZz48L3N2Zz4=') 57 | no-repeat center; 58 | } 59 | 60 | &--pause { 61 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgZmlsbD0id2hpdGUiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDg3LjcyIDEyMi44OCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgODcuNzIgMTIyLjg4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5zdDB7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7fTwvc3R5bGU+PGc+PHBhdGggY2xhc3M9InN0MCIgZD0iTTAsMGgzNS41NHYxMjIuODhsLTM1LjU0LDBWMEwwLDB6IE01Mi4xOCwwaDM1LjU0djEyMi44OGwtMzUuNTQsMFYwTDUyLjE4LDB6Ii8+PC9nPjwvc3ZnPg==') 62 | no-repeat center; 63 | } 64 | 65 | &:focus { 66 | outline: 2px solid var(--primary); 67 | outline-offset: variables.toRem(4); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dxp/component-service/banner/example-data/example-video.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "mediaType": "video", 3 | "heading": "Subpage Example", 4 | "videoSource": { 5 | "text": "Decorative video", 6 | "url": "https://bytes.co/wp-content/uploads/2022/03/BYTES.CO-VIDEO-CARBONATE-MEDIA-1080HD-HD-1080p.m4v" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dxp/component-service/banner/example-data/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "mediaType": "image", 3 | "heading": "Welcome to All Components Page", 4 | "image": { 5 | "name": "My Image", 6 | "alt": "This is the image alt text", 7 | "imageVariations": { 8 | "original": { 9 | "url": "https://picsum.photos/800/600", 10 | "width": 1500, 11 | "height": 500, 12 | "byteSize": 1000, 13 | "mimeType": "image/jpeg", 14 | "aspectRatio": "1:1", 15 | "sha1Hash": "1234567890abcdef1234567890abcdef12345678" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dxp/component-service/banner/js/frontend.js: -------------------------------------------------------------------------------- 1 | // Function to initialize a single banner 2 | export const initializeBanner = (banner) => { 3 | // Select the toggle button for play/pause 4 | const toggleButton = banner.querySelector('.banner__button'); 5 | // Select the video element, if it exists 6 | const video = banner.querySelector('video'); 7 | 8 | if (video && toggleButton) { 9 | toggleButton.addEventListener('click', () => { 10 | // Check if the video is currently playing 11 | const isPlaying = !video.paused; 12 | 13 | // Play or pause the video based on its current state 14 | video[isPlaying ? 'pause' : 'play'](); 15 | toggleButton.classList.toggle('banner__button--play', isPlaying); 16 | toggleButton.classList.toggle('banner__button--pause', !isPlaying); 17 | 18 | // Set the appropriate aria-label for the button 19 | toggleButton.setAttribute( 20 | 'aria-label', 21 | isPlaying 22 | ? "Play decorative video in the banner's background" 23 | : "Pause decorative video in the banner's background" 24 | ); 25 | }); 26 | } else if (toggleButton) { 27 | // If the toggle button exists but there's no video, hide the button 28 | toggleButton.style.display = 'none'; 29 | } 30 | }; 31 | 32 | // Initialize all banners on the page 33 | const initializeAllBanners = () => { 34 | const banners = document.querySelectorAll('.banner'); 35 | banners.forEach((banner) => initializeBanner(banner)); 36 | }; 37 | 38 | initializeAllBanners(); 39 | 40 | export { initializeAllBanners }; 41 | -------------------------------------------------------------------------------- /dxp/component-service/banner/js/frontend.test.js: -------------------------------------------------------------------------------- 1 | import { initializeBanner } from './frontend.js'; 2 | 3 | describe('Banner - Frontend', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = ` 6 | 10 | `; 11 | }); 12 | 13 | it('should toggle play/pause on button click', () => { 14 | const banner = document.querySelector('.banner'); 15 | const video = banner.querySelector('.banner__media'); 16 | const button = banner.querySelector('.banner__button'); 17 | 18 | video.play = vi.fn(() => { 19 | Object.defineProperty(video, 'paused', { value: false, writable: true }); 20 | }); 21 | video.pause = vi.fn(() => { 22 | Object.defineProperty(video, 'paused', { value: true, writable: true }); 23 | }); 24 | 25 | initializeBanner(banner); 26 | 27 | button.click(); 28 | 29 | expect(video.play).toHaveBeenCalled(); 30 | expect(button.classList.contains('banner__button--play')).toBe(false); 31 | expect(button.classList.contains('banner__button--pause')).toBe(true); 32 | 33 | button.click(); 34 | 35 | expect(video.pause).toHaveBeenCalled(); 36 | expect(button.classList.contains('banner__button--play')).toBe(true); 37 | expect(button.classList.contains('banner__button--pause')).toBe(false); 38 | }); 39 | 40 | it('should hide button if no video is present', () => { 41 | document.body.innerHTML = ` 42 | 45 | `; 46 | 47 | const banner = document.querySelector('.banner'); 48 | const button = banner.querySelector('.banner__button'); 49 | 50 | initializeBanner(banner); 51 | 52 | expect(button.style.display).toBe('none'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /dxp/component-service/banner/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | 6 | // This module takes an object with "heading", "videoSource", and "image" properties as input. 7 | export default { 8 | async main({ mediaType, heading, videoSource, image }) { 9 | // Generate a unique ID for each banner instance to ensure unique element IDs - this allow to put as many of them as needed. 10 | const uniqueId = `banner-${Math.floor(Math.random() * 9999)}`; 11 | 12 | let mediaElement = ''; 13 | 14 | // If a video source is provided, create a video element. 15 | if (mediaType === 'video') { 16 | mediaElement = ``; 17 | } 18 | // If an image is provided, create an image element. 19 | else if (mediaType === 'image') { 20 | mediaElement = ``; 21 | } 22 | // If neither a video source nor an image is provided, create a default background element. 23 | else { 24 | mediaElement = ``; 25 | } 26 | 27 | return html` 28 | 44 | `; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /dxp/component-service/banner/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/README.md: -------------------------------------------------------------------------------- 1 | # Block Quote Component 2 | 3 | ## Description 4 | 5 | This component displays a quote along with the author's name. It is designed to be used wherever you want to highlight a specific quotation. 6 | 7 | ## Editing 8 | 9 | To customize and configure the component, you can pass different properties to modify its behavior or appearance. Specifically, you can change the text of the quote and the name of the author. 10 | 11 | ## Properties Example: 12 | 13 | ```json 14 | { 15 | "title": "Blockquote component", 16 | "quote": "Well begun is half done.", 17 | "author": "Aristotle" 18 | } 19 | ``` 20 | 21 | ## Component Properties 22 | 23 | | Property | Property Description | Property Type | Is Required | Default | 24 | | :------- | :------------------------------------------------: | :-----------: | :---------: | :-----: | 25 | | title | The section title | string | | | 26 | | quote | The text of the quote to be displayed | FormattedText | ✓ | | 27 | | author | The name of the person who said or wrote the quote | string | | | 28 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/css/blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/variables'; 2 | 3 | .blockquote { 4 | border-left: 1px solid var(--black); 5 | color: var(--black); 6 | font-size: var(--base-font-size); 7 | line-height: var(--line-height-md); 8 | display: flex; 9 | flex-direction: column; 10 | 11 | &__content { 12 | padding: 0 variables.toRem(30); 13 | } 14 | 15 | &__author { 16 | margin-top: variables.toRem(8); 17 | font-size: var(--font-size-xs); 18 | font-weight: var(--font-semibold); 19 | font-style: normal; 20 | display: inline-block; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Blockquote component", 3 | "quote": "

Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roaste.

", 4 | "author": "Aristotle" 5 | } 6 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | 6 | // This module takes an object with "quote" and "author" properties as input. 7 | export default { 8 | async main({ title, quote, author }) { 9 | return html` 10 |
11 | 12 | ${title 13 | ? `

${xssSafeContent(title)}

` 14 | : ''} 15 | 16 |
17 |
18 | ${xssSafeContent(quote)} 19 | 20 | 21 | 22 | ${author 23 | ? `${xssSafeContent(author)}` 24 | : ''} 25 |
26 |
27 |
28 | `; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/main.test.js: -------------------------------------------------------------------------------- 1 | import { xssSafeContent } from '../../utils/xss'; 2 | import Blockquote from './main.js'; 3 | 4 | const mockData = { 5 | title: 'Blockquote section', 6 | quote: '

Quote content

', 7 | author: 'Author' 8 | }; 9 | 10 | describe('Blockquote', () => { 11 | /* General */ 12 | it('should return valid HTML structure', async () => { 13 | const result = await Blockquote.main(mockData); 14 | 15 | expect(result).toBeDefined(); 16 | expect(result).toContain('
'); 17 | }); 18 | 19 | /* Title */ 20 | it('should render the title if provided', async () => { 21 | const result = await Blockquote.main(mockData); 22 | 23 | expect(result).toContain( 24 | '

Blockquote section

' 25 | ); 26 | }); 27 | 28 | it('should not render the title if it is null or undefined', async () => { 29 | const dataWithoutTitle = { ...mockData, title: null }; 30 | const result = await Blockquote.main(dataWithoutTitle); 31 | 32 | expect(result).not.toContain('

'); 33 | }); 34 | 35 | it('should not render the heading tag if title is empty', async () => { 36 | const dataWithoutTitle = { ...mockData, title: '' }; 37 | const result = await Blockquote.main(dataWithoutTitle); 38 | 39 | expect(result).not.toContain('

'); 40 | // Check if it's still render Blockquote Section 41 | expect(result).toContain('
'); 42 | }); 43 | 44 | /* Quote Content */ 45 | it('should render the blockquote content', async () => { 46 | const result = await Blockquote.main(mockData); 47 | 48 | expect(result).toContain( 49 | '
' 50 | ); 51 | expect(result).toContain('

Quote content

'); 52 | }); 53 | 54 | /* Author */ 55 | it('should render the author if provided', async () => { 56 | const result = await Blockquote.main(mockData); 57 | 58 | expect(result).toContain( 59 | 'Author' 60 | ); 61 | }); 62 | 63 | it('should not render the author tag if it is null or undefined', async () => { 64 | const dataWithoutAuthor = { ...mockData, author: null }; 65 | const result = await Blockquote.main(dataWithoutAuthor); 66 | 67 | expect(result).not.toContain(''); 68 | }); 69 | 70 | /* XSS */ 71 | it('should escape XSS in content', async () => { 72 | const blockquoteWithScripts = { 73 | title: '', 74 | quote: '', 75 | author: 'Click me' 76 | }; 77 | 78 | const result = await Blockquote.main(blockquoteWithScripts); 79 | 80 | const fields = Object.entries(blockquoteWithScripts); 81 | 82 | fields.forEach(([field, value]) => { 83 | expect(result).toContain( 84 | xssSafeContent(value), 85 | `${field} should be safe` 86 | ); 87 | expect(result).not.toContain(value); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "blockquote", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays a quote with the author's name.", 6 | "displayName": "Block Quote", 7 | "version": "2.0.1", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "announcement", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "functions": [ 18 | { 19 | "name": "main", 20 | "entry": "main.js", 21 | "input": { 22 | "type": "object", 23 | "title": "Block Quote content", 24 | "required": ["quote"], 25 | "properties": { 26 | "title": { 27 | "type": "string", 28 | "title": "Title", 29 | "description": "The title displayed above the quote.", 30 | "default": "Section title", 31 | "ui:metadata": { 32 | "inlineEditable": true 33 | } 34 | }, 35 | "quote": { 36 | "type": "FormattedText", 37 | "title": "Quote", 38 | "description": "The text of the quote.", 39 | "ui:metadata": { 40 | "inlineEditable": true 41 | } 42 | }, 43 | "author": { 44 | "type": "string", 45 | "title": "Author", 46 | "description": "The name of the quote's author.", 47 | "default": "Author", 48 | "ui:metadata": { 49 | "inlineEditable": true 50 | } 51 | } 52 | } 53 | }, 54 | "output": { 55 | "responseType": "html" 56 | } 57 | } 58 | ], 59 | "staticFiles": { 60 | "locationRoot": "" 61 | }, 62 | "previews": { 63 | "default": { 64 | "functionData": { 65 | "main": { 66 | "inputData": { 67 | "type": "file", 68 | "path": "example.data.json" 69 | }, 70 | "wrapper": { 71 | "path": "preview.html" 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /dxp/component-service/blockquote/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/cards-manual/css/cards-manual.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/mixins'; 2 | @use '@styles/common/variables'; 3 | 4 | .cards { 5 | display: grid; 6 | gap: var(--spacing-md); 7 | padding: 0; 8 | grid-template-columns: repeat(4, 1fr); 9 | 10 | @media screen and (max-width: variables.$breakpoint-md) { 11 | grid-template-columns: repeat(2, 1fr); 12 | } 13 | 14 | @media screen and (max-width: variables.$breakpoint-sm) { 15 | grid-template-columns: repeat(1, 1fr); 16 | } 17 | 18 | &__header { 19 | display: flex; 20 | gap: var(--spacing-sm) var(--spacing-md); 21 | margin-bottom: var(--spacing-md); 22 | 23 | @media screen and (max-width: variables.$breakpoint-md) { 24 | flex-direction: column; 25 | } 26 | } 27 | 28 | &__link { 29 | margin-left: auto; 30 | 31 | @media screen and (max-width: variables.$breakpoint-md) { 32 | margin-left: 0; 33 | } 34 | } 35 | 36 | &__card { 37 | overflow: hidden; 38 | display: flex; 39 | flex-direction: column; 40 | gap: var(--spacing-xs); 41 | position: relative; 42 | aspect-ratio: 0.55 / 1; 43 | background-color: var(--color-bg-alt); 44 | 45 | &--has-image { 46 | color: var(--color-bg); 47 | background-color: transparent; 48 | } 49 | 50 | &:hover { 51 | .cards__image { 52 | transform: scale(1.05); 53 | } 54 | } 55 | } 56 | 57 | &__heading { 58 | color: inherit; 59 | @include mixins.clip-text(2); 60 | } 61 | 62 | &__image { 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | width: 100%; 67 | height: 100%; 68 | object-fit: cover; 69 | object-position: center; 70 | transform: scale(1); 71 | transition: all 0.5s ease; 72 | z-index: var(--z-index-below); 73 | 74 | &::before { 75 | content: ''; 76 | width: inherit; 77 | height: inherit; 78 | position: absolute; 79 | top: inherit; 80 | left: inherit; 81 | background-color: var(--black); 82 | opacity: 0.3; 83 | } 84 | } 85 | 86 | &__content-type { 87 | text-transform: uppercase; 88 | @include mixins.clip-text(2); 89 | } 90 | 91 | &__card-link { 92 | color: inherit; 93 | display: flex; 94 | flex-direction: column; 95 | gap: var(--spacing-xs); 96 | height: 100%; 97 | width: 100%; 98 | position: relative; 99 | text-decoration: none; 100 | } 101 | 102 | &__supporting-text { 103 | @include mixins.clip-text(6); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /dxp/component-service/cards-manual/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/cards-matrix/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Cards - Matrix", 3 | "link": { 4 | "text": "CTA text link", 5 | "url": "https://squiz.net", 6 | "target": "_blank" 7 | }, 8 | "cards": [ 9 | { 10 | "asset": "matrix-asset://api-identifier/79607" 11 | }, 12 | { 13 | "asset": "matrix-asset://api-identifier/79607" 14 | }, 15 | { 16 | "asset": "matrix-asset://api-identifier/79605" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /dxp/component-service/cards-matrix/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "cards-matrix", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays a grid of cards populated with information from Matrix assets.", 6 | "displayName": "Cards - Matrix", 7 | "version": "2.0.4", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "grid_view", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "environment": [ 18 | { 19 | "name": "API_IDENTIFIER", 20 | "required": false 21 | }, 22 | { 23 | "name": "BASE_DOMAIN", 24 | "required": false 25 | }, 26 | { 27 | "name": "BASE_PATH", 28 | "required": false 29 | } 30 | ], 31 | "functions": [ 32 | { 33 | "name": "main", 34 | "entry": "main.js", 35 | "input": { 36 | "type": "object", 37 | "required": ["cards"], 38 | "properties": { 39 | "title": { 40 | "type": "string", 41 | "title": "Title", 42 | "description": "The title displayed above the cards.", 43 | "default": "Section title", 44 | "ui:metadata": { 45 | "inlineEditable": true 46 | } 47 | }, 48 | "link": { 49 | "type": "SquizLink", 50 | "title": "Link", 51 | "description": "A link displayed next to the title.", 52 | "ui:metadata": { 53 | "inlineEditable": true 54 | } 55 | }, 56 | "cards": { 57 | "type": "array", 58 | "title": "Cards", 59 | "description": "A list of cards to display in the grid.", 60 | "minItems": 1, 61 | "maxItems": 8, 62 | "items": { 63 | "type": "object", 64 | "title": "Card", 65 | "properties": { 66 | "asset": { 67 | "type": "string", 68 | "title": "Card", 69 | "format": "matrix-asset-uri", 70 | "description": "Select a Matrix asset to display as a card in the grid." 71 | } 72 | }, 73 | "required": ["asset"] 74 | } 75 | } 76 | } 77 | }, 78 | "output": { 79 | "responseType": "html" 80 | } 81 | } 82 | ], 83 | "staticFiles": { 84 | "locationRoot": "./" 85 | }, 86 | "previews": { 87 | "default": { 88 | "functionData": { 89 | "main": { 90 | "inputData": { 91 | "type": "file", 92 | "path": "example.data.json" 93 | }, 94 | "wrapper": { 95 | "path": "preview.html" 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /dxp/component-service/cards-matrix/mock-data/mockDataWrapper.js: -------------------------------------------------------------------------------- 1 | export const mockDataWrapper = { 2 | title: 'Cards - Matrix Asset', 3 | link: { 4 | text: 'CTA text link', 5 | url: 'https://squiz.net', 6 | target: '_blank' 7 | }, 8 | cards: [ 9 | { 10 | id: '12345', 11 | heading: 'Test Card', 12 | link: 'https://test.co', 13 | contentType: 'Content Type', 14 | supportingText: 'Supporting Text', 15 | image: { 16 | url: 'https://picsum.photos/600/900', 17 | attributes: { 18 | id: '44058', 19 | alt: 'A large estate set against a backdrop of snow-covered mountains, surrounded by a serene winter landscape.', 20 | width: '6000', 21 | height: '4000' 22 | } 23 | } 24 | }, 25 | { 26 | id: '45000', 27 | heading: 'Card Two', 28 | link: 'https://mtx.cloud/card-two', 29 | contentType: 'Another type', 30 | supportingText: 'Here is more info', 31 | image: { 32 | url: 'https://picsum.photos/600/700', 33 | attributes: { 34 | id: '44058', 35 | alt: 'A large estate set against a backdrop of snow-covered mountains, surrounded by a serene winter landscape.', 36 | width: '6000', 37 | height: '4000' 38 | } 39 | } 40 | }, 41 | { 42 | id: '44955', 43 | heading: 'Card One', 44 | link: 'https://mtx.cloud/card-one', 45 | contentType: 'Content Type', 46 | supportingText: 'Optional supporting text', 47 | image: { 48 | url: 'https://picsum.photos/600/800', 49 | attributes: { 50 | id: '44058', 51 | alt: 'A large estate set against a backdrop of snow-covered mountains, surrounded by a serene winter landscape.', 52 | width: '6000', 53 | height: '4000' 54 | } 55 | } 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /dxp/component-service/cards-matrix/mock-data/mockResolvedData.js: -------------------------------------------------------------------------------- 1 | export const mockResolvedData = [ 2 | { 3 | id: '44955', 4 | heading: 'Card One', 5 | link: 'https://example.com/card-one', 6 | contentType: 'Content Type', 7 | supportingText: 'Optional supporting text' 8 | }, 9 | { 10 | id: '45649', 11 | heading: 'Card Two', 12 | link: 'https://example.com/card-two', 13 | contentType: 'Another type', 14 | supportingText: 'Additional text' 15 | }, 16 | { 17 | id: '45000', 18 | heading: 'Card Three', 19 | link: 'https://example.com/card-three', 20 | contentType: 'Different type', 21 | supportingText: 'Supporting text here', 22 | image: { 23 | url: 'https://example.com/image.jpg', 24 | attributes: { alt: 'Alt text' } 25 | } 26 | } 27 | ]; 28 | -------------------------------------------------------------------------------- /dxp/component-service/cards-matrix/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/cards-root/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Cards", 3 | "link": { 4 | "text": "CTA text link", 5 | "url": "https://squiz.net", 6 | "target": "_blank" 7 | }, 8 | "rootnode": "matrix-asset://api-identifier/44954" 9 | } 10 | -------------------------------------------------------------------------------- /dxp/component-service/cards-root/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "cards-root", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays child assets from a selected Matrix node as a grid of cards.", 6 | "displayName": "Cards - Matrix Root Node", 7 | "version": "2.0.2", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "grid_view", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "environment": [ 18 | { 19 | "name": "API_IDENTIFIER", 20 | "required": false 21 | }, 22 | { 23 | "name": "BASE_DOMAIN", 24 | "required": false 25 | }, 26 | { 27 | "name": "BASE_PATH", 28 | "required": false 29 | } 30 | ], 31 | "functions": [ 32 | { 33 | "name": "main", 34 | "entry": "main.js", 35 | "input": { 36 | "type": "object", 37 | "required": ["rootnode"], 38 | "properties": { 39 | "title": { 40 | "type": "string", 41 | "title": "Title", 42 | "description": "The title displayed above the cards.", 43 | "default": "Section title", 44 | "ui:metadata": { 45 | "inlineEditable": true 46 | } 47 | }, 48 | "link": { 49 | "type": "SquizLink", 50 | "title": "Link", 51 | "description": "A link displayed next to the title.", 52 | "ui:metadata": { 53 | "inlineEditable": true 54 | } 55 | }, 56 | "rootnode": { 57 | "type": "string", 58 | "title": "Cards", 59 | "format": "matrix-asset-uri", 60 | "description": "Select a Matrix node to display its child pages as cards." 61 | } 62 | } 63 | }, 64 | "output": { 65 | "responseType": "html" 66 | } 67 | } 68 | ], 69 | "staticFiles": { 70 | "locationRoot": "./" 71 | }, 72 | "previews": { 73 | "default": { 74 | "functionData": { 75 | "main": { 76 | "inputData": { 77 | "type": "file", 78 | "path": "example.data.json" 79 | }, 80 | "wrapper": { 81 | "path": "preview.html" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /dxp/component-service/cards-root/mock-data/mockDataWrapper.js: -------------------------------------------------------------------------------- 1 | export const mockDataWrapper = { 2 | title: 'Cards - Root Node', 3 | link: { 4 | text: 'CTA text link', 5 | url: 'https://squiz.net', 6 | target: '_blank' 7 | }, 8 | cards: [ 9 | { 10 | id: '12345', 11 | heading: 'Test Card', 12 | link: 'https://test.co', 13 | contentType: 'Content Type', 14 | supportingText: 'Supporting Text', 15 | image: { 16 | url: 'https://picsum.photos/700/700', 17 | attributes: { 18 | id: '44058', 19 | alt: 'A large estate set against a backdrop of snow-covered mountains, surrounded by a serene winter landscape.', 20 | width: '6000', 21 | height: '4000' 22 | } 23 | } 24 | }, 25 | { 26 | id: '45000', 27 | heading: 'Card Two', 28 | link: 'https://mtx.cloud/card-two', 29 | contentType: 'Another type', 30 | supportingText: 'Here is more info', 31 | image: { 32 | url: 'https://picsum.photos/600/1000', 33 | attributes: { 34 | id: '44058', 35 | alt: 'A large estate set against a backdrop of snow-covered mountains, surrounded by a serene winter landscape.', 36 | width: '6000', 37 | height: '4000' 38 | } 39 | } 40 | }, 41 | { 42 | id: '44955', 43 | heading: 'Card One', 44 | link: 'https://mtx.cloud/card-one', 45 | contentType: 'Content Type', 46 | supportingText: 'Optional supporting text' 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /dxp/component-service/cards-root/mock-data/mockRootNodeData.js: -------------------------------------------------------------------------------- 1 | export const mockRootNodeData = [ 2 | { 3 | id: '12345', 4 | heading: 'Card 1', 5 | contentType: 'Type 1', 6 | supportingText: 'Supporting text for card 1', 7 | image: { 8 | url: 'https://example.com/image1.jpg', 9 | attributes: { alt: 'Alt Text 1' } 10 | }, 11 | link: 'https://example.com/card1' 12 | }, 13 | { 14 | id: '67890', 15 | heading: 'Card 2', 16 | contentType: 'Type 2', 17 | supportingText: 'Supporting text for card 2', 18 | image: null, 19 | link: 'https://example.com/card2' 20 | } 21 | ]; 22 | -------------------------------------------------------------------------------- /dxp/component-service/cards-root/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Header 2 | 3 | ## Description 4 | 5 | The `Dynamic Header` component allows the user to create content with a heading and some text to display on the page. The heading size can be selected by the user (e.g., H1, H2, H3, etc.), making it flexible for various uses like main headers, subheaders, and sections. 6 | 7 | ## Editing 8 | 9 | Users can customize the Header + Paragraph component by providing the text for the heading and the content. The heading level can be chosen from H1 to H6, allowing users to define the importance and size of the heading. 10 | 11 | The content is optional and will only be displayed if provided. 12 | 13 | ## Properties Example: 14 | 15 | ```json 16 | { 17 | "title": "Main title", 18 | "titleLevel": "h1", 19 | "content": "A Simple paragraph." 20 | } 21 | ``` 22 | 23 | ## Component Properties 24 | 25 | | Property | Property Description | Property Type | Is Required | Default | 26 | | :--------- | :-------------------------------------------: | :-------------------------------------------: | :---------: | :-----: | 27 | | title | The text of the heading | string | ✓ | | 28 | | titleLevel | Determine the level of title | one of (['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) | ✓ | h2 | 29 | | content | The text content to display below the heading | string | | | 30 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/css/dynamic-header.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/variables'; 2 | 3 | .header-paragraph { 4 | font-size: var(--base-font-size); 5 | 6 | .header-paragraph__title { 7 | margin-bottom: variables.toRem(16); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Welcome to Dynamic Header component!", 3 | "titleLevel": "h2", 4 | "content": "

A large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country.

The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn't listen. She packed her seven versalia.

" 5 | } 6 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | 6 | // Create a dynamic class name based on the title level (e.g., h1, h2, etc.) 7 | export const titleClassMap = { 8 | h1: 'heading-primary', 9 | h2: 'heading-secondary', 10 | h3: 'heading-tertiary', 11 | h4: 'heading-quaternary', 12 | h5: 'heading-quinary', 13 | h6: 'heading-senary' 14 | }; 15 | 16 | // This module takes an object with "title", "titleLevel" and optional "content" properties as input. 17 | export default { 18 | async main({ title, titleLevel, content }) { 19 | // Map heading level with proper CSS class or use h2 class by default. 20 | const titleClass = titleClassMap[titleLevel] || 'heading-secondary'; 21 | 22 | return html` 23 |
24 |
25 | 26 | <${titleLevel} data-sq-field="title" class="header-paragraph__title ${titleClass}">${xssSafeContent(title)} 27 | 28 | ${content ? `${xssSafeContent(content)}` : ''} 29 |
30 |
31 | `; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/main.test.js: -------------------------------------------------------------------------------- 1 | import { xssSafeContent } from '../../utils/xss'; 2 | import DynamicHeader, { titleClassMap } from './main.js'; 3 | 4 | const mockData = { 5 | title: 'Dynamic Header Component', 6 | titleLevel: 'h2', 7 | content: '

Text content

' 8 | }; 9 | 10 | const titleLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; 11 | 12 | describe('Dynamic Header', () => { 13 | /* General */ 14 | it('should return valid HTML structure', async () => { 15 | const result = await DynamicHeader.main(mockData); 16 | 17 | expect(result).toBeDefined(); 18 | expect(result).toContain('
'); 19 | }); 20 | 21 | /* Title */ 22 | it('should render the title if provided', async () => { 23 | const result = await DynamicHeader.main(mockData); 24 | 25 | expect(result).toContain( 26 | '

Dynamic Header Component

' 27 | ); 28 | }); 29 | 30 | /* Title Levels */ 31 | titleLevels.forEach((level) => { 32 | it(`should render ${level} with the correct class`, async () => { 33 | const mockData = { 34 | title: 'Dynamic Header Test', 35 | titleLevel: level, 36 | content: '

Sample Content

' 37 | }; 38 | 39 | const result = await DynamicHeader.main(mockData); 40 | 41 | expect(result).toContain( 42 | `<${level} data-sq-field="title" class="header-paragraph__title ${titleClassMap[level]}">Dynamic Header Test` 43 | ); 44 | }); 45 | }); 46 | 47 | /* Text Content */ 48 | it('should render the content if provided', async () => { 49 | const result = await DynamicHeader.main(mockData); 50 | 51 | expect(result).toContain( 52 | '

Text content

' 53 | ); 54 | }); 55 | 56 | it('should not render content if it is null or undefined', async () => { 57 | const mockDataWithoutContent = { 58 | ...mockData, 59 | content: null 60 | }; 61 | 62 | const result = await DynamicHeader.main(mockDataWithoutContent); 63 | 64 | expect(result).not.toContain('

'); 65 | }); 66 | 67 | /* XSS */ 68 | it('should escape XSS in content', async () => { 69 | const dynamicHeaderWithScripts = { 70 | title: '', 71 | content: 'Click me' 72 | }; 73 | 74 | const result = await DynamicHeader.main(dynamicHeaderWithScripts); 75 | 76 | const fields = Object.entries(dynamicHeaderWithScripts); 77 | 78 | fields.forEach(([field, value]) => { 79 | expect(result).toContain( 80 | xssSafeContent(value), 81 | `${field} should be safe` 82 | ); 83 | expect(result).not.toContain(value); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "dynamic-header", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays a heading and supporting text with a user-defined heading level.", 6 | "displayName": "Dynamic Header", 7 | "version": "2.0.1", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "title", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "functions": [ 18 | { 19 | "name": "main", 20 | "entry": "main.js", 21 | "input": { 22 | "type": "object", 23 | "title": "Dynamic Header Content", 24 | "required": ["title"], 25 | "properties": { 26 | "title": { 27 | "type": "string", 28 | "title": "Title", 29 | "description": "The heading text to display.", 30 | "default": "Section title", 31 | "ui:metadata": { 32 | "inlineEditable": true 33 | } 34 | }, 35 | "titleLevel": { 36 | "type": "string", 37 | "title": "Title Level", 38 | "description": "Select the heading level from h1 to h6.", 39 | "enum": ["h1", "h2", "h3", "h4", "h5", "h6"], 40 | "default": "h2", 41 | "ui:metadata": { 42 | "quickOption": true 43 | } 44 | }, 45 | "content": { 46 | "type": "FormattedText", 47 | "title": "Content", 48 | "description": "The text displayed below the heading.", 49 | "ui:metadata": { 50 | "inlineEditable": true 51 | } 52 | } 53 | } 54 | }, 55 | "output": { 56 | "responseType": "html" 57 | } 58 | } 59 | ], 60 | "staticFiles": { 61 | "locationRoot": "./" 62 | }, 63 | "previews": { 64 | "default": { 65 | "functionData": { 66 | "main": { 67 | "inputData": { 68 | "type": "file", 69 | "path": "example.data.json" 70 | }, 71 | "wrapper": { 72 | "path": "preview.html" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /dxp/component-service/dynamic-header/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/icon-card/README.md: -------------------------------------------------------------------------------- 1 | # Icon Cards 2 | 3 | ## Description 4 | 5 | Icon Cards is a component that displays a collection of cards, each featuring an icon, a heading, text content, and an optional link. This component allows users to configure the number of columns (2, 3 or 4). 6 | 7 | ## Editing 8 | 9 | Users can customize the Icon Cards component by manually providing a card content and configuration option. The content array includes the set of predefined icons, heading, text content, and an optional link for each card. The configuration allows setting the number of columns to either 2, 3 or 4. 10 | 11 | ## Properties Example: 12 | 13 | ``` 14 | { 15 | "componentContent": [ 16 | { 17 | "icon": "alarm", 18 | "heading": "First heading", 19 | "textContent": "Text Content" 20 | }, 21 | { 22 | "icon": "confused", 23 | "heading": "Second heading", 24 | "textContent": "Text Content" 25 | } 26 | ], 27 | "componentConfiguration": { 28 | "numberOfColumns": 2 29 | } 30 | } 31 | ``` 32 | 33 | ## Component Properties 34 | 35 | | Property | Property Description | Property Type | Is Required | Default | 36 | | --------------- | ----------------------------- | ------------- | ----------- | ------- | 37 | | title | Main title of the section | string | ✓ | | 38 | | icon | The class name for the icon | string | | | 39 | | heading | Text for the card heading | string | ✓ | | 40 | | textContent | Text content for the card | FormattedText | ✓ | | 41 | | link | The link for the card | SquizLink | | | 42 | | numberOfColumns | Number of columns (2, 3 or 4) | number | ✓ | 2 | 43 | -------------------------------------------------------------------------------- /dxp/component-service/icon-card/css/icon-card.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/mixins'; 2 | @use '@styles/common/variables'; 3 | 4 | .icon-cards { 5 | display: flex; 6 | flex-wrap: wrap; 7 | gap: variables.toRem(32); 8 | padding: 0; 9 | align-items: baseline; 10 | 11 | &.col-2 { 12 | .icon-card { 13 | flex: 0 0 calc(50% - variables.toRem(32)); 14 | } 15 | } 16 | 17 | &.col-3 { 18 | .icon-card { 19 | flex: 0 0 calc(33.33% - variables.toRem(32)); 20 | } 21 | } 22 | 23 | &.col-4 { 24 | .icon-card { 25 | flex: 0 0 calc(25% - variables.toRem(32)); 26 | } 27 | } 28 | 29 | @media screen and (max-width: variables.$breakpoint-md) { 30 | &.col-2, 31 | &.col-3, 32 | &.col-4 { 33 | .icon-card { 34 | flex: 1 1 100%; 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .icon-card { 42 | list-style: none; 43 | text-align: center; 44 | position: relative; 45 | 46 | &__heading { 47 | font-size: variables.toRem(24); 48 | font-weight: var(--font-semibold); 49 | margin-bottom: variables.toRem(16); 50 | line-height: var(--font-size-lg); 51 | } 52 | 53 | a { 54 | text-decoration: none; 55 | width: 100%; 56 | height: 100%; 57 | display: block; 58 | z-index: var(--z-index-default); 59 | cursor: pointer; 60 | 61 | &:hover { 62 | text-decoration: underline; 63 | } 64 | } 65 | 66 | &__icon { 67 | position: relative; 68 | width: variables.toRem(64); 69 | height: variables.toRem(64); 70 | margin: 0 auto; 71 | margin-bottom: variables.toRem(16); 72 | 73 | &::after { 74 | content: ''; 75 | width: 100%; 76 | height: 100%; 77 | background-color: var(--primary); 78 | 79 | @include mixins.center-absolute-xy; 80 | 81 | border-radius: 50%; 82 | z-index: var(--z-index-below); 83 | } 84 | 85 | svg { 86 | @include mixins.center-absolute-xy; 87 | 88 | width: variables.toRem(36); 89 | height: auto; 90 | } 91 | } 92 | 93 | &__link { 94 | display: block; 95 | text-decoration: underline; 96 | font-weight: var(--font-semibold); 97 | margin-top: variables.toRem(16); 98 | } 99 | 100 | &:hover { 101 | .icon-card__link { 102 | text-decoration: none; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /dxp/component-service/icon-card/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentContent": { 3 | "title": "Our Services", 4 | "cards": [ 5 | { 6 | "heading": "test", 7 | "icon": "pen", 8 | "textContent": "The Big Oxmox advised her not to do so, because there were thousands." 9 | }, 10 | { 11 | "icon": "tools", 12 | "textContent": "Wild Question Marks and devious", 13 | "link": { 14 | "text": "Jelly liquorice icing gummi", 15 | "url": "https://squiz.net", 16 | "target": "_self" 17 | } 18 | }, 19 | { 20 | "heading": "Short Heading", 21 | "icon": "paintBucket", 22 | "textContent": "Named Duden flows by their place and supplies it", 23 | "link": { 24 | "text": "Jelly liquorice icing gummi", 25 | "url": "https://squiz.net", 26 | "target": "_self" 27 | } 28 | }, 29 | { 30 | "icon": "printer", 31 | "textContent": "Named Duden flows by their place and supplies it", 32 | "link": { 33 | "text": "Jelly liquorice icing gummi", 34 | "url": "https://squiz.net", 35 | "target": "_self" 36 | } 37 | } 38 | ] 39 | }, 40 | "componentConfiguration": { 41 | "numberOfColumns": "4 Columns" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /dxp/component-service/icon-card/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/image-text-row/README.md: -------------------------------------------------------------------------------- 1 | # Image Text Row Component 2 | 3 | ## Description 4 | 5 | The Image Text Row component allows users to create a layout with an image and a content. The component offers two variants: one with the image on the left and the content on the right, and another with the image on the right and the content on the left. 6 | 7 | ## Editing 8 | 9 | Users can customize the Image Text Row component by configuring its content and layout. The content includes an image, heading, text content, and link. The layout can be adjusted to place the image either on the left or the right side. 10 | 11 | ## Properties Example: 12 | 13 | ```json 14 | { 15 | "title": "Section Title", 16 | "image": { 17 | "alt": "Sample Image", 18 | "imageVariations": { 19 | "original": { 20 | "url": "https://picsum.photos/800/600", 21 | "width": 800, 22 | "height": 600 23 | } 24 | } 25 | }, 26 | "heading": "Main Heading Title", 27 | "textContent": "Short paragraph", 28 | "link": { 29 | "text": "Read more", 30 | "url": "https://squiz.net", 31 | "target": "_blank" 32 | }, 33 | "contentType": "Content Type" 34 | }, 35 | "componentConfiguration": { 36 | "variant": "text-left" 37 | } 38 | ``` 39 | 40 | ## Component Properties 41 | 42 | | Property | Property Description | Property Type | Is Required | Default | 43 | | :---------- | :-----------------------: | :--------------------------------: | :---------: | :-------: | 44 | | title | The section title | string | | | 45 | | image | The image to display | object (SquizImage) | ✓ | | 46 | | heading | Text for the heading | string | ✓ | | 47 | | textContent | Text content | string (FormattedText) | ✓ | | 48 | | link | The link to display | object (SquizLink) | ✓ | | 49 | | contentType | Text for the content type | string | ✓ | | 50 | | variant | One of variants | one of ('text-left', 'text-right') | ✓ | text-left | 51 | -------------------------------------------------------------------------------- /dxp/component-service/image-text-row/css/single-column.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/variables'; 2 | 3 | .image-text-row { 4 | display: flex; 5 | align-items: center; 6 | gap: variables.toRem(60); 7 | 8 | @media screen and (max-width: variables.$breakpoint-md) { 9 | gap: variables.toRem(24); 10 | flex-direction: column; 11 | } 12 | 13 | &.text-left { 14 | .image-text-row__image { 15 | order: 2; 16 | 17 | @media screen and (max-width: variables.$breakpoint-md) { 18 | order: 1; 19 | } 20 | } 21 | 22 | .image-text-row__content { 23 | order: 1; 24 | } 25 | } 26 | 27 | &.text-right { 28 | justify-content: end; 29 | 30 | .image-text-row__image { 31 | order: 1; 32 | 33 | @media screen and (max-width: variables.$breakpoint-md) { 34 | order: 1; 35 | } 36 | } 37 | 38 | .image-text-row__content { 39 | order: 2; 40 | } 41 | 42 | &.no-image { 43 | justify-content: end; 44 | } 45 | } 46 | 47 | &__image { 48 | width: 50%; 49 | max-height: variables.toRem(550); 50 | overflow: hidden; 51 | 52 | @media screen and (max-width: variables.$breakpoint-md) { 53 | width: 100%; 54 | order: 1; 55 | } 56 | } 57 | 58 | &__content { 59 | width: 50%; 60 | 61 | @media screen and (max-width: variables.$breakpoint-md) { 62 | width: 100%; 63 | order: 2; 64 | } 65 | } 66 | 67 | img { 68 | height: auto; 69 | width: 100%; 70 | } 71 | 72 | &__heading { 73 | margin-bottom: variables.toRem(16); 74 | font-size: var(--font-size-md); 75 | font-weight: var(--font-semibold); 76 | } 77 | 78 | &__link { 79 | display: inline-block; 80 | margin-top: variables.toRem(24); 81 | border-left: variables.toRem(2) solid var(--black); 82 | padding: variables.toRem(2) variables.toRem(24); 83 | font-weight: var(--font-semibold); 84 | font-size: variables.toRem(14); 85 | color: var(--black); 86 | 87 | &:hover { 88 | text-decoration: none; 89 | } 90 | 91 | &::after { 92 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg=='); 93 | height: variables.toRem(12); 94 | width: variables.toRem(12); 95 | margin-left: variables.toRem(24); 96 | display: inline-block; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /dxp/component-service/image-text-row/example-data/example-switched.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentContent": { 3 | "image": { 4 | "name": "My Image", 5 | "alt": "This is the image alt text", 6 | "imageVariations": { 7 | "original": { 8 | "url": "https://picsum.photos/800/600", 9 | "width": 1500, 10 | "height": 500, 11 | "byteSize": 1000, 12 | "mimeType": "image/jpeg", 13 | "aspectRatio": "1:1", 14 | "sha1Hash": "1234567890abcdef1234567890abcdef12345678" 15 | } 16 | } 17 | }, 18 | "contentType": "Content type", 19 | "heading": "Ut placet inquam tum est laborum", 20 | "textContent": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vel quam at nibh vehicula consequat. Suspendisse varius pharetra mi, ac fringilla nisi elementum nec. Nullam ac dignissim ex, at auctor dolor. Nullam tempus, nisi eget ornare tempus, ipsum justo consectetur arcu, et ultricies eros dui at est. Cras tincidunt eget ante vel porttitor. Vestibulum eu purus ut turpis dictum laoreet eu nec leo. Cras eget bibendum mi. Ut ullamcorper ultrices metus a vestibulum.

", 21 | "link": { 22 | "text": "CTA text link", 23 | "url": "https://squiz.net", 24 | "target": "_blank" 25 | } 26 | }, 27 | "componentConfiguration": { 28 | "variant": "text-right" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dxp/component-service/image-text-row/example-data/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentContent": { 3 | "image": { 4 | "name": "My Image", 5 | "alt": "This is the image alt text", 6 | "imageVariations": { 7 | "original": { 8 | "url": "https://picsum.photos/800/800", 9 | "width": 1500, 10 | "height": 500, 11 | "byteSize": 1000, 12 | "mimeType": "image/jpeg", 13 | "aspectRatio": "1:1", 14 | "sha1Hash": "1234567890abcdef1234567890abcdef12345678" 15 | } 16 | } 17 | }, 18 | "contentType": "Content type", 19 | "heading": "Ut placet inquam tum est laborum", 20 | "textContent": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vel quam at nibh vehicula consequat. Suspendisse varius pharetra mi, ac fringilla nisi elementum nec. Nullam ac dignissim ex, at auctor dolor. Nullam tempus, nisi eget ornare tempus, ipsum justo consectetur arcu, et ultricies eros dui at est. Cras tincidunt eget ante vel porttitor. Vestibulum eu purus ut turpis dictum laoreet eu nec leo. Cras eget bibendum mi. Ut ullamcorper ultrices metus a vestibulum.

", 21 | "link": { 22 | "text": "CTA text link", 23 | "url": "https://squiz.net", 24 | "target": "_blank" 25 | } 26 | }, 27 | "componentConfiguration": { 28 | "variant": "text-left" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dxp/component-service/image-text-row/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/README.md: -------------------------------------------------------------------------------- 1 | # Key Statistic Component 2 | 3 | ## Description 4 | 5 | The Key Statistics component displays statistics in few cards. Each card shows a value and supporting text. 6 | 7 | ## Editing 8 | 9 | Users can customize the Key Statistics component by providing an array of statistics. Each statistic consists of a numeric value and supporting text. The array can contain between three to five items. 10 | 11 | ## Properties Example: 12 | 13 | ```json 14 | { 15 | "title": "Key Statistics Component", 16 | "stats": [ 17 | { 18 | "value": "53$" 19 | }, 20 | { 21 | "value": "13", 22 | "text": "Deployed Components" 23 | }, 24 | { 25 | "value": "999", 26 | "text": "Lines of Code" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | ## Component Properties 33 | 34 | | Property | Property Description | Property Type | Is Required | Default | 35 | | :------- | :------------------------------------ | :-----------: | :---------: | :-----: | 36 | | title | Main title of the section | string | ✓ | | 37 | | stats | An array of four key statistics | array | ✓ | | 38 | | value | The numeric value of the statistic | string | ✓ | | 39 | | text | The supporting text for the statistic | string | | | 40 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/css/key-statistics.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/mixins'; 2 | @use '@styles/common/variables'; 3 | 4 | .stats-cards { 5 | display: flex; 6 | gap: variables.toRem(36); 7 | justify-content: center; 8 | flex-wrap: wrap; 9 | padding: 0; 10 | 11 | &__title { 12 | @include mixins.flex-center-content; 13 | 14 | margin-bottom: variables.toRem(36); 15 | } 16 | 17 | .stat-card { 18 | @include mixins.flex-center-vertical; 19 | // plus the size of the gap 20 | max-width: calc(variables.toRem(200) + variables.toRem(36)); 21 | 22 | &__value { 23 | background-color: var(--primary); 24 | font-weight: var(--font-semibold); 25 | font-size: var(--font-size-huge); 26 | width: variables.toRem(200); 27 | border-radius: 50%; 28 | min-height: variables.toRem(200); 29 | @include mixins.flex-center-content; 30 | 31 | margin-bottom: var(--spacing-sm); 32 | } 33 | 34 | &__text { 35 | font-size: var(--base-font-size); 36 | text-align: center; 37 | line-height: var(--font-size-sm); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Key Statistics Component", 3 | "stats": [ 4 | { 5 | "value": "53$" 6 | }, 7 | { 8 | "value": "13", 9 | "text": "Deployed Components" 10 | }, 11 | { 12 | "value": "999", 13 | "text": "Lines of Code" 14 | }, 15 | { 16 | "value": "36.6", 17 | "text": "Celsius Degrees" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | 6 | // This module takes an object with "stats" properties as input. 7 | export default { 8 | async main({ stats, title }) { 9 | return html` 10 |
11 | 12 | ${title 13 | ? `

${xssSafeContent(title)}

` 14 | : ''} 15 | 16 |
    17 | 18 | ${stats 19 | .map(({ value, text }, index) => { 20 | return ` 21 |
  • 22 |

    23 | ${xssSafeContent(value)} 24 |

    25 | 26 | 27 | 28 | ${ 29 | text 30 | ? `

    ${xssSafeContent(text)}

    ` 31 | : '' 32 | } 33 |
  • 34 | `; 35 | }) 36 | .join('')} 37 |
38 |
39 | `; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/main.test.js: -------------------------------------------------------------------------------- 1 | import { xssSafeContent } from '../../utils/xss'; 2 | import KeyStatistics from './main.js'; 3 | 4 | const mockData = { 5 | title: 'Key Statistics Section', 6 | stats: [ 7 | { value: '1234', text: 'Content 1' }, 8 | { value: '0.01', content: 'Content 2' }, 9 | { value: '20$', content: 'Content 3' } 10 | ] 11 | }; 12 | 13 | describe('Key Statistics', () => { 14 | /* General */ 15 | it('should return valid HTML structure', async () => { 16 | const result = await KeyStatistics.main(mockData); 17 | 18 | expect(result).toBeDefined(); 19 | expect(result).toContain('
'); 20 | }); 21 | 22 | /* Title */ 23 | it('should render the title if provided', async () => { 24 | const result = await KeyStatistics.main(mockData); 25 | 26 | expect(result).toContain( 27 | '

Key Statistics Section

' 28 | ); 29 | }); 30 | 31 | it('should not render the title if it is null or undefined', async () => { 32 | const dataWithoutTitle = { ...mockData, title: null }; 33 | const result = await KeyStatistics.main(dataWithoutTitle); 34 | 35 | expect(result).not.toContain( 36 | '

' 37 | ); 38 | }); 39 | 40 | it('should not render the heading tag if title is empty', async () => { 41 | const dataWithoutTitle = { ...mockData, title: '' }; 42 | const result = await KeyStatistics.main(dataWithoutTitle); 43 | 44 | expect(result).not.toContain( 45 | '

' 46 | ); 47 | // Check if it's still render Key Statistics Section 48 | expect(result).toContain('
'); 49 | }); 50 | 51 | /* Items - Stats */ 52 | it('should render all stats items', async () => { 53 | const result = await KeyStatistics.main(mockData); 54 | 55 | const statsCount = (result.match(/
  • /g) || []).length; 56 | 57 | expect(statsCount).toBe(mockData.stats.length); 58 | }); 59 | 60 | it('should not render Key Statistics items if the array is empty', async () => { 61 | const emptyKeyStats = { ...mockData, stats: [] }; 62 | const result = await KeyStatistics.main(emptyKeyStats); 63 | 64 | expect(result).not.toContain('
  • '); 65 | }); 66 | 67 | /* XSS */ 68 | it('should escape XSS in stats content', async () => { 69 | const statsDataWithScripts = { 70 | ...mockData, 71 | stats: [ 72 | { value: '', text: 'Safe text 1' }, 73 | { value: '100', text: '' } 74 | ] 75 | }; 76 | 77 | const result = await KeyStatistics.main(statsDataWithScripts); 78 | 79 | expect(result).toContain(xssSafeContent('')); 80 | expect(result).toContain(xssSafeContent('')); 81 | 82 | expect(result).not.toContain(''); 83 | expect(result).not.toContain(''); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "key-statistics", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays numeric statistics in up to five cards.", 6 | "displayName": "Key Statistics", 7 | "version": "2.0.4", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "insert_chart", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "functions": [ 18 | { 19 | "name": "main", 20 | "entry": "main.js", 21 | "input": { 22 | "type": "object", 23 | "title": "Key Statistics Content", 24 | "required": ["stats"], 25 | "properties": { 26 | "title": { 27 | "type": "string", 28 | "title": "Title", 29 | "description": "The title displayed above the key statistics.", 30 | "default": "Section title", 31 | "ui:metadata": { 32 | "inlineEditable": true 33 | } 34 | }, 35 | "stats": { 36 | "type": "array", 37 | "title": "Statistics", 38 | "description": "A list of up to five key statistics displayed in cards.", 39 | "minItems": 3, 40 | "maxItems": 5, 41 | "items": { 42 | "type": "object", 43 | "title": "Statistic", 44 | "properties": { 45 | "value": { 46 | "type": "string", 47 | "title": "Value", 48 | "description": "The number representing the statistic.", 49 | "minLength": 1, 50 | "maxLength": 5, 51 | "default": "01", 52 | "ui:metadata": { 53 | "inlineEditable": true 54 | } 55 | }, 56 | "text": { 57 | "type": "string", 58 | "title": "Supporting Text", 59 | "description": "The text providing additional context for the statistic.", 60 | "default": "Supporting text", 61 | "ui:metadata": { 62 | "inlineEditable": true 63 | } 64 | } 65 | }, 66 | "required": ["value"] 67 | } 68 | } 69 | } 70 | }, 71 | "output": { 72 | "responseType": "html" 73 | } 74 | } 75 | ], 76 | "staticFiles": { 77 | "locationRoot": "./" 78 | }, 79 | "previews": { 80 | "default": { 81 | "functionData": { 82 | "main": { 83 | "inputData": { 84 | "type": "file", 85 | "path": "example.data.json" 86 | }, 87 | "wrapper": { 88 | "path": "preview.html" 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /dxp/component-service/key-statistics/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/README.md: -------------------------------------------------------------------------------- 1 | # Testimonials 2 | 3 | ## Description 4 | 5 | The Testimonials component displays a list of testimonials in a carousel slider. Each testimonial consists of a text and an author. 6 | 7 | ## Editing 8 | 9 | Users can customize the Testimonials component by providing an array of testimonials. Each testimonial contains a text and an author. The component also includes navigation buttons to move between the testimonial slides. 10 | 11 | ## Properties Example: 12 | 13 | Provide an example of the properties that can be passed to the component. 14 | 15 | ```json 16 | { 17 | "title": "What our customers are saying", 18 | "testimonials": [ 19 | { 20 | "text": "This is an amazing product!", 21 | "author": "John Doe" 22 | }, 23 | { 24 | "text": "I highly recommend this service.", 25 | "author": "John Smith" 26 | } 27 | ] 28 | } 29 | ``` 30 | 31 | ## Component Properties 32 | 33 | | Property | Property Description | Property Type | Is Required | Default | 34 | | :----------- | :---------------------------------: | :-----------: | :---------: | :-----: | 35 | | title | The section title | string | | | 36 | | testimonials | An array of testimonial objects | array | ✓ | | 37 | | text | The text content of the testimonial | string | ✓ | | 38 | | author | The author of the testimonial | string | ✓ | | 39 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/css/testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/mixins'; 2 | @use '@styles/common/variables'; 3 | 4 | .testimonials { 5 | position: relative; 6 | 7 | &__item { 8 | @include mixins.flex-center-content; 9 | 10 | flex-direction: column; 11 | width: 100%; 12 | padding: variables.toRem(24) variables.toRem(64); 13 | background: var(--white); 14 | color: var(--color-text); 15 | 16 | @media screen and (max-width: variables.$breakpoint-md) { 17 | padding: 0; 18 | } 19 | 20 | p { 21 | font-size: var(--font-size-sm); 22 | font-weight: var(--font-normal); 23 | line-height: var(--line-height-md); 24 | text-align: center; 25 | } 26 | } 27 | 28 | &__author { 29 | margin-top: var(--spacing-md); 30 | font-weight: var(--font-semibold); 31 | } 32 | 33 | &__track { 34 | display: flex; 35 | overflow-x: auto; 36 | scroll-snap-type: x mandatory; 37 | scroll-behavior: smooth; 38 | padding: 0; 39 | // Hide scrollbar 40 | scrollbar-width: none; 41 | 42 | > li { 43 | flex: 0 0 100%; 44 | scroll-snap-align: start; 45 | scroll-snap-stop: always; 46 | list-style-type: none; 47 | } 48 | 49 | &::-webkit-scrollbar { 50 | display: none; 51 | } 52 | } 53 | 54 | &__buttons { 55 | position: absolute; 56 | top: calc(50% - var(--spacing-lg)); 57 | width: 100%; 58 | display: flex; 59 | justify-content: space-between; 60 | 61 | @media screen and (max-width: variables.$breakpoint-md) { 62 | top: 100%; 63 | } 64 | 65 | [disabled] { 66 | cursor: not-allowed; 67 | opacity: 0.5; 68 | } 69 | } 70 | 71 | &__button { 72 | background-size: contain; 73 | background-repeat: no-repeat; 74 | padding: var(--spacing-xs); 75 | background-color: transparent; 76 | cursor: pointer; 77 | 78 | &--prev { 79 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDE4IDM0IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTE3IDAuOTk5OTk5TDEgMTdMMTcgMzMiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg=='); 80 | } 81 | 82 | &--next { 83 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIzNCIgdmlld0JveD0iMCAwIDE4IDM0IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTE3IDAuOTk5OTk5TDEgMTdMMTcgMzMiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg=='); 84 | transform: rotate(180deg); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/example.data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "What our customers are saying", 3 | "testimonials": [ 4 | { 5 | "text": "

    Amazing service and workshops. Highly recommended. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies.

    ", 6 | "author": "- Great mind told that" 7 | }, 8 | { 9 | "text": "

    Keep in mind that if you take a tour through a hospital and look at every machine with an on and off switch that is brought into the service of diagnosing the human condition.

    That machine is based on principles of physics discovered by a physicist in a machine designed by an engineer.

    ", 10 | "author": "Neil deGrasse Tyson Ipsum" 11 | }, 12 | { 13 | "text": "

    Ever since the Industrial Revolution, investments in science and technology have proved to be reliable engines of economic growth. If homegrown interest in those fields is not regenerated soon, the comfortable lifestyle to which Americans have become accustomed will draw to a rapid close.

    ", 14 | "author": "Neil deGrasse" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/js/frontend.js: -------------------------------------------------------------------------------- 1 | // Function to initialize a single testimonials slider 2 | export const initializeTestimonials = (testimonials) => { 3 | // Find the track, previous button, and next button inside the testimonials container 4 | const track = testimonials.querySelector('[data-testimonials-track]'); 5 | const prev = testimonials.querySelector('[data-testimonials-prev]'); 6 | const next = testimonials.querySelector('[data-testimonials-next]'); 7 | 8 | if (track && prev && next) { 9 | let currentIndex = 0; 10 | const slides = Array.from(track.children); 11 | 12 | // Function to update aria-disabled and disabled attributes for buttons 13 | const updateAriaDisabled = (button, isDisabled) => { 14 | button.setAttribute('aria-disabled', isDisabled); 15 | if (isDisabled) { 16 | button.setAttribute('disabled', ''); 17 | } else { 18 | button.removeAttribute('disabled'); 19 | } 20 | }; 21 | 22 | // Function to update the slides' ARIA attributes and tabindex 23 | const updateSlides = () => { 24 | slides.forEach((slide, index) => { 25 | slide.removeAttribute('aria-current'); 26 | slide.setAttribute('tabindex', '-1'); 27 | if (index === currentIndex) { 28 | slide.setAttribute('aria-current', 'true'); 29 | slide.setAttribute('tabindex', '0'); 30 | slide.focus(); 31 | } 32 | }); 33 | 34 | // Update the disabled state of the prev and next buttons 35 | updateAriaDisabled(prev, currentIndex === 0); 36 | updateAriaDisabled(next, currentIndex === slides.length - 1); 37 | }; 38 | 39 | // Add event listener for the previous button 40 | prev.addEventListener('click', () => { 41 | if (currentIndex > 0) { 42 | currentIndex--; 43 | updateSlides(); 44 | } 45 | }); 46 | 47 | // Add event listener for the next button 48 | next.addEventListener('click', () => { 49 | if (currentIndex < slides.length - 1) { 50 | currentIndex++; 51 | updateSlides(); 52 | } 53 | }); 54 | 55 | slides.forEach((slide) => { 56 | slide.setAttribute('tabindex', '-1'); 57 | }); 58 | 59 | updateSlides(); 60 | } 61 | }; 62 | 63 | // Function to initialize all testimonials sliders on the page 64 | export const initializeAllTestimonials = () => { 65 | const testimonialsContainers = document.querySelectorAll( 66 | '[data-testimonials]' 67 | ); 68 | testimonialsContainers.forEach((testimonials) => 69 | initializeTestimonials(testimonials) 70 | ); 71 | }; 72 | 73 | // Initialize all testimonials sliders 74 | initializeAllTestimonials(); 75 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/main.js: -------------------------------------------------------------------------------- 1 | // Utility function to render raw HTML without extra parsing 2 | import { html } from '../../utils/html'; 3 | // Sanitizes dynamic content to prevent XSS attacks 4 | import { xssSafeContent } from '../../utils/xss'; 5 | 6 | // This module takes an object with "testimonials" properties as input. 7 | export default { 8 | async main({ title, testimonials }) { 9 | return html` 10 |
    11 | 12 | ${title 13 | ? `

    ${xssSafeContent(title)}

    ` 14 | : ''} 15 | 16 |
    23 | 24 |
      25 | 26 | ${testimonials 27 | .map( 28 | (testimonial, idx) => ` 29 | 30 |
    • 31 |
      32 | ${xssSafeContent(testimonial.text)} 33 | ${xssSafeContent(testimonial.author)} 34 |
      35 |
    • 36 | ` 37 | ) 38 | .join('')} 39 |
    40 | 41 | 42 | 43 |
    48 | 55 | 56 | 62 |
    63 |
    64 |
    65 | `; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/main.test.js: -------------------------------------------------------------------------------- 1 | import { xssSafeContent } from '../../utils/xss'; 2 | import Testimonials from './main.js'; 3 | 4 | const mockData = { 5 | title: 'Testimonials Section', 6 | testimonials: [ 7 | { text: 'This is a great product!', author: 'John Doe' }, 8 | { text: 'I love it!', author: 'Jane Smith' }, 9 | { text: 'Highly recommend to everyone.', author: 'Someone Famous' } 10 | ] 11 | }; 12 | 13 | describe('Testimonials', () => { 14 | /* General */ 15 | it('should return valid HTML structure', async () => { 16 | const result = await Testimonials.main(mockData); 17 | 18 | expect(result).toBeDefined(); 19 | expect(result).toContain('
    '); 20 | }); 21 | 22 | /* Title */ 23 | it('should render the title if provided', async () => { 24 | const result = await Testimonials.main(mockData); 25 | 26 | expect(result).toContain( 27 | '

    Testimonials Section

    ' 28 | ); 29 | }); 30 | 31 | it('should not render the title if it is null or undefined', async () => { 32 | const dataWithoutTitle = { ...mockData, title: null }; 33 | const result = await Testimonials.main(dataWithoutTitle); 34 | 35 | expect(result).not.toContain( 36 | '

    ' 37 | ); 38 | }); 39 | 40 | it('should not render the heading tag if title is empty', async () => { 41 | const dataWithoutTitle = { ...mockData, title: '' }; 42 | const result = await Testimonials.main(dataWithoutTitle); 43 | 44 | expect(result).not.toContain('

    '); 45 | expect(result).toContain('
    '); 46 | }); 47 | 48 | /* Testimonials items */ 49 | it('should render all testimonials items', async () => { 50 | const result = await Testimonials.main(mockData); 51 | const listItemCount = ( 52 | result.match(/
  • /g) || [] 53 | ).length; 54 | expect(listItemCount).toBe(mockData.testimonials.length); 55 | }); 56 | 57 | it('should not render testimonials items if the array is empty', async () => { 58 | const emptyTestimonialsData = { ...mockData, testimonials: [] }; 59 | const result = await Testimonials.main(emptyTestimonialsData); 60 | 61 | expect(result).not.toContain( 62 | '
  • ' 63 | ); 64 | }); 65 | 66 | /* XSS */ 67 | it('should escape XSS in testimonials content', async () => { 68 | const testimonialsDataWithScripts = { 69 | ...mockData, 70 | testimonials: [ 71 | { 72 | text: '', 73 | author: '' 74 | } 75 | ] 76 | }; 77 | 78 | const result = await Testimonials.main(testimonialsDataWithScripts); 79 | 80 | expect(result).toContain(xssSafeContent('')); 81 | expect(result).toContain(xssSafeContent('')); 82 | expect(result).not.toContain(''); 83 | expect(result).not.toContain(''); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://localhost:3000/schemas/v1.json", 3 | "name": "testimonials", 4 | "namespace": "edge-dxp-comp-lib", 5 | "description": "Displays testimonials in a slider format.", 6 | "displayName": "Testimonials", 7 | "version": "2.0.2", 8 | "type": "edge", 9 | "mainFunction": "main", 10 | "icon": { 11 | "id": "recent_actors", 12 | "color": { 13 | "type": "enum", 14 | "value": "gray" 15 | } 16 | }, 17 | "functions": [ 18 | { 19 | "name": "main", 20 | "entry": "main.js", 21 | "input": { 22 | "type": "object", 23 | "properties": { 24 | "title": { 25 | "type": "string", 26 | "title": "Section Title", 27 | "description": "The title displayed above the testimonials section.", 28 | "default": "Section Title", 29 | "ui:metadata": { 30 | "inlineEditable": true 31 | } 32 | }, 33 | "testimonials": { 34 | "title": "Testimonial Items", 35 | "type": "array", 36 | "minItems": 1, 37 | "maxItems": 20, 38 | "items": { 39 | "type": "object", 40 | "title": "Testimonial", 41 | "properties": { 42 | "text": { 43 | "type": "FormattedText", 44 | "title": "Text Content", 45 | "description": "The testimonial text displayed inside the slide.", 46 | "ui:metadata": { 47 | "inlineEditable": true 48 | } 49 | }, 50 | "author": { 51 | "type": "string", 52 | "title": "Author", 53 | "description": "The author's name displayed below the testimonial text.", 54 | "default": "Author", 55 | "ui:metadata": { 56 | "inlineEditable": true 57 | } 58 | } 59 | }, 60 | "required": ["text", "author"] 61 | } 62 | } 63 | }, 64 | "required": ["testimonials"] 65 | }, 66 | "output": { 67 | "responseType": "html" 68 | } 69 | } 70 | ], 71 | "staticFiles": { 72 | "locationRoot": "./" 73 | }, 74 | "previews": { 75 | "default": { 76 | "functionData": { 77 | "main": { 78 | "inputData": { 79 | "type": "file", 80 | "path": "example.data.json" 81 | }, 82 | "wrapper": { 83 | "path": "preview.html" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /dxp/component-service/testimonials/preview.html: -------------------------------------------------------------------------------- 1 | [component://output] 2 | -------------------------------------------------------------------------------- /dxp/utils/html.js: -------------------------------------------------------------------------------- 1 | export const html = String.raw; 2 | -------------------------------------------------------------------------------- /dxp/utils/xss.js: -------------------------------------------------------------------------------- 1 | import xss, { getDefaultWhiteList } from 'xss'; 2 | 3 | var specificTagChanges = { 4 | circle: ['cx', 'cy', 'r', 'stroke', 'stroke-width', 'fill'], 5 | clipPath: [], 6 | defs: [], 7 | ellipse: ['cx', 'cy', 'rx', 'ry', 'style'], 8 | g: ['clip-path', 'fill'], 9 | line: ['x1', 'y1', 'x2', 'y2', 'style'], 10 | path: ['d', 'fill', 'stroke', 'stroke-width'], 11 | polygon: ['points', 'style'], 12 | polyline: ['points', 'style'], 13 | rect: ['width', 'height', 'fill'], 14 | svg: ['width', 'height', 'viewBox', 'fill'] 15 | }; 16 | var addToAll = [ 17 | 'class', 18 | 'id', 19 | // WCAG ARIA related roles (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes) 20 | // Allow on anything and leave it to the developer to use it on the correct tags. 21 | 'role', 22 | 'aria-activedescendant', 23 | 'aria-atomic', 24 | 'aria-autocomplete', 25 | 'aria-braillelabel', 26 | 'aria-brailleroledescription', 27 | 'aria-busy', 28 | 'aria-checked', 29 | 'aria-colcount', 30 | 'aria-colindex', 31 | 'aria-colindextext', 32 | 'aria-colspan', 33 | 'aria-controls', 34 | 'aria-current', 35 | 'aria-describedby', 36 | 'aria-description', 37 | 'aria-details', 38 | 'aria-disabled', 39 | 'aria-dropeffect', 40 | 'aria-errormessage', 41 | 'aria-expanded', 42 | 'aria-flowto', 43 | 'aria-grabbed', 44 | 'aria-haspopup', 45 | 'aria-hidden', 46 | 'aria-invalid', 47 | 'aria-keyshortcuts', 48 | 'aria-label', 49 | 'aria-labelledby', 50 | 'aria-level', 51 | 'aria-live', 52 | 'aria-modal', 53 | 'aria-multiline', 54 | 'aria-multiselectable', 55 | 'aria-orientation', 56 | 'aria-owns', 57 | 'aria-placeholder', 58 | 'aria-posinset', 59 | 'aria-pressed', 60 | 'aria-readonly', 61 | 'aria-relevant', 62 | 'aria-required', 63 | 'aria-roledescription', 64 | 'aria-rowcount', 65 | 'aria-rowindex', 66 | 'aria-rowindextext', 67 | 'aria-rowspan', 68 | 'aria-selected', 69 | 'aria-setsize', 70 | 'aria-sort', 71 | 'aria-valuemax', 72 | 'aria-valuemin', 73 | 'aria-valuenow', 74 | 'aria-valuetext' 75 | ]; 76 | 77 | export function xssSafeContent(content) { 78 | const customWhitelist = getDefaultWhiteList(); 79 | for (const [key, value] of Object.entries(specificTagChanges)) { 80 | customWhitelist[key] = value; 81 | } 82 | Object.keys(customWhitelist).forEach((tag) => { 83 | var _a; 84 | return (_a = customWhitelist[tag]) == null ? void 0 : _a.push(...addToAll); 85 | }); 86 | 87 | return xss(content, { whiteList: customWhitelist }); 88 | } 89 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintRecommended from '@eslint/js'; 2 | import importPlugin from 'eslint-plugin-import'; 3 | 4 | export default [ 5 | eslintRecommended.configs.recommended, 6 | { 7 | files: ['dxp/component-service/**/*.js', 'src/scripts/**/*.js'], 8 | languageOptions: { 9 | ecmaVersion: 2021, 10 | sourceType: 'module', 11 | globals: { 12 | fetch: 'readonly', 13 | console: 'readonly', 14 | window: 'readonly', 15 | document: 'readonly', 16 | URL: 'readonly', 17 | localStorage: 'readonly', 18 | setTimeout: 'readonly' 19 | } 20 | }, 21 | settings: { 22 | 'import/resolver': { 23 | alias: { 24 | map: [ 25 | ['@global_components', './src/global_components'], 26 | ['@dxp', './dxp'], 27 | ['@components', './dxp/component-service'], 28 | ['@styles', './src/styles'] 29 | ], 30 | extensions: ['.js', '.scss'] 31 | } 32 | } 33 | }, 34 | plugins: { 35 | import: importPlugin 36 | }, 37 | rules: { 38 | 'no-console': 'off', 39 | semi: ['warn', 'always'], 40 | 'import/no-unresolved': 'warn' 41 | } 42 | }, 43 | { 44 | ignores: [ 45 | 'node_modules/', 46 | 'dist/', 47 | '**/*.test.js', 48 | 'vite.config.js', 49 | '*.js', 50 | 'dxp/01_compilers/*.js', 51 | 'src/entry-server.js' 52 | ] 53 | } 54 | ]; 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 16 | 17 | 22 | 23 | 24 | 25 | DXP Component Library 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dxp-component-library", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "concurrently --kill-others \"npm run cmp\" \"sleep 2 && node server\"", 8 | "build": "npm run build:client && npm run build:server", 9 | "build:client": "vite build --ssrManifest", 10 | "build:server": "vite build --ssr src/entry-server.js", 11 | "cmp": "dxp-next cmp dev-ui ./dxp/component-service", 12 | "test": "vitest --coverage", 13 | "lint:css": "stylelint \"**/*.scss\"", 14 | "lint:css:fix": "stylelint \"**/*.scss\" --fix", 15 | "prettier": "npx prettier .", 16 | "prettier:fix": "npx prettier . --write", 17 | "deploy": "npm run deploy:component --", 18 | "deploy:component": "FEATURE_EDGE_COMPONENTS=true dxp-next cmp deploy ./dxp/component-service/$npm_config_name", 19 | "vermgmt": "vermgmt", 20 | "lint:js": "eslint dxp/component-service src/scripts", 21 | "lint:js:fix": "eslint dxp/component-service src/scripts --fix", 22 | "fix:all": "npm run lint:css:fix && npm run lint:js:fix && npm run prettier:fix" 23 | }, 24 | "devDependencies": { 25 | "@squiz/component-lib": "^1.7.0", 26 | "@vitest/coverage-v8": "^3.1.3", 27 | "autoprefixer": "^10.4.20", 28 | "compression": "^1.7.4", 29 | "concurrently": "^9.0.1", 30 | "cross-env": "^7.0.3", 31 | "eslint": "^9.16.0", 32 | "eslint-config-prettier": "^9.1.0", 33 | "eslint-import-resolver-alias": "^1.1.2", 34 | "eslint-plugin-import": "^2.31.0", 35 | "express": "^4.19.2", 36 | "glob": "^11.0.0", 37 | "globals": "^15.14.0", 38 | "happy-dom": "^15.11.7", 39 | "kill-port": "^2.0.1", 40 | "sass": "^1.79.5", 41 | "sirv": "^2.0.4", 42 | "stylelint": "^16.10.0", 43 | "stylelint-config-standard": "^36.0.1", 44 | "stylelint-config-standard-scss": "^13.1.0", 45 | "vite": "^6.3.5", 46 | "vite-plugin-eslint": "^1.8.1", 47 | "vite-plugin-sass-glob-import": "^5.0.0", 48 | "vitest": "^3.1.3" 49 | }, 50 | "dependencies": { 51 | "@squiz/vermgmt": "^1.0.2", 52 | "node-html-parser": "^6.1.13", 53 | "stylelint-selector-bem-pattern": "^4.0.1", 54 | "vite-plugin-static-copy": "^2.2.0", 55 | "xss": "^1.0.15" 56 | }, 57 | "overrides": { 58 | "axios": "^1.8.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import express from 'express'; 3 | 4 | // Constants 5 | const isProduction = process.env.NODE_ENV === 'production'; 6 | const port = process.env.PORT || 4000; 7 | const base = process.env.BASE || '/'; 8 | 9 | // Cached production assets 10 | const ssrManifest = isProduction 11 | ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8') 12 | : undefined; 13 | 14 | // Create http server 15 | const app = express(); 16 | 17 | // Add Vite or respective production middlewares 18 | let vite; 19 | if (!isProduction) { 20 | const { createServer } = await import('vite'); 21 | vite = await createServer({ 22 | server: { middlewareMode: true }, 23 | appType: 'custom', 24 | base 25 | }); 26 | app.use(vite.middlewares); 27 | } else { 28 | const compression = (await import('compression')).default; 29 | const sirv = (await import('sirv')).default; 30 | app.use(compression()); 31 | app.use(base, sirv('./dist/client', { extensions: [] })); 32 | } 33 | 34 | app.use('/scripts', express.static('./src/scripts')); 35 | app.use('/styles', express.static('./src/styles')); 36 | 37 | app.use('*', async (req, res) => { 38 | try { 39 | const url = req.originalUrl.replace(base, ''); 40 | 41 | let template; 42 | let render; 43 | if (!isProduction) { 44 | template = await fs.readFile('./index.html', 'utf-8'); 45 | template = await vite.transformIndexHtml(url, template); 46 | render = (await vite.ssrLoadModule('/src/entry-server.js')).render; 47 | } 48 | 49 | const rendered = await render(url, ssrManifest); 50 | 51 | const html = template 52 | .replace(``, rendered.head ?? '') 53 | .replace(``, rendered.html ?? ''); 54 | 55 | res.status(200).set({ 'Content-Type': 'text/html' }).send(html); 56 | } catch (e) { 57 | vite?.ssrFixStacktrace(e); 58 | console.log(e.stack); 59 | res.status(500).end(e.stack); 60 | } 61 | }); 62 | 63 | // Start http server 64 | app.listen(port, () => { 65 | console.log(`Server started at http://localhost:${port}`); 66 | }); 67 | -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import Index from '@pages/index.js'; 2 | import Homepage from '@pages/homepage.js'; 3 | import WysiwygElements from '@pages/wysiwyg-elements.js'; 4 | import Subpage from '@pages/subpage.js'; 5 | import ArticlePage from '@pages/article-page.js'; 6 | 7 | export async function render(url, ssrManifest) { 8 | // Define basic page templates or import your component-rendering logic here 9 | let htmlContent; 10 | let pageTitle; 11 | switch (url) { 12 | case '': 13 | pageTitle = 'coreDXP'; 14 | htmlContent = await Index(); 15 | break; 16 | case 'homepage': 17 | pageTitle = 'Home Page'; 18 | htmlContent = await Homepage(); 19 | break; 20 | case 'wysiwyg-elements': 21 | pageTitle = 'WysiwygElements'; 22 | htmlContent = await WysiwygElements(); 23 | break; 24 | case 'article': 25 | pageTitle = 'Article Page'; 26 | htmlContent = await ArticlePage(); 27 | break; 28 | case 'subpage': 29 | pageTitle = 'Subpage'; 30 | htmlContent = await Subpage(); 31 | break; 32 | default: 33 | pageTitle = '404 Not Found'; 34 | htmlContent = `

    404 - Page not found

    35 |

    😥 It is more than likely that you just need to add 36 | your page page to this file ./src/entry-server.js

    37 | `; 38 | break; 39 | } 40 | return { 41 | head: `${pageTitle}`, // Add other head elements if needed 42 | html: htmlContent 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/global_components/content/article.js: -------------------------------------------------------------------------------- 1 | export const Article = ` 2 |
    3 |

    Discover the art of sculpture in our engaging workshop designed for beginners.

    4 |

    Whether you are an aspiring artist or simply looking to explore a new hobby, this workshop will provide you with the foundational skills needed to create your own sculptures. Led by experienced instructors, you will learn various techniques, from clay modeling to carving, in a supportive and inspiring environment. 5 |

    6 |

    In this workshop, participants will explore the basic principles of sculpture, including form, texture, and proportion. The session will cover a range of materials and techniques, such as clay modeling, wood carving, and metalworking. Participants will have the opportunity to create their own pieces under the guidance of professional sculptors, gaining hands-on experience in shaping and crafting their visions.

    7 |
    8 | `; 9 | -------------------------------------------------------------------------------- /src/global_components/content/homepage.js: -------------------------------------------------------------------------------- 1 | export const Homepagecontent = `
    2 |
    3 |

    DXP Component Library enablement

    4 |
    5 |

    The DXP Component Library refers to 3 critical product capabilities on the DXP: Component Service, Page Builder, Personalisation

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

    Enhancing Developer Experience

    14 |

    15 | This repository aims to provide a seamless, scalable, and efficient environment for developers working with the Component Service, improving productivity and performance. 16 |

    17 |

    18 | Combining Vite's modern frontend tooling with Squiz DXP's Component Service empowers developers to build scalable and maintainable web applications. 19 |

    20 |
    21 | 22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    `; 29 | -------------------------------------------------------------------------------- /src/global_components/footer/footer.js: -------------------------------------------------------------------------------- 1 | // This component will not be added as a Component Service but will be integrated directly into the CMS. 2 | import PaintTool from '@images/paint_icon.png'; 3 | 4 | export const Footer = ` 5 | 29 | `; 30 | -------------------------------------------------------------------------------- /src/global_components/navigation/navigation.js: -------------------------------------------------------------------------------- 1 | // This component was added solely to improve navigation on the site during local development.It will not be added to the CMS. 2 | 3 | import Logo from '@images/favicon/favicon-96x96.png'; 4 | 5 | export const Navigation = ` 6 | 57 | `; 58 | -------------------------------------------------------------------------------- /src/html/article-page.js: -------------------------------------------------------------------------------- 1 | // template imports 2 | import { Navigation } from '@global_components/navigation/navigation'; 3 | import { Footer } from '@global_components/footer/footer'; 4 | import { Article } from '@global_components/content/article'; 5 | 6 | // render template 7 | export default async function () { 8 | return ` 9 | ${Navigation} 10 |
    11 | 12 | ${Article} 13 | 14 | 15 | 16 |
    17 | ${Footer} 18 | `; 19 | } 20 | -------------------------------------------------------------------------------- /src/html/homepage.js: -------------------------------------------------------------------------------- 1 | // template imports 2 | import { Navigation } from '@global_components/navigation/navigation'; 3 | import { Footer } from '@global_components/footer/footer'; 4 | 5 | // render template 6 | export default async function () { 7 | return ` 8 | ${Navigation} 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 | ${Footer} 23 | `; 24 | } 25 | -------------------------------------------------------------------------------- /src/html/index.js: -------------------------------------------------------------------------------- 1 | // template imports 2 | import { Navigation } from '@global_components/navigation/navigation'; 3 | import { Footer } from '@global_components/footer/footer'; 4 | import { getStarted } from '@global_components/content/getting-started'; 5 | 6 | // render template 7 | export default async function () { 8 | return ` 9 | ${Navigation} 10 | ${getStarted} 11 | ${Footer} 12 | `; 13 | } 14 | -------------------------------------------------------------------------------- /src/html/subpage.js: -------------------------------------------------------------------------------- 1 | // template imports 2 | import { Navigation } from '@global_components/navigation/navigation'; 3 | import { Footer } from '@global_components/footer/footer'; 4 | import { Article } from '@global_components/content/article'; 5 | 6 | // render template 7 | export default async function () { 8 | return ` 9 | ${Navigation} 10 |
    11 | 12 | ${Article} 13 | 14 | 15 | 16 |
    17 | ${Footer} 18 | `; 19 | } 20 | -------------------------------------------------------------------------------- /src/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/images/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /src/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/favicon/favicon.ico -------------------------------------------------------------------------------- /src/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyWebSite", 3 | "short_name": "MySite", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /src/images/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /src/images/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/logo.png -------------------------------------------------------------------------------- /src/images/paint_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/paint_icon.png -------------------------------------------------------------------------------- /src/images/squiz-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/dxp-component-library/4661c611935e65ae0b2ab54f540c3fcec91ed528/src/images/squiz-logo.png -------------------------------------------------------------------------------- /src/scripts/common/navbar.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | // Navbar toggler 3 | const navbarToggler = document.querySelector('.navbar__toggler'); 4 | const navbarCollapse = document.querySelector('.collapse'); 5 | 6 | if (navbarToggler && navbarCollapse) { 7 | navbarToggler.addEventListener('click', () => { 8 | navbarCollapse.classList.toggle('show'); 9 | }); 10 | } 11 | 12 | // Highlight active link 13 | const links = document.querySelectorAll('.navbar__link'); 14 | const currentPath = window.location.pathname; 15 | 16 | links.forEach((link) => { 17 | const linkPath = new URL(link.href, window.location.origin).pathname; 18 | link.classList.toggle('active', linkPath === currentPath); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/scripts/common/switcher.js: -------------------------------------------------------------------------------- 1 | // Export for tests 2 | export const logThemeError = (theme) => { 3 | console.error(`No element found for theme: ${theme}`); 4 | }; 5 | 6 | export const setActiveTheme = (theme, switcherCss) => { 7 | const html = document.documentElement; 8 | 9 | // Remove existing theme classes 10 | switcherCss.forEach((themeInput) => { 11 | html.classList.remove(themeInput.getAttribute('value')); 12 | themeInput.disabled = false; 13 | themeInput.nextElementSibling?.classList.remove('active'); 14 | }); 15 | 16 | // Add the new class 17 | html.classList.add(theme); 18 | 19 | // Mark the active theme 20 | const activeInput = document.querySelector( 21 | `.style-switcher__css[value="${theme}"]` 22 | ); 23 | if (!activeInput) { 24 | logThemeError(theme); 25 | return; 26 | } 27 | 28 | // Save to localStorage 29 | localStorage.setItem('theme', theme); 30 | }; 31 | 32 | export const initializeSwitcher = () => { 33 | const switcherToggle = document.querySelector( 34 | '[aria-controls="style-switcher"]' 35 | ); 36 | const switcherDropdown = document.querySelector('#style-switcher'); 37 | const switcherCss = document.querySelectorAll('.style-switcher__css'); 38 | 39 | if (switcherToggle && switcherDropdown && switcherCss.length) { 40 | // Load saved theme 41 | const savedTheme = localStorage.getItem('theme'); 42 | if (savedTheme) { 43 | setActiveTheme(savedTheme, switcherCss); 44 | } 45 | 46 | // Toggle switcher dropdown 47 | switcherToggle.addEventListener('click', () => { 48 | const isExpanded = 49 | switcherToggle.getAttribute('aria-expanded') === 'true'; 50 | switcherToggle.setAttribute('aria-expanded', !isExpanded); 51 | switcherDropdown.hidden = isExpanded; 52 | }); 53 | 54 | // Change theme on input click 55 | switcherCss.forEach((toggle) => { 56 | toggle.addEventListener('click', (e) => { 57 | const theme = e.target.getAttribute('value'); 58 | setActiveTheme(theme, switcherCss); 59 | }); 60 | }); 61 | 62 | // Close switcher if clicked outside 63 | document.addEventListener('click', (event) => { 64 | if ( 65 | switcherToggle.getAttribute('aria-expanded') === 'true' && 66 | !event.target.closest('#style-switcher') && 67 | event.target !== switcherToggle 68 | ) { 69 | switcherToggle.setAttribute('aria-expanded', 'false'); 70 | switcherDropdown.hidden = true; 71 | } 72 | }); 73 | } 74 | }; 75 | 76 | // Automatically initialize when the DOM is ready 77 | document.addEventListener('DOMContentLoaded', initializeSwitcher); 78 | -------------------------------------------------------------------------------- /src/scripts/common/tests/navbar.test.js: -------------------------------------------------------------------------------- 1 | import '../navbar'; 2 | 3 | describe('Navbar - Frontend', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = ` 6 | 12 | `; 13 | document.dispatchEvent(new Event('DOMContentLoaded')); 14 | }); 15 | 16 | /* Navbar toggler */ 17 | it('should toggle the visibility of the collapse element on button click', () => { 18 | const navbarToggler = document.querySelector('.navbar__toggler'); 19 | const navbarCollapse = document.querySelector('.collapse'); 20 | 21 | navbarToggler.click(); 22 | expect(navbarCollapse.classList.contains('show')).toBe(true); 23 | 24 | navbarToggler.click(); 25 | expect(navbarCollapse.classList.contains('show')).toBe(false); 26 | }); 27 | 28 | /* Highlight active link */ 29 | it('should highlight the active link based on the current path', () => { 30 | const currentPath = '/about'; 31 | Object.defineProperty(window, 'location', { 32 | value: { pathname: currentPath }, 33 | writable: true 34 | }); 35 | 36 | document.dispatchEvent(new Event('DOMContentLoaded')); 37 | 38 | const activeLink = document.querySelector('.navbar__link.active'); 39 | expect(activeLink).not.toBeNull(); 40 | expect(activeLink.textContent).toBe('About'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/scripts/common/tests/switcher.test.js: -------------------------------------------------------------------------------- 1 | import { initializeSwitcher, setActiveTheme } from '../switcher'; 2 | 3 | describe('Switcher - Frontend', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = ` 6 |
    7 | 8 | 12 |
    13 | `; 14 | 15 | initializeSwitcher(); 16 | }); 17 | 18 | // Toggle the dropdown visibility 19 | it('should toggle the visibility of the dropdown on button click', () => { 20 | const switcherToggle = document.querySelector( 21 | '[aria-controls="style-switcher"]' 22 | ); 23 | const switcherDropdown = document.querySelector('#style-switcher'); 24 | 25 | switcherToggle.click(); 26 | expect(switcherToggle.getAttribute('aria-expanded')).toBe('true'); 27 | expect(switcherDropdown.hidden).toBe(false); 28 | 29 | switcherToggle.click(); 30 | expect(switcherToggle.getAttribute('aria-expanded')).toBe('false'); 31 | expect(switcherDropdown.hidden).toBe(true); 32 | }); 33 | 34 | // Active theme on LocalStorage 35 | it('should set the active theme and save it to localStorage', () => { 36 | const themeLight = document.querySelector( 37 | '.style-switcher__css[value="theme-light"]' 38 | ); 39 | const themeDark = document.querySelector( 40 | '.style-switcher__css[value="theme-dark"]' 41 | ); 42 | 43 | themeDark.click(); 44 | expect(document.documentElement.classList.contains('theme-dark')).toBe( 45 | true 46 | ); 47 | expect(localStorage.getItem('theme')).toBe('theme-dark'); 48 | 49 | themeLight.click(); 50 | expect(document.documentElement.classList.contains('theme-light')).toBe( 51 | true 52 | ); 53 | expect(localStorage.getItem('theme')).toBe('theme-light'); 54 | }); 55 | 56 | // Closing dropdown when clicking outside 57 | it('should close the dropdown when clicking outside', () => { 58 | const switcherToggle = document.querySelector( 59 | '[aria-controls="style-switcher"]' 60 | ); 61 | const switcherDropdown = document.querySelector('#style-switcher'); 62 | 63 | switcherToggle.click(); 64 | expect(switcherToggle.getAttribute('aria-expanded')).toBe('true'); 65 | expect(switcherDropdown.hidden).toBe(false); 66 | 67 | document.body.click(); 68 | expect(switcherToggle.getAttribute('aria-expanded')).toBe('false'); 69 | expect(switcherDropdown.hidden).toBe(true); 70 | }); 71 | 72 | // Test if switcher exists 73 | it('should log an error when theme element is not found', () => { 74 | const consoleErrorSpy = vi 75 | .spyOn(console, 'error') 76 | .mockImplementation(() => {}); 77 | 78 | const theme = 'nonexistent-theme'; 79 | const switcherCss = document.querySelectorAll('.style-switcher__css'); 80 | 81 | setActiveTheme(theme, switcherCss); 82 | 83 | expect(consoleErrorSpy).toHaveBeenCalledWith( 84 | `No element found for theme: ${theme}` 85 | ); 86 | consoleErrorSpy.mockRestore(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/scripts/main.js: -------------------------------------------------------------------------------- 1 | // Global switcher styles 2 | import './common/switcher'; 3 | import './common/navbar'; 4 | 5 | // import CSS 6 | import '@styles/main.scss'; 7 | 8 | // Dynamically load in client side component JS 9 | export const components = import.meta.glob( 10 | '../../dxp/component-service/**/js/frontend.js', 11 | { eager: true } 12 | ); 13 | -------------------------------------------------------------------------------- /src/styles/common/_variables.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | /* transform px to rem 4 | Output example: #{toRem(12)}; */ 5 | @function toRem($px-value) { 6 | @return calc(#{$px-value} / var(--px-base) * var(--px-to-rem)); 7 | } 8 | 9 | :root { 10 | --px-base: 16; 11 | --px-to-rem: 1rem; 12 | 13 | /* General */ 14 | --font: 'Open Sans', sans-serif; 15 | --font-headings: 'Roboto Condensed', sans-serif; 16 | --white: rgb(255 255 255); 17 | --black: rgb(0 0 0); 18 | --color-bg: rgb(255 255 255); 19 | --color-bg-alt: rgb(239 239 239); 20 | --color-text: rgb(0 0 0); 21 | --color-focus: rgb(0 0 0); 22 | --primary: rgb(242 242 242); 23 | --accent: rgb(191 24 35); 24 | --overlay-background: rgb(0 0 0 / 50%); 25 | --default-background-color: rgb(204 204 204); 26 | --gray: rgb(217 217 217); 27 | --primary-darker: color-mix(in srgb, var(--primary) 80%, rgb(0 0 0) 20%); 28 | 29 | /* Font sizes */ 30 | --font-size-huge: #{toRem(56)}; 31 | --font-size-xxl: #{toRem(48)}; // Very large text, e.g., main headers 32 | --font-size-xl: #{toRem(40)}; // Extra large text, e.g., section headers 33 | --font-size-lg: #{toRem(32)}; // Large text, e.g., subheaders 34 | --font-size-md: #{toRem(24)}; // Medium text, e.g., sub-subheaders 35 | --font-size-sm: #{toRem(20)}; // Small text, e.g., emphasized text 36 | --font-size-xs: #{toRem(14)}; // Small text, e.g., spans 37 | --base-font-size: #{toRem(16)}; // Base font size for paragraphs 38 | 39 | /* Spacing */ 40 | --spacing-xs: #{toRem(8)}; 41 | --spacing-sm: #{toRem(16)}; 42 | --spacing-md: #{toRem(24)}; 43 | --spacing-lg: #{toRem(48)}; 44 | --spacing-xl: #{toRem(64)}; 45 | --spacing-xxl: #{toRem(160)}; 46 | 47 | /* Weights */ 48 | --font-normal: 400; 49 | --font-semibold: 500; 50 | --font-bold: 600; 51 | 52 | /* Line heights */ 53 | --base-line-height: #{toRem(24)}; // for paragraphs 54 | --line-height-xxl: #{toRem(48)}; // for h1 55 | --line-height-xl: #{toRem(40)}; // for h2 56 | --line-height-lg: #{toRem(32)}; // for h3 57 | --line-height-md: #{toRem(28)}; // for h4, blockquote 58 | --line-height-sm: #{toRem(24)}; // for h5, h6 59 | 60 | /* Z-indexes */ 61 | --z-index-below: -1; 62 | --z-index-default: 1; 63 | --z-index-above: 2; 64 | --z-index-menus: 5; 65 | 66 | // global styles for switcher 67 | --switcher-bg: rgb(255 255 255); 68 | --switcher-text: rgb(0 0 0); 69 | --switcher-bg-hover: rgb(242 242 242); 70 | 71 | /* Container width */ 72 | --container: #{toRem(1200)}; 73 | } 74 | 75 | /* Breakpoints */ 76 | $breakpoint-xs: 30rem; 77 | $breakpoint-sm: 48rem; 78 | $breakpoint-md: 62rem; 79 | $breakpoint-lg: 75rem; 80 | -------------------------------------------------------------------------------- /src/styles/common/fonts.scss: -------------------------------------------------------------------------------- 1 | @use 'variables'; 2 | 3 | @use './mixins' as mixins; 4 | 5 | @import 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800&display=swap'; 6 | @import 'https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&family=Roboto:wght@400;700&display=swap'; 7 | @import 'https://fonts.googleapis.com/css2?family=Poppins:wght@600&display=swap'; 8 | @import 'https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap'; 9 | 10 | h1, 11 | .heading-primary { 12 | @include mixins.heading-styles( 13 | var(--font-size-xxl), 14 | var(--line-height-xxl), 15 | var(--font-semibold), 16 | var(--color-text), 17 | var(--spacing-lg), 18 | var(--spacing-lg) 19 | ); 20 | } 21 | 22 | h2, 23 | .heading-secondary { 24 | @include mixins.heading-styles( 25 | var(--font-size-xl), 26 | var(--line-height-xl), 27 | var(--font-semibold), 28 | var(--color-text), 29 | var(--spacing-md), 30 | var(--spacing-md) 31 | ); 32 | } 33 | 34 | h3, 35 | .heading-tertiary { 36 | @include mixins.heading-styles( 37 | var(--font-size-lg), 38 | var(--line-height-lg), 39 | var(--font-semibold), 40 | var(--spacing-md), 41 | var(--spacing-md) 42 | ); 43 | } 44 | 45 | h4, 46 | .heading-quaternary { 47 | @include mixins.heading-styles( 48 | var(--font-size-md), 49 | var(--line-height-md), 50 | var(--font-semibold), 51 | var(--spacing-sm), 52 | var(--spacing-sm), 53 | var(--spacing-sm) 54 | ); 55 | } 56 | 57 | h5, 58 | .heading-quinary { 59 | @include mixins.heading-styles( 60 | var(--font-size-sm), 61 | var(--line-height-sm), 62 | var(--font-semibold), 63 | var(--spacing-sm), 64 | var(--spacing-sm) 65 | ); 66 | } 67 | 68 | h6, 69 | .heading-senary { 70 | @include mixins.heading-styles( 71 | var(--base-font-size), 72 | var(--line-height-sm), 73 | var(--font-semibold), 74 | var(--spacing-sm), 75 | var(--spacing-sm) 76 | ); 77 | } 78 | 79 | p { 80 | margin-bottom: var(--spacing-sm); 81 | line-height: var(--spacing-md); 82 | font-size: var(--base-font-size); 83 | } 84 | 85 | // text styles for tags, categories 86 | .text-tag { 87 | font-size: var(--font-size-xs); 88 | text-transform: uppercase; 89 | margin-bottom: variables.toRem(16); 90 | display: block; 91 | } 92 | -------------------------------------------------------------------------------- /src/styles/common/footer.scss: -------------------------------------------------------------------------------- 1 | @use 'mixins'; 2 | @use 'variables'; 3 | 4 | // This component will not be added as a Component Service but will be integrated directly into the CMS. 5 | .footer { 6 | border-top: 1px solid var(--color-text); 7 | background: var(--color-bg); 8 | 9 | .container { 10 | @include mixins.flex-center-content; 11 | 12 | justify-content: space-between; 13 | align-items: flex-start; 14 | flex-wrap: wrap; 15 | gap: variables.toRem(40); 16 | padding: var(--spacing-lg) var(--spacing-sm) var(--spacing-sm); 17 | } 18 | 19 | .heading-quinary { 20 | color: var(--color-text); 21 | font-size: var(--font-size-md); 22 | margin: 0 0 var(--spacing-xs) 0; 23 | line-height: var(--line-height-md); 24 | } 25 | 26 | &__details { 27 | display: flex; 28 | align-items: center; 29 | gap: var(--spacing-md); 30 | } 31 | 32 | &__company-details { 33 | display: flex; 34 | flex-direction: column; 35 | } 36 | 37 | ul { 38 | font-size: var(--base-font-size); 39 | padding: 0; 40 | } 41 | 42 | li { 43 | margin-bottom: variables.toRem(10); 44 | list-style-type: none; 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | 50 | &:hover { 51 | text-decoration: underline; 52 | } 53 | } 54 | 55 | &__copyright { 56 | width: 100%; 57 | border-top: 1px solid var(--color-bg); 58 | padding: var(--spacing-sm) 0 0; 59 | } 60 | } 61 | 62 | .violet-theme .footer, 63 | .green-theme .footer { 64 | background: var(--primary); 65 | border-top: none; 66 | } 67 | 68 | .violet-theme { 69 | .footer { 70 | color: var(--color-bg); 71 | 72 | .heading-quinary { 73 | color: var(--primary); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/styles/common/mixins.scss: -------------------------------------------------------------------------------- 1 | // For all fonts on website 2 | @mixin heading-styles( 3 | $font-size, 4 | $line-height, 5 | $font-weight, 6 | $color: var(--color-text), 7 | $margin-bottom: var(--spacing-md), 8 | $margin-top: var(--spacing-md) 9 | ) { 10 | font-size: $font-size; 11 | line-height: $line-height; 12 | font-weight: $font-weight; 13 | color: $color; 14 | margin-bottom: $margin-bottom; 15 | margin-top: $margin-top; 16 | } 17 | 18 | // Outline focus 19 | @mixin focus-outline($color: var(--color-focus)) { 20 | outline: 2px solid $color; 21 | } 22 | 23 | // Flex mixins 24 | @mixin flex-center-vertical { 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | } 29 | 30 | @mixin flex-center-content { 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | margin: 0 auto; 35 | } 36 | 37 | @mixin center-overlay { 38 | width: 100%; 39 | height: 100%; 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | } 44 | 45 | // Centering absolute element in both directions 46 | @mixin center-absolute-xy { 47 | position: absolute; 48 | left: 50%; 49 | top: 50%; 50 | transform: translate(-50%, -50%); 51 | } 52 | 53 | // Centering absolute element vertically 54 | @mixin center-absolute-y { 55 | position: absolute; 56 | top: 50%; 57 | transform: translateY(-50%); 58 | } 59 | 60 | // function for offset of banner 61 | @mixin set-left-offset($offsets) { 62 | @each $breakpoint, $value in $offsets { 63 | @media screen and (min-width: $breakpoint) { 64 | left: $value; 65 | } 66 | } 67 | } 68 | 69 | @mixin border-gradient($direction, $width, $gradient) { 70 | border: $width solid transparent; 71 | border-image-slice: 1; 72 | border-image-source: $gradient; 73 | border-width: 0; 74 | 75 | @if $direction == top { 76 | border-top-width: $width; 77 | } @else if $direction == bottom { 78 | border-bottom-width: $width; 79 | } @else if $direction == left { 80 | border-left-width: $width; 81 | } @else if $direction == right { 82 | border-right-width: $width; 83 | } @else if $direction == vertical { 84 | border-top-width: $width; 85 | border-bottom-width: $width; 86 | } @else if $direction == horizontal { 87 | border-left-width: $width; 88 | border-right-width: $width; 89 | } @else if $direction == all { 90 | border-width: $width; 91 | } 92 | } 93 | 94 | /* Expand sections outside container */ 95 | // Styles to expand over the container - not needed in every project 96 | @mixin expand-container { 97 | margin-left: calc(-50vw + 50%); 98 | margin-right: calc(-50vw + 50%); 99 | width: 100vw; 100 | } 101 | 102 | // Clip text with customizable number of lines 103 | @mixin clip-text($lines) { 104 | display: -webkit-box; 105 | -webkit-box-orient: vertical; 106 | -webkit-line-clamp: $lines; 107 | overflow: hidden; 108 | text-overflow: ellipsis; 109 | } 110 | -------------------------------------------------------------------------------- /src/styles/common/navbar.scss: -------------------------------------------------------------------------------- 1 | @use 'variables'; 2 | 3 | .navbar { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | flex-wrap: wrap; 8 | background-color: var(--color-bg); 9 | padding: variables.toRem(8) variables.toRem(16); 10 | z-index: var(--z-index-menu); 11 | overflow: visible; 12 | 13 | &__brand img { 14 | max-height: variables.toRem(50); 15 | display: inline-block; 16 | } 17 | 18 | &__toggler { 19 | display: none; 20 | background-color: var(--accent); 21 | border: none; 22 | padding: var(--spacing-sm); 23 | border-radius: variables.toRem(4); 24 | 25 | @media screen and (max-width: variables.$breakpoint-md) { 26 | display: block; 27 | } 28 | } 29 | 30 | &__toggler-icon { 31 | display: block; 32 | width: var(--spacing-md); 33 | height: variables.toRem(2); 34 | background-color: var(--color-bg); 35 | position: relative; 36 | 37 | &::after, 38 | &::before { 39 | content: ''; 40 | position: absolute; 41 | width: var(--spacing-md); 42 | height: variables.toRem(2); 43 | background-color: var(--color-bg); 44 | left: 0; 45 | } 46 | 47 | &::before { 48 | top: variables.toRem(-6); 49 | } 50 | 51 | &::after { 52 | top: variables.toRem(6); 53 | } 54 | } 55 | 56 | &__list { 57 | display: flex; 58 | gap: var(--spacing-sm); 59 | padding: 0; 60 | margin: 0 var(--spacing-sm) 0 0; 61 | list-style: none; 62 | justify-content: center; 63 | 64 | @media screen and (max-width: variables.$breakpoint-md) { 65 | flex-direction: column; 66 | align-items: flex-end; 67 | width: 100%; 68 | } 69 | 70 | li { 71 | list-style-type: none; 72 | margin-bottom: 0; 73 | } 74 | } 75 | 76 | &__item { 77 | display: inline-block; 78 | } 79 | 80 | &__link { 81 | text-decoration: none; 82 | color: var(--color-text); 83 | padding: var(--spacing-xs) var(--spacing-xs); 84 | transition: 85 | color 0.3s, 86 | background-color 0.3s; 87 | border-radius: variables.toRem(4); 88 | 89 | &:hover, 90 | &.active { 91 | color: var(--color-bg); 92 | background-color: var(--accent); 93 | } 94 | 95 | &.active { 96 | color: var(--color-bg); 97 | } 98 | } 99 | } 100 | 101 | .collapse { 102 | display: flex; 103 | flex-direction: row; 104 | align-items: center; 105 | justify-content: space-between; 106 | 107 | @media screen and (max-width: variables.$breakpoint-md) { 108 | display: none; 109 | flex-direction: column; 110 | align-items: flex-start; 111 | width: 100%; 112 | padding: var(--spacing-sm) 0; 113 | gap: var(--spacing-sm); 114 | 115 | &.show { 116 | display: flex; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/README.md: -------------------------------------------------------------------------------- 1 | # Shared Styles for Black and White Themes 2 | 3 | This folder contains the shared styles for the black and white themes, as they are very similar. To avoid duplicating code and the need to update component styles in two places, the common styles are centralized here. 4 | 5 | The black and white themes differ only in a few SVG assets, which will be overridden directly in the specific theme folder. 6 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/_shared-theme-mixin.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | @mixin shared-theme-settings { 4 | /* Font settings */ 5 | --font: 'Roboto', sans-serif; // Default font for body text 6 | --font-headings: 7 | 'Roboto Condensed', sans-serif; // Font for headings and titles 8 | 9 | /* Font sizes */ 10 | --font-size-xxl: #{variables.toRem(64)}; 11 | --font-size-xl: #{variables.toRem(38)}; 12 | --font-size-lg: #{variables.toRem(24)}; 13 | --font-size-xs: #{variables.toRem(16)}; 14 | --base-font-size: #{variables.toRem(18)}; 15 | 16 | /* Spacing settings */ 17 | --spacing-xs: #{variables.toRem(8)}; 18 | --spacing-sm: #{variables.toRem(16)}; 19 | --spacing-md: #{variables.toRem(24)}; 20 | --spacing-lg: #{variables.toRem(64)}; 21 | --spacing-xl: #{variables.toRem(80)}; 22 | --spacing-xxl: #{variables.toRem(128)}; 23 | 24 | /* Line height settings */ 25 | --base-line-height: #{variables.toRem(24)}; 26 | --line-height-xl: #{variables.toRem(48)}; 27 | --line-height-lg: #{variables.toRem(40)}; 28 | --line-height-md: #{variables.toRem(28)}; 29 | --line-height-sm: #{variables.toRem(24)}; 30 | 31 | /* Container width */ 32 | --container: #{variables.toRem(1300)}; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-accordion.scss: -------------------------------------------------------------------------------- 1 | @use '@styles/common/variables'; 2 | 3 | @mixin shared-accordion-theme { 4 | .accordion { 5 | &__item { 6 | border-bottom: 1px solid var(--color-text); 7 | 8 | &:first-child { 9 | color: var(--color-text); 10 | } 11 | } 12 | 13 | &__heading { 14 | background-color: transparent; 15 | font-weight: var(--font-bold); 16 | padding: var(--spacing-md); 17 | font-size: var(--base-font-size); 18 | display: flex; 19 | align-items: center; 20 | 21 | &:hover { 22 | background-color: var(--gray); 23 | } 24 | 25 | &::after { 26 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAxNSAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGxpbmUgeDE9IjcuNzI2MDciIHkxPSI3LjY5MDMyZS0wOCIgeDI9IjcuNzI2MDciIHkyPSIxNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIi8+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iNy41IiB4Mj0iMC40MjgyODgiIHkyPSI3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 27 | display: flex; 28 | width: auto; 29 | height: 50px; 30 | align-items: center; 31 | transform: inherit; 32 | border: 2px solid var(--color-text); 33 | padding: var(--spacing-sm) var(--spacing-md); 34 | } 35 | } 36 | 37 | &__item[open] .accordion__heading::after { 38 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMyIgdmlld0JveD0iMCAwIDE1IDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iMS41IiB4Mj0iMC40MjgyODgiIHkyPSIxLjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 39 | transform: inherit; 40 | padding: variables.toRem(22) var(--spacing-md); 41 | } 42 | 43 | &__content { 44 | padding: var(--spacing-md); 45 | } 46 | 47 | &__item[open] .accordion__content { 48 | border-top: none; 49 | padding: var(--spacing-sm) var(--spacing-md) var(--spacing-md); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-banner.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins'; 2 | @use '../variables'; 3 | 4 | @mixin shared-banner-theme { 5 | .banner { 6 | @include mixins.expand-container; 7 | 8 | position: relative; 9 | min-height: variables.toRem(560); 10 | overflow: hidden; 11 | 12 | &__media { 13 | right: 0; 14 | left: auto; 15 | width: 70%; 16 | filter: saturate(50%); 17 | 18 | @media screen and (max-width: variables.$breakpoint-md) { 19 | width: 100%; 20 | } 21 | } 22 | 23 | &::after { 24 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYwIiBoZWlnaHQ9IjY3NiIgdmlld0JveD0iMCAwIDg2MCA2NzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjM0OS42MTIiIHk9IjI5NS4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMzQ5LjYxMiAyOTUuMzI4KSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC42MTIzNzIiIHk9IjEwMi4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMC42MTIzNzIgMTAyLjMyOCkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxyZWN0IHg9IjM3Ny42MTIiIHk9IjI0OC4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMzc3LjYxMiAyNDguMzI4KSIgc3Ryb2tlPSIjQzM2NDdDIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg=='); 25 | position: absolute; 26 | left: calc(var(--spacing-xxl) * -1); 27 | top: calc(var(--spacing-xxl) * -1); 28 | z-index: var(--z-index-below); 29 | } 30 | 31 | &__title { 32 | margin-bottom: 0; 33 | } 34 | 35 | &__content { 36 | background: none; 37 | display: inline-flex; 38 | overflow: hidden; 39 | min-height: variables.toRem(560); 40 | padding: var(--spacing-lg) var(--spacing-xl); 41 | 42 | @media screen and (max-width: variables.$breakpoint-md) { 43 | padding: var(--spacing-md) var(--spacing-sm); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '../mixins'; 2 | 3 | @mixin shared-blockquote-theme { 4 | .blockquote { 5 | border-left: var(--spacing-sm) solid var(--color-text); 6 | color: var(--color-text); 7 | position: relative; 8 | font-style: italic; 9 | 10 | &::after { 11 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjI1IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIyNSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xNi43NTAxIDE1OS43OTJINjQuNjI1MUw5Ni41NDE3IDk1Ljk1ODNWMC4yMDgyNTJIMC43OTE3NDhWOTUuOTU4M0g0OC42NjY3TDE2Ljc1MDEgMTU5Ljc5MlpNMTQ0LjQxNyAxNTkuNzkySDE5Mi4yOTJMMjI0LjIwOCA5NS45NTgzVjAuMjA4MjUySDEyOC40NThWOTUuOTU4M0gxNzYuMzMzTDE0NC40MTcgMTU5Ljc5MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMDUiLz4KPC9zdmc+Cg=='); 12 | position: absolute; 13 | right: 0; 14 | z-index: var(--z-index-below); 15 | 16 | @include mixins.center-absolute-y; 17 | } 18 | 19 | &__author { 20 | margin-top: var(--spacing-sm); 21 | font-size: var(--base-font-size); 22 | font-weight: var(--font-bold); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-dynamic-header.scss: -------------------------------------------------------------------------------- 1 | @mixin shared-dynamic-header-theme { 2 | .header-paragraph { 3 | h2 { 4 | margin-bottom: var(--spacing-lg); 5 | } 6 | 7 | p { 8 | margin-bottom: var(--spacing-sm); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-icon-cards.scss: -------------------------------------------------------------------------------- 1 | @mixin shared-icon-cards-theme { 2 | display: block; 3 | 4 | .icon-card { 5 | text-align: center; 6 | 7 | &__icon { 8 | margin: 0 auto var(--spacing-md); 9 | } 10 | 11 | &__icon::after { 12 | background-color: transparent; 13 | } 14 | 15 | svg { 16 | width: 100%; 17 | 18 | * { 19 | fill: var(--color-text); 20 | } 21 | } 22 | 23 | // Classes of specific svg paths 24 | .pen-tip, 25 | .mouse-pad, 26 | .print, 27 | .drop { 28 | fill: var(--primary); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-image-text-row.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | @mixin shared-image-text-row-theme { 4 | .image-text-row { 5 | gap: variables.toRem(80); 6 | 7 | &__heading { 8 | font-size: var(--font-size-xl); 9 | font-family: var(--font-headings); 10 | color: var(--color-text); 11 | font-weight: var(--font-bold); 12 | margin-bottom: variables.toRem(30); 13 | } 14 | 15 | &__content { 16 | font-size: var(--base-font-size); 17 | color: var(--color-text); 18 | } 19 | 20 | p { 21 | font-size: var(--base-font-size); 22 | color: var(--color-text); 23 | line-height: var(--line-height-sm); 24 | } 25 | 26 | &__image { 27 | border: variables.toRem(10) solid var(--color-text); 28 | } 29 | 30 | .text-tag { 31 | color: var(--primary); 32 | font-size: var(--font-size-xs); 33 | text-transform: capitalize; 34 | letter-spacing: variables.toRem(3); 35 | } 36 | 37 | &__link { 38 | margin-top: variables.toRem(30); 39 | border: none; 40 | padding: variables.toRem(4); 41 | text-decoration: none; 42 | position: relative; 43 | 44 | &::before { 45 | content: ''; 46 | width: calc(100% - variables.toRem(32)); 47 | height: variables.toRem(2); 48 | background: var(--primary); 49 | position: absolute; 50 | bottom: 0; 51 | left: 0; 52 | } 53 | 54 | &::after { 55 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iI0MzNjQ3QyIvPgo8L3N2Zz4KCg=='); 56 | } 57 | 58 | &:hover::before { 59 | background: var(--primary-darker); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-key-statistics.scss: -------------------------------------------------------------------------------- 1 | @mixin shared-key-statistics-theme { 2 | .stats-cards { 3 | .stat-card__value { 4 | color: var(--color-bg); 5 | background-color: var(--color-text); 6 | border-radius: 0; 7 | font-family: var(--font-headings); 8 | font-weight: var(--font-bold); 9 | font-size: var(--font-size-xxl); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/common/shared-themes/shared-testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '../variables'; 2 | 3 | @mixin shared-testimonials-theme { 4 | .testimonials { 5 | &__item { 6 | padding: var(--spacing-sm) var(--spacing-xxl); 7 | background: var(--color-bg); 8 | 9 | @media screen and (max-width: variables.$breakpoint-md) { 10 | padding: 0; 11 | } 12 | 13 | p { 14 | font-style: italic; 15 | } 16 | } 17 | 18 | &__author { 19 | font-weight: var(--font-bold); 20 | } 21 | 22 | &__button { 23 | height: auto; 24 | width: variables.toRem(76); 25 | 26 | &--prev, 27 | &--after { 28 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MiAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcxLjcwNzEgOC4yMDcxMUM3Mi4wOTc2IDcuODE2NTggNzIuMDk3NiA3LjE4MzQyIDcxLjcwNzEgNi43OTI4OUw2NS4zNDMxIDAuNDI4OTMyQzY0Ljk1MjYgMC4wMzg0MDc4IDY0LjMxOTUgMC4wMzg0MDc4IDYzLjkyODkgMC40Mjg5MzJDNjMuNTM4NCAwLjgxOTQ1NyA2My41Mzg0IDEuNDUyNjIgNjMuOTI4OSAxLjg0MzE1TDY5LjU4NTggNy41TDYzLjkyODkgMTMuMTU2OUM2My41Mzg0IDEzLjU0NzQgNjMuNTM4NCAxNC4xODA1IDYzLjkyODkgMTQuNTcxMUM2NC4zMTk1IDE0Ljk2MTYgNjQuOTUyNiAxNC45NjE2IDY1LjM0MzEgMTQuNTcxMUw3MS43MDcxIDguMjA3MTFaTTAgOC41TDcxIDguNVY2LjVMMCA2LjVMMCA4LjVaIiBmaWxsPSIjQzM2NDdDIi8+Cjwvc3ZnPgo='); 29 | transform: rotate(180deg); 30 | } 31 | 32 | &--next { 33 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MiAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcxLjcwNzEgOC4yMDcxMUM3Mi4wOTc2IDcuODE2NTggNzIuMDk3NiA3LjE4MzQyIDcxLjcwNzEgNi43OTI4OUw2NS4zNDMxIDAuNDI4OTMyQzY0Ljk1MjYgMC4wMzg0MDc4IDY0LjMxOTUgMC4wMzg0MDc4IDYzLjkyODkgMC40Mjg5MzJDNjMuNTM4NCAwLjgxOTQ1NyA2My41Mzg0IDEuNDUyNjIgNjMuOTI4OSAxLjg0MzE1TDY5LjU4NTggNy41TDYzLjkyODkgMTMuMTU2OUM2My41Mzg0IDEzLjU0NzQgNjMuNTM4NCAxNC4xODA1IDYzLjkyODkgMTQuNTcxMUM2NC4zMTk1IDE0Ljk2MTYgNjQuOTUyNiAxNC45NjE2IDY1LjM0MzEgMTQuNTcxMUw3MS43MDcxIDguMjA3MTFaTTAgOC41TDcxIDguNVY2LjVMMCA2LjVMMCA4LjVaIiBmaWxsPSIjQzM2NDdDIi8+Cjwvc3ZnPgo='); 34 | transform: rotate(0deg); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // All common files - fonts, variables, etc. It also contains styles for themes switcher. 2 | @use './common/**/*'; 3 | 4 | // All color themes 5 | @use 'themes/**/*'; 6 | 7 | // Components - styles for all components in the default theme. 8 | // @import 'components/**/*'; 9 | @use '../../dxp/component-service/**/css/*'; 10 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/_black-variables.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-theme-mixin'; 2 | 3 | /* Black theme overrides */ 4 | .black-theme { 5 | --color-bg: rgb(37 37 37); 6 | --color-text: rgb(255 255 255); 7 | --primary: rgb(195 100 124); 8 | --accent: rgb(191 24 35); 9 | --primary-darker: color-mix(in srgb, var(--primary) 80%, rgb(0 0 0) 20%); 10 | --dark-gray: rgb(27 27 27); 11 | --light-gray: rgb(48 48 48); 12 | 13 | // Include shared theme settings 14 | @include shared-theme-mixin.shared-theme-settings; 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-accordion.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-accordion'; 2 | 3 | .black-theme { 4 | @include shared-accordion.shared-accordion-theme; 5 | 6 | .accordion { 7 | &__heading { 8 | &::after { 9 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAxNSAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGxpbmUgeDE9IjcuNzI2MDciIHkxPSI3LjY5MDMyZS0wOCIgeDI9IjcuNzI2MDciIHkyPSIxNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIzIi8+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iNy41IiB4Mj0iMC40MjgyODgiIHkyPSI3LjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 10 | } 11 | 12 | &:hover { 13 | background-color: var(--primary); 14 | } 15 | } 16 | 17 | &__item[open] .accordion__heading::after { 18 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMyIgdmlld0JveD0iMCAwIDE1IDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iMS41IiB4Mj0iMC40MjgyODgiIHkyPSIxLjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-banner.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-banner'; 2 | 3 | .black-theme { 4 | @include shared-banner.shared-banner-theme; 5 | 6 | .banner::after { 7 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYwIiBoZWlnaHQ9IjY3NiIgdmlld0JveD0iMCAwIDg2MCA2NzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjM0OS42MTIiIHk9IjI5NS4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMzQ5LjYxMiAyOTUuMzI4KSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC42MTIzNzIiIHk9IjEwMi4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMC42MTIzNzIgMTAyLjMyOCkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxyZWN0IHg9IjM3Ny42MTIiIHk9IjI0OC4zMjgiIHdpZHRoPSIzOTMiIGhlaWdodD0iMzkzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMzc3LjYxMiAyNDguMzI4KSIgc3Ryb2tlPSIjQzM2NDdDIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg=='); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-blockquote'; 2 | 3 | .black-theme { 4 | @include shared-blockquote.shared-blockquote-theme; 5 | 6 | .blockquote::after { 7 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjI1IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIyNSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xNi43NTAxIDE1OS43OTJINjQuNjI1MUw5Ni41NDE3IDk1Ljk1ODNWMC4yMDgyNTJIMC43OTE3NDhWOTUuOTU4M0g0OC42NjY3TDE2Ljc1MDEgMTU5Ljc5MlpNMTQ0LjQxNyAxNTkuNzkySDE5Mi4yOTJMMjI0LjIwOCA5NS45NTgzVjAuMjA4MjUySDEyOC40NThWOTUuOTU4M0gxNzYuMzMzTDE0NC40MTcgMTU5Ljc5MloiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuMDUiLz4KPC9zdmc+Cg=='); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-cards'; 2 | 3 | .black-theme { 4 | @include shared-cards.shared-cards-theme; 5 | 6 | .cards__link { 7 | &::after { 8 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iI0ZGRkZGRiIvPgo8L3N2Zz4KCg=='); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-dynamic-header.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-dynamic-header'; 2 | 3 | .black-theme { 4 | @include shared-dynamic-header.shared-dynamic-header-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-global.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-global'; 2 | @use '@common-styles/variables'; 3 | 4 | .black-theme { 5 | @include shared-global.shared-global-theme; 6 | 7 | body { 8 | &::before { 9 | background-image: 10 | linear-gradient( 11 | to bottom, 12 | transparent 0%, 13 | transparent 50%, 14 | var(--color-bg) 50%, 15 | var(--color-bg) 16 | ), 17 | url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgzIiBoZWlnaHQ9IjMyOCIgdmlld0JveD0iMCAwIDI4MyAzMjgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzA2MDI2IiB5PSItMS4xOTIwOWUtMDciIHdpZHRoPSIxOTkuMzA2IiBoZWlnaHQ9IjE5OS4zMDYiIHRyYW5zZm9ybT0ibWF0cml4KDAuNzA2MDI2IC0wLjcwODE4NiAwLjcwNjAyNiAwLjcwODE4NiAwLjIwNzU1MyAxNDIuMzU0KSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC43MDYwMjYiIHk9Ii0xLjE5MjA5ZS0wNyIgd2lkdGg9IjE5OS4zMDYiIGhlaWdodD0iMTk5LjMwNiIgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDYwMjYgLTAuNzA4MTg2IDAuNzA2MDI2IDAuNzA4MTg2IDAuMjA3NTUzIDE4Ni40ODkpIiBzdHJva2U9IiNDMzY0N0MiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K'); 18 | background-size: 19 | 0 155vw, 20 | variables.toRem(300) 250vw; 21 | right: 0; 22 | left: variables.toRem(-150); 23 | } 24 | 25 | &::after { 26 | background-image: 27 | linear-gradient( 28 | to bottom, 29 | transparent 0%, 30 | transparent 50%, 31 | var(--color-bg) 50%, 32 | var(--color-bg) 33 | ), 34 | url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgzIiBoZWlnaHQ9IjM1OCIgdmlld0JveD0iMCAwIDI4MyAzNTgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzA3MTA3IiB5PSIxNDEuNDIxIiB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE5OSIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDAuNzA3MTA3IDE0MS40MjEpIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cmVjdCB4PSIwLjcwNzEwNyIgeT0iMTg1LjQyMSIgd2lkdGg9IjE5OSIgaGVpZ2h0PSIxOTkiIHRyYW5zZm9ybT0icm90YXRlKC00NSAwLjcwNzEwNyAxODUuNDIxKSIgc3Ryb2tlPSIjQzM2NDdDIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC43MDcxMDciIHk9IjIxNi40MjEiIHdpZHRoPSIxOTkiIGhlaWdodD0iMTk5IiB0cmFuc2Zvcm09InJvdGF0ZSgtNDUgMC43MDcxMDcgMjE2LjQyMSkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo='); 35 | background-size: 36 | 0 155vw, 37 | variables.toRem(300) 150vw; 38 | left: 0; 39 | right: variables.toRem(-150); 40 | transform: scaleX(-1); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-icon-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-icon-cards'; 2 | 3 | .black-theme { 4 | @include shared-icon-cards.shared-icon-cards-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-image-text-row.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-image-text-row'; 2 | 3 | .black-theme { 4 | @include shared-image-text-row.shared-image-text-row-theme; 5 | 6 | .image-text-row__link { 7 | &::after { 8 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iI0MzNjQ3QyIvPgo8L3N2Zz4KCg=='); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-key-statistics.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-key-statistics'; 2 | 3 | .black-theme { 4 | @include shared-key-statistics.shared-key-statistics-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/black-theme/black-testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-testimonials'; 2 | 3 | .black-theme { 4 | @include shared-testimonials.shared-testimonials-theme; 5 | 6 | .testimonial__button { 7 | &--prev, 8 | &--after { 9 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MiAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcxLjcwNzEgOC4yMDcxMUM3Mi4wOTc2IDcuODE2NTggNzIuMDk3NiA3LjE4MzQyIDcxLjcwNzEgNi43OTI4OUw2NS4zNDMxIDAuNDI4OTMyQzY0Ljk1MjYgMC4wMzg0MDc4IDY0LjMxOTUgMC4wMzg0MDc4IDYzLjkyODkgMC40Mjg5MzJDNjMuNTM4NCAwLjgxOTQ1NyA2My41Mzg0IDEuNDUyNjIgNjMuOTI4OSAxLjg0MzE1TDY5LjU4NTggNy41TDYzLjkyODkgMTMuMTU2OUM2My41Mzg0IDEzLjU0NzQgNjMuNTM4NCAxNC4xODA1IDYzLjkyODkgMTQuNTcxMUM2NC4zMTk1IDE0Ljk2MTYgNjQuOTUyNiAxNC45NjE2IDY1LjM0MzEgMTQuNTcxMUw3MS43MDcxIDguMjA3MTFaTTAgOC41TDcxIDguNVY2LjVMMCA2LjVMMCA4LjVaIiBmaWxsPSIjQzM2NDdDIi8+Cjwvc3ZnPgo='); 10 | } 11 | 12 | &--next { 13 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzIiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MiAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcxLjcwNzEgOC4yMDcxMUM3Mi4wOTc2IDcuODE2NTggNzIuMDk3NiA3LjE4MzQyIDcxLjcwNzEgNi43OTI4OUw2NS4zNDMxIDAuNDI4OTMyQzY0Ljk1MjYgMC4wMzg0MDc4IDY0LjMxOTUgMC4wMzg0MDc4IDYzLjkyODkgMC40Mjg5MzJDNjMuNTM4NCAwLjgxOTQ1NyA2My41Mzg0IDEuNDUyNjIgNjMuOTI4OSAxLjg0MzE1TDY5LjU4NTggNy41TDYzLjkyODkgMTMuMTU2OUM2My41Mzg0IDEzLjU0NzQgNjMuNTM4NCAxNC4xODA1IDYzLjkyODkgMTQuNTcxMUM2NC4zMTk1IDE0Ljk2MTYgNjQuOTUyNiAxNC45NjE2IDY1LjM0MzEgMTQuNTcxMUw3MS43MDcxIDguMjA3MTFaTTAgOC41TDcxIDguNVY2LjVMMCA2LjVMMCA4LjVaIiBmaWxsPSIjQzM2NDdDIi8+Cjwvc3ZnPgo='); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/_green-variables.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | /* Green theme overrides */ 4 | .green-theme { 5 | --color-bg: rgb(255 255 255); 6 | --color-text: rgb(37 37 37); 7 | --primary: rgb(201 218 207); 8 | --accent: rgb(0 98 111); 9 | --primary-darker: color-mix(in srgb, var(--primary) 80%, rgb(0 0 0) 20%); 10 | --overlay: rgba(45 87 85 / 70%); 11 | --shadow: rgb(233 233 233); 12 | --font: 'Lato', sans-serif; 13 | 14 | /* Font sizes */ 15 | --font-size-xxl: #{variables.toRem(68)}; 16 | --font-size-xl: #{variables.toRem(46)}; 17 | --font-size-lg: #{variables.toRem(38)}; 18 | --font-size-md: #{variables.toRem(32)}; 19 | --font-size-xs: #{variables.toRem(16)}; 20 | --base-font-size: #{variables.toRem(18)}; 21 | 22 | /* Spacing settings */ 23 | --spacing-xl: #{variables.toRem(80)}; 24 | --spacing-lg: #{variables.toRem(50)}; 25 | --spacing-md: #{variables.toRem(36)}; 26 | --spacing-sm: #{variables.toRem(20)}; 27 | --spacing-xm: #{variables.toRem(14)}; 28 | 29 | /* Line height settings */ 30 | --line-height-xl: #{variables.toRem(56)}; 31 | --line-height-lg: #{variables.toRem(38)}; 32 | --line-height-md: #{variables.toRem(28)}; 33 | --line-height-sm: #{variables.toRem(24)}; 34 | 35 | /* Additional Green theme specific properties */ 36 | --box-shadow: 0 2px 9px 0 rgb(83 145 158 / 30%); 37 | --border-radius: #{variables.toRem(20)}; 38 | 39 | /* Container width */ 40 | --container: #{variables.toRem(1100)}; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-accordion.scss: -------------------------------------------------------------------------------- 1 | .green-theme { 2 | .accordion { 3 | &__item { 4 | box-shadow: var(--box-shadow); 5 | border-bottom: 1px solid var(--accent); 6 | background-color: var(--primary); 7 | overflow: hidden; 8 | 9 | &:first-of-type { 10 | border-top-right-radius: var(--border-radius); 11 | border-top-left-radius: var(--border-radius); 12 | } 13 | 14 | &:last-child { 15 | border-bottom-right-radius: var(--border-radius); 16 | border-bottom-left-radius: var(--border-radius); 17 | border-bottom: none; 18 | } 19 | } 20 | 21 | &__heading { 22 | background-color: transparent; 23 | font-weight: var(--font-bold); 24 | padding: var(--spacing-sm); 25 | font-size: var(--base-font-size); 26 | color: var(--color-text); 27 | display: flex; 28 | align-items: center; 29 | 30 | &::after { 31 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjYiIGhlaWdodD0iMjYiIHZpZXdCb3g9IjAgMCAyNiAyNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMSIgeT0iLTEiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgcng9IjEyIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDUuNTYzNjNlLTA4IDUuNTYzNjNlLTA4IC0xIDUuNTYzNjNlLTA4IDI0KSIgc3Ryb2tlPSIjMDA2MjZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPGxpbmUgeTE9Ii0xIiB4Mj0iOS44OTk1IiB5Mj0iLTEiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjcwNzEwNyAtMC43MDcxMDcgLTAuNzA3MTA3IDAuNzA3MTA3IDEzIDE4KSIgc3Ryb2tlPSIjMDA2MjZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPGxpbmUgeTE9Ii0xIiB4Mj0iOS44OTk1IiB5Mj0iLTEiIHRyYW5zZm9ybT0ibWF0cml4KC0wLjcwNzEwNyAwLjcwNzEwNyAwLjcwNzEwNyAwLjcwNzEwNyAyMCAxMSkiIHN0cm9rZT0iIzAwNjI2RiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo='); 32 | display: flex; 33 | width: auto; 34 | height: auto; 35 | border: none; 36 | transform: inherit; 37 | } 38 | } 39 | 40 | &__item[open] .accordion__heading::after { 41 | transform: rotate(180deg); 42 | } 43 | 44 | &__item[open] .accordion__content { 45 | border-top: none; 46 | padding: var(--spacing-sm); 47 | background-color: var(--color-bg); 48 | } 49 | 50 | &__item[open] .accordion__heading { 51 | color: var(--accent); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-banner.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/mixins'; 2 | 3 | .green-theme { 4 | .banner { 5 | @include mixins.expand-container; 6 | 7 | &__content { 8 | background-color: var(--overlay); 9 | text-align: center; 10 | } 11 | 12 | &__title { 13 | margin-bottom: 0; 14 | color: var(--white); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | .blockquote { 5 | border-left: none; 6 | font-style: italic; 7 | box-shadow: var(--box-shadow); 8 | background-color: var(--color-bg); 9 | text-align: center; 10 | border-radius: var(--border-radius); 11 | 12 | &__content { 13 | padding: var(--spacing-md) var(--spacing-xl); 14 | 15 | @media screen and (max-width: variables.$breakpoint-md) { 16 | padding: var(--spacing-md); 17 | } 18 | } 19 | 20 | &__author { 21 | margin-top: var(--spacing-sm); 22 | font-size: var(--base-font-size); 23 | font-weight: var(--font-bold); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/mixins'; 2 | @use '@common-styles/variables'; 3 | 4 | .green-theme { 5 | .cards { 6 | position: relative; 7 | grid-template-columns: repeat(2, 1fr); 8 | 9 | &__heading, 10 | &__content-type, 11 | &__supporting-text { 12 | padding: 0 var(--spacing-md); 13 | } 14 | 15 | &__link { 16 | display: none; 17 | } 18 | 19 | &::after { 20 | content: ''; 21 | background-color: var(--primary); 22 | z-index: var(--z-index-below); 23 | width: 100vw; 24 | position: absolute; 25 | height: 80%; 26 | top: 10%; 27 | left: 50%; 28 | transform: translateX(-50%); 29 | } 30 | 31 | @media screen and (max-width: variables.$breakpoint-md) { 32 | grid-template-columns: repeat(1, 1fr); 33 | } 34 | 35 | &__header { 36 | justify-content: center; 37 | } 38 | 39 | &__card { 40 | color: var(--color-text); 41 | border-radius: var(--border-radius); 42 | background: var(--white); 43 | position: relative; 44 | overflow: hidden; 45 | padding: 0; 46 | gap: var(--spacing-sm); 47 | cursor: pointer; 48 | height: variables.toRem(400); 49 | width: 100%; 50 | box-shadow: var(--box-shadow); 51 | 52 | // for hover animation 53 | &::before { 54 | content: ''; 55 | position: absolute; 56 | top: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 100%; 60 | background: var(--primary); 61 | opacity: 0; 62 | transition: opacity 0.3s ease; 63 | z-index: var(--z-index-default); 64 | } 65 | 66 | &:not(.cards__card--has-image) { 67 | padding: var(--spacing-md) 0; 68 | } 69 | 70 | > p, 71 | h3 { 72 | /* padding: 0 var(--spacing-md); */ 73 | margin: 0; 74 | } 75 | 76 | &:hover { 77 | .cards__heading { 78 | text-decoration: underline; 79 | } 80 | } 81 | } 82 | 83 | &__heading { 84 | margin: 0; 85 | } 86 | 87 | &__image { 88 | max-height: variables.toRem(160); 89 | border-radius: var(--border-radius); 90 | z-index: var(--z-index-default); 91 | position: relative; 92 | top: 0; 93 | left: 0; 94 | width: 100%; 95 | height: auto; 96 | } 97 | 98 | &__content-type { 99 | line-height: var(--line-height-sm); 100 | font-size: var(--font-size-xs); 101 | text-transform: capitalize; 102 | color: var(--accent); 103 | margin: var(--spacing-sm) 0 0; 104 | } 105 | 106 | &__supporting-text { 107 | @include mixins.clip-text(3); 108 | 109 | line-height: var(--line-height-md); 110 | font-size: var(--base-font-size); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-dynamic-header.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | .header-paragraph { 5 | text-align: center; 6 | box-shadow: var(--box-shadow); 7 | padding: var(--spacing-xl); 8 | background-color: var(--color-bg); 9 | border-radius: var(--border-radius); 10 | 11 | // Styles for general matrix view 12 | position: relative; 13 | z-index: var(--z-index-default); 14 | 15 | &__title { 16 | margin-bottom: var(--spacing-md); 17 | } 18 | 19 | @media screen and (max-width: variables.$breakpoint-md) { 20 | padding: var(--spacing-sm); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-global.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | body { 5 | font-family: var(--font); 6 | transition: all 0.5s ease-in-out; 7 | color: var(--color-text); 8 | position: relative; 9 | 10 | &::after { 11 | content: ''; 12 | position: absolute; 13 | width: 100%; 14 | height: 100%; 15 | opacity: 0.05; 16 | top: 0; 17 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI4IiB2aWV3Qm94PSIwIDAgOCA4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB3aWR0aD0iMiIgaGVpZ2h0PSIyIiBmaWxsPSJibGFjayIvPgo8L3N2Zz4K'); 18 | z-index: var(--z-index-below); 19 | } 20 | 21 | /* 22 | ⚠️ Warning: Do not use "* { transition: all ... }" in production! 23 | 24 | This rule applies transitions to all elements and properties, which can cause performance issues and unintended animations. 25 | 26 | It is left here for educational purposes only to demonstrate how all styles react when switching color themes. Use more specific transitions in real projects. 27 | */ 28 | * { 29 | transition: 30 | all 0.5s ease-in-out, 31 | color 0.2s ease; 32 | } 33 | } 34 | 35 | h1 { 36 | font-size: var(--font-size-xxl); 37 | text-align: center; 38 | 39 | @media screen and (max-width: variables.$breakpoint-md) { 40 | font-size: var(--font-size-lg); 41 | } 42 | } 43 | 44 | h2 { 45 | font-size: var(--font-size-xl); 46 | color: var(--color-text); 47 | font-weight: var(--font-semibold); 48 | text-align: center; 49 | margin-bottom: var(--spacing-xl); 50 | 51 | @media screen and (max-width: variables.$breakpoint-md) { 52 | font-size: var(--font-size-md); 53 | line-height: var(--line-height-lg); 54 | } 55 | } 56 | 57 | h3 { 58 | font-size: var(--font-size-md); 59 | line-height: var(--line-height-lg); 60 | } 61 | 62 | thead th { 63 | background-color: var(--primary); 64 | border-bottom: 1px solid var(--accent); 65 | } 66 | 67 | tbody td { 68 | border-bottom: none; 69 | } 70 | 71 | tbody tr { 72 | background-color: var(--color-bg); 73 | border-bottom: 1px solid var(--accent); 74 | } 75 | 76 | pre { 77 | background: var(--color-bg); 78 | border-left: variables.toRem(10) solid var(--primary); 79 | color: var(--color-text); 80 | box-shadow: 1px 1px 5px var(--shadow); 81 | } 82 | 83 | kbd { 84 | color: var(--color-text); 85 | } 86 | 87 | li::marker { 88 | color: var(--accent); 89 | } 90 | 91 | .container { 92 | background-color: transparent; 93 | overflow: visible; 94 | } 95 | 96 | .banner + .header-paragraph { 97 | margin-top: variables.toRem(-150); 98 | margin-bottom: variables.toRem(100); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-icon-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/mixins'; 2 | 3 | .green-theme { 4 | .icon-cards-section { 5 | @include mixins.expand-container; 6 | 7 | background-color: var(--primary); 8 | padding: var(--spacing-lg); 9 | } 10 | 11 | .icon-card { 12 | text-align: center; 13 | 14 | &__icon { 15 | margin: 0 auto var(--spacing-md); 16 | } 17 | 18 | &__icon::after { 19 | background-color: var(--color-bg); 20 | border-radius: var(--border-radius); 21 | padding: var(--spacing-lg); 22 | z-index: inherit; 23 | } 24 | 25 | svg { 26 | width: 100%; 27 | z-index: 1; 28 | 29 | * { 30 | fill: var(--accent); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-image-text-row.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | .image-text-row { 5 | color: var(--color-text); 6 | font-size: var(--base-font-size); 7 | line-height: var(--line-height-md); 8 | 9 | &__image { 10 | justify-content: center; 11 | display: flex; 12 | align-self: center; 13 | position: relative; 14 | overflow: visible; 15 | 16 | &::after { 17 | content: ''; 18 | background-color: var(--primary); 19 | width: 100%; 20 | height: 80%; 21 | position: absolute; 22 | border-radius: var(--border-radius); 23 | bottom: variables.toRem(-48); 24 | z-index: var(--z-index-below); 25 | } 26 | 27 | @media screen and (max-width: variables.$breakpoint-md) { 28 | margin-bottom: var(--spacing-lg); 29 | } 30 | } 31 | 32 | img { 33 | width: 80%; 34 | background-color: var(--white); 35 | box-shadow: var(--box-shadow); 36 | border-radius: var(--border-radius); 37 | } 38 | 39 | &__heading { 40 | font-weight: var(--font-semibold); 41 | font-size: var(--font-size-lg); 42 | margin: 0 0 var(--spacing-md) 0; 43 | line-height: var(--line-height-xl); 44 | } 45 | 46 | &__link { 47 | margin-top: var(--spacing-md); 48 | border: none; 49 | padding: variables.toRem(4); 50 | position: relative; 51 | color: var(--color-text); 52 | font-size: var(--base-font-size); 53 | display: flex; 54 | 55 | &::after { 56 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjciIGhlaWdodD0iMjYiIHZpZXdCb3g9IjAgMCAyNyAyNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwLjIwMjEgMTIuNjY0NEMyMC41OTI2IDEzLjA1NDkgMjAuNTkyNiAxMy42ODggMjAuMjAyMSAxNC4wNzg2TDEzLjgzODEgMjAuNDQyNUMxMy40NDc2IDIwLjgzMzEgMTIuODE0NCAyMC44MzMxIDEyLjQyMzkgMjAuNDQyNUMxMi4wMzM0IDIwLjA1MiAxMi4wMzM0IDE5LjQxODggMTIuNDIzOSAxOS4wMjgzTDE4LjA4MDggMTMuMzcxNUwxMi40MjM5IDcuNzE0NjFDMTIuMDMzNCA3LjMyNDA4IDEyLjAzMzQgNi42OTA5MiAxMi40MjM5IDYuMzAwMzlDMTIuODE0NCA1LjkwOTg3IDEzLjQ0NzYgNS45MDk4NyAxMy44MzgxIDYuMzAwMzlMMjAuMjAyMSAxMi42NjQ0Wk02LjEyMzU0IDEyLjM3MTVMMTkuNDk1IDEyLjM3MTVMMTkuNDk1IDE0LjM3MTVMNi4xMjM1NCAxNC4zNzE1TDYuMTIzNTQgMTIuMzcxNVoiIGZpbGw9IiMwMDYyNkYiLz4KPGNpcmNsZSBjeD0iMTMiIGN5PSIxMyIgcj0iMTIiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDI2LjE4MDcgMCkiIHN0cm9rZT0iIzAwNjI2RiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPg=='); 57 | } 58 | 59 | &:hover { 60 | text-decoration: none; 61 | } 62 | } 63 | 64 | .text-tag { 65 | letter-spacing: variables.toRem(3); 66 | color: var(--secondary); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-key-statistics.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | .stats-cards { 5 | .stat-card { 6 | background-color: var(--primary); 7 | border-radius: var(--border-radius); 8 | font-family: var(--font-headings); 9 | font-weight: var(--font-semibold); 10 | font-size: var(--font-size-xxl); 11 | 12 | &__text { 13 | margin: calc(var(--spacing-sm) * -1) 0 var(--spacing-md); 14 | } 15 | 16 | &__value { 17 | color: var(--accent); 18 | margin-bottom: 0; 19 | font-weight: var(--font-bold); 20 | height: #{variables.toRem(150)}; 21 | min-height: auto; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/themes/green-theme/green-testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .green-theme { 4 | .testimonials { 5 | overflow: visible; 6 | 7 | &__track { 8 | background-color: var(--color-bg); 9 | box-shadow: var(--box-shadow); 10 | padding: 0; 11 | border-radius: var(--border-radius); 12 | } 13 | 14 | &__item { 15 | p { 16 | font-style: italic; 17 | font-size: var(--base-font-size); 18 | } 19 | @media screen and (max-width: variables.$breakpoint-md) { 20 | padding: var(--spacing-sm); 21 | } 22 | } 23 | 24 | &__author { 25 | font-weight: var(--font-bold); 26 | margin-top: var(--spacing-sm); 27 | } 28 | 29 | &__buttons { 30 | top: 50%; 31 | transform: translate(0, -50%); 32 | 33 | @media screen and (max-width: variables.$breakpoint-md) { 34 | top: calc(100% + var(--spacing-md)); 35 | } 36 | } 37 | 38 | &__button { 39 | width: variables.toRem(72); 40 | height: variables.toRem(32); 41 | 42 | &--prev { 43 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzUiIGhlaWdodD0iMzUiIHZpZXdCb3g9IjAgMCAzNSAzNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI2LjcwNzEgMTcuMjkyOUMyNy4wOTc2IDE3LjY4MzQgMjcuMDk3NiAxOC4zMTY2IDI2LjcwNzEgMTguNzA3MUwyMC4zNDMxIDI1LjA3MTFDMTkuOTUyNiAyNS40NjE2IDE5LjMxOTUgMjUuNDYxNiAxOC45Mjg5IDI1LjA3MTFDMTguNTM4NCAyNC42ODA1IDE4LjUzODQgMjQuMDQ3NCAxOC45Mjg5IDIzLjY1NjlMMjQuNTg1OCAxOEwxOC45Mjg5IDEyLjM0MzFDMTguNTM4NCAxMS45NTI2IDE4LjUzODQgMTEuMzE5NSAxOC45Mjg5IDEwLjkyODlDMTkuMzE5NSAxMC41Mzg0IDE5Ljk1MjYgMTAuNTM4NCAyMC4zNDMxIDEwLjkyODlMMjYuNzA3MSAxNy4yOTI5Wk04IDE3TDI2IDE3TDI2IDE5TDggMTlMOCAxN1oiIGZpbGw9IiMwMDYyNkYiLz4KPGNpcmNsZSBjeD0iMTcuNSIgY3k9IjE3LjUiIHI9IjE2LjUiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDM1IDApIiBzdHJva2U9IiMwMDYyNkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'); 44 | transform: rotate(180deg); 45 | margin-left: -10%; 46 | } 47 | 48 | &--next { 49 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzUiIGhlaWdodD0iMzUiIHZpZXdCb3g9IjAgMCAzNSAzNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI2LjcwNzEgMTcuMjkyOUMyNy4wOTc2IDE3LjY4MzQgMjcuMDk3NiAxOC4zMTY2IDI2LjcwNzEgMTguNzA3MUwyMC4zNDMxIDI1LjA3MTFDMTkuOTUyNiAyNS40NjE2IDE5LjMxOTUgMjUuNDYxNiAxOC45Mjg5IDI1LjA3MTFDMTguNTM4NCAyNC42ODA1IDE4LjUzODQgMjQuMDQ3NCAxOC45Mjg5IDIzLjY1NjlMMjQuNTg1OCAxOEwxOC45Mjg5IDEyLjM0MzFDMTguNTM4NCAxMS45NTI2IDE4LjUzODQgMTEuMzE5NSAxOC45Mjg5IDEwLjkyODlDMTkuMzE5NSAxMC41Mzg0IDE5Ljk1MjYgMTAuNTM4NCAyMC4zNDMxIDEwLjkyODlMMjYuNzA3MSAxNy4yOTI5Wk04IDE3TDI2IDE3TDI2IDE5TDggMTlMOCAxN1oiIGZpbGw9IiMwMDYyNkYiLz4KPGNpcmNsZSBjeD0iMTcuNSIgY3k9IjE3LjUiIHI9IjE2LjUiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDM1IDApIiBzdHJva2U9IiMwMDYyNkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'); 50 | transform: rotate(0deg); 51 | margin-right: -10%; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/_violet-variables.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | /* Violet theme overrides */ 4 | .violet-theme { 5 | /* General color settings */ 6 | --color-bg: rgb(255 255 255); 7 | --color-text: rgb(37 37 37); 8 | --primary: linear-gradient( 9 | 18deg, 10 | var(--gradient-blue) 35%, 11 | var(--gradient-violet) 12 | ); 13 | --accent: rgb(237 170 92); 14 | --secondary: rgb(245 237 251); 15 | 16 | /* gradient colors */ 17 | --gradient-blue: rgba(48 63 131 / 100%); 18 | --gradient-violet: rgba(116 70 151 / 100%); 19 | 20 | /* Font settings */ 21 | --font: 'Open Sans', sans-serif; // Default font for body text 22 | --font-headings: 'Poppins', sans-serif; // Font for headings and titles 23 | 24 | /* Font sizes */ 25 | --font-size-huge: #{variables.toRem(90)}; // for number section 26 | --font-size-xxl: #{variables.toRem(64)}; // XXL text, e.g., h0 27 | --font-size-xl: #{variables.toRem(42)}; // Extra large text, e.g., main headers 28 | --font-size-lg: #{variables.toRem(38)}; // Large text, e.g., section headers 29 | --font-size-md: #{variables.toRem(20)}; // Medium text, e.g., subheaders 30 | --font-size-xs: #{variables.toRem(16)}; // Small text, e.g., emphasized text 31 | --base-font-size: #{variables.toRem(18)}; // Base font size for body text 32 | 33 | /* Spacing settings */ 34 | --spacing-sm: #{variables.toRem(16)}; 35 | --spacing-md: #{variables.toRem(32)}; 36 | --spacing-lg: #{variables.toRem(64)}; 37 | --spacing-xl: #{variables.toRem(80)}; 38 | --spacing-xxl: #{variables.toRem(150)}; 39 | 40 | /* Line height settings */ 41 | --line-height-huge: #{variables.toRem(140)}; // for numbers section 42 | --line-height-xl: #{variables.toRem(70)}; // Line height for main headers 43 | --line-height-lg: #{variables.toRem(50)}; // Line height for section headers 44 | --line-height-md: #{variables.toRem(32)}; // Line height for subheaders 45 | --line-height-sm: #{variables.toRem(26)}; // Line height for body text 46 | 47 | /* Additional Violet theme specific properties */ 48 | --box-shadow: 1px 1px 5px 0 rgb(117 71 151 / 20%); 49 | --border-radius: #{variables.toRem(20)}; 50 | 51 | /* Media Queries */ 52 | $breakpoint-lg: 82rem; 53 | 54 | /* Container width */ 55 | --container: #{variables.toRem(1150)}; 56 | } 57 | 58 | $banner-left-offsets: ( 59 | 82rem: -80%, 60 | 100rem: -50%, 61 | 125rem: -25% 62 | ); 63 | 64 | @mixin width-container { 65 | display: flex; 66 | max-width: var(--container); 67 | margin: 0 auto; 68 | } 69 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/violet-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/mixins'; 2 | @use '@common-styles/variables'; 3 | 4 | .violet-theme { 5 | .blockquote { 6 | @include mixins.border-gradient( 7 | left, 8 | 10px, 9 | linear-gradient(to bottom, var(--gradient-blue), var(--gradient-violet)) 10 | ); 11 | 12 | padding: var(--spacing-md) var(--spacing-lg); 13 | color: var(--color-text); 14 | font-style: italic; 15 | 16 | &__content { 17 | padding: 0; 18 | } 19 | 20 | @media screen and (max-width: variables.$breakpoint-md) { 21 | padding: var(--spacing-md); 22 | } 23 | 24 | &__author { 25 | margin-top: var(--spacing-md); 26 | font-size: var(--base-font-size); 27 | font-weight: var(--font-bold); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/violet-global.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .violet-theme { 4 | body { 5 | background-color: var(--color-bg); 6 | font-family: var(--font); 7 | transition: all 0.5s ease-in-out; 8 | color: var(--color-text); 9 | 10 | /* 11 | ⚠️ Warning: Do not use "* { transition: all ... }" in production! 12 | 13 | This rule applies transitions to all elements and properties, which can cause performance issues and unintended animations. 14 | 15 | It is left here for educational purposes only to demonstrate how all styles react when switching color themes. Use more specific transitions in real projects. 16 | */ 17 | * { 18 | transition: 19 | all 0.5s ease-in-out, 20 | color 0.2s ease; 21 | } 22 | } 23 | 24 | h2 { 25 | font-size: var(--font-size-xl); 26 | color: var(--color-text); 27 | font-weight: var(--font-semibold); 28 | font-family: var(--font-headings); 29 | margin-bottom: var(--spacing-lg); 30 | line-height: var(--line-height-xl); 31 | 32 | @media screen and (max-width: variables.$breakpoint-md) { 33 | font-size: var(--font-size-lg); 34 | line-height: var(--line-height-lg); 35 | } 36 | } 37 | 38 | h3 { 39 | font-size: var(--font-size-md); 40 | color: var(--color-text); 41 | font-weight: var(--font-semibold); 42 | font-family: var(--font-headings); 43 | line-height: var(--line-height-md); 44 | } 45 | 46 | p { 47 | font-size: var(--base-font-size); 48 | font-weight: var(--font-normal); 49 | line-height: var(--line-height-sm); 50 | } 51 | 52 | thead th { 53 | background-color: var(--secondary); 54 | border-bottom: 1px solid var(--gradient-blue); 55 | } 56 | 57 | tbody td { 58 | border-bottom: none; 59 | } 60 | 61 | tbody tr { 62 | background-color: var(--color-bg); 63 | border-bottom: 1px solid var(--gradient-blue); 64 | } 65 | 66 | pre { 67 | background: var(--secondary); 68 | border-left: variables.toRem(10) solid var(--gradient-blue); 69 | color: var(--color-text); 70 | } 71 | 72 | kbd { 73 | color: var(--color-text); 74 | } 75 | 76 | li::marker { 77 | color: var(--accent); 78 | } 79 | 80 | .container { 81 | background-color: transparent; 82 | overflow: visible; 83 | } 84 | 85 | .banner + .header-paragraph { 86 | margin-top: variables.toRem(-150); 87 | margin-bottom: variables.toRem(150); 88 | } 89 | 90 | section { 91 | margin-bottom: variables.toRem(150); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/violet-image-text-row.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | @use './violet-variables' as violet-theme-variables; 3 | 4 | .violet-theme { 5 | .image-text-row { 6 | background: var(--primary); 7 | color: var(--color-bg); 8 | border-radius: var(--border-radius); 9 | overflow: hidden; 10 | font-size: var(--base-font-size); 11 | padding: var(--spacing-lg); 12 | 13 | @include violet-theme-variables.width-container; 14 | 15 | @media screen and (max-width: variables.$breakpoint-md) { 16 | padding: 0; 17 | } 18 | 19 | &__content { 20 | @media screen and (max-width: variables.$breakpoint-md) { 21 | padding: var(--spacing-md) var(--spacing-sm); 22 | } 23 | } 24 | 25 | &__heading { 26 | color: var(--color-bg); 27 | font-weight: var(--font-semibold); 28 | font-family: var(--font-headings); 29 | font-size: var(--font-size-lg); 30 | margin-bottom: var(--spacing-md); 31 | line-height: var(--line-height-lg); 32 | } 33 | 34 | &__link { 35 | margin-top: var(--spacing-md); 36 | border: none; 37 | padding: variables.toRem(4); 38 | text-decoration: none; 39 | position: relative; 40 | color: var(--color-bg); 41 | font-size: var(--base-font-size); 42 | 43 | &::before { 44 | content: ''; 45 | width: calc(100% - variables.toRem(32)); 46 | height: variables.toRem(2); 47 | background: var(--accent); 48 | position: absolute; 49 | bottom: 0; 50 | left: 0; 51 | transition: background 0.2s ease; 52 | } 53 | 54 | &::after { 55 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iI0ZGRkZGRiIvPgo8L3N2Zz4KCg=='); 56 | } 57 | 58 | &:hover::before { 59 | background: var(--primary-darker); 60 | } 61 | } 62 | 63 | .text-tag { 64 | letter-spacing: variables.toRem(3); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/violet-key-statistics.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .violet-theme { 4 | .stats-cards { 5 | justify-content: space-between; 6 | 7 | @media screen and (max-width: variables.$breakpoint-md) { 8 | justify-content: center; 9 | } 10 | 11 | &__title { 12 | justify-content: flex-start; 13 | } 14 | } 15 | 16 | .stat-card { 17 | &__value { 18 | background-image: var(--primary); 19 | color: transparent; 20 | background-clip: text; 21 | font-size: var(--font-size-huge); 22 | line-height: var(--line-height-huge); 23 | height: auto; 24 | margin: 0; 25 | } 26 | 27 | &__text { 28 | font-weight: var(--font-semibold); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/themes/violet-theme/violet-testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/variables'; 2 | 3 | .violet-theme { 4 | .testimonials { 5 | &__track { 6 | padding: 0 0 variables.toRem(50); 7 | } 8 | 9 | &__item { 10 | padding: var(--spacing-lg) var(--spacing-md); 11 | background: var(--primary); 12 | border-radius: var(--border-radius); 13 | color: var(--color-bg); 14 | padding-bottom: variables.toRem(50); 15 | position: relative; 16 | align-items: flex-start; 17 | font-style: italic; 18 | 19 | p { 20 | text-align: left; 21 | } 22 | 23 | &::before { 24 | content: ''; 25 | position: absolute; 26 | z-index: var(--z-index-below); 27 | bottom: variables.toRem(-30); 28 | left: variables.toRem(150); 29 | width: variables.toRem(100); 30 | height: variables.toRem(100); 31 | background-color: var(--gradient-blue); 32 | border-radius: var(--border-radius); 33 | transform: rotate(45deg); 34 | } 35 | } 36 | 37 | &__author { 38 | font-style: normal; 39 | } 40 | 41 | &__button { 42 | width: variables.toRem(40); 43 | height: variables.toRem(60); 44 | 45 | &--prev { 46 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iNjIiIHZpZXdCb3g9IjAgMCAzNiA2MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMkwzMyAzMC42NDEyIiBzdHJva2U9IiNFREFBNUMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0zMyAzMC42NDEyTDMgNTkuMjgyNSIgc3Ryb2tlPSIjRURBQTVDIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K'); 47 | transform: rotate(180deg); 48 | // Arrows styles are adjusted in matrix 49 | margin-left: calc(-20vw + 20%); 50 | } 51 | 52 | &--next { 53 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iNjIiIHZpZXdCb3g9IjAgMCAzNiA2MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMkwzMyAzMC42NDEyIiBzdHJva2U9IiNFREFBNUMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0zMyAzMC42NDEyTDMgNTkuMjgyNSIgc3Ryb2tlPSIjRURBQTVDIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K'); 54 | transform: rotate(0deg); 55 | margin-right: calc(-20vw + 20%); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/_white-variables.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-theme-mixin'; 2 | 3 | .white-theme { 4 | --color-bg: rgb(255 255 255); 5 | --color-text: rgb(37 37 37); 6 | --primary: rgb(191 24 35); 7 | --accent: rgb(195 100 124); 8 | --light-primary: rgb(250 240 240); 9 | // Include shared theme settings 10 | @include shared-theme-mixin.shared-theme-settings; 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-accordion.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-accordion'; 2 | 3 | .white-theme { 4 | @include shared-accordion.shared-accordion-theme; 5 | 6 | .accordion { 7 | &__heading::after { 8 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCAxNSAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGxpbmUgeDE9IjcuNzI2MDciIHkxPSI3LjY5MDMyZS0wOCIgeDI9IjcuNzI2MDciIHkyPSIxNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIi8+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iNy41IiB4Mj0iMC40MjgyODgiIHkyPSI3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 9 | } 10 | 11 | &__item[open] .accordion__heading::after { 12 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMyIgdmlld0JveD0iMCAwIDE1IDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxsaW5lIHgxPSIxNC45MjI0IiB5MT0iMS41IiB4Mj0iMC40MjgyODgiIHkyPSIxLjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIvPgo8L3N2Zz4K'); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-banner.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-banner'; 2 | 3 | .white-theme { 4 | @include shared-banner.shared-banner-theme; 5 | 6 | .banner::after { 7 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODYwIiBoZWlnaHQ9IjY3NiIgdmlld0JveD0iMCAwIDg2MCA2NzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8yMzVfMTg3MzQpIj4KPHBhdGggZD0iTTcyOS4yMjEgMTkzLjYxMkwzNDkuNjEyIDI5NS4zMjhMNDUxLjMyOCA2NzQuOTM3TDgzMC45MzcgNTczLjIyMUw3MjkuMjIxIDE5My42MTJaIiBzdHJva2U9IiMyNTI1MjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMzgwLjIyMSAwLjYxMjExOUwwLjYxMjMwNSAxMDIuMzI4TDEwMi4zMjggNDgxLjkzN0w0ODEuOTM3IDM4MC4yMjFMMzgwLjIyMSAwLjYxMjExOVoiIHN0cm9rZT0iIzI1MjUyNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik03NTcuMjIxIDE0Ni42MTJMMzc3LjYxMiAyNDguMzI4TDQ3OS4zMjggNjI3LjkzN0w4NTguOTM3IDUyNi4yMjFMNzU3LjIyMSAxNDYuNjEyWiIgc3Ryb2tlPSIjQkYxODIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8yMzVfMTg3MzQiPgo8cmVjdCB3aWR0aD0iODYwIiBoZWlnaHQ9IjY3NiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-blockquote'; 2 | 3 | .white-theme { 4 | @include shared-blockquote.shared-blockquote-theme; 5 | 6 | .blockquote::after { 7 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjI1IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIyNSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xNi43NTAxIDE1OS43OTJINjQuNjI1MUw5Ni41NDE3IDk1Ljk1ODNWMC4yMDgyNTJIMC43OTE3NDhWOTUuOTU4M0g0OC42NjY3TDE2Ljc1MDEgMTU5Ljc5MlpNMTQ0LjQxNyAxNTkuNzkySDE5Mi4yOTJMMjI0LjIwOCA5NS45NTgzVjAuMjA4MjUySDEyOC40NThWOTUuOTU4M0gxNzYuMzMzTDE0NC40MTcgMTU5Ljc5MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuMDUiLz4KPC9zdmc+Cg=='); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-cards'; 2 | 3 | .white-theme { 4 | @include shared-cards.shared-cards-theme; 5 | 6 | .cards__link { 7 | &::after { 8 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg=='); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-dynamic-header.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-dynamic-header'; 2 | 3 | .white-theme { 4 | @include shared-dynamic-header.shared-dynamic-header-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-global.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-global'; 2 | @use '@common-styles/variables'; 3 | 4 | .white-theme { 5 | @include shared-global.shared-global-theme; 6 | 7 | body { 8 | &::before { 9 | background-image: 10 | linear-gradient( 11 | to bottom, 12 | transparent 0%, 13 | transparent 50%, 14 | var(--color-bg) 50%, 15 | var(--color-bg) 16 | ), 17 | url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgzIiBoZWlnaHQ9IjM1OCIgdmlld0JveD0iMCAwIDI4MyAzNTgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzA3MTA3IiB5PSIxNDEuNDIxIiB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE5OSIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDAuNzA3MTA3IDE0MS40MjEpIiBzdHJva2U9IiMyNTI1MjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cmVjdCB4PSIwLjcwNzEwNyIgeT0iMTg1LjQyMSIgd2lkdGg9IjE5OSIgaGVpZ2h0PSIxOTkiIHRyYW5zZm9ybT0icm90YXRlKC00NSAwLjcwNzEwNyAxODUuNDIxKSIgc3Ryb2tlPSIjQzM2NDdDIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC43MDcxMDciIHk9IjIxNi40MjEiIHdpZHRoPSIxOTkiIGhlaWdodD0iMTk5IiB0cmFuc2Zvcm09InJvdGF0ZSgtNDUgMC43MDcxMDcgMjE2LjQyMSkiIHN0cm9rZT0iIzI1MjUyNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo='); 18 | background-size: 19 | 0 155vw, 20 | variables.toRem(300) 250vw; 21 | right: 0; 22 | left: variables.toRem(-150); 23 | } 24 | 25 | &::after { 26 | background-image: 27 | linear-gradient( 28 | to bottom, 29 | transparent 0%, 30 | transparent 50%, 31 | var(--color-bg) 50%, 32 | var(--color-bg) 33 | ), 34 | url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgzIiBoZWlnaHQ9IjMyOCIgdmlld0JveD0iMCAwIDI4MyAzMjgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzA2MDI2IiB5PSItMS4xOTIwOWUtMDciIHdpZHRoPSIxOTkuMzA2IiBoZWlnaHQ9IjE5OS4zMDYiIHRyYW5zZm9ybT0ibWF0cml4KDAuNzA2MDI2IC0wLjcwODE4NiAwLjcwNjAyNiAwLjcwODE4NiAwLjIwNzU1MyAxNDIuMzU0KSIgc3Ryb2tlPSIjMjUyNTI1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iMC43MDYwMjYiIHk9Ii0xLjE5MjA5ZS0wNyIgd2lkdGg9IjE5OS4zMDYiIGhlaWdodD0iMTk5LjMwNiIgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDYwMjYgLTAuNzA4MTg2IDAuNzA2MDI2IDAuNzA4MTg2IDAuMjA3NTUzIDE4Ni40ODkpIiBzdHJva2U9IiNDMzY0N0MiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K'); 35 | background-size: 36 | 0 155vw, 37 | variables.toRem(300) 150vw; 38 | left: 0; 39 | right: variables.toRem(-150); 40 | transform: scaleX(-1); 41 | } 42 | } 43 | 44 | kbd { 45 | color: var(--color-text); 46 | } 47 | 48 | pre { 49 | color: var(--color-bg); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-icon-cards.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-icon-cards'; 2 | 3 | .white-theme { 4 | @include shared-icon-cards.shared-icon-cards-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-image-text-row.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-image-text-row'; 2 | 3 | .white-theme { 4 | @include shared-image-text-row.shared-image-text-row-theme; 5 | 6 | .image-text-row__link { 7 | &::after { 8 | content: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDggMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xLjcwNDkyIDBMMC4yOTQ5MjIgMS40MUw0Ljg3NDkyIDZMMC4yOTQ5MjIgMTAuNTlMMS43MDQ5MiAxMkw3LjcwNDkyIDZMMS43MDQ5MiAwWiIgZmlsbD0iI0MzNjQ3QyIvPgo8L3N2Zz4KCg=='); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-key-statistics.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-key-statistics'; 2 | 3 | .white-theme { 4 | @include shared-key-statistics.shared-key-statistics-theme; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/themes/white-theme/white-testimonials.scss: -------------------------------------------------------------------------------- 1 | @use '@common-styles/shared-themes/shared-testimonials'; 2 | 3 | .white-theme { 4 | @include shared-testimonials.shared-testimonials-theme; 5 | 6 | .testimonial__button { 7 | &--prev, 8 | &--after { 9 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzMiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MyAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcyLjIwNzEgOC4yMDcxMUM3Mi41OTc2IDcuODE2NTggNzIuNTk3NiA3LjE4MzQyIDcyLjIwNzEgNi43OTI4OUw2NS44NDMxIDAuNDI4OTMyQzY1LjQ1MjYgMC4wMzg0MDc4IDY0LjgxOTUgMC4wMzg0MDc4IDY0LjQyODkgMC40Mjg5MzJDNjQuMDM4NCAwLjgxOTQ1NyA2NC4wMzg0IDEuNDUyNjIgNjQuNDI4OSAxLjg0MzE1TDcwLjA4NTggNy41TDY0LjQyODkgMTMuMTU2OUM2NC4wMzg0IDEzLjU0NzQgNjQuMDM4NCAxNC4xODA1IDY0LjQyODkgMTQuNTcxMUM2NC44MTk1IDE0Ljk2MTYgNjUuNDUyNiAxNC45NjE2IDY1Ljg0MzEgMTQuNTcxMUw3Mi4yMDcxIDguMjA3MTFaTTAuNSA4LjVMNzEuNSA4LjVWNi41TDAuNSA2LjVMMC41IDguNVoiIGZpbGw9IiNCRjE4MjMiLz4KPC9zdmc+Cg=='); 10 | } 11 | 12 | &--next { 13 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzMiIGhlaWdodD0iMTUiIHZpZXdCb3g9IjAgMCA3MyAxNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcyLjIwNzEgOC4yMDcxMUM3Mi41OTc2IDcuODE2NTggNzIuNTk3NiA3LjE4MzQyIDcyLjIwNzEgNi43OTI4OUw2NS44NDMxIDAuNDI4OTMyQzY1LjQ1MjYgMC4wMzg0MDc4IDY0LjgxOTUgMC4wMzg0MDc4IDY0LjQyODkgMC40Mjg5MzJDNjQuMDM4NCAwLjgxOTQ1NyA2NC4wMzg0IDEuNDUyNjIgNjQuNDI4OSAxLjg0MzE1TDcwLjA4NTggNy41TDY0LjQyODkgMTMuMTU2OUM2NC4wMzg0IDEzLjU0NzQgNjQuMDM4NCAxNC4xODA1IDY0LjQyODkgMTQuNTcxMUM2NC44MTk1IDE0Ljk2MTYgNjUuNDUyNiAxNC45NjE2IDY1Ljg0MzEgMTQuNTcxMUw3Mi4yMDcxIDguMjA3MTFaTTAuNSA4LjVMNzEuNSA4LjVWNi41TDAuNSA2LjVMMC41IDguNVoiIGZpbGw9IiNCRjE4MjMiLz4KPC9zdmc+Cg=='); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /verMgmt.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestGlob": "dxp/component-service/*/manifest.json", 3 | "customCommands": [ 4 | { 5 | "name": "Deploy Edge Components", 6 | "value": "FEATURE_EDGE_COMPONENTS=true dxp-next cmp deploy ." 7 | }, 8 | { 9 | "name": "List root files (command: ls -l)", 10 | "value": "ls -l" 11 | } 12 | ] 13 | } 14 | --------------------------------------------------------------------------------