diff --git a/assets/icons/pause.svg b/assets/icons/pause.svg
new file mode 100644
index 0000000..5bb17c7
--- /dev/null
+++ b/assets/icons/pause.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/play.svg b/assets/icons/play.svg
new file mode 100644
index 0000000..c54977e
--- /dev/null
+++ b/assets/icons/play.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/banner/banner.component.yml b/src/components/banner/banner.component.yml
new file mode 100644
index 0000000..1c83fc0
--- /dev/null
+++ b/src/components/banner/banner.component.yml
@@ -0,0 +1,43 @@
+$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json
+
+name: Banner
+group: Components
+status: stable
+props:
+ type: object
+ required:
+ - banner__heading
+ - banner__text
+ properties:
+ banner__heading:
+ type: string
+ title: Title
+ description: 'Specifies the title of the banner'
+ data: 'THIS IS A BANNER HEADING'
+ banner__text:
+ type: string
+ title: Content
+ description: 'Specifies the main content of the banner'
+ data: '
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
'
+ banner__link:
+ type: string
+ title: Link Content
+ description: 'Specifies the text displayed on the link'
+ data: 'this is a link'
+ banner__image:
+ type: string
+ title: Image's source
+ description: The image's source of the banner.
+ data: 'https://picsum.photos/1480/360'
+ banner__video:
+ type: string
+ title: Video URL
+ description: 'Specifies the URL of the video to be displayed. Either this or banner__image should be provided.'
+ data:
+ - 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4'
+ - 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-webm-file.webm'
+ banner__alignment:
+ type: string
+ title: Alignment
+ description: 'Specifies the alignment of the content within the banner. Options include: left, center, right.'
+ data: 'left'
diff --git a/src/components/banner/banner.scss b/src/components/banner/banner.scss
new file mode 100644
index 0000000..a85a100
--- /dev/null
+++ b/src/components/banner/banner.scss
@@ -0,0 +1,123 @@
+@use '../../foundation/typography/typography' as *;
+
+@mixin overlay-darken($color: var(--color-primary-dark), $opacity: 0.75, $blend-mode: multiply) {
+ &::before {
+ content: '';
+ top: 0;
+ left: 0;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ opacity: $opacity;
+ display: block;
+ position: absolute;
+ pointer-events: none;
+ mix-blend-mode: $blend-mode;
+ background-color: $color;
+ }
+}
+
+.banner {
+ z-index: 1;
+ min-height: 16rem;
+ position: relative;
+}
+
+.banner__media {
+ @include overlay-darken;
+
+ & {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ background-size: cover;
+ z-index: 0;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+}
+
+.banner__video {
+ @include overlay-darken;
+
+ & {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ z-index: 0;
+ }
+
+ video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+}
+
+.banner__playback {
+ z-index: 5;
+ position: absolute;
+ bottom: var(--spacing-xl);
+ right: var(--spacing-xl);
+}
+
+.banner__content {
+ z-index: 2;
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ color: var(--color-white);
+ row-gap: var(--spacing-xl);
+ padding: var(--spacing-2xl) 0;
+
+ [data-banner-alignment='center'] & {
+ align-items: center;
+ }
+}
+
+.banner__heading {
+ @include h2;
+
+ & {
+ margin: 0;
+ }
+}
+
+.banner__text {
+ @include h5;
+
+ & {
+ font-weight: initial;
+ margin: 0;
+ }
+
+ p {
+ margin: 0 0 var(--spacing-lg);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.banner__links {
+ display: flex;
+ flex-direction: row;
+ row-gap: var(--spacing);
+}
+
+.banner-list {
+ display: flex;
+ flex-direction: column;
+ row-gap: var(--spacing-xl);
+ margin: var(--spacing-lg) 0;
+}
diff --git a/src/components/banner/banner.stories.js b/src/components/banner/banner.stories.js
new file mode 100644
index 0000000..400dde1
--- /dev/null
+++ b/src/components/banner/banner.stories.js
@@ -0,0 +1,66 @@
+import bannerTwig from './banner.twig';
+import { props } from './banner.component.yml';
+
+import '../video/playback';
+
+const bannerData = props.properties;
+
+export default {
+ title: 'Components/Banner',
+ argTypes: {
+ type: {
+ name: 'Media Type',
+ control: { type: 'select' },
+ options: {
+ Image: 'image',
+ Video: 'video',
+ },
+ },
+ alignment: {
+ name: 'Banner Alignment',
+ type: 'select',
+ options: {
+ 'Left': 'left',
+ 'Center': 'center',
+ },
+ },
+ heading: {
+ name: 'Banner Heading',
+ type: 'string',
+ },
+ text: {
+ name: 'Banner Text',
+ type: 'string',
+ },
+ link: {
+ name: 'Banner Link',
+ type: 'string',
+ },
+ },
+ args: {
+ type: 'image',
+ alignment: bannerData.banner__alignment.data,
+ heading: bannerData.banner__heading.data,
+ text: bannerData.banner__text.data,
+ link: bannerData.banner__link.data,
+ },
+};
+
+export const Banner = ({
+ type,
+ alignment,
+ heading,
+ text,
+ link,
+}) => bannerTwig({
+ banner__image: bannerData.banner__image.data,
+ banner__video: bannerData.banner__video.data,
+ banner__media: type,
+ banner__alignment: alignment,
+ banner__heading: heading,
+ banner__text: text,
+ banner__link_items: [{
+ url: '#',
+ content: link,
+ }]
+});
diff --git a/src/components/banner/banner.twig b/src/components/banner/banner.twig
new file mode 100644
index 0000000..4fc92dd
--- /dev/null
+++ b/src/components/banner/banner.twig
@@ -0,0 +1,114 @@
+{#
+ # Available Variables:
+ # - banner__heading: The title of the banner.
+ # - banner__text: The main content of the banner.
+ # - banner__link_items: links or buttons below the main content.
+ #
+ # - banner__image: Decorative background image.
+ # - banner__video: The URL of the video to be displayed. If provided, background_image will be ignored.
+ #
+ # - banner__alignment: Specifies the alignment of the content within the banner. Options include 'left', 'center'.
+ # - banner__variant: Specifies the style variant for the banner component (e.g., default, light, dark).
+ # - banner__attributes: HTML attributes for the banner container.
+ #
+ # Available Blocks:
+ # - banner_media
+ # - banner__media_caption
+#}
+
+{% set banner__base_class = 'banner' %}
+{% set banner__modifiers = banner__modifiers|default([]) %}
+{% set banner__attributes = banner__attributes|default({}) %}
+
+{% set banner__attributes = banner__attributes|merge({
+ 'class': bem(banner__base_class, banner__modifiers, banner__blockname),
+ 'data-banner-alignment': banner__alignment|default('left'),
+}) %}
+
+{% set banner_media %}
+
+ {% block banner__media %}
+ {% if banner__media == 'image' %}
+

+ {% else %}
+ {% set video_id = 'video-' ~ random('1234567890') ~ random('1234567890') ~ random('1234567890') ~ random('1234567890') %}
+
+ {% include "@components/video/video.twig" with {
+ video__type: 'html5',
+ video__urls: banner__video,
+ video__show_placeholder: false,
+ video__blockname: banner__base_class,
+ video_object__attributes: {
+ 'id': video_id,
+ 'preload': 'true',
+ 'muted': 'true',
+ 'playsinline': 'true',
+ 'webkit-playsinline': 'true',
+ 'aria-hidden': 'true',
+ 'loop': 'true',
+ 'autoplay': 'true',
+ },
+ } %}
+
+ {% include "@components/video/playback-button.twig" with {
+ playback__blockname: banner__base_class,
+ playback__video_id: video_id,
+ } %}
+ {% endif %}
+ {% endblock %}
+
+{% block banner__media_caption %}{% endblock %}
+{% endset %}
+
+{% set banner %}
+
+
+ {% if banner__heading %}
+ {% include "@components/typography/heading/heading.twig" with {
+ heading: banner__heading,
+ heading__level: banner__heading__level|default('4'),
+ heading__blockname: banner__base_class,
+ } %}
+ {% endif %}
+
+ {% if banner__text %}
+ {% include "@components/typography/text/text.twig" with {
+ text__content: banner__text,
+ text__blockname: banner__base_class,
+ } %}
+ {% endif %}
+
+ {% if banner__link_items|length > 0 %}
+
+ {% for item in banner__link_items %}
+ {% include "@components/button/button.twig" with {
+ button__element: 'a',
+ button__style: 'primary',
+ button__content: item.content,
+ button__attributes: {
+ 'href': item.url,
+ }
+ } %}
+ {% endfor %}
+
+ {% endif %}
+
+
+{% endset %}
+
+{# Render the banner within a generic container #}
+{% embed "@layout/container/container.twig" with {
+ container__component_width: banner__width|default('compressed'),
+ container__modifiers: banner__container_modifiers,
+ container__component_alignment: 'center',
+ container__theme: banner__theme|default('inverse'),
+}%}
+ {% block container__content %}
+ {{ banner }}
+ {% endblock %}
+
+ {% block container__suffix %}
+ {{ banner_media }}
+ {% endblock %}
+{% endembed %}
+
diff --git a/src/components/icons/_icon.twig b/src/components/icons/_icon.twig
index bd93c9b..2f2668b 100644
--- a/src/components/icons/_icon.twig
+++ b/src/components/icons/_icon.twig
@@ -10,7 +10,7 @@
* - icon__desc - a11y description
*/
#}
-{% set icon__base_class = icon_base_class|default('icon') %}
+{% set icon__base_class = icon__base_class|default('icon') %}
{% set icon__attributes = icon__attributes|default([]) %}
{# If `directory` is defined, set the path relative for Drupal.
# Otherwise, set the path relative to the Component Library. #}
diff --git a/src/components/icons/icons.md b/src/components/icons/icons.md
index e821a7e..b8bc85c 100644
--- a/src/components/icons/icons.md
+++ b/src/components/icons/icons.md
@@ -34,8 +34,8 @@ Complex (BEM classes):
```
{% include "@atoms/04-images/icons/_icon.twig" with {
- icon_base_class: 'toggle',
- icon_blockname: 'main-nav',
+ icon__base_class: 'toggle',
+ icon__blockname: 'main-nav',
icon__name: 'menu',
} %}
```
diff --git a/src/components/video/playback-button.twig b/src/components/video/playback-button.twig
new file mode 100644
index 0000000..f711eb8
--- /dev/null
+++ b/src/components/video/playback-button.twig
@@ -0,0 +1,37 @@
+{#
+ # Available variables:
+ # - playback__base_class - the base classname
+ # - playback__video_id - id from video tag
+ # - playback__modifiers
+ # - playback__blockname
+ #
+ # Available blocks:
+ # - none
+#}
+
+{% set playback__base_class = playback__base_class|default('playback') %}
+{% set playback__attributes = playback__attributes|default({}) %}
+{% set playback__modifiers = playback__modifiers|default([]) %}
+
+{% set playback__attributes = playback__attributes|merge({
+ 'class': bem(playback__base_class, playback__modifiers, playback__blockname),
+ 'data-video-id': playback__video_id,
+}) %}
+
+
+
+
diff --git a/src/components/video/playback.js b/src/components/video/playback.js
new file mode 100644
index 0000000..8aaf0c4
--- /dev/null
+++ b/src/components/video/playback.js
@@ -0,0 +1,113 @@
+Drupal.behaviors.playbackButtons = {
+ attach(context) {
+ const buttons = context.querySelectorAll('.playback__button');
+
+ /**
+ * fetchButtonRefs
+ *
+ * @description Returns references to playback button elements.
+ * @param {HTMLElement} button button element.
+ * @return {Object} References to label, button, video, pause, and play elements.
+ */
+ function fetchButtonRefs(button) {
+ const { videoId } = button.parentElement.dataset;
+
+ return {
+ button,
+ video: context.querySelector(`#${videoId}`),
+ label: button.querySelector('.playback__label'),
+ pause: button.querySelector('.playback__pause'),
+ play: button.querySelector('.playback__play'),
+ };
+ }
+
+ /**
+ * playVideo
+ *
+ * @description Starts video playback and updates UI to reflect the playing state.
+ * @param {Object} refs Object containing element references.
+ * @return {Promise} Resolves when the video starts playing or rejects with an error.
+ */
+ function playVideo(refs) {
+ return new Promise((resolve, reject) => {
+ refs.video
+ .play()
+ .then(() => {
+ refs.pause.classList.remove('visually-hidden');
+ refs.play.classList.add('visually-hidden');
+ refs.label.textContent = 'Pause video';
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ }
+
+ /**
+ * pauseVideo
+ *
+ * @description Pauses video playback and updates UI to reflect the paused state.
+ * @param {Object} refs Object containing element references.
+ */
+ function pauseVideo(refs) {
+ refs.play.classList.remove('visually-hidden');
+ refs.pause.classList.add('visually-hidden');
+ refs.label.textContent = 'Play video';
+ refs.video.pause();
+ }
+
+ /**
+ * toggleVideo
+ *
+ * @description Toggles video playback state and updates UI accordingly.
+ * @param {Object} refs Object containing element references.
+ */
+ function toggleVideo(refs) {
+ if (refs.video.paused) {
+ playVideo(refs);
+ } else {
+ pauseVideo(refs);
+ }
+ }
+
+ /**
+ * setInitialState
+ *
+ * @description Sets the initial state for a video element and performs associated actions.
+ * @param {Object} refs Object containing element references.
+ * @throws {DOMException} - If an error occurs while attempting to play the video.
+ */
+ async function setInitialState(refs) {
+ const reduceMotion = window.matchMedia(
+ '(prefers-reduced-motion: reduce)',
+ );
+
+ // Check if the user disabled 'prefer reduced motion' on their system.
+ if (!reduceMotion.matches) {
+ // Check if the user's browser allows video autoplay.
+ try {
+ await playVideo(refs);
+ refs.actions[0].classList.add('hidden');
+ } catch (err) {
+ if (err.name === 'NotAllowedError') {
+ pauseVideo(refs);
+ }
+ }
+ } else {
+ pauseVideo(refs);
+ }
+ }
+
+ buttons?.forEach((button) => {
+ const refs = fetchButtonRefs(button);
+
+ console.log(refs);
+
+ if (refs.button && refs.video) {
+ refs.button.addEventListener('click', () => toggleVideo(refs));
+ setInitialState(refs);
+ }
+ });
+ },
+};
diff --git a/src/components/video/video.scss b/src/components/video/video.scss
index e49e1f9..e5a4314 100644
--- a/src/components/video/video.scss
+++ b/src/components/video/video.scss
@@ -16,3 +16,17 @@
overflow: hidden;
position: relative;
}
+
+.playback__button {
+ cursor: pointer;
+ border-radius: 50%;
+ width: var(--spacing-xl);
+ height: var(--spacing-xl);
+ border: 3px solid var(--color-white);
+ background-color: var(--color-primary-lighter);
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+}