From 4bbf2e6a49aeaf850a60b4a6838bd8c95cf8ad28 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 10:51:22 +0200 Subject: [PATCH 01/26] feat: add basic settings to footer component --- component-design-tokens.json | 326 +++++++++++++- source/components/footer/component.json | 331 +++++++++++++- .../components/footer/examples/examples.json | 38 +- .../lorem-columns-with-subfooter.blade.php | 107 +++++ source/components/footer/style.scss | 412 ++++++++++++------ 5 files changed, 1057 insertions(+), 157 deletions(-) create mode 100644 source/components/footer/examples/lorem-columns-with-subfooter.blade.php diff --git a/component-design-tokens.json b/component-design-tokens.json index d27aeb4b7..62927e416 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -863,17 +863,62 @@ "name": "Footer", "slug": "footer", "tokens": [ + "base", "space", + "outer-space", "border-width", "color--surface", - "color--surface-contrast" + "color--surface-contrast", + "color--surface-border", + "color--surface-alt", + "color--surface-contrast-muted", + "color--primary", + "color--primary-contrast", + "color--primary-border", + "color--primary-alt", + "color--secondary", + "color--secondary-contrast", + "color--secondary-border", + "color--secondary-alt", + "color--background", + "color--background-contrast", + "color--background-border", + "color--background-alt", + "color--background-contrast-muted", + "font-size-90", + "font-size-200", + "font-size-300" ], "componentSettings": [ + { + "id": "colors", + "label": "Colors", + "description": "Component-local footer color scheme.", + "settings": [ + { + "label": "Color Scheme", + "description": "Controls the footer background, text contrast, border color, and alternate subfooter background.", + "token": "color--surface", + "includeStateColors": false + } + ] + }, { "id": "branding", "label": "Branding", "description": "Component-local branding adjustments.", "settings": [ + { + "variable": "--logotype-height-multiplier", + "label": "Logotype Height Multiplier", + "description": "Scales the main footer logotype height locally without changing the global base size.", + "type": "range", + "default": "6", + "min": 2, + "max": 12, + "step": 0.25, + "unit": "" + }, { "variable": "--home-link-height-multiplier", "label": "Home Link Height Multiplier", @@ -884,6 +929,285 @@ "max": 3, "step": 0.05, "unit": "" + }, + { + "variable": "--subfooter-logotype-height-multiplier", + "label": "Subfooter Logotype Height Multiplier", + "description": "Scales the subfooter logotype height locally without changing the global base size.", + "type": "range", + "default": "6", + "min": 2, + "max": 12, + "step": 0.25, + "unit": "" + } + ] + }, + { + "id": "layout", + "label": "Layout", + "description": "Component-local footer spacing, columns, and alignment.", + "settings": [ + { + "variable": "--inset-multiplier", + "label": "Outer Padding Inset Multiplier", + "description": "Scales footer outer padding from the inherited inset rhythm.", + "type": "range", + "default": "1", + "min": 0, + "max": 4, + "step": 0.125, + "unit": "" + }, + { + "variable": "--columns-count", + "label": "Number of Columns", + "description": "How many columns to display in the footer.", + "type": "select", + "default": "3", + "options": [ + { + "value": "1", + "label": "1" + }, + { + "value": "2", + "label": "2" + }, + { + "value": "3", + "label": "3" + }, + { + "value": "4", + "label": "4" + } + ] + }, + { + "variable": "--logotype-justify-content", + "label": "Logotype Alignment", + "description": "How to align the footer logotype.", + "type": "select", + "default": "flex-start", + "options": [ + { + "value": "flex-start", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "Right" + } + ] + }, + { + "variable": "--text-align", + "label": "Text Alignment", + "description": "How to align text in the footer columns.", + "type": "select", + "default": "left", + "options": [ + { + "value": "left", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right", + "label": "Right" + } + ] + }, + { + "variable": "--prefooter-text-align", + "label": "Prefooter Text Alignment", + "description": "How to align text in the prefooter.", + "type": "select", + "default": "left", + "options": [ + { + "value": "left", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right", + "label": "Right" + } + ] + } + ] + }, + { + "id": "background", + "label": "Background Image", + "description": "Component-local background image controls.", + "settings": [ + { + "variable": "--background-position", + "label": "Background Position", + "description": "Controls the CSS background-position value.", + "type": "select", + "default": "center", + "options": [ + { + "value": "left top", + "label": "Left Top" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right bottom", + "label": "Right Bottom" + } + ] + }, + { + "variable": "--background-repeat", + "label": "Background Repeat", + "description": "Controls the CSS background-repeat value.", + "type": "select", + "default": "no-repeat", + "options": [ + { + "value": "no-repeat", + "label": "No Repeat" + }, + { + "value": "repeat", + "label": "Repeat" + }, + { + "value": "repeat-x", + "label": "Repeat X" + }, + { + "value": "repeat-y", + "label": "Repeat Y" + } + ] + }, + { + "variable": "--background-size", + "label": "Background Size", + "description": "Controls the CSS background-size value.", + "type": "select", + "default": "cover", + "options": [ + { + "value": "cover", + "label": "Cover" + }, + { + "value": "contain", + "label": "Contain" + }, + { + "value": "auto", + "label": "Auto" + } + ] + }, + { + "variable": "--background-attachment", + "label": "Background Attachment", + "description": "Controls the CSS background-attachment value.", + "type": "select", + "default": "scroll", + "options": [ + { + "value": "scroll", + "label": "Scroll" + }, + { + "value": "fixed", + "label": "Fixed" + }, + { + "value": "local", + "label": "Local" + } + ] + }, + { + "variable": "--background-origin", + "label": "Background Origin", + "description": "Controls the CSS background-origin value.", + "type": "select", + "default": "padding-box", + "options": [ + { + "value": "border-box", + "label": "Border Box" + }, + { + "value": "padding-box", + "label": "Padding Box" + }, + { + "value": "content-box", + "label": "Content Box" + } + ] + }, + { + "variable": "--background-clip", + "label": "Background Clip", + "description": "Controls the CSS background-clip value.", + "type": "select", + "default": "border-box", + "options": [ + { + "value": "border-box", + "label": "Border Box" + }, + { + "value": "padding-box", + "label": "Padding Box" + }, + { + "value": "content-box", + "label": "Content Box" + } + ] + }, + { + "variable": "--background-blend-mode", + "label": "Background Blend Mode", + "description": "Controls the CSS background-blend-mode value.", + "type": "select", + "default": "normal", + "options": [ + { + "value": "normal", + "label": "Normal" + }, + { + "value": "multiply", + "label": "Multiply" + }, + { + "value": "screen", + "label": "Screen" + }, + { + "value": "overlay", + "label": "Overlay" + } + ] } ] } diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 5d09ab056..70d6f212b 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -3,13 +3,63 @@ "apiVersion": 1, "name": "Footer", "slug": "footer", - "tokens": ["space", "border-width", "color--surface", "color--surface-contrast"], + "tokens": [ + "base", + "space", + "outer-space", + "border-width", + "color--surface", + "color--surface-contrast", + "color--surface-border", + "color--surface-alt", + "color--surface-contrast-muted", + "color--primary", + "color--primary-contrast", + "color--primary-border", + "color--primary-alt", + "color--secondary", + "color--secondary-contrast", + "color--secondary-border", + "color--secondary-alt", + "color--background", + "color--background-contrast", + "color--background-border", + "color--background-alt", + "color--background-contrast-muted", + "font-size-90", + "font-size-200", + "font-size-300" + ], "componentSettings": [ + { + "id": "colors", + "label": "Colors", + "description": "Component-local footer color scheme.", + "settings": [ + { + "label": "Color Scheme", + "description": "Controls the footer background, text contrast, border color, and alternate subfooter background.", + "token": "color--surface", + "includeStateColors": false + } + ] + }, { "id": "branding", "label": "Branding", "description": "Component-local branding adjustments.", "settings": [ + { + "variable": "--logotype-height-multiplier", + "label": "Logotype Height Multiplier", + "description": "Scales the main footer logotype height locally without changing the global base size.", + "type": "range", + "default": "6", + "min": 2, + "max": 12, + "step": 0.25, + "unit": "" + }, { "variable": "--home-link-height-multiplier", "label": "Home Link Height Multiplier", @@ -20,6 +70,285 @@ "max": 3, "step": 0.05, "unit": "" + }, + { + "variable": "--subfooter-logotype-height-multiplier", + "label": "Subfooter Logotype Height Multiplier", + "description": "Scales the subfooter logotype height locally without changing the global base size.", + "type": "range", + "default": "6", + "min": 2, + "max": 12, + "step": 0.25, + "unit": "" + } + ] + }, + { + "id": "layout", + "label": "Layout", + "description": "Component-local footer spacing, columns, and alignment.", + "settings": [ + { + "variable": "--inset-multiplier", + "label": "Outer Padding Inset Multiplier", + "description": "Scales footer outer padding from the inherited inset rhythm.", + "type": "range", + "default": "1", + "min": 0, + "max": 4, + "step": 0.125, + "unit": "" + }, + { + "variable": "--columns-count", + "label": "Number of Columns", + "description": "How many columns to display in the footer.", + "type": "select", + "default": "3", + "options": [ + { + "value": "1", + "label": "1" + }, + { + "value": "2", + "label": "2" + }, + { + "value": "3", + "label": "3" + }, + { + "value": "4", + "label": "4" + } + ] + }, + { + "variable": "--logotype-justify-content", + "label": "Logotype Alignment", + "description": "How to align the footer logotype.", + "type": "select", + "default": "flex-start", + "options": [ + { + "value": "flex-start", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "Right" + } + ] + }, + { + "variable": "--text-align", + "label": "Text Alignment", + "description": "How to align text in the footer columns.", + "type": "select", + "default": "left", + "options": [ + { + "value": "left", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right", + "label": "Right" + } + ] + }, + { + "variable": "--prefooter-text-align", + "label": "Prefooter Text Alignment", + "description": "How to align text in the prefooter.", + "type": "select", + "default": "left", + "options": [ + { + "value": "left", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right", + "label": "Right" + } + ] + } + ] + }, + { + "id": "background", + "label": "Background Image", + "description": "Component-local background image controls.", + "settings": [ + { + "variable": "--background-position", + "label": "Background Position", + "description": "Controls the CSS background-position value.", + "type": "select", + "default": "center", + "options": [ + { + "value": "left top", + "label": "Left Top" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "right bottom", + "label": "Right Bottom" + } + ] + }, + { + "variable": "--background-repeat", + "label": "Background Repeat", + "description": "Controls the CSS background-repeat value.", + "type": "select", + "default": "no-repeat", + "options": [ + { + "value": "no-repeat", + "label": "No Repeat" + }, + { + "value": "repeat", + "label": "Repeat" + }, + { + "value": "repeat-x", + "label": "Repeat X" + }, + { + "value": "repeat-y", + "label": "Repeat Y" + } + ] + }, + { + "variable": "--background-size", + "label": "Background Size", + "description": "Controls the CSS background-size value.", + "type": "select", + "default": "cover", + "options": [ + { + "value": "cover", + "label": "Cover" + }, + { + "value": "contain", + "label": "Contain" + }, + { + "value": "auto", + "label": "Auto" + } + ] + }, + { + "variable": "--background-attachment", + "label": "Background Attachment", + "description": "Controls the CSS background-attachment value.", + "type": "select", + "default": "scroll", + "options": [ + { + "value": "scroll", + "label": "Scroll" + }, + { + "value": "fixed", + "label": "Fixed" + }, + { + "value": "local", + "label": "Local" + } + ] + }, + { + "variable": "--background-origin", + "label": "Background Origin", + "description": "Controls the CSS background-origin value.", + "type": "select", + "default": "padding-box", + "options": [ + { + "value": "border-box", + "label": "Border Box" + }, + { + "value": "padding-box", + "label": "Padding Box" + }, + { + "value": "content-box", + "label": "Content Box" + } + ] + }, + { + "variable": "--background-clip", + "label": "Background Clip", + "description": "Controls the CSS background-clip value.", + "type": "select", + "default": "border-box", + "options": [ + { + "value": "border-box", + "label": "Border Box" + }, + { + "value": "padding-box", + "label": "Padding Box" + }, + { + "value": "content-box", + "label": "Content Box" + } + ] + }, + { + "variable": "--background-blend-mode", + "label": "Background Blend Mode", + "description": "Controls the CSS background-blend-mode value.", + "type": "select", + "default": "normal", + "options": [ + { + "value": "normal", + "label": "Normal" + }, + { + "value": "multiply", + "label": "Multiply" + }, + { + "value": "screen", + "label": "Screen" + }, + { + "value": "overlay", + "label": "Overlay" + } + ] } ] } diff --git a/source/components/footer/examples/examples.json b/source/components/footer/examples/examples.json index 1e371cf23..49bbb532f 100644 --- a/source/components/footer/examples/examples.json +++ b/source/components/footer/examples/examples.json @@ -1,19 +1,23 @@ { - "footer": { - "grid": "", - "heading": "Footer", - "text": "Renders a footer in default mode with a logotype and grouped navigation links." - }, - "slot-only": { - "heading": "Footer with slot content", - "text": "Renders the footer in slot-only mode, where arbitrary content is placed inside the component tags instead of using the built-in links structure." - }, - "with-subfooter": { - "heading": "Footer with subfooter", - "text": "Renders the footer with a subfooter bar containing a list of contact items, demonstrating the subfooter section with horizontal layout." - }, - "with-prefooter": { - "heading": "Footer with prefooter and postfooter", - "text": "Renders the footer in slot-only mode with prefooter content placed above and postfooter content placed below the main slot area." - } + "footer": { + "grid": "", + "heading": "Footer", + "text": "Renders a footer in default mode with a logotype and grouped navigation links." + }, + "lorem-columns-with-subfooter": { + "heading": "Footer with three columns and subfooter", + "text": "Renders a slot-only footer with a three-column content area and a horizontal subfooter link list, using lorem ipsum placeholder content." + }, + "slot-only": { + "heading": "Footer with slot content", + "text": "Renders the footer in slot-only mode, where arbitrary content is placed inside the component tags instead of using the built-in links structure." + }, + "with-subfooter": { + "heading": "Footer with subfooter", + "text": "Renders the footer with a subfooter bar containing a list of contact items, demonstrating the subfooter section with horizontal layout." + }, + "with-prefooter": { + "heading": "Footer with prefooter and postfooter", + "text": "Renders the footer in slot-only mode with prefooter content placed above and postfooter content placed below the main slot area." + } } diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php new file mode 100644 index 000000000..9d18284f9 --- /dev/null +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -0,0 +1,107 @@ +@footer([ + 'slotOnly' => true, + 'id' => 'site-footer', + 'classList' => ['site-footer', 's-footer'], + 'logotype' => '', + 'subfooter' => [ + 'flexDirection' => 'row', + 'alignment' => 'center', + 'content' => [ + [ + 'title' => '', + 'content' => 'Lorem ipsum', + 'link' => '#lorem-ipsum', + ], + [ + 'title' => '', + 'content' => 'Dolor sit amet', + 'link' => '#dolor-sit-amet', + ], + [ + 'title' => '', + 'content' => 'Consectetur adipiscing', + 'link' => '#consectetur-adipiscing', + ], + ], + ], +]) +
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+@endfooter diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 4c27f71e0..f59095f49 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -1,3 +1,4 @@ +@use "../../sass/mixin/inset-multiplier" as insetMultiplier; @use "../../sass/mixin/mq"; @use "../../sass/mixin/tokens"; @@ -5,31 +6,182 @@ $_: "c-footer"; /* Design tokens */ +@include insetMultiplier.property(); @include tokens.create($_, getComponentTokens($_)); /* Component */ .#{$_} { - --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, 6) * var(--base)); - --#{$_}--home-link-height: calc(var(--#{$_}--home-link-height-multiplier, 1) * 60px); - --#{$_}--header-border-color: var(--c-footer-header-border-color, rgba(0, 0, 0, 0.55)); - --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, 6) * var(--base)); - --#{$_}--subfooter-color-background: var(--c-footer-subfooter-color-background, var(--color--white-100, #fff)); - --#{$_}--subfooter-color-text: var(--c-footer-subfooter-color-text, var(--color--black-100, #000)); + @include insetMultiplier.consume($_, var(--#{$_}--inset-multiplier, var(--inherit-inset-multiplier, 1))); + + --#{$_}--background-color: #{tokens.getRawValue($_, "color--surface")}; + --#{$_}--color: #{tokens.getRawValue($_, "color--surface-contrast")}; + --#{$_}--color-muted: #{tokens.getRawValue($_, "color--surface-contrast-muted")}; + --#{$_}--border-color: var(--c-footer-header-border-color, #{tokens.getRawValue($_, "color--surface-border")}); + --#{$_}--subfooter-background-color: var(--c-footer-subfooter-color-background, #{tokens.getRawValue($_, "color--surface-alt")}); + --#{$_}--subfooter-color: var(--c-footer-subfooter-color-text, #{tokens.getRawValue($_, "color--surface-contrast")}); + --#{$_}--subfooter-separator-color: var(--c-footer-subfooter-color-separator, #{tokens.getRawValue($_, "color--surface-border")}); + --#{$_}--background-image: none; + --#{$_}--background-position: center; + --#{$_}--background-repeat: no-repeat; + --#{$_}--background-size: cover; + --#{$_}--background-attachment: scroll; + --#{$_}--background-origin: padding-box; + --#{$_}--background-clip: border-box; + --#{$_}--background-blend-mode: normal; + --#{$_}--main-padding-y: #{tokens.getCalculatedValue($_, "outer-space", 3)}; + --#{$_}--main-padding-x: var(--#{$_}--inset-padding-x); + --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; + --#{$_}--body-padding-x: 0; + --#{$_}--column-gap: #{tokens.getCalculatedValue($_, "space", 2)}; + --#{$_}--columns-count: 3; + --#{$_}--text-align: left; + --#{$_}--prefooter-text-align: left; + --#{$_}--logotype-justify-content: flex-start; + --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, var(--#{$_}--logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); + --#{$_}--home-link-height: calc(var(--#{$_}--home-link-height-multiplier, 1) * #{tokens.getCalculatedValue($_, "base", 7.5)}); + --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, var(--#{$_}--subfooter-logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-alignment: var(--c-footer-subfooter-alignment, center); --#{$_}--subfooter-flex-direction: var(--c-footer-subfooter-flex-direction, row); - --#{$_}--subfooter-color-separator: var(--c-footer-subfooter-color-separator, #a3a3a3); - background-color: tokens.getRawValue($_, "color--surface"); - color: tokens.getRawValue($_, "color--surface-contrast"); + --#{$_}--subfooter-list-margin-block: #{tokens.getCalculatedValue($_, "space", -1)}; + --#{$_}--subfooter-list-margin-inline: #{tokens.getCalculatedValue($_, "space", -5)}; + --#{$_}--subfooter-list-margin-left: 0; + --#{$_}--subfooter-list-margin-right: var(--#{$_}--subfooter-list-margin-inline); + --#{$_}--subfooter-item-padding-block: #{tokens.getCalculatedValue($_, "space")}; + --#{$_}--subfooter-item-padding-inline: #{tokens.getCalculatedValue($_, "space", 5)}; + --#{$_}--subfooter-item-padding-left: 0; + --#{$_}--subfooter-item-padding-right: var(--#{$_}--subfooter-item-padding-inline); + --#{$_}--subfooter-separator-size: #{tokens.getCalculatedValue($_, "base", 0.75)}; + --#{$_}--subfooter-separator-left: #{tokens.getCalculatedValue($_, "base", -2.875)}; + --#{$_}--subfooter-separator-right: auto; + --#{$_}--subfooter-separator-display: none; + --#{$_}--subfooter-separator-display-between: block; + --#{$_}--subfooter-separator-display-before-last: none; + --inherit-color-background: var(--#{$_}--background-color); + --inherit-color-contrast: var(--#{$_}--color); + position: relative; + background-color: var(--#{$_}--background-color); + background-image: var(--#{$_}--background-image); + background-position: var(--#{$_}--background-position); + background-repeat: var(--#{$_}--background-repeat); + background-size: var(--#{$_}--background-size); + background-attachment: var(--#{$_}--background-attachment); + background-origin: var(--#{$_}--background-origin); + background-clip: var(--#{$_}--background-clip); + background-blend-mode: var(--#{$_}--background-blend-mode); + color: var(--#{$_}--color); a, a:hover, a:visited, a:active { - color: tokens.getRawValue($_, "color--surface-contrast"); + color: var(--#{$_}--color); + } + + &--primary { + --#{$_}--background-color: #{tokens.getRawValue($_, "color--primary")}; + --#{$_}--color: #{tokens.getRawValue($_, "color--primary-contrast")}; + --#{$_}--color-muted: #{tokens.getRawValue($_, "color--primary-contrast")}; + --#{$_}--border-color: #{tokens.getRawValue($_, "color--primary-border")}; + --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--primary-alt")}; + --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--primary-contrast")}; + --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--primary-border")}; + } + + &--secondary { + --#{$_}--background-color: #{tokens.getRawValue($_, "color--secondary")}; + --#{$_}--color: #{tokens.getRawValue($_, "color--secondary-contrast")}; + --#{$_}--color-muted: #{tokens.getRawValue($_, "color--secondary-contrast")}; + --#{$_}--border-color: #{tokens.getRawValue($_, "color--secondary-border")}; + --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--secondary-alt")}; + --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--secondary-contrast")}; + --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--secondary-border")}; + } + + &--background { + --#{$_}--background-color: #{tokens.getRawValue($_, "color--background")}; + --#{$_}--color: #{tokens.getRawValue($_, "color--background-contrast")}; + --#{$_}--color-muted: #{tokens.getRawValue($_, "color--background-contrast-muted")}; + --#{$_}--border-color: #{tokens.getRawValue($_, "color--background-border")}; + --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--background-alt")}; + --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--background-contrast")}; + --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--background-border")}; + } + + &--logotype-align__left, + &--logotype-left { + --#{$_}--logotype-justify-content: flex-start; + } + + &--logotype-align__center, + &--logotype-center { + --#{$_}--logotype-justify-content: center; + } + + &--logotype-align__right, + &--logotype-right { + --#{$_}--logotype-justify-content: flex-end; + } + + &--text-align__left, + &--text-left { + --#{$_}--text-align: left; + } + + &--text-align__center, + &--text-center { + --#{$_}--text-align: center; + } + + &--text-align__right, + &--text-right { + --#{$_}--text-align: right; + } + + &--prefooter-text-align__left, + &--prefooter-text-left { + --#{$_}--prefooter-text-align: left; + } + + &--prefooter-text-align__center, + &--prefooter-text-center { + --#{$_}--prefooter-text-align: center; + } + + &--prefooter-text-align__right, + &--prefooter-text-right { + --#{$_}--prefooter-text-align: right; + } + + &--columns-1 { + --#{$_}--columns-count: 1; + } + + &--columns-2 { + --#{$_}--columns-count: 2; + } + + &--columns-3 { + --#{$_}--columns-count: 3; + } + + &--columns-4 { + --#{$_}--columns-count: 4; } &__main-wrapper { - padding: tokens.getCalculatedValue($_, "space", 3) 0; + padding: var(--#{$_}--main-padding-y) var(--#{$_}--main-padding-x); + } + + &__main-wrapper > .o-container > .o-grid { + display: grid; + grid-template-columns: repeat(var(--#{$_}--columns-count), minmax(0, 1fr)); + gap: var(--#{$_}--column-gap); + + > [class*="o-grid-"] { + flex: initial; + max-width: none; + width: auto; + } } &__body { @@ -37,13 +189,23 @@ $_: "c-footer"; max-width: var(--container-width); margin-left: auto; margin-right: auto; - padding: tokens.getCalculatedValue($_, "space", 5) 0; + padding: var(--#{$_}--body-padding-y) var(--#{$_}--body-padding-x); display: flex; flex-wrap: nowrap; + text-align: var(--#{$_}--text-align); + } + + &__header-wrapper { + margin-bottom: tokens.getCalculatedValue($_, "space", 3); + + .o-grid-12 { + display: flex; + justify-content: var(--#{$_}--logotype-justify-content); + } } &__home-link { - margin-right: 5%; + margin-right: tokens.getCalculatedValue($_, "outer-space", 2); line-height: 0; height: var(--#{$_}--home-link-height); @@ -56,27 +218,22 @@ $_: "c-footer"; &__links { margin-bottom: tokens.getCalculatedValue($_, "space", 3); - & h4 { + h4 { font-weight: var(--h4-font-weight); - font-size: 18px; + font-size: tokens.getRawValue($_, "font-size-200"); } - & a { - color: #707070; + a, + div { + color: var(--#{$_}--color-muted); text-decoration: none; display: inline-block; } } - &__links div { - color: #707070; - text-decoration: none; - display: inline-block; - } - &__link-divider { display: inline-block; - background-color: #565656; + background-color: var(--#{$_}--border-color); border-radius: 50%; height: tokens.getCalculatedValue($_, "space", 0.5); width: tokens.getCalculatedValue($_, "space", 0.5); @@ -86,7 +243,8 @@ $_: "c-footer"; } &__widget-area { - gap: tokens.getCalculatedValue($_, "space", 2); + gap: var(--#{$_}--column-gap); + text-align: var(--#{$_}--text-align); .footer-title { margin-bottom: tokens.getCalculatedValue($_, "space", 2); @@ -97,30 +255,16 @@ $_: "c-footer"; } h2 { - --inherit-font-size: var(--font-size-300); + --inherit-font-size: #{tokens.getRawValue($_, "font-size-300")}; } } - // Logo - &__logotype { height: var(--#{$_}--logotype-height); - - @include mq.mq("xs") { - height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(#{var(--#{$_}--logotype-height)} * 0.75), #{var(--#{$_}--logotype-height)}); - } - - @include mq.mq("md") { - height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(#{var(--#{$_}--logotype-height)} * 0.75), #{var(--#{$_}--logotype-height)}); - } - - @include mq.mq("lg") { - height: var(--#{$_}--logotype-height); - } } - &__header-wrapper { - margin-bottom: tokens.getCalculatedValue($_, "space", 3); + &__prefooter-wrapper { + text-align: var(--#{$_}--prefooter-text-align); } &__prefooter-wrapper, @@ -135,23 +279,29 @@ $_: "c-footer"; } &--header-border { - .#{$_}__header-wrapper { - border-bottom: tokens.getCalculatedValue($_, "border-width") solid var(--#{$_}--header-border-color); - padding-bottom: tokens.getCalculatedValue($_, "space", 3); - } + --#{$_}--header-border-style: solid; + } + + &__header-wrapper { + border-bottom: tokens.getCalculatedValue($_, "border-width") var(--#{$_}--header-border-style, none) var(--#{$_}--border-color); + padding-bottom: var(--#{$_}--header-border-padding-bottom, 0); + } + + &--header-border { + --#{$_}--header-border-padding-bottom: #{tokens.getCalculatedValue($_, "space", 3)}; } &__subfooter { - color: var(--#{$_}--subfooter-color-text); - background: var(--#{$_}--subfooter-color-background); + color: var(--#{$_}--subfooter-color); + background: var(--#{$_}--subfooter-background-color); padding: tokens.getCalculatedValue($_, "space", 3) 0; - font-size: .9rem; + font-size: tokens.getRawValue($_, "font-size-90"); a, a:hover, a:visited, a:active { - color: var(--#{$_}--subfooter-color-text); + color: var(--#{$_}--subfooter-color); } &__wrapper { @@ -160,6 +310,7 @@ $_: "c-footer"; justify-content: var(--#{$_}--subfooter-alignment); align-items: var(--#{$_}--subfooter-alignment); flex-direction: var(--#{$_}--subfooter-flex-direction); + overflow: hidden; } &__list { @@ -168,8 +319,9 @@ $_: "c-footer"; justify-content: var(--#{$_}--subfooter-alignment); align-items: var(--#{$_}--subfooter-alignment); flex-direction: var(--#{$_}--subfooter-flex-direction); - margin: tokens.getCalculatedValue($_, "space", -1) tokens.getCalculatedValue($_, "space", -5); - margin-left: 0; + margin: var(--#{$_}--subfooter-list-margin-block) var(--#{$_}--subfooter-list-margin-inline); + margin-left: var(--#{$_}--subfooter-list-margin-left); + margin-right: var(--#{$_}--subfooter-list-margin-right); padding: 0; list-style: none; overflow: hidden; @@ -178,105 +330,118 @@ $_: "c-footer"; li { position: relative; margin: 0; - padding: tokens.getCalculatedValue($_, "space") tokens.getCalculatedValue($_, "space", 5); - padding-left: 0; + padding: var(--#{$_}--subfooter-item-padding-block) var(--#{$_}--subfooter-item-padding-inline); + padding-left: var(--#{$_}--subfooter-item-padding-left); + padding-right: var(--#{$_}--subfooter-item-padding-right); line-height: 1; &:before { content: ""; border-radius: 50%; - background: var(--#{$_}--subfooter-color-separator); - display: none; + background: var(--#{$_}--subfooter-separator-color); + display: var(--#{$_}--subfooter-separator-display); position: absolute; - width: 6px; - height: 6px; - left: -23px; + width: var(--#{$_}--subfooter-separator-size); + height: var(--#{$_}--subfooter-separator-size); + left: var(--#{$_}--subfooter-separator-left); + right: var(--#{$_}--subfooter-separator-right); top: 50%; transform: translateY(-50%); } &:not(:first-child):before { - display: block; + display: var(--#{$_}--subfooter-separator-display-between); + } + + &:not(:last-child):before { + display: var(--#{$_}--subfooter-separator-display-before-last); } } } - // Logo - &__logotype { height: var(--#{$_}--subfooter-logotype-height); } - // Fix for separator dots showing on new lines + &__logotype-wrapper { + margin-right: 0; + margin-bottom: tokens.getCalculatedValue($_, "space", 3); + } + + &--align-flex-start { + --#{$_}--subfooter-alignment: flex-start; + } + + &--align-center { + --#{$_}--subfooter-alignment: center; + } &--align-flex-end { - .#{$_}__subfooter__list { - margin: tokens.getCalculatedValue($_, "space", -1) tokens.getCalculatedValue($_, "space", -5); - margin-right: 0; - - li { - padding: tokens.getCalculatedValue($_, "space") tokens.getCalculatedValue($_, "space", 5); - padding-right: 0; - - &:before { - display: none; - } - - &:not(:last-child):before { - display: block; - left: auto; - right: -23px; - } - } - } + --#{$_}--subfooter-alignment: flex-end; + --#{$_}--subfooter-list-margin-left: var(--#{$_}--subfooter-list-margin-inline); + --#{$_}--subfooter-list-margin-right: 0; + --#{$_}--subfooter-item-padding-left: var(--#{$_}--subfooter-item-padding-inline); + --#{$_}--subfooter-item-padding-right: 0; + --#{$_}--subfooter-separator-left: auto; + --#{$_}--subfooter-separator-right: #{tokens.getCalculatedValue($_, "base", -2.875)}; + --#{$_}--subfooter-separator-display-between: none; + --#{$_}--subfooter-separator-display-before-last: block; } &--horizontal { - .#{$_}__subfooter__wrapper { - align-items: center; - } - - .#{$_}__subfooter__logotype-wrapper { - margin-right: tokens.getCalculatedValue($_, "space", 7); - margin-bottom: 0; - } + --#{$_}--subfooter-flex-direction: row; + --#{$_}--subfooter-alignment: center; + --#{$_}--subfooter-logotype-wrapper-margin-right: #{tokens.getCalculatedValue($_, "space", 7)}; + --#{$_}--subfooter-logotype-wrapper-margin-bottom: 0; } &--vertical { - .#{$_}__subfooter__list { - - li:not(:first-child):before, - li:not(:last-child):before { - display: none; - } - } - - .#{$_}__subfooter__logotype-wrapper { - margin-bottom: tokens.getCalculatedValue($_, "space", 3); - margin-right: 0; - } + --#{$_}--subfooter-flex-direction: column; + --#{$_}--subfooter-separator-display-between: none; + --#{$_}--subfooter-separator-display-before-last: none; + --#{$_}--subfooter-logotype-wrapper-margin-right: 0; + --#{$_}--subfooter-logotype-wrapper-margin-bottom: #{tokens.getCalculatedValue($_, "space", 3)}; } - } - //Todo: whole subfooter list implementation can/should be refactored and simplified - .#{$_}__subfooter__wrapper { - overflow: hidden; //Quick fix until refactor + &__logotype-wrapper { + margin-right: var(--#{$_}--subfooter-logotype-wrapper-margin-right, 0); + margin-bottom: var(--#{$_}--subfooter-logotype-wrapper-margin-bottom, #{tokens.getCalculatedValue($_, "space", 3)}); + } } } @include mq.mq("xs", "md") { .#{$_} { + --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; + --#{$_}--body-padding-x: #{tokens.getCalculatedValue($_, "space", 4)}; + --#{$_}--text-align: center; + --#{$_}--subfooter-flex-direction: column; + --#{$_}--subfooter-alignment: center; + --#{$_}--subfooter-separator-display-between: none; + --#{$_}--subfooter-separator-display-before-last: none; + + &__main-wrapper > .o-container > .o-grid { + grid-template-columns: 1fr; + } &__body { - padding: tokens.getCalculatedValue($_, "space", 5) tokens.getCalculatedValue($_, "space", 4); display: block; - text-align: center; } &__home-link { display: block; margin-bottom: tokens.getCalculatedValue($_, "space", 5); + margin-right: 0; + } + + &__logotype, + &__subfooter__logotype { + height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(var(--#{$_}--logotype-height) * 0.75), var(--#{$_}--logotype-height)); + } + + &__subfooter__logotype { + height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(var(--#{$_}--subfooter-logotype-height) * 0.75), var(--#{$_}--subfooter-logotype-height)); } &__link-divider { @@ -287,34 +452,5 @@ $_: "c-footer"; display: block; margin-bottom: tokens.getCalculatedValue($_, "space", 2); } - - &__subfooter { - - &__list, - &__wrapper { - flex-direction: column; - justify-content: center; - align-items: center; - - >li:not(:first-child):before { - display: none; - } - } - - &--align-flex-end .#{$_}__subfooter__list li:not(:last-child):before { - display: none; - } - - &__logotype { - height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(#{var(--#{$_}--subfooter-logotype-height)} * 0.75), #{var(--#{$_}--subfooter-logotype-height)}); - } - - &--horizontal { - .#{$_}__subfooter__logotype-wrapper { - margin-bottom: tokens.getCalculatedValue($_, "space", 3); - margin-right: 0; - } - } - } } -} \ No newline at end of file +} From 5d857bd0ba6cf660a319fd90e50b513d3075710b Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 14:30:23 +0200 Subject: [PATCH 02/26] fix: example with bg --- assets/img/squares-in-squares.svg | 13 +++++++++++++ .../examples/lorem-columns-with-subfooter.blade.php | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 assets/img/squares-in-squares.svg diff --git a/assets/img/squares-in-squares.svg b/assets/img/squares-in-squares.svg new file mode 100644 index 000000000..c6d71599b --- /dev/null +++ b/assets/img/squares-in-squares.svg @@ -0,0 +1,13 @@ + + + + squares-in-squares + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index 9d18284f9..edd5a3248 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -1,6 +1,11 @@ + @footer([ 'slotOnly' => true, - 'id' => 'site-footer', + 'id' => 'site-footer-abc', 'classList' => ['site-footer', 's-footer'], 'logotype' => '', 'subfooter' => [ From c589667c44f8486f52dee6f083dfac3ab1cff844 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 14:37:49 +0200 Subject: [PATCH 03/26] fix: adjust settings. --- index.php | 7 +++++ source/components/footer/component.json | 28 +++++++++++++------ .../lorem-columns-with-subfooter.blade.php | 3 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/index.php b/index.php index 04bead313..cffe1d147 100644 --- a/index.php +++ b/index.php @@ -14,6 +14,13 @@ error_reporting(0); } +if (!function_exists('__')) { + function __($text, $domain = 'default') + { + return $text; + } +} + define('BASEPATH', dirname(__FILE__) . '/'); require_once BASEPATH . 'config.php'; require_once __DIR__ . '/vendor/autoload.php'; diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 70d6f212b..328387e9b 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -206,6 +206,14 @@ "value": "left top", "label": "Left Top" }, + { + "value": "center top", + "label": "Center Top" + }, + { + "value": "right top", + "label": "Right Top" + }, { "value": "center", "label": "Center" @@ -213,6 +221,18 @@ { "value": "right bottom", "label": "Right Bottom" + }, + { + "value": "center bottom", + "label": "Center Bottom" + }, + { + "value": "left bottom", + "label": "Left Bottom" + } + { + "value": "left bottom", + "label": "Left Bottom" } ] }, @@ -336,14 +356,6 @@ "value": "normal", "label": "Normal" }, - { - "value": "multiply", - "label": "Multiply" - }, - { - "value": "screen", - "label": "Screen" - }, { "value": "overlay", "label": "Overlay" diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index edd5a3248..a5fce8009 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -7,7 +7,8 @@ 'slotOnly' => true, 'id' => 'site-footer-abc', 'classList' => ['site-footer', 's-footer'], - 'logotype' => '', + 'logotype' => '/assets/img/logotype-grey-full.svg', + 'prefooter' => 'This prefooter text appears above the footer columns and uses the footer prefooter alignment token.', 'subfooter' => [ 'flexDirection' => 'row', 'alignment' => 'center', From 1525d6789acd84f0edd4a7d4a210df4672bc9c06 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 14:45:27 +0200 Subject: [PATCH 04/26] fix: minimize amount of tilt --- component-design-tokens.json | 46 +++++++++++++++---- source/components/footer/component.json | 26 +++++++++-- source/components/footer/style.scss | 45 ++++++++++++------ .../controls/RangeControl.test.ts | 16 +++++++ .../control-elements/controls/RangeControl.ts | 25 +++++++++- 5 files changed, 131 insertions(+), 27 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 62927e416..792030812 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1054,6 +1054,28 @@ "label": "Background Image", "description": "Component-local background image controls.", "settings": [ + { + "variable": "--background-opacity", + "label": "Background Opacity", + "description": "Controls the opacity of the footer background layer without changing content opacity.", + "type": "range", + "default": "1", + "min": 0, + "max": 1, + "step": 0.05, + "unit": "" + }, + { + "variable": "--background-rotation", + "label": "Background Rotation", + "description": "Rotates the footer background layer without rotating footer content.", + "type": "range", + "default": "0", + "min": -9, + "max": 9, + "step": 0.5, + "unit": "deg" + }, { "variable": "--background-position", "label": "Background Position", @@ -1065,6 +1087,14 @@ "value": "left top", "label": "Left Top" }, + { + "value": "center top", + "label": "Center Top" + }, + { + "value": "right top", + "label": "Right Top" + }, { "value": "center", "label": "Center" @@ -1072,6 +1102,14 @@ { "value": "right bottom", "label": "Right Bottom" + }, + { + "value": "center bottom", + "label": "Center Bottom" + }, + { + "value": "left bottom", + "label": "Left Bottom" } ] }, @@ -1195,14 +1233,6 @@ "value": "normal", "label": "Normal" }, - { - "value": "multiply", - "label": "Multiply" - }, - { - "value": "screen", - "label": "Screen" - }, { "value": "overlay", "label": "Overlay" diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 328387e9b..568593d5d 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -195,6 +195,28 @@ "label": "Background Image", "description": "Component-local background image controls.", "settings": [ + { + "variable": "--background-opacity", + "label": "Background Opacity", + "description": "Controls the opacity of the footer background layer without changing content opacity.", + "type": "range", + "default": "1", + "min": 0, + "max": 1, + "step": 0.05, + "unit": "" + }, + { + "variable": "--background-rotation", + "label": "Background Rotation", + "description": "Rotates the footer background layer without rotating footer content.", + "type": "range", + "default": "0", + "min": -7, + "max": 7, + "step": 0.5, + "unit": "deg" + }, { "variable": "--background-position", "label": "Background Position", @@ -230,10 +252,6 @@ "value": "left bottom", "label": "Left Bottom" } - { - "value": "left bottom", - "label": "Left Bottom" - } ] }, { diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index f59095f49..a68091adb 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -28,6 +28,8 @@ $_: "c-footer"; --#{$_}--background-origin: padding-box; --#{$_}--background-clip: border-box; --#{$_}--background-blend-mode: normal; + --#{$_}--background-opacity: 1; + --#{$_}--background-rotation: 0deg; --#{$_}--main-padding-y: #{tokens.getCalculatedValue($_, "outer-space", 3)}; --#{$_}--main-padding-x: var(--#{$_}--inset-padding-x); --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; @@ -59,17 +61,34 @@ $_: "c-footer"; --inherit-color-background: var(--#{$_}--background-color); --inherit-color-contrast: var(--#{$_}--color); position: relative; - background-color: var(--#{$_}--background-color); - background-image: var(--#{$_}--background-image); - background-position: var(--#{$_}--background-position); - background-repeat: var(--#{$_}--background-repeat); - background-size: var(--#{$_}--background-size); - background-attachment: var(--#{$_}--background-attachment); - background-origin: var(--#{$_}--background-origin); - background-clip: var(--#{$_}--background-clip); - background-blend-mode: var(--#{$_}--background-blend-mode); + isolation: isolate; + overflow: hidden; color: var(--#{$_}--color); + &::before { + content: ""; + position: absolute; + inset: -50%; + z-index: 0; + pointer-events: none; + opacity: var(--#{$_}--background-opacity); + transform: rotate(clamp(-9deg, var(--#{$_}--background-rotation), 9deg)); + background-color: var(--#{$_}--background-color); + background-image: var(--#{$_}--background-image); + background-position: var(--#{$_}--background-position); + background-repeat: var(--#{$_}--background-repeat); + background-size: var(--#{$_}--background-size); + background-attachment: var(--#{$_}--background-attachment); + background-origin: var(--#{$_}--background-origin); + background-clip: var(--#{$_}--background-clip); + background-blend-mode: var(--#{$_}--background-blend-mode); + } + + >* { + position: relative; + z-index: 1; + } + a, a:hover, a:visited, @@ -172,12 +191,12 @@ $_: "c-footer"; padding: var(--#{$_}--main-padding-y) var(--#{$_}--main-padding-x); } - &__main-wrapper > .o-container > .o-grid { + &__main-wrapper>.o-container>.o-grid { display: grid; grid-template-columns: repeat(var(--#{$_}--columns-count), minmax(0, 1fr)); gap: var(--#{$_}--column-gap); - > [class*="o-grid-"] { + >[class*="o-grid-"] { flex: initial; max-width: none; width: auto; @@ -421,7 +440,7 @@ $_: "c-footer"; --#{$_}--subfooter-separator-display-between: none; --#{$_}--subfooter-separator-display-before-last: none; - &__main-wrapper > .o-container > .o-grid { + &__main-wrapper>.o-container>.o-grid { grid-template-columns: 1fr; } @@ -453,4 +472,4 @@ $_: "c-footer"; margin-bottom: tokens.getCalculatedValue($_, "space", 2); } } -} +} \ No newline at end of file diff --git a/source/design-builder/shared/control-elements/controls/RangeControl.test.ts b/source/design-builder/shared/control-elements/controls/RangeControl.test.ts index 9499ab449..e4375f9b3 100644 --- a/source/design-builder/shared/control-elements/controls/RangeControl.test.ts +++ b/source/design-builder/shared/control-elements/controls/RangeControl.test.ts @@ -107,4 +107,20 @@ describe('RangeControl', () => { expect(input.step).toBe('2'); expect(input.value).toBe('160'); }); + + it('clamps out-of-range values to the configured bounds', () => { + const rangeControl = document.createElement('range-control'); + rangeControl.setAttribute('min', '-9'); + rangeControl.setAttribute('max', '9'); + rangeControl.setAttribute('step', '0.5'); + rangeControl.setAttribute('value', '360'); + rangeControl.setAttribute('unit', 'deg'); + document.body.appendChild(rangeControl); + + const input = rangeControl.querySelector('input[type="range"]') as HTMLInputElement; + const output = rangeControl.querySelector('output') as HTMLOutputElement; + + expect(input.value).toBe('9'); + expect(output.textContent).toBe('9 deg'); + }); }); diff --git a/source/design-builder/shared/control-elements/controls/RangeControl.ts b/source/design-builder/shared/control-elements/controls/RangeControl.ts index 8bc2c5645..11e7880f0 100644 --- a/source/design-builder/shared/control-elements/controls/RangeControl.ts +++ b/source/design-builder/shared/control-elements/controls/RangeControl.ts @@ -25,12 +25,32 @@ class RangeControl extends HTMLElement { this.render(); } + private getClampedValue(value: string, min: string, max: string): string { + if (value === '') { + return value; + } + + const numericValue = Number.parseFloat(value); + const numericMin = Number.parseFloat(min); + const numericMax = Number.parseFloat(max); + + if (Number.isNaN(numericValue)) { + return value; + } + + const lowerBound = Number.isNaN(numericMin) ? numericValue : numericMin; + const upperBound = Number.isNaN(numericMax) ? numericValue : numericMax; + const clampedValue = Math.min(Math.max(numericValue, lowerBound), upperBound); + + return String(clampedValue); + } + render() { const isDisabled = this.hasAttribute('locked'); const min = this.getAttribute('min') || '0'; const max = this.getAttribute('max') || '100'; const step = this.getAttribute('step') || '1'; - const value = this.getAttribute('value') || ''; + const value = this.getClampedValue(this.getAttribute('value') || '', min, max); const unit = this.getAttribute('unit') || ''; const markup = () => @@ -43,7 +63,8 @@ class RangeControl extends HTMLElement { } _onInput(e: Event) { - const value = (e.target as HTMLInputElement).value; + const input = e.target as HTMLInputElement; + const value = this.getClampedValue(input.value, input.min, input.max); this.dispatchEvent( new CustomEvent('change', { From 32573ead75a758f261e758048e8035ec92fbfb60 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 14:47:34 +0200 Subject: [PATCH 05/26] fix: reduce amount of options --- source/components/footer/component.json | 63 ------------------------- 1 file changed, 63 deletions(-) diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 568593d5d..2fd761087 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -300,69 +300,6 @@ } ] }, - { - "variable": "--background-attachment", - "label": "Background Attachment", - "description": "Controls the CSS background-attachment value.", - "type": "select", - "default": "scroll", - "options": [ - { - "value": "scroll", - "label": "Scroll" - }, - { - "value": "fixed", - "label": "Fixed" - }, - { - "value": "local", - "label": "Local" - } - ] - }, - { - "variable": "--background-origin", - "label": "Background Origin", - "description": "Controls the CSS background-origin value.", - "type": "select", - "default": "padding-box", - "options": [ - { - "value": "border-box", - "label": "Border Box" - }, - { - "value": "padding-box", - "label": "Padding Box" - }, - { - "value": "content-box", - "label": "Content Box" - } - ] - }, - { - "variable": "--background-clip", - "label": "Background Clip", - "description": "Controls the CSS background-clip value.", - "type": "select", - "default": "border-box", - "options": [ - { - "value": "border-box", - "label": "Border Box" - }, - { - "value": "padding-box", - "label": "Padding Box" - }, - { - "value": "content-box", - "label": "Content Box" - } - ] - }, { "variable": "--background-blend-mode", "label": "Background Blend Mode", From df39aa1c9acd090f0da07377f37f7834b77d9a77 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 14:57:24 +0200 Subject: [PATCH 06/26] fix: build tokens --- component-design-tokens.json | 67 ++---------------------------------- 1 file changed, 2 insertions(+), 65 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 792030812..36b9d7fdf 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1071,8 +1071,8 @@ "description": "Rotates the footer background layer without rotating footer content.", "type": "range", "default": "0", - "min": -9, - "max": 9, + "min": -7, + "max": 7, "step": 0.5, "unit": "deg" }, @@ -1159,69 +1159,6 @@ } ] }, - { - "variable": "--background-attachment", - "label": "Background Attachment", - "description": "Controls the CSS background-attachment value.", - "type": "select", - "default": "scroll", - "options": [ - { - "value": "scroll", - "label": "Scroll" - }, - { - "value": "fixed", - "label": "Fixed" - }, - { - "value": "local", - "label": "Local" - } - ] - }, - { - "variable": "--background-origin", - "label": "Background Origin", - "description": "Controls the CSS background-origin value.", - "type": "select", - "default": "padding-box", - "options": [ - { - "value": "border-box", - "label": "Border Box" - }, - { - "value": "padding-box", - "label": "Padding Box" - }, - { - "value": "content-box", - "label": "Content Box" - } - ] - }, - { - "variable": "--background-clip", - "label": "Background Clip", - "description": "Controls the CSS background-clip value.", - "type": "select", - "default": "border-box", - "options": [ - { - "value": "border-box", - "label": "Border Box" - }, - { - "value": "padding-box", - "label": "Padding Box" - }, - { - "value": "content-box", - "label": "Content Box" - } - ] - }, { "variable": "--background-blend-mode", "label": "Background Blend Mode", From 6df2b850bfeb8d8e5f25007281a23ed0eb86fc7b Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 15:04:09 +0200 Subject: [PATCH 07/26] fix: inset multiplier. --- component-design-tokens.json | 6 +++--- source/components/footer/component.json | 17 +++-------------- source/components/footer/style.scss | 4 ++-- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 36b9d7fdf..495ba05cf 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -949,13 +949,13 @@ "description": "Component-local footer spacing, columns, and alignment.", "settings": [ { - "variable": "--inset-multiplier", + "variable": "--outer-padding-inset-multiplier", "label": "Outer Padding Inset Multiplier", - "description": "Scales footer outer padding from the inherited inset rhythm.", + "description": "Scales footer outer padding from the inherited inset rhythm without overriding the internal resolved inset variable.", "type": "range", "default": "1", "min": 0, - "max": 4, + "max": 8, "step": 0.125, "unit": "" }, diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 2fd761087..f4a44b137 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -60,17 +60,6 @@ "step": 0.25, "unit": "" }, - { - "variable": "--home-link-height-multiplier", - "label": "Home Link Height Multiplier", - "description": "Scales the footer home link height locally without changing global sizing.", - "type": "range", - "default": "1", - "min": 0.5, - "max": 3, - "step": 0.05, - "unit": "" - }, { "variable": "--subfooter-logotype-height-multiplier", "label": "Subfooter Logotype Height Multiplier", @@ -90,13 +79,13 @@ "description": "Component-local footer spacing, columns, and alignment.", "settings": [ { - "variable": "--inset-multiplier", + "variable": "--outer-padding-inset-multiplier", "label": "Outer Padding Inset Multiplier", - "description": "Scales footer outer padding from the inherited inset rhythm.", + "description": "Scales footer outer padding from the inherited inset rhythm without overriding the internal resolved inset variable.", "type": "range", "default": "1", "min": 0, - "max": 4, + "max": 8, "step": 0.125, "unit": "" }, diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index a68091adb..cabc4bbd6 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -11,7 +11,7 @@ $_: "c-footer"; /* Component */ .#{$_} { - @include insetMultiplier.consume($_, var(--#{$_}--inset-multiplier, var(--inherit-inset-multiplier, 1))); + @include insetMultiplier.consume($_, var(--#{$_}--outer-padding-inset-multiplier, var(--inherit-inset-multiplier, 1))); --#{$_}--background-color: #{tokens.getRawValue($_, "color--surface")}; --#{$_}--color: #{tokens.getRawValue($_, "color--surface-contrast")}; @@ -30,7 +30,7 @@ $_: "c-footer"; --#{$_}--background-blend-mode: normal; --#{$_}--background-opacity: 1; --#{$_}--background-rotation: 0deg; - --#{$_}--main-padding-y: #{tokens.getCalculatedValue($_, "outer-space", 3)}; + --#{$_}--main-padding-y: calc(#{tokens.getCalculatedValue($_, "outer-space", 3)} * var(--#{$_}--inset-multiplier)); --#{$_}--main-padding-x: var(--#{$_}--inset-padding-x); --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; --#{$_}--body-padding-x: 0; From 3419d0fb3ec39af3101b9981ad552eb389d96d48 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 15:08:05 +0200 Subject: [PATCH 08/26] fix: remove home link adjustment --- component-design-tokens.json | 11 ----------- source/components/footer/style.scss | 2 -- 2 files changed, 13 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 495ba05cf..2898b1806 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -919,17 +919,6 @@ "step": 0.25, "unit": "" }, - { - "variable": "--home-link-height-multiplier", - "label": "Home Link Height Multiplier", - "description": "Scales the footer home link height locally without changing global sizing.", - "type": "range", - "default": "1", - "min": 0.5, - "max": 3, - "step": 0.05, - "unit": "" - }, { "variable": "--subfooter-logotype-height-multiplier", "label": "Subfooter Logotype Height Multiplier", diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index cabc4bbd6..111416649 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -40,7 +40,6 @@ $_: "c-footer"; --#{$_}--prefooter-text-align: left; --#{$_}--logotype-justify-content: flex-start; --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, var(--#{$_}--logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); - --#{$_}--home-link-height: calc(var(--#{$_}--home-link-height-multiplier, 1) * #{tokens.getCalculatedValue($_, "base", 7.5)}); --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, var(--#{$_}--subfooter-logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-alignment: var(--c-footer-subfooter-alignment, center); --#{$_}--subfooter-flex-direction: var(--c-footer-subfooter-flex-direction, row); @@ -226,7 +225,6 @@ $_: "c-footer"; &__home-link { margin-right: tokens.getCalculatedValue($_, "outer-space", 2); line-height: 0; - height: var(--#{$_}--home-link-height); img, svg { From 8fc549f44f4f089228b79887fdac6a67cbfae4c7 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 15:17:15 +0200 Subject: [PATCH 09/26] fix: background --- component-design-tokens.json | 11 +++++++++++ source/components/footer/component.json | 11 +++++++++++ .../examples/lorem-columns-with-subfooter.blade.php | 1 + source/components/footer/style.scss | 5 ++++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 2898b1806..58b36177b 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1054,6 +1054,17 @@ "step": 0.05, "unit": "" }, + { + "variable": "--subfooter-background-opacity", + "label": "Subfooter Background Opacity", + "description": "Controls how much of the subfooter background color is mixed into the rendered background.", + "type": "range", + "default": "100", + "min": 0, + "max": 100, + "step": 1, + "unit": "%" + }, { "variable": "--background-rotation", "label": "Background Rotation", diff --git a/source/components/footer/component.json b/source/components/footer/component.json index f4a44b137..9b8e41909 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -195,6 +195,17 @@ "step": 0.05, "unit": "" }, + { + "variable": "--subfooter-background-opacity", + "label": "Subfooter Background Opacity", + "description": "Controls how much of the subfooter background color is mixed into the rendered background.", + "type": "range", + "default": "100", + "min": 0, + "max": 100, + "step": 1, + "unit": "%" + }, { "variable": "--background-rotation", "label": "Background Rotation", diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index a5fce8009..efb00933e 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -8,6 +8,7 @@ 'id' => 'site-footer-abc', 'classList' => ['site-footer', 's-footer'], 'logotype' => '/assets/img/logotype-grey-full.svg', + 'subfooterLogotype' => '/assets/img/logotype-grey-full.svg', 'prefooter' => 'This prefooter text appears above the footer columns and uses the footer prefooter alignment token.', 'subfooter' => [ 'flexDirection' => 'row', diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 111416649..dde67d42c 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -18,6 +18,8 @@ $_: "c-footer"; --#{$_}--color-muted: #{tokens.getRawValue($_, "color--surface-contrast-muted")}; --#{$_}--border-color: var(--c-footer-header-border-color, #{tokens.getRawValue($_, "color--surface-border")}); --#{$_}--subfooter-background-color: var(--c-footer-subfooter-color-background, #{tokens.getRawValue($_, "color--surface-alt")}); + --#{$_}--subfooter-background-opacity: 100%; + --#{$_}--subfooter-background: color-mix(in srgb, var(--#{$_}--subfooter-background-color) var(--#{$_}--subfooter-background-opacity), transparent); --#{$_}--subfooter-color: var(--c-footer-subfooter-color-text, #{tokens.getRawValue($_, "color--surface-contrast")}); --#{$_}--subfooter-separator-color: var(--c-footer-subfooter-color-separator, #{tokens.getRawValue($_, "color--surface-border")}); --#{$_}--background-image: none; @@ -63,6 +65,7 @@ $_: "c-footer"; isolation: isolate; overflow: hidden; color: var(--#{$_}--color); + background-color: var(--#{$_}--background-color); &::before { content: ""; @@ -310,7 +313,7 @@ $_: "c-footer"; &__subfooter { color: var(--#{$_}--subfooter-color); - background: var(--#{$_}--subfooter-background-color); + background: var(--#{$_}--subfooter-background); padding: tokens.getCalculatedValue($_, "space", 3) 0; font-size: tokens.getRawValue($_, "font-size-90"); From 9a6e2be6fe1a7d62b08544fd73aad568e2521f81 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 15:47:14 +0200 Subject: [PATCH 10/26] fix: ul lists in footer --- component-design-tokens.json | 28 ++++ source/components/footer/component.json | 28 ++++ source/components/footer/style.scss | 169 +++++++++++++++++++----- 3 files changed, 194 insertions(+), 31 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 58b36177b..0ea171163 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -948,6 +948,34 @@ "step": 0.125, "unit": "" }, + { + "variable": "--gap-multiplier", + "label": "Gap Multiplier", + "description": "Scales footer grid gaps from the component space token independently of outer padding.", + "type": "range", + "default": "1", + "min": 1, + "max": 2.5, + "step": 0.125, + "unit": "" + }, + { + "variable": "--subfooter-border-width", + "label": "Subfooter Top Border", + "description": "Toggles the subfooter top border between no border and the component border width token.", + "type": "select", + "default": "0", + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "var(--c-footer--subfooter-border-width-token)", + "label": "On" + } + ] + }, { "variable": "--columns-count", "label": "Number of Columns", diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 9b8e41909..6378f7a34 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -89,6 +89,34 @@ "step": 0.125, "unit": "" }, + { + "variable": "--gap-multiplier", + "label": "Gap Multiplier", + "description": "Scales footer grid gaps from the component space token independently of outer padding.", + "type": "range", + "default": "1", + "min": 1, + "max": 2.5, + "step": 0.125, + "unit": "" + }, + { + "variable": "--subfooter-border-width", + "label": "Subfooter Top Border", + "description": "Toggles the subfooter top border between no border and the component border width token.", + "type": "select", + "default": "0", + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "var(--c-footer--subfooter-border-width-token)", + "label": "On" + } + ] + }, { "variable": "--columns-count", "label": "Number of Columns", diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index dde67d42c..7bd8c3b2b 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -22,6 +22,8 @@ $_: "c-footer"; --#{$_}--subfooter-background: color-mix(in srgb, var(--#{$_}--subfooter-background-color) var(--#{$_}--subfooter-background-opacity), transparent); --#{$_}--subfooter-color: var(--c-footer-subfooter-color-text, #{tokens.getRawValue($_, "color--surface-contrast")}); --#{$_}--subfooter-separator-color: var(--c-footer-subfooter-color-separator, #{tokens.getRawValue($_, "color--surface-border")}); + --#{$_}--subfooter-border-width: 0; + --#{$_}--subfooter-border-width-token: #{tokens.getCalculatedValue($_, "border-width")}; --#{$_}--background-image: none; --#{$_}--background-position: center; --#{$_}--background-repeat: no-repeat; @@ -34,9 +36,10 @@ $_: "c-footer"; --#{$_}--background-rotation: 0deg; --#{$_}--main-padding-y: calc(#{tokens.getCalculatedValue($_, "outer-space", 3)} * var(--#{$_}--inset-multiplier)); --#{$_}--main-padding-x: var(--#{$_}--inset-padding-x); + --#{$_}--gap: calc(#{tokens.getCalculatedValue($_, "space", 3)} * var(--#{$_}--gap-multiplier, 1)); --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; --#{$_}--body-padding-x: 0; - --#{$_}--column-gap: #{tokens.getCalculatedValue($_, "space", 2)}; + --#{$_}--column-gap: var(--#{$_}--gap); --#{$_}--columns-count: 3; --#{$_}--text-align: left; --#{$_}--prefooter-text-align: left; @@ -66,6 +69,12 @@ $_: "c-footer"; overflow: hidden; color: var(--#{$_}--color); background-color: var(--#{$_}--background-color); + display: grid; + grid-template-areas: + "main" + "subfooter"; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto; &::before { content: ""; @@ -190,10 +199,23 @@ $_: "c-footer"; } &__main-wrapper { + display: grid; + grid-area: main; + grid-template-areas: + "header" + "prefooter" + "content" + "areas" + "body" + "postfooter"; + grid-template-columns: minmax(0, 1fr); + gap: var(--#{$_}--gap); padding: var(--#{$_}--main-padding-y) var(--#{$_}--main-padding-x); } - &__main-wrapper>.o-container>.o-grid { + &__main-wrapper>.o-container>.o-grid, + &__content-wrapper>.o-container>.o-grid, + &__areas-wrapper>.o-container>.o-grid { display: grid; grid-template-columns: repeat(var(--#{$_}--columns-count), minmax(0, 1fr)); gap: var(--#{$_}--column-gap); @@ -206,18 +228,22 @@ $_: "c-footer"; } &__body { + grid-area: body; width: 100%; max-width: var(--container-width); margin-left: auto; margin-right: auto; padding: var(--#{$_}--body-padding-y) var(--#{$_}--body-padding-x); - display: flex; - flex-wrap: nowrap; + display: grid; + grid-template-areas: "home nav"; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--#{$_}--gap); + align-items: start; text-align: var(--#{$_}--text-align); } &__header-wrapper { - margin-bottom: tokens.getCalculatedValue($_, "space", 3); + grid-area: header; .o-grid-12 { display: flex; @@ -226,7 +252,7 @@ $_: "c-footer"; } &__home-link { - margin-right: tokens.getCalculatedValue($_, "outer-space", 2); + grid-area: home; line-height: 0; img, @@ -235,6 +261,10 @@ $_: "c-footer"; } } + &__nav { + grid-area: nav; + } + &__links { margin-bottom: tokens.getCalculatedValue($_, "space", 3); @@ -263,15 +293,40 @@ $_: "c-footer"; } &__widget-area { - gap: var(--#{$_}--column-gap); + gap: calc(var(--#{$_}--column-gap) * var(--#{$_}--widget-area-gap-multiplier, 0.25)); text-align: var(--#{$_}--text-align); + max-width: var(--#{$_}--widget-area-max-width, #{tokens.getCalculatedValue($_, "space", 70)}); + margin-left: auto; + margin-right: auto; + + ul { + display: grid; + gap: tokens.getCalculatedValue($_, "space", 0.5); + justify-content: var(--#{$_}--text-align); + list-style: none; + padding-left: 0; + } .footer-title { margin-bottom: tokens.getCalculatedValue($_, "space", 2); } li { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: tokens.getCalculatedValue($_, "space"); + align-items: baseline; padding-left: 0; + text-align: left; + + &::before { + content: ""; + width: tokens.getCalculatedValue($_, "base", 0.75); + height: tokens.getCalculatedValue($_, "base", 0.75); + border-radius: 50%; + background: var(--#{$_}--color); + transform: translateY(-0.1em); + } } h2 { @@ -284,9 +339,22 @@ $_: "c-footer"; } &__prefooter-wrapper { + grid-area: prefooter; text-align: var(--#{$_}--prefooter-text-align); } + &__content-wrapper { + grid-area: content; + } + + &__areas-wrapper { + grid-area: areas; + } + + &__postfooter-wrapper { + grid-area: postfooter; + } + &__prefooter-wrapper, &__widget-area { @@ -312,8 +380,11 @@ $_: "c-footer"; } &__subfooter { + display: grid; + grid-area: subfooter; color: var(--#{$_}--subfooter-color); background: var(--#{$_}--subfooter-background); + border-top: var(--#{$_}--subfooter-border-width) solid var(--#{$_}--subfooter-separator-color); padding: tokens.getCalculatedValue($_, "space", 3) 0; font-size: tokens.getRawValue($_, "font-size-90"); @@ -325,20 +396,28 @@ $_: "c-footer"; } &__wrapper { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-areas: "list"; + grid-template-columns: minmax(0, 1fr); + gap: var(--#{$_}--gap); justify-content: var(--#{$_}--subfooter-alignment); align-items: var(--#{$_}--subfooter-alignment); - flex-direction: var(--#{$_}--subfooter-flex-direction); overflow: hidden; + + &--has-logotype { + grid-template-areas: "logotype list"; + grid-template-columns: auto minmax(0, 1fr); + } } &__list { - display: flex; - flex-wrap: wrap; + display: grid; + grid-area: list; + grid-auto-flow: column; + grid-auto-columns: max-content; + gap: 0; justify-content: var(--#{$_}--subfooter-alignment); align-items: var(--#{$_}--subfooter-alignment); - flex-direction: var(--#{$_}--subfooter-flex-direction); margin: var(--#{$_}--subfooter-list-margin-block) var(--#{$_}--subfooter-list-margin-inline); margin-left: var(--#{$_}--subfooter-list-margin-left); margin-right: var(--#{$_}--subfooter-list-margin-right); @@ -384,8 +463,7 @@ $_: "c-footer"; } &__logotype-wrapper { - margin-right: 0; - margin-bottom: tokens.getCalculatedValue($_, "space", 3); + grid-area: logotype; } &--align-flex-start { @@ -409,23 +487,34 @@ $_: "c-footer"; } &--horizontal { - --#{$_}--subfooter-flex-direction: row; --#{$_}--subfooter-alignment: center; - --#{$_}--subfooter-logotype-wrapper-margin-right: #{tokens.getCalculatedValue($_, "space", 7)}; - --#{$_}--subfooter-logotype-wrapper-margin-bottom: 0; + + .#{$_}__subfooter__wrapper--has-logotype { + grid-template-areas: "logotype list"; + grid-template-columns: auto minmax(0, 1fr); + } + + .#{$_}__subfooter__list { + grid-auto-flow: column; + grid-auto-columns: max-content; + } } &--vertical { - --#{$_}--subfooter-flex-direction: column; --#{$_}--subfooter-separator-display-between: none; --#{$_}--subfooter-separator-display-before-last: none; - --#{$_}--subfooter-logotype-wrapper-margin-right: 0; - --#{$_}--subfooter-logotype-wrapper-margin-bottom: #{tokens.getCalculatedValue($_, "space", 3)}; - } - &__logotype-wrapper { - margin-right: var(--#{$_}--subfooter-logotype-wrapper-margin-right, 0); - margin-bottom: var(--#{$_}--subfooter-logotype-wrapper-margin-bottom, #{tokens.getCalculatedValue($_, "space", 3)}); + .#{$_}__subfooter__wrapper { + grid-template-areas: + "logotype" + "list"; + grid-template-columns: minmax(0, 1fr); + } + + .#{$_}__subfooter__list { + grid-auto-flow: row; + grid-auto-columns: initial; + } } } } @@ -436,23 +525,41 @@ $_: "c-footer"; --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; --#{$_}--body-padding-x: #{tokens.getCalculatedValue($_, "space", 4)}; --#{$_}--text-align: center; - --#{$_}--subfooter-flex-direction: column; --#{$_}--subfooter-alignment: center; --#{$_}--subfooter-separator-display-between: none; --#{$_}--subfooter-separator-display-before-last: none; - &__main-wrapper>.o-container>.o-grid { + &__main-wrapper>.o-container>.o-grid, + &__content-wrapper>.o-container>.o-grid, + &__areas-wrapper>.o-container>.o-grid { grid-template-columns: 1fr; } &__body { - display: block; + grid-template-areas: + "home" + "nav"; + grid-template-columns: minmax(0, 1fr); } &__home-link { - display: block; - margin-bottom: tokens.getCalculatedValue($_, "space", 5); - margin-right: 0; + justify-self: center; + } + + &__subfooter__wrapper { + grid-template-areas: "list"; + grid-template-columns: minmax(0, 1fr); + + &--has-logotype { + grid-template-areas: + "logotype" + "list"; + } + } + + &__subfooter__list { + grid-auto-flow: row; + grid-auto-columns: initial; } &__logotype, From 3e15562f490a6d8bbe91e1d8cd680134425b3886 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 16:06:17 +0200 Subject: [PATCH 11/26] fix: subfooter settings --- component-design-tokens.json | 75 +++++++++++++++++++ source/components/footer/component.json | 75 +++++++++++++++++++ source/components/footer/style.scss | 99 ++++++------------------- 3 files changed, 172 insertions(+), 77 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 0ea171163..30fefd19d 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1063,6 +1063,81 @@ "label": "Right" } ] + }, + { + "variable": "--subfooter-alignment", + "label": "Subfooter Justify Content", + "description": "How to distribute the subfooter logo and links group inside the wrapper.", + "type": "select", + "default": "center", + "options": [ + { + "value": "flex-start", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "Right" + }, + { + "value": "space-between", + "label": "Space Between" + }, + { + "value": "space-around", + "label": "Space Around" + }, + { + "value": "space-evenly", + "label": "Space Evenly" + } + ] + }, + { + "variable": "--subfooter-flex-direction", + "label": "Subfooter Direction", + "description": "Controls whether the subfooter wrapper displays the logo before or after the links.", + "type": "select", + "default": "row", + "options": [ + { + "value": "row", + "label": "Default" + }, + { + "value": "row-reverse", + "label": "Reversed" + } + ] + }, + { + "variable": "--subfooter-align-items", + "label": "Subfooter Align Items", + "description": "How to align subfooter items across the selected direction.", + "type": "select", + "default": "center", + "options": [ + { + "value": "flex-start", + "label": "Start" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "End" + }, + { + "value": "stretch", + "label": "Stretch" + } + ] } ] }, diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 6378f7a34..781d75e09 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -204,6 +204,81 @@ "label": "Right" } ] + }, + { + "variable": "--subfooter-alignment", + "label": "Subfooter Justify Content", + "description": "How to distribute the subfooter logo and links group inside the wrapper.", + "type": "select", + "default": "center", + "options": [ + { + "value": "flex-start", + "label": "Left" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "Right" + }, + { + "value": "space-between", + "label": "Space Between" + }, + { + "value": "space-around", + "label": "Space Around" + }, + { + "value": "space-evenly", + "label": "Space Evenly" + } + ] + }, + { + "variable": "--subfooter-flex-direction", + "label": "Subfooter Direction", + "description": "Controls whether the subfooter wrapper displays the logo before or after the links.", + "type": "select", + "default": "row", + "options": [ + { + "value": "row", + "label": "Default" + }, + { + "value": "row-reverse", + "label": "Reversed" + } + ] + }, + { + "variable": "--subfooter-align-items", + "label": "Subfooter Align Items", + "description": "How to align subfooter items across the selected direction.", + "type": "select", + "default": "center", + "options": [ + { + "value": "flex-start", + "label": "Start" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "flex-end", + "label": "End" + }, + { + "value": "stretch", + "label": "Stretch" + } + ] } ] }, diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 7bd8c3b2b..66ce3d88e 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -46,8 +46,9 @@ $_: "c-footer"; --#{$_}--logotype-justify-content: flex-start; --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, var(--#{$_}--logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, var(--#{$_}--subfooter-logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); - --#{$_}--subfooter-alignment: var(--c-footer-subfooter-alignment, center); - --#{$_}--subfooter-flex-direction: var(--c-footer-subfooter-flex-direction, row); + --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, var(--c-footer-subfooter-alignment, center)); + --#{$_}--subfooter-align-items-value: var(--#{$_}--subfooter-align-items, center); + --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, var(--c-footer-subfooter-flex-direction, row)); --#{$_}--subfooter-list-margin-block: #{tokens.getCalculatedValue($_, "space", -1)}; --#{$_}--subfooter-list-margin-inline: #{tokens.getCalculatedValue($_, "space", -5)}; --#{$_}--subfooter-list-margin-left: 0; @@ -61,7 +62,6 @@ $_: "c-footer"; --#{$_}--subfooter-separator-right: auto; --#{$_}--subfooter-separator-display: none; --#{$_}--subfooter-separator-display-between: block; - --#{$_}--subfooter-separator-display-before-last: none; --inherit-color-background: var(--#{$_}--background-color); --inherit-color-contrast: var(--#{$_}--color); position: relative; @@ -380,7 +380,8 @@ $_: "c-footer"; } &__subfooter { - display: grid; + display: flex; + flex-direction: column; grid-area: subfooter; color: var(--#{$_}--subfooter-color); background: var(--#{$_}--subfooter-background); @@ -396,35 +397,29 @@ $_: "c-footer"; } &__wrapper { - display: grid; - grid-template-areas: "list"; - grid-template-columns: minmax(0, 1fr); + display: flex; + flex-direction: var(--#{$_}--subfooter-flex-direction-value); + flex-wrap: wrap; gap: var(--#{$_}--gap); - justify-content: var(--#{$_}--subfooter-alignment); - align-items: var(--#{$_}--subfooter-alignment); + justify-content: var(--#{$_}--subfooter-justify-content-value); + align-items: var(--#{$_}--subfooter-align-items-value); overflow: hidden; - - &--has-logotype { - grid-template-areas: "logotype list"; - grid-template-columns: auto minmax(0, 1fr); - } } &__list { - display: grid; - grid-area: list; - grid-auto-flow: column; - grid-auto-columns: max-content; + display: flex; + flex-direction: row; + flex-wrap: wrap; gap: 0; - justify-content: var(--#{$_}--subfooter-alignment); - align-items: var(--#{$_}--subfooter-alignment); + justify-content: center; + align-items: var(--#{$_}--subfooter-align-items-value); margin: var(--#{$_}--subfooter-list-margin-block) var(--#{$_}--subfooter-list-margin-inline); margin-left: var(--#{$_}--subfooter-list-margin-left); margin-right: var(--#{$_}--subfooter-list-margin-right); padding: 0; list-style: none; overflow: hidden; - flex: 1; + flex: 0 1 auto; li { position: relative; @@ -451,10 +446,6 @@ $_: "c-footer"; &:not(:first-child):before { display: var(--#{$_}--subfooter-separator-display-between); } - - &:not(:last-child):before { - display: var(--#{$_}--subfooter-separator-display-before-last); - } } } @@ -463,58 +454,30 @@ $_: "c-footer"; } &__logotype-wrapper { - grid-area: logotype; + flex: 0 0 auto; } &--align-flex-start { - --#{$_}--subfooter-alignment: flex-start; + --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, flex-start); } &--align-center { - --#{$_}--subfooter-alignment: center; + --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, center); } &--align-flex-end { - --#{$_}--subfooter-alignment: flex-end; + --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, flex-end); --#{$_}--subfooter-list-margin-left: var(--#{$_}--subfooter-list-margin-inline); --#{$_}--subfooter-list-margin-right: 0; - --#{$_}--subfooter-item-padding-left: var(--#{$_}--subfooter-item-padding-inline); - --#{$_}--subfooter-item-padding-right: 0; - --#{$_}--subfooter-separator-left: auto; - --#{$_}--subfooter-separator-right: #{tokens.getCalculatedValue($_, "base", -2.875)}; - --#{$_}--subfooter-separator-display-between: none; - --#{$_}--subfooter-separator-display-before-last: block; } &--horizontal { - --#{$_}--subfooter-alignment: center; - - .#{$_}__subfooter__wrapper--has-logotype { - grid-template-areas: "logotype list"; - grid-template-columns: auto minmax(0, 1fr); - } - - .#{$_}__subfooter__list { - grid-auto-flow: column; - grid-auto-columns: max-content; - } + --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, row); } &--vertical { --#{$_}--subfooter-separator-display-between: none; - --#{$_}--subfooter-separator-display-before-last: none; - - .#{$_}__subfooter__wrapper { - grid-template-areas: - "logotype" - "list"; - grid-template-columns: minmax(0, 1fr); - } - - .#{$_}__subfooter__list { - grid-auto-flow: row; - grid-auto-columns: initial; - } + --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, column); } } } @@ -525,9 +488,7 @@ $_: "c-footer"; --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; --#{$_}--body-padding-x: #{tokens.getCalculatedValue($_, "space", 4)}; --#{$_}--text-align: center; - --#{$_}--subfooter-alignment: center; --#{$_}--subfooter-separator-display-between: none; - --#{$_}--subfooter-separator-display-before-last: none; &__main-wrapper>.o-container>.o-grid, &__content-wrapper>.o-container>.o-grid, @@ -546,22 +507,6 @@ $_: "c-footer"; justify-self: center; } - &__subfooter__wrapper { - grid-template-areas: "list"; - grid-template-columns: minmax(0, 1fr); - - &--has-logotype { - grid-template-areas: - "logotype" - "list"; - } - } - - &__subfooter__list { - grid-auto-flow: row; - grid-auto-columns: initial; - } - &__logotype, &__subfooter__logotype { height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(var(--#{$_}--logotype-height) * 0.75), var(--#{$_}--logotype-height)); From e5c860fd052fa39fc7ca27bd7267830dfcfba2a0 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 16:17:50 +0200 Subject: [PATCH 12/26] fix: add align --- component-design-tokens.json | 25 ------------------------- source/components/footer/component.json | 25 ------------------------- source/components/footer/style.scss | 8 +++++++- 3 files changed, 7 insertions(+), 51 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 30fefd19d..0ae90ef3d 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1113,31 +1113,6 @@ "label": "Reversed" } ] - }, - { - "variable": "--subfooter-align-items", - "label": "Subfooter Align Items", - "description": "How to align subfooter items across the selected direction.", - "type": "select", - "default": "center", - "options": [ - { - "value": "flex-start", - "label": "Start" - }, - { - "value": "center", - "label": "Center" - }, - { - "value": "flex-end", - "label": "End" - }, - { - "value": "stretch", - "label": "Stretch" - } - ] } ] }, diff --git a/source/components/footer/component.json b/source/components/footer/component.json index 781d75e09..fb7a6551b 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -254,31 +254,6 @@ "label": "Reversed" } ] - }, - { - "variable": "--subfooter-align-items", - "label": "Subfooter Align Items", - "description": "How to align subfooter items across the selected direction.", - "type": "select", - "default": "center", - "options": [ - { - "value": "flex-start", - "label": "Start" - }, - { - "value": "center", - "label": "Center" - }, - { - "value": "flex-end", - "label": "End" - }, - { - "value": "stretch", - "label": "Stretch" - } - ] } ] }, diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 66ce3d88e..4d78e36a9 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -47,7 +47,7 @@ $_: "c-footer"; --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, var(--#{$_}--logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, var(--#{$_}--subfooter-logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, var(--c-footer-subfooter-alignment, center)); - --#{$_}--subfooter-align-items-value: var(--#{$_}--subfooter-align-items, center); + --#{$_}--subfooter-align-items-value: center; --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, var(--c-footer-subfooter-flex-direction, row)); --#{$_}--subfooter-list-margin-block: #{tokens.getCalculatedValue($_, "space", -1)}; --#{$_}--subfooter-list-margin-inline: #{tokens.getCalculatedValue($_, "space", -5)}; @@ -389,6 +389,12 @@ $_: "c-footer"; padding: tokens.getCalculatedValue($_, "space", 3) 0; font-size: tokens.getRawValue($_, "font-size-90"); + .o-container { + width: 100%; + margin-left: auto; + margin-right: auto; + } + a, a:hover, a:visited, From 6074b3d63caa21ce6c83368eac3b577c3a831dc1 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 20:50:05 +0200 Subject: [PATCH 13/26] fix: mix a more saturated list bullet --- source/components/footer/style.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 4d78e36a9..839d90ccb 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -22,6 +22,8 @@ $_: "c-footer"; --#{$_}--subfooter-background: color-mix(in srgb, var(--#{$_}--subfooter-background-color) var(--#{$_}--subfooter-background-opacity), transparent); --#{$_}--subfooter-color: var(--c-footer-subfooter-color-text, #{tokens.getRawValue($_, "color--surface-contrast")}); --#{$_}--subfooter-separator-color: var(--c-footer-subfooter-color-separator, #{tokens.getRawValue($_, "color--surface-border")}); + --#{$_}--list-marker-color: color-mix(in srgb, var(--#{$_}--color) 30%, var(--#{$_}--border-color)); + --#{$_}--subfooter-separator-dot-color: color-mix(in srgb, var(--#{$_}--subfooter-color) 30%, var(--#{$_}--subfooter-separator-color)); --#{$_}--subfooter-border-width: 0; --#{$_}--subfooter-border-width-token: #{tokens.getCalculatedValue($_, "border-width")}; --#{$_}--background-image: none; @@ -324,7 +326,7 @@ $_: "c-footer"; width: tokens.getCalculatedValue($_, "base", 0.75); height: tokens.getCalculatedValue($_, "base", 0.75); border-radius: 50%; - background: var(--#{$_}--color); + background: var(--#{$_}--list-marker-color); transform: translateY(-0.1em); } } @@ -438,7 +440,7 @@ $_: "c-footer"; &:before { content: ""; border-radius: 50%; - background: var(--#{$_}--subfooter-separator-color); + background: var(--#{$_}--subfooter-separator-dot-color); display: var(--#{$_}--subfooter-separator-display); position: absolute; width: var(--#{$_}--subfooter-separator-size); From 8de47a855ee162688c8f78399b9ab3b251d86e27 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 20:55:31 +0200 Subject: [PATCH 14/26] fix: reduce number of vars, replace logotype --- assets/img/logotype-white-full.svg | 6 ++ assets/img/logotype-white.svg | 20 ++++ .../lorem-columns-with-subfooter.blade.php | 4 +- source/components/footer/style.scss | 101 +++++++----------- 4 files changed, 66 insertions(+), 65 deletions(-) create mode 100755 assets/img/logotype-white-full.svg create mode 100644 assets/img/logotype-white.svg diff --git a/assets/img/logotype-white-full.svg b/assets/img/logotype-white-full.svg new file mode 100755 index 000000000..1d9ed0ea7 --- /dev/null +++ b/assets/img/logotype-white-full.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/img/logotype-white.svg b/assets/img/logotype-white.svg new file mode 100644 index 000000000..c41975019 --- /dev/null +++ b/assets/img/logotype-white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index efb00933e..05fe2eac8 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -7,8 +7,8 @@ 'slotOnly' => true, 'id' => 'site-footer-abc', 'classList' => ['site-footer', 's-footer'], - 'logotype' => '/assets/img/logotype-grey-full.svg', - 'subfooterLogotype' => '/assets/img/logotype-grey-full.svg', + 'logotype' => '/assets/img/logotype-white-full.svg', + 'subfooterLogotype' => '/assets/img/logotype-white-full.svg', 'prefooter' => 'This prefooter text appears above the footer columns and uses the footer prefooter alignment token.', 'subfooter' => [ 'flexDirection' => 'row', diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 839d90ccb..4e6fd9502 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -16,12 +16,12 @@ $_: "c-footer"; --#{$_}--background-color: #{tokens.getRawValue($_, "color--surface")}; --#{$_}--color: #{tokens.getRawValue($_, "color--surface-contrast")}; --#{$_}--color-muted: #{tokens.getRawValue($_, "color--surface-contrast-muted")}; - --#{$_}--border-color: var(--c-footer-header-border-color, #{tokens.getRawValue($_, "color--surface-border")}); - --#{$_}--subfooter-background-color: var(--c-footer-subfooter-color-background, #{tokens.getRawValue($_, "color--surface-alt")}); + --#{$_}--border-color: #{tokens.getRawValue($_, "color--surface-border")}; + --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--surface-alt")}; --#{$_}--subfooter-background-opacity: 100%; --#{$_}--subfooter-background: color-mix(in srgb, var(--#{$_}--subfooter-background-color) var(--#{$_}--subfooter-background-opacity), transparent); - --#{$_}--subfooter-color: var(--c-footer-subfooter-color-text, #{tokens.getRawValue($_, "color--surface-contrast")}); - --#{$_}--subfooter-separator-color: var(--c-footer-subfooter-color-separator, #{tokens.getRawValue($_, "color--surface-border")}); + --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--surface-contrast")}; + --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--surface-border")}; --#{$_}--list-marker-color: color-mix(in srgb, var(--#{$_}--color) 30%, var(--#{$_}--border-color)); --#{$_}--subfooter-separator-dot-color: color-mix(in srgb, var(--#{$_}--subfooter-color) 30%, var(--#{$_}--subfooter-separator-color)); --#{$_}--subfooter-border-width: 0; @@ -30,40 +30,20 @@ $_: "c-footer"; --#{$_}--background-position: center; --#{$_}--background-repeat: no-repeat; --#{$_}--background-size: cover; - --#{$_}--background-attachment: scroll; - --#{$_}--background-origin: padding-box; - --#{$_}--background-clip: border-box; --#{$_}--background-blend-mode: normal; --#{$_}--background-opacity: 1; --#{$_}--background-rotation: 0deg; --#{$_}--main-padding-y: calc(#{tokens.getCalculatedValue($_, "outer-space", 3)} * var(--#{$_}--inset-multiplier)); --#{$_}--main-padding-x: var(--#{$_}--inset-padding-x); --#{$_}--gap: calc(#{tokens.getCalculatedValue($_, "space", 3)} * var(--#{$_}--gap-multiplier, 1)); - --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; - --#{$_}--body-padding-x: 0; - --#{$_}--column-gap: var(--#{$_}--gap); --#{$_}--columns-count: 3; --#{$_}--text-align: left; --#{$_}--prefooter-text-align: left; --#{$_}--logotype-justify-content: flex-start; - --#{$_}--logotype-height: calc(var(--c-footer-height-logotype, var(--#{$_}--logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); - --#{$_}--subfooter-logotype-height: calc(var(--c-footer-subfooter-height-logotype, var(--#{$_}--subfooter-logotype-height-multiplier, 6)) * #{tokens.getRawValue($_, "base")}); - --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, var(--c-footer-subfooter-alignment, center)); - --#{$_}--subfooter-align-items-value: center; - --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, var(--c-footer-subfooter-flex-direction, row)); - --#{$_}--subfooter-list-margin-block: #{tokens.getCalculatedValue($_, "space", -1)}; - --#{$_}--subfooter-list-margin-inline: #{tokens.getCalculatedValue($_, "space", -5)}; - --#{$_}--subfooter-list-margin-left: 0; - --#{$_}--subfooter-list-margin-right: var(--#{$_}--subfooter-list-margin-inline); - --#{$_}--subfooter-item-padding-block: #{tokens.getCalculatedValue($_, "space")}; - --#{$_}--subfooter-item-padding-inline: #{tokens.getCalculatedValue($_, "space", 5)}; - --#{$_}--subfooter-item-padding-left: 0; - --#{$_}--subfooter-item-padding-right: var(--#{$_}--subfooter-item-padding-inline); - --#{$_}--subfooter-separator-size: #{tokens.getCalculatedValue($_, "base", 0.75)}; - --#{$_}--subfooter-separator-left: #{tokens.getCalculatedValue($_, "base", -2.875)}; - --#{$_}--subfooter-separator-right: auto; - --#{$_}--subfooter-separator-display: none; - --#{$_}--subfooter-separator-display-between: block; + --#{$_}--logotype-height: calc(var(--#{$_}--logotype-height-multiplier, 6) * #{tokens.getRawValue($_, "base")}); + --#{$_}--subfooter-logotype-height: calc(var(--#{$_}--subfooter-logotype-height-multiplier, 6) * #{tokens.getRawValue($_, "base")}); + --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, center); + --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, row); --inherit-color-background: var(--#{$_}--background-color); --inherit-color-contrast: var(--#{$_}--color); position: relative; @@ -91,9 +71,6 @@ $_: "c-footer"; background-position: var(--#{$_}--background-position); background-repeat: var(--#{$_}--background-repeat); background-size: var(--#{$_}--background-size); - background-attachment: var(--#{$_}--background-attachment); - background-origin: var(--#{$_}--background-origin); - background-clip: var(--#{$_}--background-clip); background-blend-mode: var(--#{$_}--background-blend-mode); } @@ -220,7 +197,7 @@ $_: "c-footer"; &__areas-wrapper>.o-container>.o-grid { display: grid; grid-template-columns: repeat(var(--#{$_}--columns-count), minmax(0, 1fr)); - gap: var(--#{$_}--column-gap); + gap: var(--#{$_}--gap); >[class*="o-grid-"] { flex: initial; @@ -235,7 +212,7 @@ $_: "c-footer"; max-width: var(--container-width); margin-left: auto; margin-right: auto; - padding: var(--#{$_}--body-padding-y) var(--#{$_}--body-padding-x); + padding: #{tokens.getCalculatedValue($_, "space", 5)} 0; display: grid; grid-template-areas: "home nav"; grid-template-columns: auto minmax(0, 1fr); @@ -295,9 +272,9 @@ $_: "c-footer"; } &__widget-area { - gap: calc(var(--#{$_}--column-gap) * var(--#{$_}--widget-area-gap-multiplier, 0.25)); + gap: calc(var(--#{$_}--gap) * 0.25); text-align: var(--#{$_}--text-align); - max-width: var(--#{$_}--widget-area-max-width, #{tokens.getCalculatedValue($_, "space", 70)}); + max-width: #{tokens.getCalculatedValue($_, "space", 70)}; margin-left: auto; margin-right: auto; @@ -368,17 +345,16 @@ $_: "c-footer"; } } - &--header-border { - --#{$_}--header-border-style: solid; - } - &__header-wrapper { - border-bottom: tokens.getCalculatedValue($_, "border-width") var(--#{$_}--header-border-style, none) var(--#{$_}--border-color); - padding-bottom: var(--#{$_}--header-border-padding-bottom, 0); + border-bottom: none; + padding-bottom: 0; } &--header-border { - --#{$_}--header-border-padding-bottom: #{tokens.getCalculatedValue($_, "space", 3)}; + .#{$_}__header-wrapper { + border-bottom: tokens.getCalculatedValue($_, "border-width") solid var(--#{$_}--border-color); + padding-bottom: tokens.getCalculatedValue($_, "space", 3); + } } &__subfooter { @@ -410,7 +386,7 @@ $_: "c-footer"; flex-wrap: wrap; gap: var(--#{$_}--gap); justify-content: var(--#{$_}--subfooter-justify-content-value); - align-items: var(--#{$_}--subfooter-align-items-value); + align-items: center; overflow: hidden; } @@ -420,10 +396,8 @@ $_: "c-footer"; flex-wrap: wrap; gap: 0; justify-content: center; - align-items: var(--#{$_}--subfooter-align-items-value); - margin: var(--#{$_}--subfooter-list-margin-block) var(--#{$_}--subfooter-list-margin-inline); - margin-left: var(--#{$_}--subfooter-list-margin-left); - margin-right: var(--#{$_}--subfooter-list-margin-right); + align-items: center; + margin: #{tokens.getCalculatedValue($_, "space", -1)} #{tokens.getCalculatedValue($_, "space", -5)}; padding: 0; list-style: none; overflow: hidden; @@ -432,27 +406,25 @@ $_: "c-footer"; li { position: relative; margin: 0; - padding: var(--#{$_}--subfooter-item-padding-block) var(--#{$_}--subfooter-item-padding-inline); - padding-left: var(--#{$_}--subfooter-item-padding-left); - padding-right: var(--#{$_}--subfooter-item-padding-right); + padding: #{tokens.getCalculatedValue($_, "space")} #{tokens.getCalculatedValue($_, "space", 5)} #{tokens.getCalculatedValue($_, "space")} 0; line-height: 1; &:before { content: ""; border-radius: 50%; background: var(--#{$_}--subfooter-separator-dot-color); - display: var(--#{$_}--subfooter-separator-display); + display: none; position: absolute; - width: var(--#{$_}--subfooter-separator-size); - height: var(--#{$_}--subfooter-separator-size); - left: var(--#{$_}--subfooter-separator-left); - right: var(--#{$_}--subfooter-separator-right); + width: #{tokens.getCalculatedValue($_, "base", 0.75)}; + height: #{tokens.getCalculatedValue($_, "base", 0.75)}; + left: #{tokens.getCalculatedValue($_, "base", -2.875)}; + right: auto; top: 50%; transform: translateY(-50%); } &:not(:first-child):before { - display: var(--#{$_}--subfooter-separator-display-between); + display: block; } } } @@ -475,8 +447,6 @@ $_: "c-footer"; &--align-flex-end { --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, flex-end); - --#{$_}--subfooter-list-margin-left: var(--#{$_}--subfooter-list-margin-inline); - --#{$_}--subfooter-list-margin-right: 0; } &--horizontal { @@ -484,8 +454,11 @@ $_: "c-footer"; } &--vertical { - --#{$_}--subfooter-separator-display-between: none; - --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, column); + --#{$_}--subfooter-flex-direction-value: column; + + .#{$_}__subfooter__list li:not(:first-child):before { + display: none; + } } } } @@ -493,10 +466,7 @@ $_: "c-footer"; @include mq.mq("xs", "md") { .#{$_} { - --#{$_}--body-padding-y: #{tokens.getCalculatedValue($_, "space", 5)}; - --#{$_}--body-padding-x: #{tokens.getCalculatedValue($_, "space", 4)}; --#{$_}--text-align: center; - --#{$_}--subfooter-separator-display-between: none; &__main-wrapper>.o-container>.o-grid, &__content-wrapper>.o-container>.o-grid, @@ -509,12 +479,17 @@ $_: "c-footer"; "home" "nav"; grid-template-columns: minmax(0, 1fr); + padding: #{tokens.getCalculatedValue($_, "space", 5)} #{tokens.getCalculatedValue($_, "space", 4)}; } &__home-link { justify-self: center; } + &__subfooter__list li:not(:first-child):before { + display: none; + } + &__logotype, &__subfooter__logotype { height: clamp(#{tokens.getCalculatedValue($_, "space", 3)}, calc(var(--#{$_}--logotype-height) * 0.75), var(--#{$_}--logotype-height)); From 73038f336ff5cd50cea645c86ed2e91a8b6e8cc8 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 25 Jun 2026 20:59:14 +0200 Subject: [PATCH 15/26] feat: add e2e tests --- source/components/footer/tests/footer.spec.ts | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 source/components/footer/tests/footer.spec.ts diff --git a/source/components/footer/tests/footer.spec.ts b/source/components/footer/tests/footer.spec.ts new file mode 100644 index 000000000..29c197c3a --- /dev/null +++ b/source/components/footer/tests/footer.spec.ts @@ -0,0 +1,368 @@ +import { expect, type Locator, type Page, test } from '@playwright/test'; + +/** + * Footer component - Design panel integration tests. + * + * These tests verify that every Footer component setting exposed in the + * Design Builder changes the rendered footer or its computed component values. + */ + +const FOOTER_PAGE = '/components/footer'; +const FAB_TRIGGER_LABEL = 'Open component customizer'; +const COMPONENT_SELECT = '#db-component-select'; +const FAB_PANEL_OPEN_CLASS = 'c-fab__panel--open'; +const CUSTOMIZER_PANEL = '.db-builder-customizer'; +const FOOTER_TARGET = '#site-footer-abc'; +const FOOTER_COMPONENT = '[data-component="footer"]'; + +const COLOR_SCHEME_LABEL = 'Color Scheme'; +const LOGOTYPE_HEIGHT_LABEL = 'Logotype Height Multiplier'; +const SUBFOOTER_LOGOTYPE_HEIGHT_LABEL = 'Subfooter Logotype Height Multiplier'; +const OUTER_PADDING_LABEL = 'Outer Padding Inset Multiplier'; +const GAP_LABEL = 'Gap Multiplier'; +const SUBFOOTER_BORDER_LABEL = 'Subfooter Top Border'; +const COLUMNS_LABEL = 'Number of Columns'; +const LOGOTYPE_ALIGNMENT_LABEL = 'Logotype Alignment'; +const TEXT_ALIGNMENT_LABEL = 'Text Alignment'; +const PREFOOTER_TEXT_ALIGNMENT_LABEL = 'Prefooter Text Alignment'; +const SUBFOOTER_JUSTIFY_LABEL = 'Subfooter Justify Content'; +const SUBFOOTER_DIRECTION_LABEL = 'Subfooter Direction'; +const BACKGROUND_OPACITY_LABEL = 'Background Opacity'; +const SUBFOOTER_BACKGROUND_OPACITY_LABEL = 'Subfooter Background Opacity'; +const BACKGROUND_ROTATION_LABEL = 'Background Rotation'; +const BACKGROUND_POSITION_LABEL = 'Background Position'; +const BACKGROUND_REPEAT_LABEL = 'Background Repeat'; +const BACKGROUND_SIZE_LABEL = 'Background Size'; +const BACKGROUND_BLEND_MODE_LABEL = 'Background Blend Mode'; + +const EXPECTED_VISIBLE_CONTROLS = [ + COLOR_SCHEME_LABEL, + LOGOTYPE_HEIGHT_LABEL, + SUBFOOTER_LOGOTYPE_HEIGHT_LABEL, + OUTER_PADDING_LABEL, + GAP_LABEL, + SUBFOOTER_BORDER_LABEL, + COLUMNS_LABEL, + LOGOTYPE_ALIGNMENT_LABEL, + TEXT_ALIGNMENT_LABEL, + PREFOOTER_TEXT_ALIGNMENT_LABEL, + SUBFOOTER_JUSTIFY_LABEL, + SUBFOOTER_DIRECTION_LABEL, + BACKGROUND_OPACITY_LABEL, + SUBFOOTER_BACKGROUND_OPACITY_LABEL, + BACKGROUND_ROTATION_LABEL, + BACKGROUND_POSITION_LABEL, + BACKGROUND_REPEAT_LABEL, + BACKGROUND_SIZE_LABEL, + BACKGROUND_BLEND_MODE_LABEL, +]; + +async function setRangeValue(rangeLocator: Locator, value: string): Promise { + await rangeLocator.evaluate((input: HTMLInputElement, val: string) => { + input.value = val; + input.dispatchEvent(new Event('input', { bubbles: true })); + }, value); +} + +async function expandCategory(categoryHeader: Locator): Promise { + const category = categoryHeader.locator('..'); + const isCollapsed = await category.evaluate((el) => el.classList.contains('db-category-collapsed')); + if (isCollapsed) { + await categoryHeader.click(); + } +} + +async function expandAllCategories(page: Page): Promise { + const headers = page.locator('.db-category-header'); + const count = await headers.count(); + + for (let index = 0; index < count; index += 1) { + await expandCategory(headers.nth(index)); + } +} + +async function getComputedPx(locator: Locator, property: string): Promise { + return locator.evaluate((el, prop) => Number.parseFloat(window.getComputedStyle(el).getPropertyValue(prop)), property); +} + +async function getComputedCss(locator: Locator, property: string): Promise { + return locator.evaluate((el, prop) => window.getComputedStyle(el).getPropertyValue(prop).trim(), property); +} + +async function getPseudoCss(locator: Locator, pseudo: string, property: string): Promise { + return locator.evaluate((el, { pseudoSelector, cssProperty }) => window.getComputedStyle(el, pseudoSelector).getPropertyValue(cssProperty).trim(), { + pseudoSelector: pseudo, + cssProperty: property, + }); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function getControlRow(page: Page, label: string): Promise { + const exactLabel = page.locator('.db-control-row-label-text').filter({ hasText: new RegExp(`^${escapeRegExp(label)}$`) }); + const row = page.locator('db-control-row').filter({ has: exactLabel }).first(); + await expect(row).toBeVisible(); + return row; +} + +async function setRangeControl(page: Page, label: string, value: string): Promise { + const row = await getControlRow(page, label); + const input = row.locator('input[type="range"]'); + await expect(input).toBeVisible(); + await setRangeValue(input, value); +} + +async function selectControlOption(page: Page, label: string, optionValue: string): Promise { + const row = await getControlRow(page, label); + const select = row.locator('select').first(); + await expect(select).toBeVisible(); + await select.selectOption(optionValue); +} + +async function chooseNextSwatch(page: Page, label: string): Promise { + const row = await getControlRow(page, label); + const swatchOption = row.locator('.db-swatch-select-option[aria-selected="false"]').first(); + await expect(swatchOption).toBeVisible(); + await swatchOption.click(); +} + +test.describe('Footer - design panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(FOOTER_PAGE); + await expect(page.locator(FOOTER_TARGET)).toBeVisible(); + + await page.getByRole('button', { name: FAB_TRIGGER_LABEL }).click(); + await expect(page.locator(`.${FAB_PANEL_OPEN_CLASS}`)).toBeVisible(); + await expect(page.locator(CUSTOMIZER_PANEL)).toBeVisible(); + await page.locator(COMPONENT_SELECT).selectOption({ value: 'footer' }); + }); + + test('opens the design panel when the FAB button is clicked', async ({ page }) => { + await expect(page.locator(CUSTOMIZER_PANEL)).toBeVisible(); + }); + + test('design panel shows footer component settings', async ({ page }) => { + await expect(page.locator('.db-category-header').filter({ hasText: 'Layout' })).toBeVisible(); + await expect(page.locator('.db-category-header').filter({ hasText: 'Background Image' })).toBeVisible(); + }); + + test('footer panel exposes the full expected control inventory', async ({ page }) => { + await expandAllCategories(page); + + const labels = await page.locator('db-control-row .db-control-row-label-text').evaluateAll((nodes) => nodes.map((node) => node.textContent?.trim() ?? '').filter(Boolean)); + + expect(labels.length).toBe(EXPECTED_VISIBLE_CONTROLS.length); + expect(labels.sort()).toEqual([...EXPECTED_VISIBLE_CONTROLS].sort()); + }); + + test('changing Color Scheme updates the footer background color', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Colors' })); + + const footer = page.locator(FOOTER_TARGET); + const initialBackground = await getComputedCss(footer, 'background-color'); + await chooseNextSwatch(page, COLOR_SCHEME_LABEL); + const updatedBackground = await getComputedCss(footer, 'background-color'); + + expect(updatedBackground).not.toBe(initialBackground); + }); + + test('changing Logotype Height Multiplier updates the footer logotype height', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Branding' })); + + const logotype = page.locator(`${FOOTER_TARGET} .c-footer__logotype`).first(); + const initialHeight = await getComputedPx(logotype, 'height'); + await setRangeControl(page, LOGOTYPE_HEIGHT_LABEL, '12'); + const updatedHeight = await getComputedPx(logotype, 'height'); + + expect(updatedHeight).toBeGreaterThan(initialHeight); + }); + + test('changing Subfooter Logotype Height Multiplier updates the subfooter logotype height', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Branding' })); + + const logotype = page.locator(`${FOOTER_TARGET} .c-footer__subfooter__logotype`).first(); + const initialHeight = await getComputedPx(logotype, 'height'); + await setRangeControl(page, SUBFOOTER_LOGOTYPE_HEIGHT_LABEL, '12'); + const updatedHeight = await getComputedPx(logotype, 'height'); + + expect(updatedHeight).toBeGreaterThan(initialHeight); + }); + + test('changing Outer Padding Inset Multiplier updates main footer padding', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const mainWrapper = page.locator(`${FOOTER_TARGET} .c-footer__main-wrapper`).first(); + const initialPadding = await getComputedPx(mainWrapper, 'padding-left'); + await setRangeControl(page, OUTER_PADDING_LABEL, '8'); + const updatedPadding = await getComputedPx(mainWrapper, 'padding-left'); + + expect(updatedPadding).toBeGreaterThan(initialPadding); + }); + + test('changing Gap Multiplier updates footer grid gap', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const mainWrapper = page.locator(`${FOOTER_TARGET} .c-footer__main-wrapper`).first(); + const initialGap = await getComputedPx(mainWrapper, 'gap'); + await setRangeControl(page, GAP_LABEL, '2.5'); + const updatedGap = await getComputedPx(mainWrapper, 'gap'); + + expect(updatedGap).toBeGreaterThan(initialGap); + }); + + test('changing Subfooter Top Border toggles the subfooter border width', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const subfooter = page.locator(`${FOOTER_TARGET} .c-footer__subfooter`).first(); + const initialBorderWidth = await getComputedPx(subfooter, 'border-top-width'); + await selectControlOption(page, SUBFOOTER_BORDER_LABEL, 'var(--c-footer--subfooter-border-width-token)'); + const updatedBorderWidth = await getComputedPx(subfooter, 'border-top-width'); + + expect(initialBorderWidth).toBe(0); + expect(updatedBorderWidth).toBeGreaterThan(initialBorderWidth); + }); + + test('changing Number of Columns updates the footer content grid columns', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const grid = page.locator(`${FOOTER_TARGET} .c-footer__content-wrapper > .o-container > .o-grid`).first(); + await selectControlOption(page, COLUMNS_LABEL, '4'); + const columns = await getComputedCss(grid, 'grid-template-columns'); + + expect(columns.split(' ').length).toBe(4); + }); + + test('changing Logotype Alignment updates the logotype wrapper alignment', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const headerGrid = page.locator(`${FOOTER_TARGET} .c-footer__header-wrapper .o-grid-12`).first(); + await selectControlOption(page, LOGOTYPE_ALIGNMENT_LABEL, 'flex-end'); + await expect(headerGrid).toHaveCSS('justify-content', 'flex-end'); + }); + + test('changing Text Alignment updates footer widget text alignment', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const widget = page.locator(`${FOOTER_TARGET} .c-footer__widget-area`).first(); + await selectControlOption(page, TEXT_ALIGNMENT_LABEL, 'center'); + await expect(widget).toHaveCSS('text-align', 'center'); + }); + + test('changing Prefooter Text Alignment updates prefooter text alignment', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const prefooter = page.locator(`${FOOTER_TARGET} .c-footer__prefooter-wrapper`).first(); + await selectControlOption(page, PREFOOTER_TEXT_ALIGNMENT_LABEL, 'right'); + await expect(prefooter).toHaveCSS('text-align', 'right'); + }); + + test('changing Subfooter Justify Content updates wrapper distribution', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const wrapper = page.locator(`${FOOTER_TARGET} .c-footer__subfooter__wrapper`).first(); + await selectControlOption(page, SUBFOOTER_JUSTIFY_LABEL, 'space-between'); + await expect(wrapper).toHaveCSS('justify-content', 'space-between'); + }); + + test('changing Subfooter Direction reverses the wrapper direction', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const wrapper = page.locator(`${FOOTER_TARGET} .c-footer__subfooter__wrapper`).first(); + await selectControlOption(page, SUBFOOTER_DIRECTION_LABEL, 'row-reverse'); + await expect(wrapper).toHaveCSS('flex-direction', 'row-reverse'); + }); + + test('changing Background Opacity updates the footer background layer opacity', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + await setRangeControl(page, BACKGROUND_OPACITY_LABEL, '0.25'); + const opacity = await getPseudoCss(footer, '::before', 'opacity'); + + expect(opacity).toBe('0.25'); + }); + + test('changing Subfooter Background Opacity updates the subfooter background color', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const subfooter = page.locator(`${FOOTER_TARGET} .c-footer__subfooter`).first(); + const initialBackground = await getComputedCss(subfooter, 'background-color'); + await setRangeControl(page, SUBFOOTER_BACKGROUND_OPACITY_LABEL, '30'); + const updatedBackground = await getComputedCss(subfooter, 'background-color'); + + expect(updatedBackground).not.toBe(initialBackground); + }); + + test('changing Background Rotation updates the footer background transform', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + const initialTransform = await getPseudoCss(footer, '::before', 'transform'); + await setRangeControl(page, BACKGROUND_ROTATION_LABEL, '7'); + const updatedTransform = await getPseudoCss(footer, '::before', 'transform'); + + expect(updatedTransform).not.toBe(initialTransform); + }); + + test('changing Background Position updates the footer background layer position', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + await selectControlOption(page, BACKGROUND_POSITION_LABEL, 'left top'); + const position = await getPseudoCss(footer, '::before', 'background-position'); + + expect(position).toBe('0% 0%'); + }); + + test('changing Background Repeat updates the footer background layer repeat mode', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + await selectControlOption(page, BACKGROUND_REPEAT_LABEL, 'repeat-x'); + const repeat = await getPseudoCss(footer, '::before', 'background-repeat'); + + expect(repeat).toBe('repeat-x'); + }); + + test('changing Background Size updates the footer background layer size', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + await selectControlOption(page, BACKGROUND_SIZE_LABEL, 'contain'); + const size = await getPseudoCss(footer, '::before', 'background-size'); + + expect(size).toBe('contain'); + }); + + test('changing Background Blend Mode updates the footer background layer blend mode', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Background Image' })); + + const footer = page.locator(FOOTER_TARGET); + await selectControlOption(page, BACKGROUND_BLEND_MODE_LABEL, 'overlay'); + const blendMode = await getPseudoCss(footer, '::before', 'background-blend-mode'); + + expect(blendMode).toBe('overlay'); + }); + + test('resetting the footer component restores the original gap', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); + + const mainWrapper = page.locator(`${FOOTER_TARGET} .c-footer__main-wrapper`).first(); + const initialGap = await getComputedPx(mainWrapper, 'gap'); + await setRangeControl(page, GAP_LABEL, '2.5'); + expect(await getComputedPx(mainWrapper, 'gap')).toBeGreaterThan(initialGap); + + page.once('dialog', (dialog) => dialog.accept()); + await page.locator('[data-action="reset-component"]').click(); + + const restoredGap = await getComputedPx(mainWrapper, 'gap'); + expect(restoredGap).toBeCloseTo(initialGap, 1); + }); + + test('footer page has a footer component target for design settings', async ({ page }) => { + await expect(page.locator(FOOTER_COMPONENT).first()).toBeVisible(); + await expect(page.locator(FOOTER_TARGET)).toBeVisible(); + }); +}); From 70848576aee2c4715797f0525c05c7a6764ad5cf Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 08:42:12 +0200 Subject: [PATCH 16/26] fix: rename field labels --- source/components/footer/component.json | 30 +++++++++---------- .../lorem-columns-with-subfooter.blade.php | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/source/components/footer/component.json b/source/components/footer/component.json index fb7a6551b..6b9199c01 100644 --- a/source/components/footer/component.json +++ b/source/components/footer/component.json @@ -34,7 +34,7 @@ { "id": "colors", "label": "Colors", - "description": "Component-local footer color scheme.", + "description": "Footer color scheme.", "settings": [ { "label": "Color Scheme", @@ -47,7 +47,7 @@ { "id": "branding", "label": "Branding", - "description": "Component-local branding adjustments.", + "description": "Branding adjustments.", "settings": [ { "variable": "--logotype-height-multiplier", @@ -76,12 +76,12 @@ { "id": "layout", "label": "Layout", - "description": "Component-local footer spacing, columns, and alignment.", + "description": "Footer spacing, columns, and alignment.", "settings": [ { "variable": "--outer-padding-inset-multiplier", - "label": "Outer Padding Inset Multiplier", - "description": "Scales footer outer padding from the inherited inset rhythm without overriding the internal resolved inset variable.", + "label": "Padding Multiplier", + "description": "Scales footer outer padding.", "type": "range", "default": "1", "min": 0, @@ -92,7 +92,7 @@ { "variable": "--gap-multiplier", "label": "Gap Multiplier", - "description": "Scales footer grid gaps from the component space token independently of outer padding.", + "description": "Scales footer gaps.", "type": "range", "default": "1", "min": 1, @@ -186,7 +186,7 @@ }, { "variable": "--prefooter-text-align", - "label": "Prefooter Text Alignment", + "label": "Text Alignment [Prefooter/Below Logotype]", "description": "How to align text in the prefooter.", "type": "select", "default": "left", @@ -207,8 +207,8 @@ }, { "variable": "--subfooter-alignment", - "label": "Subfooter Justify Content", - "description": "How to distribute the subfooter logo and links group inside the wrapper.", + "label": "Subfooter Alignment", + "description": "How to distribute the subfooter logo and links group.", "type": "select", "default": "center", "options": [ @@ -260,12 +260,12 @@ { "id": "background", "label": "Background Image", - "description": "Component-local background image controls.", + "description": "Background image controls.", "settings": [ { "variable": "--background-opacity", "label": "Background Opacity", - "description": "Controls the opacity of the footer background layer without changing content opacity.", + "description": "Controls the opacity of the footer background image layer.", "type": "range", "default": "1", "min": 0, @@ -276,7 +276,7 @@ { "variable": "--subfooter-background-opacity", "label": "Subfooter Background Opacity", - "description": "Controls how much of the subfooter background color is mixed into the rendered background.", + "description": "Controls the opacity of the subfooter background layer.", "type": "range", "default": "100", "min": 0, @@ -287,7 +287,7 @@ { "variable": "--background-rotation", "label": "Background Rotation", - "description": "Rotates the footer background layer without rotating footer content.", + "description": "Rotates the footer background image.", "type": "range", "default": "0", "min": -7, @@ -381,7 +381,7 @@ { "variable": "--background-blend-mode", "label": "Background Blend Mode", - "description": "Controls the CSS background-blend-mode value.", + "description": "Controls the CSS background-blend-mode.", "type": "select", "default": "normal", "options": [ @@ -391,7 +391,7 @@ }, { "value": "overlay", - "label": "Overlay" + "label": "Overlay (mixes background image with background color)" } ] } diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index 05fe2eac8..e5997c727 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -15,7 +15,7 @@ 'alignment' => 'center', 'content' => [ [ - 'title' => '', + 'title' => 'Tis is a title', 'content' => 'Lorem ipsum', 'link' => '#lorem-ipsum', ], From e67f4bb45d436e537dac084f301df248820e2fad Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 09:34:53 +0200 Subject: [PATCH 17/26] fix: resolve link colors. --- component-design-tokens.json | 30 +++++++++---------- .../lorem-columns-with-subfooter.blade.php | 5 ++++ source/components/footer/style.scss | 21 +++++-------- source/components/footer/tests/footer.spec.ts | 6 ++-- source/components/link/style.scss | 21 +++++++------ 5 files changed, 40 insertions(+), 43 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 0ae90ef3d..366bc0c9b 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -893,7 +893,7 @@ { "id": "colors", "label": "Colors", - "description": "Component-local footer color scheme.", + "description": "Footer color scheme.", "settings": [ { "label": "Color Scheme", @@ -906,7 +906,7 @@ { "id": "branding", "label": "Branding", - "description": "Component-local branding adjustments.", + "description": "Branding adjustments.", "settings": [ { "variable": "--logotype-height-multiplier", @@ -935,12 +935,12 @@ { "id": "layout", "label": "Layout", - "description": "Component-local footer spacing, columns, and alignment.", + "description": "Footer spacing, columns, and alignment.", "settings": [ { "variable": "--outer-padding-inset-multiplier", - "label": "Outer Padding Inset Multiplier", - "description": "Scales footer outer padding from the inherited inset rhythm without overriding the internal resolved inset variable.", + "label": "Padding Multiplier", + "description": "Scales footer outer padding.", "type": "range", "default": "1", "min": 0, @@ -951,7 +951,7 @@ { "variable": "--gap-multiplier", "label": "Gap Multiplier", - "description": "Scales footer grid gaps from the component space token independently of outer padding.", + "description": "Scales footer gaps.", "type": "range", "default": "1", "min": 1, @@ -1045,7 +1045,7 @@ }, { "variable": "--prefooter-text-align", - "label": "Prefooter Text Alignment", + "label": "Text Alignment [Prefooter/Below Logotype]", "description": "How to align text in the prefooter.", "type": "select", "default": "left", @@ -1066,8 +1066,8 @@ }, { "variable": "--subfooter-alignment", - "label": "Subfooter Justify Content", - "description": "How to distribute the subfooter logo and links group inside the wrapper.", + "label": "Subfooter Alignment", + "description": "How to distribute the subfooter logo and links group.", "type": "select", "default": "center", "options": [ @@ -1119,12 +1119,12 @@ { "id": "background", "label": "Background Image", - "description": "Component-local background image controls.", + "description": "Background image controls.", "settings": [ { "variable": "--background-opacity", "label": "Background Opacity", - "description": "Controls the opacity of the footer background layer without changing content opacity.", + "description": "Controls the opacity of the footer background image layer.", "type": "range", "default": "1", "min": 0, @@ -1135,7 +1135,7 @@ { "variable": "--subfooter-background-opacity", "label": "Subfooter Background Opacity", - "description": "Controls how much of the subfooter background color is mixed into the rendered background.", + "description": "Controls the opacity of the subfooter background layer.", "type": "range", "default": "100", "min": 0, @@ -1146,7 +1146,7 @@ { "variable": "--background-rotation", "label": "Background Rotation", - "description": "Rotates the footer background layer without rotating footer content.", + "description": "Rotates the footer background image.", "type": "range", "default": "0", "min": -7, @@ -1240,7 +1240,7 @@ { "variable": "--background-blend-mode", "label": "Background Blend Mode", - "description": "Controls the CSS background-blend-mode value.", + "description": "Controls the CSS background-blend-mode.", "type": "select", "default": "normal", "options": [ @@ -1250,7 +1250,7 @@ }, { "value": "overlay", - "label": "Overlay" + "label": "Overlay (mixes background image with background color)" } ] } diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index e5997c727..afe0c2d74 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -105,6 +105,11 @@ Do eiusmod tempor @endlink +
  • + + No link component + +
  • diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 4e6fd9502..1e7d1d158 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -1,4 +1,5 @@ @use "../../sass/mixin/inset-multiplier" as insetMultiplier; +@use "../../sass/mixin/inherit"; @use "../../sass/mixin/mq"; @use "../../sass/mixin/tokens"; @@ -44,8 +45,9 @@ $_: "c-footer"; --#{$_}--subfooter-logotype-height: calc(var(--#{$_}--subfooter-logotype-height-multiplier, 6) * #{tokens.getRawValue($_, "base")}); --#{$_}--subfooter-justify-content-value: var(--#{$_}--subfooter-alignment, center); --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, row); - --inherit-color-background: var(--#{$_}--background-color); - --inherit-color-contrast: var(--#{$_}--color); + @include inherit.publish("color-background", var(--#{$_}--background-color)); + @include inherit.publish("color-contrast", var(--#{$_}--color)); + @include inherit.publish("color-primary", #{tokens.getRawValue($_, "color--primary")}); position: relative; isolation: isolate; overflow: hidden; @@ -79,13 +81,6 @@ $_: "c-footer"; z-index: 1; } - a, - a:hover, - a:visited, - a:active { - color: var(--#{$_}--color); - } - &--primary { --#{$_}--background-color: #{tokens.getRawValue($_, "color--primary")}; --#{$_}--color: #{tokens.getRawValue($_, "color--primary-contrast")}; @@ -373,11 +368,9 @@ $_: "c-footer"; margin-right: auto; } - a, - a:hover, - a:visited, - a:active { - color: var(--#{$_}--subfooter-color); + [data-component="link"] { + @include inherit.publish("color-contrast", var(--#{$_}--subfooter-color)); + @include inherit.publish("color-primary", #{tokens.getRawValue($_, "color--primary")}); } &__wrapper { diff --git a/source/components/footer/tests/footer.spec.ts b/source/components/footer/tests/footer.spec.ts index 29c197c3a..a20318667 100644 --- a/source/components/footer/tests/footer.spec.ts +++ b/source/components/footer/tests/footer.spec.ts @@ -18,14 +18,14 @@ const FOOTER_COMPONENT = '[data-component="footer"]'; const COLOR_SCHEME_LABEL = 'Color Scheme'; const LOGOTYPE_HEIGHT_LABEL = 'Logotype Height Multiplier'; const SUBFOOTER_LOGOTYPE_HEIGHT_LABEL = 'Subfooter Logotype Height Multiplier'; -const OUTER_PADDING_LABEL = 'Outer Padding Inset Multiplier'; +const OUTER_PADDING_LABEL = 'Padding Multiplier'; const GAP_LABEL = 'Gap Multiplier'; const SUBFOOTER_BORDER_LABEL = 'Subfooter Top Border'; const COLUMNS_LABEL = 'Number of Columns'; const LOGOTYPE_ALIGNMENT_LABEL = 'Logotype Alignment'; const TEXT_ALIGNMENT_LABEL = 'Text Alignment'; -const PREFOOTER_TEXT_ALIGNMENT_LABEL = 'Prefooter Text Alignment'; -const SUBFOOTER_JUSTIFY_LABEL = 'Subfooter Justify Content'; +const PREFOOTER_TEXT_ALIGNMENT_LABEL = 'Text Alignment [Prefooter/Below Logotype]'; +const SUBFOOTER_JUSTIFY_LABEL = 'Subfooter Alignment'; const SUBFOOTER_DIRECTION_LABEL = 'Subfooter Direction'; const BACKGROUND_OPACITY_LABEL = 'Background Opacity'; const SUBFOOTER_BACKGROUND_OPACITY_LABEL = 'Subfooter Background Opacity'; diff --git a/source/components/link/style.scss b/source/components/link/style.scss index 19877b517..593102466 100644 --- a/source/components/link/style.scss +++ b/source/components/link/style.scss @@ -1,9 +1,8 @@ +@use "../../sass/mixin/inherit"; @use "../../sass/mixin/tokens"; -@property --inherit-color-contrast { - syntax: "*"; - inherits: true; -} +@include inherit.property("color-contrast"); +@include inherit.property("color-primary"); $_: "c-link"; @@ -11,17 +10,17 @@ $_: "c-link"; a, .#{$_} { - --#{$_}-mixable-color: #{tokens.getRawValue($_, "color--background-contrast", "color-contrast")}; - --#{$_}-mixable-color-saturated: #{tokens.getRawValue($_, "color--primary")}; - --#{$_}-link-color-mix: color-mix(in srgb, var(--#{$_}-mixable-color) var(--#{$_}--color-mix-amount, 90%), var(--#{$_}-mixable-color-saturated)); - --#{$_}-state-link-color-mix: color-mix(in srgb, var(--#{$_}-mixable-color) var(--#{$_}--state-color-mix-amount, 50%), var(--#{$_}-mixable-color-saturated)); + @include inherit.consume($_, "color-contrast", tokens.getRawValue($_, "color--background-contrast"), "mixable-color"); + @include inherit.consume($_, "color-primary", tokens.getRawValue($_, "color--primary"), "mixable-color-saturated"); + --#{$_}--link-color-mix: color-mix(in srgb, var(--#{$_}--mixable-color) var(--#{$_}--color-mix-amount, 90%), var(--#{$_}--mixable-color-saturated)); + --#{$_}--state-link-color-mix: color-mix(in srgb, var(--#{$_}--mixable-color) var(--#{$_}--state-color-mix-amount, 50%), var(--#{$_}--mixable-color-saturated)); } a:not([class^="c-"]):not([class*=" c-"]), .#{$_} { text-decoration-style: solid; transition: 100ms color; - color: var(--#{$_}-link-color-mix); + color: var(--#{$_}--link-color-mix); i { text-decoration: none; @@ -39,12 +38,12 @@ a:not([class^="c-"]):not([class*=" c-"]), &:active, &:hover { - color: var(--#{$_}-state-link-color-mix); + color: var(--#{$_}--state-link-color-mix); } &:visited, &:visited:hover, &:visited:active { - color: var(--#{$_}-state-link-color-mix); + color: var(--#{$_}--state-link-color-mix); } } \ No newline at end of file From 5478ecab99a784904b67676a0f297d0a213c2c00 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 11:31:27 +0200 Subject: [PATCH 18/26] fix: native masking ability in logotype --- component-design-tokens.json | 5 ++- source/components/logotype/component.json | 2 +- .../logotype/examples/examples.json | 4 +++ .../logotype/examples/masked.blade.php | 10 ++++++ source/components/logotype/style.scss | 33 ++++++++++++++++++- views/layout/master.blade.php | 25 +++----------- views/layout/partials/doc-nav.blade.php | 5 ++- 7 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 source/components/logotype/examples/masked.blade.php diff --git a/component-design-tokens.json b/component-design-tokens.json index 366bc0c9b..1e8a0316c 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1736,7 +1736,10 @@ "apiVersion": 1, "name": "Logotype", "slug": "logotype", - "tokens": [], + "tokens": [ + "color--surface-contrast", + "color--primary" + ], "description": "The Logotype component provides a reusable pattern for logotype in the interface.", "tags": [ "logotype", diff --git a/source/components/logotype/component.json b/source/components/logotype/component.json index 748d0f070..a1cd2d4e4 100644 --- a/source/components/logotype/component.json +++ b/source/components/logotype/component.json @@ -3,7 +3,7 @@ "apiVersion": 1, "name": "Logotype", "slug": "logotype", - "tokens": [], + "tokens": ["color--surface-contrast", "color--primary"], "description": "The Logotype component provides a reusable pattern for logotype in the interface.", "tags": ["logotype", "component"], "similarComponents": ["logotypegrid", "footer"], diff --git a/source/components/logotype/examples/examples.json b/source/components/logotype/examples/examples.json index 99646b6e0..14fd0e138 100644 --- a/source/components/logotype/examples/examples.json +++ b/source/components/logotype/examples/examples.json @@ -18,5 +18,9 @@ "linked": { "heading": "Logotype in a link", "text": "Renders a logotype wrapped in an anchor tag, activating the hover highlight effect." + }, + "masked": { + "heading": "Logotype masked", + "text": "Renders a masked logotype using --c-logotype--mask-image in markup. Color is controlled by inherited tokens such as --inherit-color-contrast and --inherit-color-primary." } } diff --git a/source/components/logotype/examples/masked.blade.php b/source/components/logotype/examples/masked.blade.php new file mode 100644 index 000000000..2ff8e8982 --- /dev/null +++ b/source/components/logotype/examples/masked.blade.php @@ -0,0 +1,10 @@ +
    + @logotype([ + 'src' => '/assets/img/logotype.svg', + 'alt' => 'Masked logotype', + 'attributeList' => [ + 'style' => '--c-logotype--mask-image: url("/assets/img/logotype.svg");' + ] + ]) + @endlogotype +
    diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index 66cfbb374..d5ea09a82 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -1,17 +1,24 @@ +@use "../../sass/mixin/inherit"; @use "../../sass/mixin/tokens"; $_: "c-logotype"; @include tokens.create($_, getComponentTokens($_)); +@include inherit.property("color-contrast"); +@include inherit.property("color-primary"); .#{$_} { + @include inherit.consume($_, "color-contrast", tokens.getRawValue($_, "color--surface-contrast"), "mask-color"); + @include inherit.consume($_, "color-primary", tokens.getRawValue($_, "color--primary"), "mask-color-hover"); margin-block-start: 0; margin-block-end: 0; margin-inline-start: 0; margin-inline-end: 0; + display: inline-block; position: relative; - img { + .#{$_}__image { + display: block; height: inherit; } @@ -26,4 +33,28 @@ $_: "c-logotype"; &.#{$_}--align-right { text-align: right; } + + &.#{$_}--masked, + &[data-logotype-mask="true"], + &[style*="--c-logotype--mask-image"] { + background-color: var(--#{$_}--mask-color); + transition: background-color 0.3s ease; + -webkit-mask-image: var(--#{$_}--mask-image); + -webkit-mask-repeat: var(--#{$_}--mask-repeat, no-repeat); + -webkit-mask-position: var(--#{$_}--mask-position, center); + -webkit-mask-size: var(--#{$_}--mask-size, contain); + mask-image: var(--#{$_}--mask-image); + mask-repeat: no-repeat; + mask-position: var(--#{$_}--mask-position, center); + mask-size: var(--#{$_}--mask-size, contain); + + .#{$_}__image { + visibility: hidden; + } + + &:hover, + a:hover & { + background-color: var(--#{$_}--mask-color-hover, var(--#{$_}--mask-color)); + } + } } \ No newline at end of file diff --git a/views/layout/master.blade.php b/views/layout/master.blade.php index 33f1ca378..0397bac86 100644 --- a/views/layout/master.blade.php +++ b/views/layout/master.blade.php @@ -45,25 +45,6 @@ - - -
    @@ -173,7 +154,11 @@ 'id' => 'main-logo', 'src' => '/assets/img/logotype.svg', 'alt' => 'Go to homepage', - 'classList' => ['c-header__logotype'] + 'classList' => ['c-header__logotype'], + 'attributeList' => [ + 'data-logotype-mask' => 'true', + 'style' => '--c-header--logotype-height: calc(var(--base) * 6); --c-logotype--mask-image: url("/assets/img/logotype.svg"); --inherit-color-contrast: var(--color--surface-contrast); --inherit-color-primary: var(--color--surface-contrast-muted);' + ], ]) @endlogotype @endlink diff --git a/views/layout/partials/doc-nav.blade.php b/views/layout/partials/doc-nav.blade.php index 51d240227..be11710a3 100644 --- a/views/layout/partials/doc-nav.blade.php +++ b/views/layout/partials/doc-nav.blade.php @@ -15,7 +15,10 @@ @logotype([ 'src'=> '/assets/img/logotype.svg', 'alt' => 'Go to homepage', - 'classList' => ['c-header__logotype'] + 'classList' => ['c-header__logotype'], + 'attributeList' => [ + 'style' => '--c-header--logotype-height: calc(var(--base) * 6); --c-logotype--mask-image: url("/assets/img/logotype.svg"); --inherit-color-contrast: var(--color--surface-contrast); --inherit-color-primary: var(--color--surface-contrast-muted);' + ] ]) @endlogotype @endlink From 9012f0c2f16df510da587c0e320556929aaa915c Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 15:35:07 +0200 Subject: [PATCH 19/26] fix: implement maskable, reduce code --- .../logotype/examples/masked.blade.php | 4 +--- source/components/logotype/style.scss | 19 +++++++++---------- views/layout/master.blade.php | 4 ++-- views/layout/partials/doc-nav.blade.php | 3 ++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/source/components/logotype/examples/masked.blade.php b/source/components/logotype/examples/masked.blade.php index 2ff8e8982..e2e2ccf97 100644 --- a/source/components/logotype/examples/masked.blade.php +++ b/source/components/logotype/examples/masked.blade.php @@ -2,9 +2,7 @@ @logotype([ 'src' => '/assets/img/logotype.svg', 'alt' => 'Masked logotype', - 'attributeList' => [ - 'style' => '--c-logotype--mask-image: url("/assets/img/logotype.svg");' - ] + 'maskable' => true, ]) @endlogotype
    diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index d5ea09a82..41cbcb994 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -8,8 +8,8 @@ $_: "c-logotype"; @include inherit.property("color-primary"); .#{$_} { - @include inherit.consume($_, "color-contrast", tokens.getRawValue($_, "color--surface-contrast"), "mask-color"); - @include inherit.consume($_, "color-primary", tokens.getRawValue($_, "color--primary"), "mask-color-hover"); + --#{$_}--mask-color: var(--inherit-color-contrast, currentColor); + --#{$_}--mask-color-hover: var(--inherit-color-primary, var(--#{$_}--mask-color)); margin-block-start: 0; margin-block-end: 0; margin-inline-start: 0; @@ -34,18 +34,17 @@ $_: "c-logotype"; text-align: right; } - &.#{$_}--masked, - &[data-logotype-mask="true"], - &[style*="--c-logotype--mask-image"] { + &[data-logotype-maskable], + &.#{$_}--maskable { background-color: var(--#{$_}--mask-color); transition: background-color 0.3s ease; - -webkit-mask-image: var(--#{$_}--mask-image); - -webkit-mask-repeat: var(--#{$_}--mask-repeat, no-repeat); - -webkit-mask-position: var(--#{$_}--mask-position, center); - -webkit-mask-size: var(--#{$_}--mask-size, contain); - mask-image: var(--#{$_}--mask-image); + -webkit-mask-image: var(--#{$_}--mask-image, none); + mask-image: var(--#{$_}--mask-image, none); + -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; + -webkit-mask-position: var(--#{$_}--mask-position, center); mask-position: var(--#{$_}--mask-position, center); + -webkit-mask-size: var(--#{$_}--mask-size, contain); mask-size: var(--#{$_}--mask-size, contain); .#{$_}__image { diff --git a/views/layout/master.blade.php b/views/layout/master.blade.php index 0397bac86..70b7bcb29 100644 --- a/views/layout/master.blade.php +++ b/views/layout/master.blade.php @@ -154,10 +154,10 @@ 'id' => 'main-logo', 'src' => '/assets/img/logotype.svg', 'alt' => 'Go to homepage', + 'maskable' => true, 'classList' => ['c-header__logotype'], 'attributeList' => [ - 'data-logotype-mask' => 'true', - 'style' => '--c-header--logotype-height: calc(var(--base) * 6); --c-logotype--mask-image: url("/assets/img/logotype.svg"); --inherit-color-contrast: var(--color--surface-contrast); --inherit-color-primary: var(--color--surface-contrast-muted);' + 'style' => '--c-header--logotype-height: calc(var(--base) * 6);' ], ]) @endlogotype diff --git a/views/layout/partials/doc-nav.blade.php b/views/layout/partials/doc-nav.blade.php index be11710a3..0afa1a667 100644 --- a/views/layout/partials/doc-nav.blade.php +++ b/views/layout/partials/doc-nav.blade.php @@ -15,9 +15,10 @@ @logotype([ 'src'=> '/assets/img/logotype.svg', 'alt' => 'Go to homepage', + 'maskable' => true, 'classList' => ['c-header__logotype'], 'attributeList' => [ - 'style' => '--c-header--logotype-height: calc(var(--base) * 6); --c-logotype--mask-image: url("/assets/img/logotype.svg"); --inherit-color-contrast: var(--color--surface-contrast); --inherit-color-primary: var(--color--surface-contrast-muted);' + 'style' => '--c-header--logotype-height: calc(var(--base) * 6);' ] ]) @endlogotype From a4b7434fe4cc56fae3a66ad124a560a916353d85 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 15:41:53 +0200 Subject: [PATCH 20/26] feat: add control to masking. --- component-design-tokens.json | 26 ++++++++++++++++ source/components/logotype/component.json | 26 ++++++++++++++++ source/components/logotype/style.scss | 37 +++++++++++++++-------- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/component-design-tokens.json b/component-design-tokens.json index 1e8a0316c..539da3513 100644 --- a/component-design-tokens.json +++ b/component-design-tokens.json @@ -1740,6 +1740,32 @@ "color--surface-contrast", "color--primary" ], + "componentSettings": [ + { + "id": "appearance", + "label": "Appearance", + "description": "Visual rendering options for logotype masking.", + "settings": [ + { + "variable": "--mask-enabled", + "label": "Masking", + "description": "Enables or disables logotype masking.", + "type": "select", + "default": "1", + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + } + ] + } + ], "description": "The Logotype component provides a reusable pattern for logotype in the interface.", "tags": [ "logotype", diff --git a/source/components/logotype/component.json b/source/components/logotype/component.json index a1cd2d4e4..7839eeb15 100644 --- a/source/components/logotype/component.json +++ b/source/components/logotype/component.json @@ -4,6 +4,32 @@ "name": "Logotype", "slug": "logotype", "tokens": ["color--surface-contrast", "color--primary"], + "componentSettings": [ + { + "id": "appearance", + "label": "Appearance", + "description": "Visual rendering options for logotype masking.", + "settings": [ + { + "variable": "--mask-enabled", + "label": "Masking", + "description": "Enables or disables logotype masking.", + "type": "select", + "default": "1", + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + } + ] + } + ], "description": "The Logotype component provides a reusable pattern for logotype in the interface.", "tags": ["logotype", "component"], "similarComponents": ["logotypegrid", "footer"], diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index 41cbcb994..e473ee104 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -10,6 +10,8 @@ $_: "c-logotype"; .#{$_} { --#{$_}--mask-color: var(--inherit-color-contrast, currentColor); --#{$_}--mask-color-hover: var(--inherit-color-primary, var(--#{$_}--mask-color)); + --#{$_}--mask-enabled: 1; + --#{$_}--mask-current-color: var(--#{$_}--mask-color); margin-block-start: 0; margin-block-end: 0; margin-inline-start: 0; @@ -36,24 +38,35 @@ $_: "c-logotype"; &[data-logotype-maskable], &.#{$_}--maskable { - background-color: var(--#{$_}--mask-color); - transition: background-color 0.3s ease; - -webkit-mask-image: var(--#{$_}--mask-image, none); - mask-image: var(--#{$_}--mask-image, none); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: var(--#{$_}--mask-position, center); - mask-position: var(--#{$_}--mask-position, center); - -webkit-mask-size: var(--#{$_}--mask-size, contain); - mask-size: var(--#{$_}--mask-size, contain); + --#{$_}--mask-opacity: calc(var(--#{$_}--mask-enabled) * 1); + --#{$_}--image-opacity: calc(1 - var(--#{$_}--mask-enabled)); + + &::before { + content: ""; + position: absolute; + inset: 0; + background-color: var(--#{$_}--mask-current-color); + opacity: var(--#{$_}--mask-opacity); + transition: opacity 0.3s ease, background-color 0.3s ease; + pointer-events: none; + -webkit-mask-image: var(--#{$_}--mask-image, none); + mask-image: var(--#{$_}--mask-image, none); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: var(--#{$_}--mask-position, center); + mask-position: var(--#{$_}--mask-position, center); + -webkit-mask-size: var(--#{$_}--mask-size, contain); + mask-size: var(--#{$_}--mask-size, contain); + } .#{$_}__image { - visibility: hidden; + opacity: var(--#{$_}--image-opacity); + transition: opacity 0.3s ease; } &:hover, a:hover & { - background-color: var(--#{$_}--mask-color-hover, var(--#{$_}--mask-color)); + --#{$_}--mask-current-color: var(--#{$_}--mask-color-hover, var(--#{$_}--mask-color)); } } } \ No newline at end of file From 7b8c21536be09cfdc4d38a02caabfd11576270d5 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Fri, 26 Jun 2026 16:10:21 +0200 Subject: [PATCH 21/26] fix: add tests for masking --- .../logotype/examples/examples.json | 2 +- .../logotype/examples/masked.blade.php | 3 +- .../logotype/tests/logotype.spec.ts | 159 ++++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 source/components/logotype/tests/logotype.spec.ts diff --git a/source/components/logotype/examples/examples.json b/source/components/logotype/examples/examples.json index 14fd0e138..fe9caa399 100644 --- a/source/components/logotype/examples/examples.json +++ b/source/components/logotype/examples/examples.json @@ -21,6 +21,6 @@ }, "masked": { "heading": "Logotype masked", - "text": "Renders a masked logotype using --c-logotype--mask-image in markup. Color is controlled by inherited tokens such as --inherit-color-contrast and --inherit-color-primary." + "text": "Renders a masked logotype using CSS mask properties." } } diff --git a/source/components/logotype/examples/masked.blade.php b/source/components/logotype/examples/masked.blade.php index e2e2ccf97..5558031d8 100644 --- a/source/components/logotype/examples/masked.blade.php +++ b/source/components/logotype/examples/masked.blade.php @@ -1,8 +1,9 @@ -
    +
    @logotype([ 'src' => '/assets/img/logotype.svg', 'alt' => 'Masked logotype', 'maskable' => true, + 'style' => 'height: 100px;', ]) @endlogotype
    diff --git a/source/components/logotype/tests/logotype.spec.ts b/source/components/logotype/tests/logotype.spec.ts new file mode 100644 index 000000000..876de2e40 --- /dev/null +++ b/source/components/logotype/tests/logotype.spec.ts @@ -0,0 +1,159 @@ +import { expect, type Locator, type Page, test } from '@playwright/test'; + +/** + * Logotype component – Design panel (component customizer) integration tests. + * + * These tests verify that the Logotype component settings exposed in the + * Design Builder change the rendered logotype or its computed CSS values. + * + * The design-builder target on the logotype page is the header logotype: + * `[data-component="logotype"]` (#main-logo), which is already maskable. + * + * Prerequisites: + * - The styleguide PHP server must be accessible at the configured base URL. + * - The project must have been built (`npm run build`) so the + * design-builder JS bundle is served. + */ + +const LOGOTYPE_PAGE = '/components/logotype'; +const FAB_TRIGGER_LABEL = 'Open component customizer'; +const COMPONENT_SELECT = '#db-component-select'; +const FAB_PANEL_OPEN_CLASS = 'c-fab__panel--open'; +const CUSTOMIZER_PANEL = '.db-builder-customizer'; +const LOGOTYPE_TARGET = '[data-component="logotype"]'; + +const MASKING_LABEL = 'Masking'; + +const EXPECTED_VISIBLE_CONTROLS = [MASKING_LABEL]; + +/** + * Expands a Design Builder category panel if it is currently collapsed. + */ +async function expandCategory(categoryHeader: Locator): Promise { + const category = categoryHeader.locator('..'); + const isCollapsed = await category.evaluate((el) => el.classList.contains('db-category-collapsed')); + if (isCollapsed) { + await categoryHeader.click(); + } +} + +/** + * Expands all customizer categories so all controls are visible and testable. + */ +async function expandAllCategories(page: Page): Promise { + const headers = page.locator('.db-category-header'); + const count = await headers.count(); + + for (let index = 0; index < count; index += 1) { + await expandCategory(headers.nth(index)); + } +} + +/** + * Returns the computed value of a CSS property on a DOM element. + */ +async function getComputedCss(locator: Locator, property: string): Promise { + return locator.evaluate((el, prop) => window.getComputedStyle(el).getPropertyValue(prop).trim(), property); +} + +/** + * Returns the computed opacity of a pseudo-element (::before or ::after). + */ +async function getPseudoOpacity(locator: Locator, pseudo: string): Promise { + return locator.evaluate((el, pseudoSelector) => window.getComputedStyle(el, pseudoSelector).getPropertyValue('opacity').trim(), pseudo); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Finds a design-builder control row by its label text. + */ +async function getControlRow(page: Page, label: string): Promise { + const exactLabel = page.locator('.db-control-row-label-text').filter({ hasText: new RegExp(`^${escapeRegExp(label)}$`) }); + const row = page.locator('db-control-row').filter({ has: exactLabel }).first(); + await expect(row).toBeVisible(); + return row; +} + +/** + * Selects an option in a select-type control row. + */ +async function selectControlOption(page: Page, label: string, optionValue: string): Promise { + const row = await getControlRow(page, label); + const select = row.locator('select').first(); + await expect(select).toBeVisible(); + await select.selectOption(optionValue); +} + +test.describe('Logotype – design panel', () => { + test.beforeEach(async ({ page }) => { + await page.goto(LOGOTYPE_PAGE); + await expect(page.locator(LOGOTYPE_TARGET)).toBeVisible(); + + await page.getByRole('button', { name: FAB_TRIGGER_LABEL }).click(); + await expect(page.locator(`.${FAB_PANEL_OPEN_CLASS}`)).toBeVisible(); + await expect(page.locator(CUSTOMIZER_PANEL)).toBeVisible(); + await page.locator(COMPONENT_SELECT).selectOption({ value: 'logotype' }); + }); + + test('opens the design panel when the FAB button is clicked', async ({ page }) => { + await expect(page.locator(CUSTOMIZER_PANEL)).toBeVisible(); + }); + + test('design panel shows logotype component settings', async ({ page }) => { + await expect(page.locator('.db-category-header').filter({ hasText: 'Appearance' })).toBeVisible(); + }); + + test('logotype panel exposes the full expected control inventory', async ({ page }) => { + await expandAllCategories(page); + + const labels = await page.locator('db-control-row .db-control-row-label-text').evaluateAll((nodes) => nodes.map((node) => node.textContent?.trim() ?? '').filter(Boolean)); + + for (const expectedLabel of EXPECTED_VISIBLE_CONTROLS) { + expect(labels).toContain(expectedLabel); + } + }); + + test('Masking defaults to On – image is hidden and ::before is visible', async ({ page }) => { + const logotype = page.locator(LOGOTYPE_TARGET).first(); + const imageOpacity = await getComputedCss(logotype.locator('.c-logotype__image'), 'opacity'); + const beforeOpacity = await getPseudoOpacity(logotype, '::before'); + + expect(imageOpacity).toBe('0'); + expect(beforeOpacity).toBe('1'); + }); + + test('setting Masking to Off reveals the image and hides the ::before mask', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Appearance' })); + + const logotype = page.locator(LOGOTYPE_TARGET).first(); + + const initialImageOpacity = await getComputedCss(logotype.locator('.c-logotype__image'), 'opacity'); + const initialBeforeOpacity = await getPseudoOpacity(logotype, '::before'); + expect(initialImageOpacity).toBe('0'); + expect(initialBeforeOpacity).toBe('1'); + + await selectControlOption(page, MASKING_LABEL, '0'); + + const updatedImageOpacity = await getComputedCss(logotype.locator('.c-logotype__image'), 'opacity'); + const updatedBeforeOpacity = await getPseudoOpacity(logotype, '::before'); + expect(updatedImageOpacity).toBe('1'); + expect(updatedBeforeOpacity).toBe('0'); + }); + + test('setting Masking back to On re-applies the mask', async ({ page }) => { + await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Appearance' })); + + await selectControlOption(page, MASKING_LABEL, '0'); + await selectControlOption(page, MASKING_LABEL, '1'); + + const logotype = page.locator(LOGOTYPE_TARGET).first(); + const imageOpacity = await getComputedCss(logotype.locator('.c-logotype__image'), 'opacity'); + const beforeOpacity = await getPseudoOpacity(logotype, '::before'); + + expect(imageOpacity).toBe('0'); + expect(beforeOpacity).toBe('1'); + }); +}); From e332ad0462ae1c9c4c36874bb8d53a3fece396a0 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Thu, 2 Jul 2026 15:48:38 +0200 Subject: [PATCH 22/26] fix: publish hover color for logo --- source/components/footer/style.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 1e7d1d158..9b090626f 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -25,6 +25,8 @@ $_: "c-footer"; --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--surface-border")}; --#{$_}--list-marker-color: color-mix(in srgb, var(--#{$_}--color) 30%, var(--#{$_}--border-color)); --#{$_}--subfooter-separator-dot-color: color-mix(in srgb, var(--#{$_}--subfooter-color) 30%, var(--#{$_}--subfooter-separator-color)); + --#{$_}--logotype-hover-color: var(--#{$_}--color); + --#{$_}--subfooter-logotype-hover-color: var(--#{$_}--subfooter-color); --#{$_}--subfooter-border-width: 0; --#{$_}--subfooter-border-width-token: #{tokens.getCalculatedValue($_, "border-width")}; --#{$_}--background-image: none; @@ -47,7 +49,7 @@ $_: "c-footer"; --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, row); @include inherit.publish("color-background", var(--#{$_}--background-color)); @include inherit.publish("color-contrast", var(--#{$_}--color)); - @include inherit.publish("color-primary", #{tokens.getRawValue($_, "color--primary")}); + @include inherit.publish("color-primary", var(--#{$_}--logotype-hover-color)); position: relative; isolation: isolate; overflow: hidden; @@ -89,6 +91,8 @@ $_: "c-footer"; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--primary-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--primary-contrast")}; --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--primary-border")}; + --#{$_}--logotype-hover-color: #{tokens.getRawValue($_, "color--secondary")}; + --#{$_}--subfooter-logotype-hover-color: #{tokens.getRawValue($_, "color--secondary")}; } &--secondary { @@ -99,6 +103,8 @@ $_: "c-footer"; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--secondary-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--secondary-contrast")}; --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--secondary-border")}; + --#{$_}--logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; + --#{$_}--subfooter-logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; } &--background { @@ -109,6 +115,8 @@ $_: "c-footer"; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--background-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--background-contrast")}; --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--background-border")}; + --#{$_}--logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; + --#{$_}--subfooter-logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; } &--logotype-align__left, @@ -370,7 +378,7 @@ $_: "c-footer"; [data-component="link"] { @include inherit.publish("color-contrast", var(--#{$_}--subfooter-color)); - @include inherit.publish("color-primary", #{tokens.getRawValue($_, "color--primary")}); + @include inherit.publish("color-primary", var(--#{$_}--subfooter-logotype-hover-color)); } &__wrapper { From 75e3019f9697afcba9a5cc360f19ab1c96ae7c57 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Sat, 4 Jul 2026 12:19:11 +0200 Subject: [PATCH 23/26] feat: check that hover colors is applied appropriate --- source/components/footer/style.scss | 9 +++++ source/components/footer/tests/footer.spec.ts | 38 +++++++++++++++++++ source/components/logotype/style.scss | 3 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 9b090626f..cae625e98 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -22,6 +22,7 @@ $_: "c-footer"; --#{$_}--subfooter-background-opacity: 100%; --#{$_}--subfooter-background: color-mix(in srgb, var(--#{$_}--subfooter-background-color) var(--#{$_}--subfooter-background-opacity), transparent); --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--surface-contrast")}; + --#{$_}--subfooter-color-muted: var(--#{$_}--color-muted); --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--surface-border")}; --#{$_}--list-marker-color: color-mix(in srgb, var(--#{$_}--color) 30%, var(--#{$_}--border-color)); --#{$_}--subfooter-separator-dot-color: color-mix(in srgb, var(--#{$_}--subfooter-color) 30%, var(--#{$_}--subfooter-separator-color)); @@ -49,6 +50,7 @@ $_: "c-footer"; --#{$_}--subfooter-flex-direction-value: var(--#{$_}--subfooter-flex-direction, row); @include inherit.publish("color-background", var(--#{$_}--background-color)); @include inherit.publish("color-contrast", var(--#{$_}--color)); + @include inherit.publish("color-contrast-muted", var(--#{$_}--color-muted)); @include inherit.publish("color-primary", var(--#{$_}--logotype-hover-color)); position: relative; isolation: isolate; @@ -378,6 +380,13 @@ $_: "c-footer"; [data-component="link"] { @include inherit.publish("color-contrast", var(--#{$_}--subfooter-color)); + @include inherit.publish("color-contrast-muted", var(--#{$_}--subfooter-color-muted)); + @include inherit.publish("color-primary", var(--#{$_}--subfooter-logotype-hover-color)); + } + + [data-component="logotype"] { + @include inherit.publish("color-contrast", var(--#{$_}--subfooter-color)); + @include inherit.publish("color-contrast-muted", var(--#{$_}--subfooter-color-muted)); @include inherit.publish("color-primary", var(--#{$_}--subfooter-logotype-hover-color)); } diff --git a/source/components/footer/tests/footer.spec.ts b/source/components/footer/tests/footer.spec.ts index a20318667..8141dc41b 100644 --- a/source/components/footer/tests/footer.spec.ts +++ b/source/components/footer/tests/footer.spec.ts @@ -96,6 +96,18 @@ async function getPseudoCss(locator: Locator, pseudo: string, property: string): }); } +async function resolveCssVariableColor(locator: Locator, variableName: string): Promise { + return locator.evaluate((el, customPropertyName) => { + const probe = document.createElement('span'); + probe.style.color = `var(${customPropertyName})`; + probe.style.display = 'none'; + el.appendChild(probe); + const resolvedColor = window.getComputedStyle(probe).color.trim(); + probe.remove(); + return resolvedColor; + }, variableName); +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -242,6 +254,17 @@ test.describe('Footer - design panel', () => { await expect(headerGrid).toHaveCSS('justify-content', 'flex-end'); }); + test('hovering the footer logotype applies the resolved hover mask color', async ({ page }) => { + const logotype = page.locator(`${FOOTER_TARGET} .c-footer__logotype`).first(); + const initialMaskColor = await getPseudoCss(logotype, '::before', 'background-color'); + const expectedHoverMaskColor = await resolveCssVariableColor(logotype, '--c-logotype--mask-color-hover'); + + await logotype.hover(); + + await expect.poll(async () => getPseudoCss(logotype, '::before', 'background-color')).toBe(expectedHoverMaskColor); + expect(expectedHoverMaskColor).not.toBe(initialMaskColor); + }); + test('changing Text Alignment updates footer widget text alignment', async ({ page }) => { await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); @@ -250,6 +273,21 @@ test.describe('Footer - design panel', () => { await expect(widget).toHaveCSS('text-align', 'center'); }); + test('hovering footer links applies the resolved link hover color', async ({ page }) => { + const componentLink = page.locator(`${FOOTER_TARGET} .c-footer__widget-area .c-link`).first(); + const plainLink = page.locator(`${FOOTER_TARGET} .c-footer__widget-area a[href="#lorem-link-5"]`).first(); + + const componentExpectedHoverColor = await resolveCssVariableColor(componentLink, '--c-link--state-link-color-mix'); + await componentLink.hover(); + await expect(componentLink).toHaveCSS('color', componentExpectedHoverColor); + + const plainInitialColor = await getComputedCss(plainLink, 'color'); + const plainExpectedHoverColor = await resolveCssVariableColor(plainLink, '--c-link--state-link-color-mix'); + await plainLink.hover(); + await expect(plainLink).toHaveCSS('color', plainExpectedHoverColor); + expect(plainExpectedHoverColor).toBeTruthy(); + }); + test('changing Prefooter Text Alignment updates prefooter text alignment', async ({ page }) => { await expandCategory(page.locator('.db-category-header').filter({ hasText: 'Layout' })); diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index e473ee104..ba3adf966 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -5,11 +5,12 @@ $_: "c-logotype"; @include tokens.create($_, getComponentTokens($_)); @include inherit.property("color-contrast"); +@include inherit.property("color-contrast-muted"); @include inherit.property("color-primary"); .#{$_} { --#{$_}--mask-color: var(--inherit-color-contrast, currentColor); - --#{$_}--mask-color-hover: var(--inherit-color-primary, var(--#{$_}--mask-color)); + --#{$_}--mask-color-hover: var(--inherit-color-contrast-muted, var(--inherit-color-primary, var(--#{$_}--mask-color))); --#{$_}--mask-enabled: 1; --#{$_}--mask-current-color: var(--#{$_}--mask-color); margin-block-start: 0; From e1d6258b9b431bbe94d8c79da2b9df2453609240 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Sat, 4 Jul 2026 12:27:53 +0200 Subject: [PATCH 24/26] fix: hover color for links --- source/components/footer/style.scss | 4 ++-- source/components/footer/tests/footer.spec.ts | 4 ++-- source/components/logotype/style.scss | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index cae625e98..8ffdf1e39 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -26,8 +26,8 @@ $_: "c-footer"; --#{$_}--subfooter-separator-color: #{tokens.getRawValue($_, "color--surface-border")}; --#{$_}--list-marker-color: color-mix(in srgb, var(--#{$_}--color) 30%, var(--#{$_}--border-color)); --#{$_}--subfooter-separator-dot-color: color-mix(in srgb, var(--#{$_}--subfooter-color) 30%, var(--#{$_}--subfooter-separator-color)); - --#{$_}--logotype-hover-color: var(--#{$_}--color); - --#{$_}--subfooter-logotype-hover-color: var(--#{$_}--subfooter-color); + --#{$_}--logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; + --#{$_}--subfooter-logotype-hover-color: #{tokens.getRawValue($_, "color--primary")}; --#{$_}--subfooter-border-width: 0; --#{$_}--subfooter-border-width-token: #{tokens.getCalculatedValue($_, "border-width")}; --#{$_}--background-image: none; diff --git a/source/components/footer/tests/footer.spec.ts b/source/components/footer/tests/footer.spec.ts index 8141dc41b..1808af40f 100644 --- a/source/components/footer/tests/footer.spec.ts +++ b/source/components/footer/tests/footer.spec.ts @@ -259,7 +259,8 @@ test.describe('Footer - design panel', () => { const initialMaskColor = await getPseudoCss(logotype, '::before', 'background-color'); const expectedHoverMaskColor = await resolveCssVariableColor(logotype, '--c-logotype--mask-color-hover'); - await logotype.hover(); + await logotype.scrollIntoViewIfNeeded(); + await logotype.hover({ force: true }); await expect.poll(async () => getPseudoCss(logotype, '::before', 'background-color')).toBe(expectedHoverMaskColor); expect(expectedHoverMaskColor).not.toBe(initialMaskColor); @@ -281,7 +282,6 @@ test.describe('Footer - design panel', () => { await componentLink.hover(); await expect(componentLink).toHaveCSS('color', componentExpectedHoverColor); - const plainInitialColor = await getComputedCss(plainLink, 'color'); const plainExpectedHoverColor = await resolveCssVariableColor(plainLink, '--c-link--state-link-color-mix'); await plainLink.hover(); await expect(plainLink).toHaveCSS('color', plainExpectedHoverColor); diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index ba3adf966..704f90e95 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -10,7 +10,7 @@ $_: "c-logotype"; .#{$_} { --#{$_}--mask-color: var(--inherit-color-contrast, currentColor); - --#{$_}--mask-color-hover: var(--inherit-color-contrast-muted, var(--inherit-color-primary, var(--#{$_}--mask-color))); + --#{$_}--mask-color-hover: var(--inherit-color-contrast-muted, var(--#{$_}--mask-color)); --#{$_}--mask-enabled: 1; --#{$_}--mask-current-color: var(--#{$_}--mask-color); margin-block-start: 0; From e292e9dcc031b8ff18fd0a00220545c476ff1c40 Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Sat, 4 Jul 2026 12:33:26 +0200 Subject: [PATCH 25/26] fix: add link to logo. --- .../footer/examples/lorem-columns-with-subfooter.blade.php | 1 + source/components/footer/tests/footer.spec.ts | 5 +++-- source/components/logotype/style.scss | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index afe0c2d74..e1c3b8243 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -6,6 +6,7 @@ @footer([ 'slotOnly' => true, 'id' => 'site-footer-abc', + 'logotypeHref' => '#logotype-link', 'classList' => ['site-footer', 's-footer'], 'logotype' => '/assets/img/logotype-white-full.svg', 'subfooterLogotype' => '/assets/img/logotype-white-full.svg', diff --git a/source/components/footer/tests/footer.spec.ts b/source/components/footer/tests/footer.spec.ts index 1808af40f..9c3fbb7c9 100644 --- a/source/components/footer/tests/footer.spec.ts +++ b/source/components/footer/tests/footer.spec.ts @@ -255,12 +255,13 @@ test.describe('Footer - design panel', () => { }); test('hovering the footer logotype applies the resolved hover mask color', async ({ page }) => { + const logotypeWrapper = page.locator(`${FOOTER_TARGET} .c-footer__header-wrapper .c-link`).first(); const logotype = page.locator(`${FOOTER_TARGET} .c-footer__logotype`).first(); const initialMaskColor = await getPseudoCss(logotype, '::before', 'background-color'); const expectedHoverMaskColor = await resolveCssVariableColor(logotype, '--c-logotype--mask-color-hover'); - await logotype.scrollIntoViewIfNeeded(); - await logotype.hover({ force: true }); + await logotypeWrapper.scrollIntoViewIfNeeded(); + await logotypeWrapper.hover({ force: true }); await expect.poll(async () => getPseudoCss(logotype, '::before', 'background-color')).toBe(expectedHoverMaskColor); expect(expectedHoverMaskColor).not.toBe(initialMaskColor); diff --git a/source/components/logotype/style.scss b/source/components/logotype/style.scss index 704f90e95..e4285a726 100644 --- a/source/components/logotype/style.scss +++ b/source/components/logotype/style.scss @@ -66,7 +66,9 @@ $_: "c-logotype"; } &:hover, - a:hover & { + a:hover &, + .c-link:hover &, + [data-component="link"]:hover & { --#{$_}--mask-current-color: var(--#{$_}--mask-color-hover, var(--#{$_}--mask-color)); } } From 0736c1bfa9fccacd2abdaca06ef7772d28fb5fec Mon Sep 17 00:00:00 2001 From: Sebastian Thulin Date: Sat, 4 Jul 2026 12:37:47 +0200 Subject: [PATCH 26/26] fix: contrast muted --- .../examples/lorem-columns-with-subfooter.blade.php | 2 +- source/components/footer/style.scss | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php index e1c3b8243..c686c264b 100644 --- a/source/components/footer/examples/lorem-columns-with-subfooter.blade.php +++ b/source/components/footer/examples/lorem-columns-with-subfooter.blade.php @@ -6,7 +6,7 @@ @footer([ 'slotOnly' => true, 'id' => 'site-footer-abc', - 'logotypeHref' => '#logotype-link', + 'logotypeHref' => '/', 'classList' => ['site-footer', 's-footer'], 'logotype' => '/assets/img/logotype-white-full.svg', 'subfooterLogotype' => '/assets/img/logotype-white-full.svg', diff --git a/source/components/footer/style.scss b/source/components/footer/style.scss index 8ffdf1e39..56794d373 100644 --- a/source/components/footer/style.scss +++ b/source/components/footer/style.scss @@ -16,7 +16,7 @@ $_: "c-footer"; --#{$_}--background-color: #{tokens.getRawValue($_, "color--surface")}; --#{$_}--color: #{tokens.getRawValue($_, "color--surface-contrast")}; - --#{$_}--color-muted: #{tokens.getRawValue($_, "color--surface-contrast-muted")}; + --#{$_}--color-muted: color-mix(in srgb, var(--#{$_}--color) 55%, var(--#{$_}--background-color)); --#{$_}--border-color: #{tokens.getRawValue($_, "color--surface-border")}; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--surface-alt")}; --#{$_}--subfooter-background-opacity: 100%; @@ -88,7 +88,7 @@ $_: "c-footer"; &--primary { --#{$_}--background-color: #{tokens.getRawValue($_, "color--primary")}; --#{$_}--color: #{tokens.getRawValue($_, "color--primary-contrast")}; - --#{$_}--color-muted: #{tokens.getRawValue($_, "color--primary-contrast")}; + --#{$_}--color-muted: color-mix(in srgb, var(--#{$_}--color) 55%, var(--#{$_}--background-color)); --#{$_}--border-color: #{tokens.getRawValue($_, "color--primary-border")}; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--primary-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--primary-contrast")}; @@ -100,7 +100,7 @@ $_: "c-footer"; &--secondary { --#{$_}--background-color: #{tokens.getRawValue($_, "color--secondary")}; --#{$_}--color: #{tokens.getRawValue($_, "color--secondary-contrast")}; - --#{$_}--color-muted: #{tokens.getRawValue($_, "color--secondary-contrast")}; + --#{$_}--color-muted: color-mix(in srgb, var(--#{$_}--color) 55%, var(--#{$_}--background-color)); --#{$_}--border-color: #{tokens.getRawValue($_, "color--secondary-border")}; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--secondary-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--secondary-contrast")}; @@ -112,7 +112,7 @@ $_: "c-footer"; &--background { --#{$_}--background-color: #{tokens.getRawValue($_, "color--background")}; --#{$_}--color: #{tokens.getRawValue($_, "color--background-contrast")}; - --#{$_}--color-muted: #{tokens.getRawValue($_, "color--background-contrast-muted")}; + --#{$_}--color-muted: color-mix(in srgb, var(--#{$_}--color) 55%, var(--#{$_}--background-color)); --#{$_}--border-color: #{tokens.getRawValue($_, "color--background-border")}; --#{$_}--subfooter-background-color: #{tokens.getRawValue($_, "color--background-alt")}; --#{$_}--subfooter-color: #{tokens.getRawValue($_, "color--background-contrast")};