diff --git a/.eslintrc.js b/.eslintrc.js index 76f220db..deac7e89 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { root: true, - extends: 'airbnb-base', + extends: ['airbnb-base', 'prettier'], env: { browser: true, }, @@ -11,11 +11,18 @@ module.exports = { requireConfigFile: false, }, rules: { + 'max-len': [2, 160, 2, { ignoreUrls: true }], + 'import/no-unresolved': [2, { commonjs: true }], + 'array-callback-return': 'off', // due to prettier + 'class-methods-use-this': 'off', // due to prettier // allow reassigning param 'no-param-reassign': [2, { props: false }], 'linebreak-style': ['error', 'unix'], - 'import/extensions': ['error', { - js: 'always', - }], + 'import/extensions': [ + 'error', + { + js: 'always', + }, + ], }, }; diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..75fac8e1 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run lint diff --git a/.stylelintrc.json b/.stylelintrc.json index 17c74ed1..b70de4de 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,3 +1,6 @@ { - "extends": ["stylelint-config-standard"] + "extends": ["stylelint-config-standard"], + "rules": { + "no-descending-specificity": null + } } \ No newline at end of file diff --git a/README.md b/README.md index 80fdb9d9..903379e0 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,7 @@ npm run lint:fix 1. Install the [AEM CLI](https://github.com/adobe/helix-cli): `npm install -g @adobe/aem-cli` 1. Start AEM EDS Proxy: `aem up` (opens your browser at `http://localhost:3000`) 1. Open the `henkel-raqn-guide` directory in your favorite IDE and start coding :) + +## Documentation + +[Documentation](docs/readme.md) \ No newline at end of file diff --git a/blocks/accordion/accordion.css b/blocks/accordion/accordion.css new file mode 100644 index 00000000..9774cf74 --- /dev/null +++ b/blocks/accordion/accordion.css @@ -0,0 +1,78 @@ +raqn-accordion { + --scope-icon-size: 1em; + --accordion-background: var(--scope-background, black); + --accordion-color: var(--scope-color, white); + + background: var(--accordion-background); + color: var(--accordion-color); + margin: var(--scope-margin, 0); + padding: var(--scope-padding, 0); + display: grid; +} + +raqn-accordion raqn-icon { + align-self: end; + transform: rotate(90deg); + transition: transform 0.2s ease-in-out; +} + +raqn-accordion accordion-control.active raqn-icon { + transform: rotate(270deg); +} + +.accordion-control { + border-block-start: var(--scope-border-block-start, none); + border-inline-start: var(--scope-border-inline-start, none); + border-inline-end: var(--scope-border-inline-end, none); + cursor: pointer; + display: flex; + align-items: center; + justify-content: start; + width: 100%; +} + +.accordion-control:first-child { + border-block-start: none; +} + +.accordion-control > * { + --scope-headings-color: var(--scope-color, black); + --scope-hover-color: var(--scope-accent-color, gray); + + width: 100%; + display: flex; + justify-content: space-between; + min-width: 100%; +} + +.accordion-control:hover { + --scope-color: var(--scope-headings-color); +} + +.accordion-content { + display: grid; + max-height: 0; + overflow: hidden; + opacity: 0; + border-block-end: var(--scope-border-block-end, none); + border-block-start: var(--scope-border-block-start, none); + margin-block-end: -1px; + transition: + max-height 0.5s ease-in-out, + opacity 0.5s ease-in-out; +} + +.accordion-content:last-child { + border-block-end: none; +} + +.accordion-content.active { + opacity: 1; + grid-template-rows: 1fr; + max-height: 100vw; +} + +.accordion-content-wrapper { + margin-block: 1em; + display: grid; +} diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js new file mode 100644 index 00000000..f3b0b0eb --- /dev/null +++ b/blocks/accordion/accordion.js @@ -0,0 +1,81 @@ +import Column from '../column/column.js'; + +export default class Accordion extends Column { + dependencies = ['icon']; + + ready() { + this.setAttribute('role', 'navigation'); + let children = Array.from(this.children); + children = children.map((child) => { + if (child.tagName !== 'DIV') { + const div = document.createElement('div'); + div.append(child); + this.append(div); + return div; + } + return child; + }); + // console.log(children) + this.setupControls(children.filter((_, ind) => ind % 2 === 0)); + this.setupContent(children.filter((_, ind) => ind % 2 === 1)); + } + + setupControls(controls) { + controls.forEach((control, index) => { + const icon = document.createElement('raqn-icon'); + icon.setAttribute('icon', 'chevron-right'); + const children = Array.from(control.children); + if (children.length === 0) { + const child = document.createElement('span'); + child.textContent = control.textContent; + control.innerHTML = ''; + control.append(child); + } + control.children[0].append(icon); + control.setAttribute('role', 'button'); + control.setAttribute('aria-expanded', 'false'); + control.setAttribute('tabindex', '0'); + control.classList.add('accordion-control'); + control.id = `accordion-${this.id}-${index}`; + control.addEventListener('click', () => this.toggleControl(control)); + control.addEventListener('keypress', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this.toggleControl(control); + } + }); + }); + } + + toggleControl(control) { + const content = control.nextElementSibling; + if (content) { + content.classList.toggle('active'); + control.classList.toggle('active'); + control.setAttribute( + 'aria-expanded', + content.classList.contains('active'), + ); + content.setAttribute( + 'aria-hidden', + !content.classList.contains('active'), + ); + } + } + + setupContent(contents) { + contents.forEach((content) => { + const internal = content.children; + const wrapper = document.createElement('div'); + wrapper.classList.add('accordion-content-wrapper'); + wrapper.append(...internal); + content.append(wrapper); + content.setAttribute('role', 'region'); + content.setAttribute('aria-hidden', true); + content.classList.add('accordion-content'); + content.setAttribute( + 'aria-labelledby', + content.previousElementSibling.id, + ); + }); + } +} diff --git a/blocks/breadcrumbs/breadcrumbs.css b/blocks/breadcrumbs/breadcrumbs.css new file mode 100644 index 00000000..f53ea6f9 --- /dev/null +++ b/blocks/breadcrumbs/breadcrumbs.css @@ -0,0 +1,40 @@ +raqn-breadcrumbs { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + align-items: center; + padding: 10px 0; + background: var(--scope-background, transparent); + color: var(--scope-color, #000); +} + +raqn-breadcrumbs ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; +} + +raqn-breadcrumbs ul li { + margin-inline-end: 1em; + font-weight: bold; +} + +raqn-breadcrumbs ul li a { + color: var(--scope-color); + font-weight: normal; +} + +@media screen and (max-width: 768px) { + raqn-breadcrumbs ul li { + display: none; + } + + raqn-breadcrumbs ul li.separator:has(+ li:last-child) { + display: block; + } + + raqn-breadcrumbs ul li:last-child { + display: block; + } +} diff --git a/blocks/breadcrumbs/breadcrumbs.js b/blocks/breadcrumbs/breadcrumbs.js new file mode 100644 index 00000000..fe3b3083 --- /dev/null +++ b/blocks/breadcrumbs/breadcrumbs.js @@ -0,0 +1,31 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class BreadCrumbs extends ComponentBase { + capitalize(string) { + return string + .split('-') + .map((str) => str.charAt(0).toUpperCase() + str.slice(1)) + .join(' '); + } + + ready() { + this.classList.add('full-width'); + this.classList.add('breadcrumbs'); + this.path = window.location.pathname.split('/'); + this.innerHTML = ` + `; + } +} diff --git a/blocks/button/button.css b/blocks/button/button.css new file mode 100644 index 00000000..b3b31b2d --- /dev/null +++ b/blocks/button/button.css @@ -0,0 +1,38 @@ +raqn-button { + width: 100%; + display: grid; + align-content: center; + justify-content: center; + align-items: center; + justify-items: var(--scope-justify, start); +} + +raqn-button > * { + background: var(--scope-accent-background, #000); + color: var(--scope-accent-color, #fff); + text-transform: none; + border-radius: var(--scope-border-radius, 0); + border-block-start: var(--scope-border-block-start, 1px solid transparent); + border-block-end: var(--scope-border-block-end, 1px solid transparent); + border-inline-start: var(--scope-border-inline-start, 1px solid transparent); + border-inline-end: var(--scope-border-inline-end, 1px solid transparent); + overflow: hidden; +} + +raqn-button > *:hover { + background: var(--scope-accent-background-hover, #fff); + color: var(--scope-accent-color-hover, #fff); + border-color: currentcolor; +} + +raqn-button a { + color: var(--scope-accent-color, currentcolor); + padding: 10px 20px; + text-decoration: none; +} + +raqn-button a:hover, +raqn-button a:visited, +raqn-button a:active { + text-decoration: none; +} diff --git a/blocks/button/button.js b/blocks/button/button.js new file mode 100644 index 00000000..227717d3 --- /dev/null +++ b/blocks/button/button.js @@ -0,0 +1,8 @@ +import Column from '../column/column.js'; + +export default class Button extends Column { + render() { + this.setAttribute('role', 'button'); + this.setAttribute('tabindex', '0'); + } +} diff --git a/blocks/card/card.css b/blocks/card/card.css new file mode 100644 index 00000000..b3f2c32f --- /dev/null +++ b/blocks/card/card.css @@ -0,0 +1,63 @@ +raqn-card { + background: var(--scope-background, transparent); + color: var(--scope-color, #fff); + display: grid; + position: relative; + grid-template-columns: var(--card-columns, 1fr); + gap: var(--scope-gap, 20px); + padding: var(--scope-padding, 20px 0); +} + +raqn-card a { + font-size: var(--raqn-font-size-3, 1.2em); + font-weight: bold; +} + +raqn-card p a { + margin-block: 0; +} + +raqn-card > picture { + grid-column: span var(--card-columns, 1fr); +} + +raqn-card > div { + position: relative; + background: var(--scope-inner-background, transparent); + padding: var(--scope-inner-padding, 20px); + border-block-start: var(--scope-border-block-start, none); + border-block-end: var(--scope-border-block-end, none); + border-inline-start: var(--scope-border-inline-start, none); + border-inline-end: var(--scope-border-inline-end, none); +} + +raqn-card > div div:last-child > a { + position: absolute; + inset-inline-start: 0; + inset-block-start: 0; + width: 100%; + height: 100%; + cursor: pointer; + text-indent: -10000px; + margin: 0; + padding: 0; +} + +raqn-card > div:has(raqn-icon) { + padding-block-end: 0; + padding-block-end: var(--scope-icon-size, 20px); +} + +raqn-card p:has(raqn-icon) { + display: inline-grid; +} + +raqn-card raqn-icon { + width: 100%; + display: flex; + position: absolute; + inset-block-end: 0; + inset-inline-end: 0; + box-sizing: border-box; + padding: var(--scope-inner-padding, 20px); +} diff --git a/blocks/card/card.js b/blocks/card/card.js new file mode 100644 index 00000000..1933582d --- /dev/null +++ b/blocks/card/card.js @@ -0,0 +1,42 @@ +import ComponentBase from '../../scripts/component-base.js'; +import { eagerImage } from '../../scripts/libs.js'; + +export default class Card extends ComponentBase { + static get observedAttributes() { + return ['columns', 'ratio', 'eager', 'background', 'button']; + } + + ready() { + if (this.getAttribute('button') === 'true') { + Array.from(this.querySelectorAll('a')).forEach((a) => + this.convertLink(a), + ); + } + this.eager = parseInt(this.getAttribute('eager') || 0, 10); + this.ratio = this.getAttribute('ratio') || '4/3'; + this.style.setProperty('--card-ratio', this.ratio); + this.classList.add('inner'); + this.setupColumns(this.getAttribute('columns')); + if (this.eager) { + eagerImage(this, this.eager); + } + } + + convertLink(a) { + const button = document.createElement('raqn-button'); + const content = a.outerHTML; + button.innerHTML = content; + a.replaceWith(button); + } + + setupColumns(columns) { + if (!columns) { + return; + } + this.columns = parseInt(columns, 10); + this.area = Array.from(Array(parseInt(this.columns, 10))) + .map(() => '1fr') + .join(' '); + this.style.setProperty('--card-columns', this.area); + } +} diff --git a/blocks/cards/cards.css b/blocks/cards/cards.css deleted file mode 100644 index a0315ede..00000000 --- a/blocks/cards/cards.css +++ /dev/null @@ -1,31 +0,0 @@ -.cards > ul { - list-style: none; - margin: 0; - padding: 0; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - grid-gap: 16px; -} - -.cards > ul > li { - border: 1px solid var(--highlight-background-color); - background-color: var(--background-color) -} - -.cards .cards-card-body { - margin: 16px; -} - -.cards .cards-card-image { - line-height: 0; -} - -.cards .cards-card-body > *:first-child { - margin-top: 0; -} - -.cards > ul > li img { - width: 100%; - aspect-ratio: 4 / 3; - object-fit: cover; -} diff --git a/blocks/cards/cards.js b/blocks/cards/cards.js deleted file mode 100644 index 717ec302..00000000 --- a/blocks/cards/cards.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createOptimizedPicture } from '../../scripts/lib-franklin.js'; - -export default function decorate(block) { - /* change to ul, li */ - const ul = document.createElement('ul'); - [...block.children].forEach((row) => { - const li = document.createElement('li'); - while (row.firstElementChild) li.append(row.firstElementChild); - [...li.children].forEach((div) => { - if (div.children.length === 1 && div.querySelector('picture')) div.className = 'cards-card-image'; - else div.className = 'cards-card-body'; - }); - ul.append(li); - }); - ul.querySelectorAll('img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }]))); - block.textContent = ''; - block.append(ul); -} diff --git a/blocks/column/column.css b/blocks/column/column.css new file mode 100644 index 00000000..1879b40e --- /dev/null +++ b/blocks/column/column.css @@ -0,0 +1,3 @@ +raqn-column { + margin: var(--scope-margin, 0); +} diff --git a/blocks/column/column.js b/blocks/column/column.js new file mode 100644 index 00000000..d4a72505 --- /dev/null +++ b/blocks/column/column.js @@ -0,0 +1,84 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class Column extends ComponentBase { + static observedAttributes() { + return ['position', 'size']; + } + + connected() { + const content = this.querySelectorAll('div > div'); + // clean up dom structure (div div div div div div) and save the content + this.contentChildren = Array.from(content).map((child) => { + const {children} = child; + const parent = child.parentNode; + if (children.length > 0) { + child.replaceWith(...children); + } + return parent; + }) + this.calculateGridTemplateColumns(); + } + + calculateGridTemplateColumns() { + this.position = parseInt(this.getAttribute('position'), 10); + this.size = this.getAttribute('size'); + this.justify = this.getAttribute('justify') || 'stretch'; + if (this.justify) { + this.style.justifyContent = this.justify; + } + if (this.position) { + const parent = this.parentElement; + const children = Array.from(parent.children); + this.parentElement.classList.add('raqn-grid'); + let parentGridTemplateColumns = parent.style.getPropertyValue( + '--grid-template-columns', + ); + if (!parentGridTemplateColumns) { + // we have no grid template columns yet + parentGridTemplateColumns = children + .map((child, index) => { + if (this.position === index + 1) { + return this.size || 'auto'; + } + return 'auto'; + }) + .join(' '); + // set the new grid template columns + parent.style.setProperty( + '--grid-template-columns', + parentGridTemplateColumns, + ); + } else { + const { position } = this; + const prio = children.indexOf(this) + 1; + parentGridTemplateColumns = parentGridTemplateColumns + .split(' ') + .map((size, i) => { + // we have a non standard value for this position + const hasValue = size !== 'auto'; + // we are at the position + const isPosition = i + 1 === position; + // we are at a position before the prio + const isBeforePrio = i + 1 <= prio; + // we have a non standard value for this position and we are at the position + if (!hasValue && isPosition) { + return this.size || 'auto'; + } + // we have a non standard value for this position and we are at a position before the prio + if (hasValue && isPosition && isBeforePrio) { + return this.size || size; + } + return size; + }) + .join(' '); + // set the new grid template columns + parent.style.setProperty( + '--grid-template-columns', + parentGridTemplateColumns, + ); + } + this.style.gridColumn = this.position; + this.style.gridRow = 1; + } + } +} diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css deleted file mode 100644 index 4b09aeea..00000000 --- a/blocks/columns/columns.css +++ /dev/null @@ -1,65 +0,0 @@ -.columns > div { - display: flex; - flex-direction: column; -} - -.columns img { - width: 100%; -} - -@media (min-width: 900px) { - .columns > div { - align-items: center; - flex-direction: unset; - gap: var(--padding-vertical) var(--padding-horizontal); - padding: var(--padding-vertical) var(--padding-horizontal); - } - - .columns > div > div:nth-child(1) { - flex: var(--column0-flex, 0); - } - - .columns > div > div:nth-child(2) { - flex: var(--column1-flex, 0); - } - - .columns > div > div:nth-child(3) { - flex: var(--column2-flex, 0); - } - - .columns > div > div:nth-child(4) { - flex: var(--column3-flex, 0); - } - - .columns > div > div:nth-child(5) { - flex: var(--column4-flex, 0); - } - - .columns > div > div:nth-child(6) { - flex: var(--column5-flex, 0); - } - - .columns > div > div:nth-child(7) { - flex: var(--column6-flex, 0); - } - - .columns > div > div:nth-child(8) { - flex: var(--column7-flex, 0); - } - - .columns > div > div:nth-child(9) { - flex: var(--column8-flex, 0); - } - - .columns > div > div:nth-child(10) { - flex: var(--column9-flex, 0); - } - - .columns > div > div:nth-child(11) { - flex: var(--column10-flex, 0); - } - - .columns > div > div:nth-child(12) { - flex: var(--column11-flex, 0); - } -} diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js deleted file mode 100644 index 823c6c18..00000000 --- a/blocks/columns/columns.js +++ /dev/null @@ -1,18 +0,0 @@ -import { - addCssVariables, -} from '../../scripts/lib-franklin.js'; - -export default function decorate(block) { - const columns = block.querySelectorAll(':scope > div > div'); - const columnCount = columns.length; - // following line regex matches partition sizes separated by dashes like 1-2-3 - const columnPartionRegex = /^\d{1,}(?:-\d{1,})*$/; - const columnPartions = [...block.classList].find((c) => columnPartionRegex.test(c))?.split('-') || []; - - const variables = {}; - for (let i = 0; i < columnCount; i += 1) { - const partition = columnPartions.length > i ? columnPartions[i] : 1; - variables[`column${i}-flex`] = partition; - } - addCssVariables(block, variables); -} diff --git a/blocks/external/external.css b/blocks/external/external.css new file mode 100644 index 00000000..cfa763db --- /dev/null +++ b/blocks/external/external.css @@ -0,0 +1,7 @@ +raqn-external { + display: none; +} + +raqn-external[initialized='true'] { + display: block; +} diff --git a/blocks/external/external.js b/blocks/external/external.js new file mode 100644 index 00000000..d7f24168 --- /dev/null +++ b/blocks/external/external.js @@ -0,0 +1,23 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class External extends ComponentBase { + static get observedAttributes() { + return ['external', 'folder']; + } + + link = false; + + get external() { + const link = this.querySelector('a'); + if (link) { + this.link = link.href; + } + return this.link; + } + + set external(value) { + if (value !== '') { + this.link = value; + } + } +} diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 19fc5607..7aa0fb17 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,14 +1,51 @@ footer { - padding: 2rem; - background-color: var(--overlay-background-color); - font-size: var(--body-font-size-s); + background: var(--scope-background-color); + width: var(--scope-max-width); + margin: 0 auto; } -footer .footer { - max-width: 1200px; - margin: auto; +raqn-footer { + background: var(--scope-background-color); + border-top: 1px solid var(--scope-color); } -footer .footer p { - margin: 0; -} \ No newline at end of file +raqn-footer ul { + list-style: none; + padding: 0; + margin: 20px 0; + min-width: 100%; +} + +raqn-footer ul li a { + color: var(--scope-color); +} + +@media screen and (min-width: 1024px) { + raqn-footer { + display: grid; + grid-template-columns: auto 20vw; + } + + raqn-footer ul li a { + padding: 10px 1.2em; + border-inline-end: 1px solid var(--scope-color); + } + + raqn-footer ul { + display: flex; + margin: 0; + } + + raqn-footer ul li { + margin: 2em 0; + } + + raqn-footer > *:last-child { + justify-self: end; + align-self: center; + } +} + +raqn-footer ul li:last-child a { + border-inline-end: none; +} diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index ca8c7a68..bedb5839 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -1,25 +1,17 @@ -import { readBlockConfig, decorateIcons } from '../../scripts/lib-franklin.js'; +import ComponentBase from '../../scripts/component-base.js'; -/** - * loads and decorates the footer - * @param {Element} block The footer block element - */ -export default async function decorate(block) { - const cfg = readBlockConfig(block); - block.textContent = ''; - - // fetch footer content - const footerPath = cfg.footer || '/footer'; - const resp = await fetch(`${footerPath}.plain.html`, window.location.pathname.endsWith('/footer') ? { cache: 'reload' } : {}); - - if (resp.ok) { - const html = await resp.text(); - - // decorate footer DOM - const footer = document.createElement('div'); - footer.innerHTML = html; +export default class Footer extends ComponentBase { + constructor() { + super(); + this.external = '/footer.plain.html'; + } - decorateIcons(footer); - block.append(footer); + ready() { + const child = this.children[0]; + child.replaceWith(...child.children); + this.nav = this.querySelector('ul'); + this.nav.setAttribute('role', 'navigation'); + this.classList.add('full-width'); + this.classList.add('horizontal'); } } diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css deleted file mode 100644 index ba453ff5..00000000 --- a/blocks/grid/grid.css +++ /dev/null @@ -1,30 +0,0 @@ -.section.grid { - display: grid; - grid-template-columns: var(--grid-template-columns, unset); - grid-template-rows: var(--grid-template-rows, unset); -} - -.section.grid .element { - grid-column: var(--grid-column-start-position, auto) / var(--grid-column-end-position, auto); - grid-row: var(--grid-row-start-position, auto) / var(--grid-row-end-position, auto); - width: 100%; - max-width: unset; - background-color: var(--background-color, transparent); - color: var(--text-color); - margin: auto; -} - -.section.grid .element .image-wrapper { - margin: 0; - padding: 0; -} - -.section.grid .element img { - width: 100%; - height: auto; -} - -.section.grid .element p { - margin: 0; - padding: var(--padding-vertical) var(--padding-horizontal); -} diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js deleted file mode 100644 index 5a9fc3f8..00000000 --- a/blocks/grid/grid.js +++ /dev/null @@ -1,29 +0,0 @@ -import { - addCssVariables, -} from '../../scripts/lib-franklin.js'; - -export default function decorate(block) { - const elements = [...block.querySelectorAll(':scope > div')]; - - const columnTemplate = elements.find((e) => e.dataset.gridColumns)?.dataset.gridColumns; - const rowTemplate = elements.find((e) => e.dataset.gridRows)?.dataset.gridRows; - if (columnTemplate || rowTemplate) { - addCssVariables(block, { - 'grid-template-columns': columnTemplate, - 'grid-template-rows': rowTemplate, - }); - } - - elements.forEach((e) => { - e.classList.add('element'); - - const [[startColumnPosition, startRowPosition], [endColumnPosition, endRowPosition]] = e.dataset.gridPosition.split(/\s*\/\s*/).map((p) => p.split(/\s*-\s*/)); - - addCssVariables(e, { - 'grid-column-start-position': startColumnPosition, - 'grid-row-start-position': startRowPosition, - 'grid-column-end-position': endColumnPosition, - 'grid-row-end-position': endRowPosition, - }); - }); -} diff --git a/blocks/header/header.css b/blocks/header/header.css index 09581712..689a4775 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,19 +1,12 @@ -header a:any-link { - color: inherit; -} - -header p, -header .section.grid .element p { - margin: 0; - padding: 0; -} - -a.button:any-link { - background-color: transparent; - color: inherit; - font-weight: normal; -} +raqn-header { + --scope-background: var(--scope-header-background, #fff); + --scope-color: var(--scope-header-color, #000); -a.button:any-link .icon { - vertical-align: middle; + position: fixed; + width: 100%; + min-height: var(--scope-header-height, 64px); + display: grid; + background: var(--scope-header-background, #fff); + align-items: center; + z-index: 100; } diff --git a/blocks/header/header.js b/blocks/header/header.js index c8512c3f..185e8f60 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,11 +1,13 @@ -import { - loadBlocks, -} from '../../scripts/lib-franklin.js'; -import { - decorateMain, -} from '../../scripts/scripts.js'; +import ComponentBase from '../../scripts/component-base.js'; +import { eagerImage } from '../../scripts/libs.js'; -export default async function decorate(block) { - decorateMain(block); - loadBlocks(block); +export default class Header extends ComponentBase { + external = '/header.plain.html'; + + dependencies = ['navigation']; + + async processExternal(response) { + await super.processExternal(response); + eagerImage(this, 1); + } } diff --git a/blocks/hero/hero.css b/blocks/hero/hero.css index cc5f4fa5..822306c0 100644 --- a/blocks/hero/hero.css +++ b/blocks/hero/hero.css @@ -1,39 +1,25 @@ /* block specific CSS goes here */ -main .hero-container > div { - max-width: unset; -} - -main .hero-container { - padding: 0; -} - -main .hero { - position: relative; - padding: 32px; - min-height: 300px; -} +raqn-hero { + --hero-background: var(--scope-background, black); + --hero-color: var(--scope-color, white); + --hero-grid-template-columns: var(--scope-hero-columns, 1fr); + --hero-hero-order: 0; + --hero-padding-block: var(--scope-hero-padding-block, 40px); -main .hero h1 { - max-width: 1200px; - margin-left: auto; - margin-right: auto; - color: white; + background: var(--hero-background); + color: var(--hero-color); + align-items: center; + grid-template-columns: var(--hero-grid-template-columns, 1fr); + padding-block: var(--hero-padding-block); } -main .hero picture { - position: absolute; - z-index: -1; - top: 0; - left: 0; - bottom: 0; - right: 0; - object-fit: cover; - box-sizing: border-box; +@media screen and (min-width: 767px) { + raqn-hero { + --hero-grid-template-columns: 0.6fr 0.4fr; + } } -main .hero img { - object-fit: cover; - width: 100%; - height: 100%; +raqn-hero > div:first-child { + order: var(--hero-hero-order); } diff --git a/blocks/hero/hero.js b/blocks/hero/hero.js index e69de29b..ce998437 100644 --- a/blocks/hero/hero.js +++ b/blocks/hero/hero.js @@ -0,0 +1,16 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class Hero extends ComponentBase { + get observedAttributes() { + return ['order']; + } + + ready() { + const child = this.children[0]; + child.replaceWith(...child.children); + this.classList.add('full-width'); + this.setAttribute('role', 'banner'); + this.setAttribute('aria-label', 'hero'); + this.style.setProperty('--hero-hero-order', this.getAttribute('order')); + } +} diff --git a/blocks/icon/icon.css b/blocks/icon/icon.css new file mode 100644 index 00000000..b34e808b --- /dev/null +++ b/blocks/icon/icon.css @@ -0,0 +1,22 @@ +raqn-icon { + display: inline-flex; + font-size: 1em; + line-height: 1em; + text-align: center; + min-width: var(--scope-icon-size, 1em); + min-height: var(--scope-icon-size, 1em); + justify-content: var(--scope-icon-align, start); + text-transform: none; + vertical-align: middle; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +raqn-icon svg { + display: inline-block; + width: var(--scope-icon-size, 1em); + height: var(--scope-icon-size, 1em); + fill: currentcolor; + overflow: hidden; + vertical-align: middle; +} diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js new file mode 100644 index 00000000..08e6e075 --- /dev/null +++ b/blocks/icon/icon.js @@ -0,0 +1,104 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class Icon extends ComponentBase { + constructor() { + super(); + this.setupSprite(); + } + + setupSprite() { + this.svgSprite = document.getElementById('franklin-svg-sprite'); + if (!this.svgSprite) { + this.svgSprite = document.createElement('div'); + this.svgSprite.id = 'franklin-svg-sprite'; + document.body.append(this.svgSprite); + } + } + + get iconUrl() { + return `/assets/icons/${this.iconName}.svg`; + } + + get cache() { + window.ICONS_CACHE = window.ICONS_CACHE || {}; + return window.ICONS_CACHE; + } + + async connected() { + this.iconName = this.getAttribute('icon'); + if (!this.cache[this.iconName]) { + this.cache[this.iconName] = { + loading: new Promise((resolve) => { + resolve(this.load(this.iconUrl)); + }), + }; + } else { + await this.cache[this.iconName].loading; + this.innerHTML = this.template(); + } + this.classList.add('loaded'); + } + + template() { + const { viewBox } = this.cache[this.iconName]; + const attributes = Object.keys({ viewBox }) + .map((k) => { + if (this.cache[this.iconName][k]) { + return `${k}="${this.cache[this.iconName][k]}"`; + } + return ''; + }) + .join(' '); + return ``; + } + + iconTemplate(iconName, svg, viewBox, width, height) { + return `${svg.innerHTML}`; + } + + async processExternal(response) { + if (response.ok) { + const { iconName } = this; + this.svg = await response.text(); + + if (this.svg.match(/( diff --git a/icons/search.svg b/icons/search.svg deleted file mode 100644 index 637c677b..00000000 --- a/icons/search.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/package-lock.json b/package-lock.json index c23bc9b6..365f926a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,10 @@ "chai": "4.3.7", "eslint": "8.35.0", "eslint-config-airbnb-base": "15.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "2.27.5", + "husky": "^8.0.0", + "prettier-eslint": "^16.2.0", "semantic-release": "21.0.5", "sinon": "15.0.1", "stylelint": "15.2.0", @@ -539,6 +542,18 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -1623,6 +1638,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -1932,6 +1953,160 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/parser": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz", + "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.16.0", + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/typescript-estree": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz", + "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz", + "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz", + "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz", + "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.16.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@web/browser-logs": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.2.5.tgz", @@ -3027,6 +3202,15 @@ "node": ">=8" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -3574,6 +3758,12 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4016,6 +4206,18 @@ "eslint-plugin-import": "^2.25.2" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", @@ -5057,6 +5259,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -5281,6 +5504,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6449,6 +6687,84 @@ "node": ">=8" } }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/loupe": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.3.tgz", @@ -10766,6 +11082,70 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-eslint": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.2.0.tgz", + "integrity": "sha512-GDTSKc62VaLceiaI/qMaKo2oco2CIWtbj4Zr6ckhbTgcBL/uR0d9jkMzh9OtBIT/Z7iBoCB4OHj/aJ5YuNgAuA==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -10912,6 +11292,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -11166,6 +11552,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -12658,6 +13050,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -12751,6 +13155,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -12926,6 +13343,82 @@ "node": ">= 0.8" } }, + "node_modules/vue-eslint-parser": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz", + "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -13587,6 +14080,15 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -14341,6 +14843,12 @@ } } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -14650,6 +15158,98 @@ "@types/node": "*" } }, + "@typescript-eslint/parser": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz", + "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.16.0", + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/typescript-estree": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz", + "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0" + } + }, + "@typescript-eslint/types": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz", + "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz", + "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.16.0", + "@typescript-eslint/visitor-keys": "6.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz", + "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.16.0", + "eslint-visitor-keys": "^3.4.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } + } + }, "@web/browser-logs": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.2.5.tgz", @@ -15441,6 +16041,12 @@ } } }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true + }, "compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -15845,6 +16451,12 @@ "path-type": "^4.0.0" } }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -16279,6 +16891,13 @@ "semver": "^6.3.0" } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, "eslint-import-resolver-node": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", @@ -16985,6 +17604,23 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -17149,6 +17785,12 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -18030,6 +18672,64 @@ } } }, + "loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "dev": true + }, + "loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, "loupe": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.3.tgz", @@ -20970,6 +21670,51 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true + }, + "prettier-eslint": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.2.0.tgz", + "integrity": "sha512-GDTSKc62VaLceiaI/qMaKo2oco2CIWtbj4Zr6ckhbTgcBL/uR0d9jkMzh9OtBIT/Z7iBoCB4OHj/aJ5YuNgAuA==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -21080,6 +21825,12 @@ } } }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -21269,6 +22020,12 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -22370,6 +23127,13 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -22441,6 +23205,12 @@ "is-typed-array": "^1.1.9" } }, + "typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true + }, "typical": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", @@ -22567,6 +23337,54 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, + "vue-eslint-parser": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz", + "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "dependencies": { + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 4587406e..589812c2 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lint:css": "stylelint blocks/**/*.css styles/*.css", "lint": "npm run lint:js && npm run lint:css", "lint:fix": "npm run lint:js -- --fix && npm run lint:css -- --fix", - "semantic-release": "semantic-release --debug" + "semantic-release": "semantic-release --debug", + "prepare": "husky install" }, "repository": { "type": "git", @@ -21,21 +22,24 @@ }, "homepage": "https://github.com/adobe/helix-project-boilerplate#readme", "devDependencies": { + "@babel/core": "7.21.0", + "@babel/eslint-parser": "7.19.1", + "@esm-bundle/chai": "4.3.4-fix.0", "@semantic-release/changelog": "6.0.3", "@semantic-release/exec": "6.0.3", "@semantic-release/git": "10.0.1", - "semantic-release": "21.0.5", - "@babel/core": "7.21.0", - "@babel/eslint-parser": "7.19.1", + "@web/test-runner": "0.15.1", + "@web/test-runner-commands": "0.6.5", "chai": "4.3.7", "eslint": "8.35.0", "eslint-config-airbnb-base": "15.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "2.27.5", - "@esm-bundle/chai": "4.3.4-fix.0", - "@web/test-runner": "0.15.1", - "@web/test-runner-commands": "0.6.5", + "semantic-release": "21.0.5", + "prettier-eslint": "^16.2.0", "sinon": "15.0.1", "stylelint": "15.2.0", - "stylelint-config-standard": "30.0.1" + "stylelint-config-standard": "30.0.1", + "husky": "^8.0.0" } } diff --git a/scripts/component-base.js b/scripts/component-base.js new file mode 100644 index 00000000..b64156b5 --- /dev/null +++ b/scripts/component-base.js @@ -0,0 +1,51 @@ +import { init, start } from './init.js'; + +export default class ComponentBase extends HTMLElement { + constructor() { + super(); + this.external = false; + this.dependencies = []; + this.uuid = `gen${crypto.randomUUID().split('-')[0]}`; + } + + async connectedCallback() { + const initialized = this.getAttribute('initialized'); + if (!initialized) { + this.setAttribute('id', this.uuid); + if (this.external) { + await this.load(this.external); + } + if (this.dependencies.length > 0) { + await Promise.all(this.dependencies.map((dep) => start({ name: dep }))); + } + this.connected(); + this.ready(); + this.setAttribute('initialized', true); + } + } + + async load(block) { + const response = await fetch( + `${block}`, + window.location.pathname.endsWith(block) ? { cache: 'reload' } : {}, + ); + return this.processExternal(response); + } + + async processExternal(response) { + if (response.ok) { + const html = await response.text(); + this.innerHTML = html; + return this.refresh(this); + } + return response; + } + + refresh(el = this) { + init(el); + } + + connected() {} + + ready() {} +} diff --git a/scripts/component-loader.js b/scripts/component-loader.js new file mode 100644 index 00000000..512a8b9c --- /dev/null +++ b/scripts/component-loader.js @@ -0,0 +1,131 @@ +import { config, getBreakPoint } from './libs.js'; + +export default class ComponentLoader { + constructor(blockName, element) { + window.raqnComponents = window.raqnComponents || {}; + this.blockName = blockName; + this.setBlockPaths(); + this.block = element; + if (this.block) { + this.setParams(); + this.content = this.block.children; + } + } + + async loadCSS(href) { + return new Promise((resolve, reject) => { + if (!document.querySelector(`head > link[href="${href}"]`)) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + link.onload = resolve; + link.onerror = reject; + document.head.append(link); + } else { + resolve(); + } + }); + } + + setParams() { + const mediaParams = {}; + this.params = { + ...Array.from(this.block.classList) + .filter((c) => c !== this.blockName && c !== 'block') + .reduce((acc, c) => { + const values = c.split('-'); + let key = values.shift(); + const breakpoint = getBreakPoint(); + if (breakpoint === key) { + key = values.shift(); + mediaParams[key] = values.join('-'); + return acc; + } + + if (config.breakpoints[key] !== undefined) { + return acc; + } + + if (acc[key] && Array.isArray(acc[key])) { + acc[key].push(values.join('-')); + } else if (acc[key]) { + acc[key] = [acc[key], values.join('-')]; + } else { + acc[key] = values.join('-'); + } + return acc; + }, {}), + ...mediaParams, + }; + } + + setBlockPaths() { + this.cssPath = `/blocks/${this.blockName}/${this.blockName}.css`; + this.jsPath = `/blocks/${this.blockName}/${this.blockName}.js`; + } + + setupElement() { + const elementName = `raqn-${this.blockName.toLowerCase()}`; + const element = document.createElement(elementName); + element.append(...this.block.children); + Object.keys(this.params).forEach((key) => { + const value = Array.isArray(this.params[key]) + ? this.params[key].join(' ') + : this.params[key]; + element.setAttribute(key, value); + }); + this.block.replaceWith(element); + } + + async loadWebComponent() { + return new Promise((resolve, reject) => { + (async () => { + try { + const mod = await import(this.jsPath); + if ( + mod.default && + mod.default.name && + mod.default.name !== 'decorate' + ) { + const { name } = mod.default; + const elementName = `raqn-${name.toLowerCase()}`; + // define the custom element if it doesn't exist + if (!window.raqnComponents[name]) { + const Contructor = mod.default; + customElements.define(elementName, Contructor); + window.raqnComponents[name] = Contructor; + } + if (this.block) { + this.setupElement(); + } + } else if (mod.default) { + await mod.default(this.block); + } + } catch (error) { + // eslint-disable-next-line no-console + console.log(`failed to load module for ${this.blockName}`, error); + return reject(error); + } + return resolve(); + })(); + }); + } + + async decorate() { + if (window.raqnComponents[this.blockName]) { + return this.setupElement(); + } + try { + const cssLoaded = this.loadCSS(this.cssPath).catch(() => + // eslint-disable-next-line no-console + console.log(`${this.cssPath} does not exist`), + ); + const decorationComplete = this.loadWebComponent(); + return Promise.all([decorationComplete, cssLoaded]); + } catch (error) { + // eslint-disable-next-line no-console + console.log(`failed to load module for ${this.blockName}`, error); + return Promise.resolve(); + } + } +} diff --git a/scripts/delayed.js b/scripts/delayed.js deleted file mode 100644 index 920b4ad8..00000000 --- a/scripts/delayed.js +++ /dev/null @@ -1,7 +0,0 @@ -// eslint-disable-next-line import/no-cycle -import { sampleRUM } from './lib-franklin.js'; - -// Core Web Vitals RUM collection -sampleRUM('cwv'); - -// add more delayed functionality here diff --git a/scripts/init.js b/scripts/init.js new file mode 100644 index 00000000..e0a607ae --- /dev/null +++ b/scripts/init.js @@ -0,0 +1,86 @@ +import ComponentLoader from './component-loader.js'; +import { config, debounce, eagerImage, getBreakPoint } from './libs.js'; + +export function getInfos(blocks) { + return blocks.map((block) => { + let el = block; + const tagName = el.tagName.toLowerCase(); + let name = tagName; + if (!config.elementBlocks.includes(tagName)) { + [name] = Array.from(el.classList); + } else { + // allow original way of defining blocks + el = document.createElement('div'); + block.append(el); + } + return { + name, + el, + }; + }); +} + +function getMeta(name) { + const meta = document.querySelector(`meta[name="${name}"]`); + if (!meta) { + return null; + } + return meta.content; +} + +function lcpPriority() { + const eagerImages = getMeta('eager-images'); + if (eagerImages) { + const length = parseInt(eagerImages, 10); + eagerImage(document.body, length); + } + const lcp = getMeta('lcp'); + window.raqnLCP = lcp ? lcp.split(',').map((name) => ({ name })) : []; +} + +export async function start({ name, el }) { + const loader = new ComponentLoader(name, el); + return loader.decorate(); +} + +export async function init(node = document) { + let blocks = Array.from(node.querySelectorAll('[class]:not([class^=style]')); + + if (node === document) { + const header = node.querySelector('header'); + const footer = node.querySelector('footer'); + blocks = [header, ...blocks, footer]; + } + + const data = getInfos(blocks); + const lcp = window.raqnLCP; + const delay = window.raqnLCPDelay || []; + const priority = data.filter(({ name }) => lcp.includes(name)); + const rest = data.filter( + ({ name }) => !lcp.includes(name) && !delay.includes(name), + ); + + // start with lcp and priority + Promise.all([ + ...lcp.map(({ name, el }) => start({ name, el })), + ...priority.map(({ name, el }) => start({ name, el })), + ]); + // timeout for the rest to proper prioritize in case of stalled loading + rest.map(({ name, el }) => setTimeout(() => start({ name, el }))); + + // reload on breakpoint change to reset params and variables + window.raqnBreakpoint = getBreakPoint(); + window.addEventListener( + 'resize', + debounce(() => { + // only on width / breakpoint changes + if (window.raqnBreakpoint !== getBreakPoint()) { + window.location.reload(); + } + }, 100), + ); +} +// mechanism of retrieving lang to be used in the app +document.documentElement.lang = document.documentElement.lang || 'en'; +lcpPriority(); +init(); diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js deleted file mode 100644 index 7b6cf0cf..00000000 --- a/scripts/lib-franklin.js +++ /dev/null @@ -1,737 +0,0 @@ -/* - * Copyright 2022 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -/** - * log RUM if part of the sample. - * @param {string} checkpoint identifies the checkpoint in funnel - * @param {Object} data additional data for RUM sample - */ -export function sampleRUM(checkpoint, data = {}) { - sampleRUM.defer = sampleRUM.defer || []; - const defer = (fnname) => { - sampleRUM[fnname] = sampleRUM[fnname] - || ((...args) => sampleRUM.defer.push({ fnname, args })); - }; - sampleRUM.drain = sampleRUM.drain - || ((dfnname, fn) => { - sampleRUM[dfnname] = fn; - sampleRUM.defer - .filter(({ fnname }) => dfnname === fnname) - .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); - }); - sampleRUM.always = sampleRUM.always || []; - sampleRUM.always.on = (chkpnt, fn) => { sampleRUM.always[chkpnt] = fn; }; - sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; - defer('observe'); - defer('cwv'); - try { - window.hlx = window.hlx || {}; - if (!window.hlx.rum) { - const usp = new URLSearchParams(window.location.search); - const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. - // eslint-disable-next-line no-bitwise - const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); - const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; - const random = Math.random(); - const isSelected = (random * weight < 1); - const urlSanitizers = { - full: () => window.location.href, - origin: () => window.location.origin, - path: () => window.location.href.replace(/\?.*$/, ''), - }; - // eslint-disable-next-line object-curly-newline, max-len - window.hlx.rum = { weight, id, random, isSelected, sampleRUM, sanitizeURL: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'] }; - } - const { weight, id } = window.hlx.rum; - if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { - const sendPing = (pdata = data) => { - // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define - const body = JSON.stringify({ weight, id, referer: window.hlx.rum.sanitizeURL(), checkpoint, ...data }); - const url = `https://rum.hlx.page/.rum/${weight}`; - // eslint-disable-next-line no-unused-expressions - navigator.sendBeacon(url, body); - // eslint-disable-next-line no-console - console.debug(`ping:${checkpoint}`, pdata); - }; - sampleRUM.cases = sampleRUM.cases || { - cwv: () => sampleRUM.cwv(data) || true, - lazy: () => { - // use classic script to avoid CORS issues - const script = document.createElement('script'); - script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; - document.head.appendChild(script); - return true; - }, - }; - sendPing(data); - if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } - } - if (sampleRUM.always[checkpoint]) { sampleRUM.always[checkpoint](data); } - } catch (error) { - // something went wrong - } -} - -/** - * Loads a CSS file. - * @param {string} href URL to the CSS file - */ -export async function loadCSS(href) { - return new Promise((resolve, reject) => { - if (!document.querySelector(`head > link[href="${href}"]`)) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = href; - link.onload = resolve; - link.onerror = reject; - document.head.append(link); - } else { - resolve(); - } - }); -} - -/** - * Loads a non module JS file. - * @param {string} src URL to the JS file - * @param {Object} attrs additional optional attributes - */ - -export async function loadScript(src, attrs) { - return new Promise((resolve, reject) => { - if (!document.querySelector(`head > script[src="${src}"]`)) { - const script = document.createElement('script'); - script.src = src; - if (attrs) { - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const attr in attrs) { - script.setAttribute(attr, attrs[attr]); - } - } - script.onload = resolve; - script.onerror = reject; - document.head.append(script); - } else { - resolve(); - } - }); -} - -/** - * Retrieves the content of metadata tags. - * @param {string} name The metadata name (or property) - * @returns {string} The metadata value(s) - */ -export function getMetadata(name) { - const attr = name && name.includes(':') ? 'property' : 'name'; - const meta = [...document.head.querySelectorAll(`meta[${attr}="${name}"]`)].map((m) => m.content).join(', '); - return meta || ''; -} - -/** - * Sanitizes a string for use as class name. - * @param {string} name The unsanitized string - * @returns {string} The class name - */ -export function toClassName(name) { - return typeof name === 'string' - ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') - : ''; -} - -/** - * Sanitizes a string for use as a js property name. - * @param {string} name The unsanitized string - * @returns {string} The camelCased name - */ -export function toCamelCase(name) { - return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); -} - -const ICONS_CACHE = {}; -/** - * Replace icons with inline SVG and prefix with codeBasePath. - * @param {Element} [element] Element containing icons - */ -export async function decorateIcons(element) { - // Prepare the inline sprite - let svgSprite = document.getElementById('franklin-svg-sprite'); - if (!svgSprite) { - const div = document.createElement('div'); - div.innerHTML = ''; - svgSprite = div.firstElementChild; - document.body.append(div.firstElementChild); - } - - // Download all new icons - const icons = [...element.querySelectorAll('span.icon')]; - await Promise.all(icons.map(async (span) => { - const iconName = Array.from(span.classList).find((c) => c.startsWith('icon-')).substring(5); - if (!ICONS_CACHE[iconName]) { - ICONS_CACHE[iconName] = true; - try { - const response = await fetch(`${window.hlx.iconsPath}/${iconName}.svg`); - if (!response.ok) { - ICONS_CACHE[iconName] = false; - return; - } - // Styled icons don't play nice with the sprite approach because of shadow dom isolation - // and same for internal references - const svg = await response.text(); - if (svg.match(/(