From dfcfd2336de15caac109644153d91121a42815e1 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Fri, 17 Jul 2026 11:59:35 +0200 Subject: [PATCH 1/7] ARC-3789: start on homepage banner --- .../ContentBlockRenderer.const.tsx | 2 + .../BlockHomepageBanner.editorconfig.ts | 71 +++++++++++++++++++ .../BlockHomepageBanner.scss | 46 ++++++++++++ .../BlockHomepageBanner.tsx | 52 ++++++++++++++ .../blocks/BlockHomepageBanner/index.ts | 2 + .../const/content-block-config-map.ts | 2 + .../const/content-block-initial-state-map.ts | 2 + .../const/get-content-block-type-options.ts | 8 +++ .../content-page/types/content-block.types.ts | 1 + .../shared/styles/settings/_colors.scss | 1 + 10 files changed, 187 insertions(+) create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/index.ts diff --git a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx index 2b41e4d2..5976717b 100644 --- a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx +++ b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx @@ -10,6 +10,7 @@ import { BlockEventbrite } from '~content-blocks/BlockEventbrite'; import { BlockHeading } from '~content-blocks/BlockHeading'; import { BlockHetArchiefHeaderSearch } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch'; import { BlockHetArchiefImageTextBackground } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { BlockHomepageBanner } from '~content-blocks/BlockHomepageBanner'; import { BlockIFrame } from '~content-blocks/BlockIFrame'; import { BlockImage } from '~content-blocks/BlockImage'; import { @@ -87,6 +88,7 @@ export function GET_BLOCK_COMPONENT( [ContentBlockType.Breadcrumbs]: BlockBreadcrumbs, [ContentBlockType.HetArchiefImageTextBackground]: BlockHetArchiefImageTextBackground, [ContentBlockType.OverviewWithCarousel]: BlockOverviewWithCarousel, + [ContentBlockType.HomepageBanner]: BlockHomepageBanner, // Avo specific blocks [ContentBlockType.MediaGrid]: loadComponentFromConfig(ContentBlockType.MediaGrid), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts new file mode 100644 index 00000000..3b1e9946 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts @@ -0,0 +1,71 @@ +import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '~content-blocks/defaults'; +import { GET_ALIGN_OPTIONS } from '~modules/content-page/const/get-align-options.ts'; +import { GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF } from '~modules/content-page/const/get-color-options.ts'; +import { + Color, + type ContentBlockConfig, + ContentBlockEditor, + ContentBlockType, + type DefaultContentBlockState, +} from '~modules/content-page/types/content-block.types'; +import { RICH_TEXT_EDITOR_OPTIONS_FULL } from '~shared/consts/rich-text-editor.consts.ts'; +import { tText } from '~shared/helpers/translation-functions'; +import { HET_ARCHIEF } from '~shared/types'; + +export const INITIAL_CONTENT_HOMEPAGE_BANNER_COMPONENTS_STATE = () => ({ + title: '', + content: '', + textAlign: 'left', + backgroundColor: Color.Transparent, + bannerColor: Color.OceanGreen, +}); + +export const INITIAL_CONTENT_HOMEPAGE_BANNER_BLOCK_STATE = (): DefaultContentBlockState => ({ + ...BLOCK_STATE_DEFAULTS(), + fullWidth: true, +}); + +export const CONTENT_HOMEPAGE_BANNER_CONFIG = (position = 0): ContentBlockConfig => ({ + position, + name: tText('Homepage banner'), + type: ContentBlockType.HomepageBanner, + components: { + state: INITIAL_CONTENT_HOMEPAGE_BANNER_COMPONENTS_STATE(), + fields: { + title: TEXT_FIELD( + { + label: tText('title', undefined, [HET_ARCHIEF]), + }, + tText('titel is verplicht') + ), + content: TEXT_FIELD({ + editorType: ContentBlockEditor.RICH_TEXT_EDITOR, + editorProps: { + controls: RICH_TEXT_EDITOR_OPTIONS_FULL, + }, + validator: undefined, + }), + textAlign: { + label: tText('admin/content-block/helpers/generators/image-grid___text-alignatie'), + editorType: ContentBlockEditor.Select, + editorProps: { + options: GET_ALIGN_OPTIONS(), + }, + }, + bannerColor: { + label: tText('Kleur banner'), + editorType: ContentBlockEditor.ColorSelect, + editorProps: { + options: GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF(), + defaultValue: GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF()[2], + }, + }, + }, + }, + block: { + state: INITIAL_CONTENT_HOMEPAGE_BANNER_BLOCK_STATE(), + fields: { + ...BLOCK_FIELD_DEFAULTS(), + }, + }, +}); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss new file mode 100644 index 00000000..76c69b71 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss @@ -0,0 +1,46 @@ +@use "sass:math"; +@use "../../../../shared/styles/settings/variables" as variables; +@use "../../../../shared/styles/settings/colors" as colors; + +.c-block-homepage-banner { + &__content { + width: 75%; + display: flex; + flex-direction: column; + gap: 3.2rem; + + &--left, + &--center { + margin-right: auto; + } + + &--right, + &--center { + margin-left: auto; + } + + &-title { + color: colors.$color-gray-1000; + + /* H4 */ + font-family: "SofiaPro" !important; + font-size: 3.2rem !important; + font-style: normal !important; + font-weight: 800 !important; + line-height: 4rem !important; + width: 100%; + white-space: wrap; + } + + &-text { + color: colors.$neutral; + + /* H4 */ + font-family: "SofiaPro" !important; + font-size: 1.8rem !important; + font-style: normal !important; + font-weight: 500 !important; + line-height: 2.8rem !important; + } + } +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx new file mode 100644 index 00000000..e19c9993 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx @@ -0,0 +1,52 @@ +import React, { type FunctionComponent, type ReactElement } from 'react'; +import type { DefaultComponentProps } from '~modules/shared/types/components'; +import './BlockHomepageBanner.scss'; +import { Container } from '@viaa/avo2-components'; +import clsx from 'clsx'; +import { BlockHeading } from '~content-blocks/BlockHeading'; +import { ContentPageWidth } from '~modules/content-page/types/content-pages.types.ts'; +import Html from '~shared/components/Html/Html.tsx'; + +export interface BlockHomepageBannerProps extends DefaultComponentProps { + title: string; + content: string; + textAlign: string; + backgroundColor: string; + bannerColor: string; + pageWidth?: string; +} + +export const BlockHomepageBanner: FunctionComponent = ({ + title, + content, + textAlign, + backgroundColor, + bannerColor, + pageWidth, +}): ReactElement => { + return ( +
+ +
+ + {title} + + +
+
+
+ ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/index.ts new file mode 100644 index 00000000..d6385e9d --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/index.ts @@ -0,0 +1,2 @@ +export * from './BlockHomepageBanner.editorconfig.ts'; +export { BlockHomepageBanner } from './BlockHomepageBanner.tsx'; diff --git a/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts b/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts index 470d2a55..b65c1214 100644 --- a/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts +++ b/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts @@ -11,6 +11,7 @@ import { EVENTBRITE_BLOCK_CONFIG } from '~content-blocks/BlockEventbrite'; import { HEADING_BLOCK_CONFIG } from '~content-blocks/BlockHeading'; import { HET_ARCHIEF_HEADER_SEARCH_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig'; import { HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { CONTENT_HOMEPAGE_BANNER_CONFIG } from '~content-blocks/BlockHomepageBanner'; import { IFRAME_BLOCK_CONFIG } from '~content-blocks/BlockIFrame'; import { IMAGE_BLOCK_CONFIG } from '~content-blocks/BlockImage'; import { @@ -87,4 +88,5 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record< [ContentBlockType.Breadcrumbs]: CONTENT_BREADCRUMBS_CONFIG, [ContentBlockType.ScrollDownNudge]: CONTENT_SCROLL_DOWN_NUDGE_CONFIG, [ContentBlockType.OverviewWithCarousel]: CONTENT_OVERVIEW_WITH_CAROUSEL_CONFIG, + [ContentBlockType.HomepageBanner]: CONTENT_HOMEPAGE_BANNER_CONFIG, }; diff --git a/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts b/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts index a864713a..2f689b91 100644 --- a/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts +++ b/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts @@ -11,6 +11,7 @@ import { INITIAL_EVENTBRITE_COMPONENTS_STATE } from '~content-blocks/BlockEventb import { INITIAL_HEADING_COMPONENTS_STATE } from '~content-blocks/BlockHeading'; import { INITIAL_HET_ARCHIEF_HEADER_SEARCH_BLOCK_STATE } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig'; import { INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_STATE } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { INITIAL_CONTENT_HOMEPAGE_BANNER_BLOCK_STATE } from '~content-blocks/BlockHomepageBanner'; import { INITIAL_IFRAME_COMPONENTS_STATE } from '~content-blocks/BlockIFrame'; import { INITIAL_IMAGE_COMPONENTS_STATE } from '~content-blocks/BlockImage'; import { @@ -90,4 +91,5 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: { [ContentBlockType.Breadcrumbs]: INITIAL_BREADCRUMBS_BLOCK_STATE, [ContentBlockType.ScrollDownNudge]: INITIAL_SCROLL_DOWN_NUDGE_BLOCK_STATE, [ContentBlockType.OverviewWithCarousel]: INITIAL_CONTENT_OVERVIEW_WITH_CAROUSEL_BLOCK_STATE, + [ContentBlockType.HomepageBanner]: INITIAL_CONTENT_HOMEPAGE_BANNER_BLOCK_STATE, }; diff --git a/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts b/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts index 94bcf7c7..e4c81af7 100644 --- a/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts +++ b/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts @@ -184,6 +184,14 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () = ), value: ContentBlockType.OverviewWithCarousel, }, + { + label: tText( + 'modules/content-page/const/get-content-block-type-options___homepage=-banner', + {}, + [HET_ARCHIEF] + ), + value: ContentBlockType.HomepageBanner, + }, ]; // Only show the content blocks that the client enabled through the config object diff --git a/ui/src/react-admin/modules/content-page/types/content-block.types.ts b/ui/src/react-admin/modules/content-page/types/content-block.types.ts index 534dfefa..980d6982 100644 --- a/ui/src/react-admin/modules/content-page/types/content-block.types.ts +++ b/ui/src/react-admin/modules/content-page/types/content-block.types.ts @@ -175,6 +175,7 @@ export enum ContentBlockType { AvoImageTextBackground = 'AVO_IMAGE_TEXT_BACKGROUND', // Avo ScrollDownNudge = 'SCROLL_DOWN_NUDGE', OverviewWithCarousel = 'OVERVIEW_WITH_CAROUSEL', + HomepageBanner = 'HOMEPAGE_BANNER', } export enum ContentBlockEditor { diff --git a/ui/src/react-admin/modules/shared/styles/settings/_colors.scss b/ui/src/react-admin/modules/shared/styles/settings/_colors.scss index 1e40099a..f57c8827 100644 --- a/ui/src/react-admin/modules/shared/styles/settings/_colors.scss +++ b/ui/src/react-admin/modules/shared/styles/settings/_colors.scss @@ -74,3 +74,4 @@ $color-klascement: #f7931b; $color-sea-green: #009690; $color-platinum: #f8f8f8; +$neutral: #727272; From 527003573cca5b8f38f1a8b7774df711d48a41fb Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Tue, 28 Jul 2026 07:26:56 +0200 Subject: [PATCH 2/7] ARC-3789: fix pattern on desktop --- .../BlockHomepageBanner.scss | 117 +++++++++++++++++- .../BlockHomepageBanner.tsx | 23 ++-- 2 files changed, 129 insertions(+), 11 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss index 76c69b71..1bfe57c0 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss @@ -3,19 +3,32 @@ @use "../../../../shared/styles/settings/colors" as colors; .c-block-homepage-banner { + // The pattern strips are position: absolute and escape all the way out to this + // element's own edges (and beyond) - clip them here so they never cause the page to + // scroll horizontally. clip-path (rather than overflow-x: hidden) avoids the CSS + // overflow-x/overflow-y coupling rule that would otherwise turn overflow-y into + // "auto" and risk an unwanted vertical scrollbar. + clip-path: inset(0); + width: 100% !important; + max-width: none !important; + &__content { - width: 75%; + position: relative; + max-width: 75%; + min-height: 8rem; display: flex; flex-direction: column; gap: 3.2rem; - &--left, - &--center { + &--left { margin-right: auto; } - &--right, &--center { + margin: 0 auto; + } + + &--right { margin-left: auto; } @@ -43,4 +56,100 @@ line-height: 2.8rem !important; } } + + // Anchored directly off the text box's own edge (not Container's), so the 4rem gap + // holds regardless of alignment or the actual (up-to-75%) width of the text. Each + // strip is a single element - overshooting far enough that it always reaches this + // component's own edge, clipped by the clip-path above. + // + // The width MUST be an exact multiple of the tile's cycle-width. Chrome has a + // rendering bug where a repeating multi-layer gradient background tears/gaps in the + // middle of the element when the element's own width isn't an exact multiple of + // background-size - clipping a partial tile at the edges is fine, but a non-multiple + // width visibly breaks the repeat. Using 100vw (or any other non-multiple width) + // reproduces this reliably. + // + // left and right are two DIFFERENT tiles (not the same tile phase-shifted via a + // background-position offset - that proved unreliable) - each built so the desired + // near-text shape sits naturally at the tile's own edge, letting both sides anchor + // with a plain background-position-x: right / left (no offset at all): + // - left: rect, circle, rect, [circle, circle, rect, circle, ...] + // - right: circle, rect, circle, circle, [rect, circle, circle, rect, circle, ...] + &__pattern { + $rect-width: 4rem; + $circle-diameter: 8rem; + $circle-radius: math.div($circle-diameter, 2); + $cycle-width: $rect-width * 2 + $circle-diameter * 3; + + position: absolute; + top: 0; + bottom: 0; + width: $cycle-width * 20; + background-repeat: repeat-x; + background-position-y: top; + background-size: $cycle-width 8rem; + + &--left { + right: calc(100% + 4rem); + background-position-x: right; + // Tile (local 0 -> cycle-width): circle, circle, rect, circle, rect. + // Its own right edge lands on the near-text edge, so reading backwards + // (away from text) gives: rect, circle, rect, circle, circle, ... + background-image: + radial-gradient( + circle at #{$circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + radial-gradient( + circle at #{$circle-diameter + $circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + radial-gradient( + circle at #{$circle-diameter * 2 + $rect-width + $circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + linear-gradient( + to right, + transparent 0 #{$circle-diameter * 2}, + var(--pattern-color, transparent) #{$circle-diameter * 2} + #{$circle-diameter * 2 + $rect-width}, + transparent #{$circle-diameter * 2 + $rect-width} #{$circle-diameter * 3 + $rect-width}, + var(--pattern-color, transparent) #{$circle-diameter * 3 + $rect-width} $cycle-width + ); + } + + &--right { + left: calc(100% + 4rem); + background-position-x: left; + // Tile (local 0 -> cycle-width): circle, rect, circle, circle, rect. + // Its own left edge lands on the near-text edge, so reading forwards + // (away from text) gives: circle, rect, circle, circle, rect, ... + background-image: + radial-gradient( + circle at #{$circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + radial-gradient( + circle at #{$circle-diameter + $rect-width + $circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + radial-gradient( + circle at #{$circle-diameter * 2 + $rect-width + $circle-radius} 50%, + var(--pattern-color, transparent) $circle-radius, + transparent #{$circle-radius + 0.02rem} + ), + linear-gradient( + to right, + transparent 0 $circle-diameter, + var(--pattern-color, transparent) $circle-diameter #{$circle-diameter + $rect-width}, + transparent #{$circle-diameter + $rect-width} #{$circle-diameter * 3 + $rect-width}, + var(--pattern-color, transparent) #{$circle-diameter * 3 + $rect-width} $cycle-width + ); + } + } } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx index e19c9993..6912fc7f 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx @@ -1,4 +1,4 @@ -import React, { type FunctionComponent, type ReactElement } from 'react'; +import React, { type CSSProperties, type FunctionComponent, type ReactElement } from 'react'; import type { DefaultComponentProps } from '~modules/shared/types/components'; import './BlockHomepageBanner.scss'; import { Container } from '@viaa/avo2-components'; @@ -26,10 +26,13 @@ export const BlockHomepageBanner: FunctionComponent = }): ReactElement => { return (
= : (pageWidth?.toLowerCase() as 'medium' | 'large') } > -
+
{title} +
From 9ecc9f9ea4248091c34f7c0bb59e61f8c790cfbe Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Tue, 28 Jul 2026 07:43:50 +0200 Subject: [PATCH 3/7] ARC-3789: optimize pattern on desktop --- .../BlockHomepageBanner.scss | 115 ++++++++++-------- .../BlockHomepageBanner.tsx | 15 ++- 2 files changed, 74 insertions(+), 56 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss index 1bfe57c0..60545de6 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss @@ -1,7 +1,48 @@ @use "sass:math"; +@use "sass:list"; @use "../../../../shared/styles/settings/variables" as variables; @use "../../../../shared/styles/settings/colors" as colors; +// Shared shape sizing for the decorative pattern strips (used by both --left and --right). +$pattern-rect-width: 4rem; +$pattern-circle-diameter: 8rem; +$pattern-circle-radius: math.div($pattern-circle-diameter, 2); +$pattern-circle-antialias-edge: $pattern-circle-radius + 0.02rem; +$pattern-cycle-width: $pattern-rect-width * 2 + $pattern-circle-diameter * 3; +// Safety multiple for $pattern-cycle-width so the strip always reaches this component's +// own edge (see the width comment below) - comfortably covers any realistic viewport. +$pattern-repeat-count: 20; + +// Renders the repeating tile (3 circles + 2 rects) for one pattern strip. $circle-centers +// is a 3-item list of x-offsets (from the tile's own left edge); $rect-ranges is a 2-item +// list of [start, end] x-offset pairs. Both sides share this shape/size, only the ORDER +// (i.e. which offsets are passed in) differs - see the comments on &--left/&--right. +@mixin pattern-tile($circle-centers, $rect-ranges) { + background-image: + radial-gradient( + circle at list.nth($circle-centers, 1) 50%, + var(--pattern-color, transparent) $pattern-circle-radius, + transparent $pattern-circle-antialias-edge + ), + radial-gradient( + circle at list.nth($circle-centers, 2) 50%, + var(--pattern-color, transparent) $pattern-circle-radius, + transparent $pattern-circle-antialias-edge + ), + radial-gradient( + circle at list.nth($circle-centers, 3) 50%, + var(--pattern-color, transparent) $pattern-circle-radius, + transparent $pattern-circle-antialias-edge + ), + linear-gradient( + to right, + transparent 0 list.nth(list.nth($rect-ranges, 1), 1), + var(--pattern-color, transparent) list.nth(list.nth($rect-ranges, 1), 1) list.nth(list.nth($rect-ranges, 1), 2), + transparent list.nth(list.nth($rect-ranges, 1), 2) list.nth(list.nth($rect-ranges, 2), 1), + var(--pattern-color, transparent) list.nth(list.nth($rect-ranges, 2), 1) list.nth(list.nth($rect-ranges, 2), 2) + ); +} + .c-block-homepage-banner { // The pattern strips are position: absolute and escape all the way out to this // element's own edges (and beyond) - clip them here so they never cause the page to @@ -76,18 +117,13 @@ // - left: rect, circle, rect, [circle, circle, rect, circle, ...] // - right: circle, rect, circle, circle, [rect, circle, circle, rect, circle, ...] &__pattern { - $rect-width: 4rem; - $circle-diameter: 8rem; - $circle-radius: math.div($circle-diameter, 2); - $cycle-width: $rect-width * 2 + $circle-diameter * 3; - position: absolute; top: 0; bottom: 0; - width: $cycle-width * 20; + width: $pattern-cycle-width * $pattern-repeat-count; background-repeat: repeat-x; background-position-y: top; - background-size: $cycle-width 8rem; + background-size: $pattern-cycle-width 8rem; &--left { right: calc(100% + 4rem); @@ -95,30 +131,17 @@ // Tile (local 0 -> cycle-width): circle, circle, rect, circle, rect. // Its own right edge lands on the near-text edge, so reading backwards // (away from text) gives: rect, circle, rect, circle, circle, ... - background-image: - radial-gradient( - circle at #{$circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} + @include pattern-tile( + ( + $pattern-circle-radius, + $pattern-circle-diameter + $pattern-circle-radius, + $pattern-circle-diameter * 2 + $pattern-rect-width + $pattern-circle-radius, ), - radial-gradient( - circle at #{$circle-diameter + $circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} - ), - radial-gradient( - circle at #{$circle-diameter * 2 + $rect-width + $circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} - ), - linear-gradient( - to right, - transparent 0 #{$circle-diameter * 2}, - var(--pattern-color, transparent) #{$circle-diameter * 2} - #{$circle-diameter * 2 + $rect-width}, - transparent #{$circle-diameter * 2 + $rect-width} #{$circle-diameter * 3 + $rect-width}, - var(--pattern-color, transparent) #{$circle-diameter * 3 + $rect-width} $cycle-width - ); + ( + ($pattern-circle-diameter * 2, $pattern-circle-diameter * 2 + $pattern-rect-width), + ($pattern-circle-diameter * 3 + $pattern-rect-width, $pattern-cycle-width), + ) + ); } &--right { @@ -127,29 +150,17 @@ // Tile (local 0 -> cycle-width): circle, rect, circle, circle, rect. // Its own left edge lands on the near-text edge, so reading forwards // (away from text) gives: circle, rect, circle, circle, rect, ... - background-image: - radial-gradient( - circle at #{$circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} - ), - radial-gradient( - circle at #{$circle-diameter + $rect-width + $circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} - ), - radial-gradient( - circle at #{$circle-diameter * 2 + $rect-width + $circle-radius} 50%, - var(--pattern-color, transparent) $circle-radius, - transparent #{$circle-radius + 0.02rem} + @include pattern-tile( + ( + $pattern-circle-radius, + $pattern-circle-diameter + $pattern-rect-width + $pattern-circle-radius, + $pattern-circle-diameter * 2 + $pattern-rect-width + $pattern-circle-radius, ), - linear-gradient( - to right, - transparent 0 $circle-diameter, - var(--pattern-color, transparent) $circle-diameter #{$circle-diameter + $rect-width}, - transparent #{$circle-diameter + $rect-width} #{$circle-diameter * 3 + $rect-width}, - var(--pattern-color, transparent) #{$circle-diameter * 3 + $rect-width} $cycle-width - ); + ( + ($pattern-circle-diameter, $pattern-circle-diameter + $pattern-rect-width), + ($pattern-circle-diameter * 3 + $pattern-rect-width, $pattern-cycle-width), + ) + ); } } } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx index 6912fc7f..265d1320 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx @@ -4,13 +4,14 @@ import './BlockHomepageBanner.scss'; import { Container } from '@viaa/avo2-components'; import clsx from 'clsx'; import { BlockHeading } from '~content-blocks/BlockHeading'; +import type { AlignOption } from '~modules/content-page/types/content-block.types'; import { ContentPageWidth } from '~modules/content-page/types/content-pages.types.ts'; import Html from '~shared/components/Html/Html.tsx'; export interface BlockHomepageBannerProps extends DefaultComponentProps { title: string; content: string; - textAlign: string; + textAlign: AlignOption; backgroundColor: string; bannerColor: string; pageWidth?: string; @@ -26,7 +27,7 @@ export const BlockHomepageBanner: FunctionComponent = }): ReactElement => { return (
= {title} -
From 757ed36801055400ced554eb1762d393c7ff7325 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Tue, 28 Jul 2026 08:29:43 +0200 Subject: [PATCH 4/7] ARC-3789: Mobile design --- .../BlockHomepageBanner.editorconfig.ts | 6 +- .../BlockHomepageBanner.scss | 80 +++++++++++++++++++ .../BlockHomepageBanner.tsx | 30 +++++-- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts index 3b1e9946..b42f2880 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.editorconfig.ts @@ -36,7 +36,7 @@ export const CONTENT_HOMEPAGE_BANNER_CONFIG = (position = 0): ContentBlockConfig { label: tText('title', undefined, [HET_ARCHIEF]), }, - tText('titel is verplicht') + tText('titel is verplicht', undefined, [HET_ARCHIEF]) ), content: TEXT_FIELD({ editorType: ContentBlockEditor.RICH_TEXT_EDITOR, @@ -46,14 +46,14 @@ export const CONTENT_HOMEPAGE_BANNER_CONFIG = (position = 0): ContentBlockConfig validator: undefined, }), textAlign: { - label: tText('admin/content-block/helpers/generators/image-grid___text-alignatie'), + label: tText('Text alignatie', undefined, [HET_ARCHIEF]), editorType: ContentBlockEditor.Select, editorProps: { options: GET_ALIGN_OPTIONS(), }, }, bannerColor: { - label: tText('Kleur banner'), + label: tText('Kleur banner', undefined, [HET_ARCHIEF]), editorType: ContentBlockEditor.ColorSelect, editorProps: { options: GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF(), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss index 60545de6..2ea7a5d9 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.scss @@ -12,6 +12,10 @@ $pattern-cycle-width: $pattern-rect-width * 2 + $pattern-circle-diameter * 3; // Safety multiple for $pattern-cycle-width so the strip always reaches this component's // own edge (see the width comment below) - comfortably covers any realistic viewport. $pattern-repeat-count: 20; +// Mobile pattern slots overshoot &__content's edge by this much so they can still bleed +// past the text's own container gutter to the true screen edge (see &__pattern-slot) - +// comfortably more than any gutter avo2's Container applies below $g-bp2. +$pattern-mobile-overshoot: 10rem; // Renders the repeating tile (3 circles + 2 rects) for one pattern strip. $circle-centers // is a 3-item list of x-offsets (from the tile's own left edge); $rect-ranges is a 2-item @@ -61,6 +65,23 @@ $pattern-repeat-count: 20; flex-direction: column; gap: 3.2rem; + // The mobile pattern slots size themselves as a % of THIS element (their + // containing block), so it needs to span the full screen width below $g-bp2 - + // otherwise "max 1/3 of the screen" would really mean "1/3 of the desktop + // 75% cap". + // + // The padding reserves normal-flow vertical space for the (absolutely positioned, + // so otherwise height-less-as-far-as-the-layout-is-concerned) pattern slots above + // the title and below the text. This matters: &__pattern-slot's height doesn't + // contribute to this element's (or, transitively, .c-block-homepage-banner's) own + // height, and .c-block-homepage-banner's clip-path: inset(0) clips to its own + // layout box - without this padding, that box would end exactly at the title/text, + // and the pattern slots (sitting outside it) would be clipped away entirely. + @media (max-width: variables.$g-bp2) { + max-width: 100%; + padding: calc(8rem + 2.4rem) 0; + } + &--left { margin-right: auto; } @@ -162,5 +183,64 @@ $pattern-repeat-count: 20; ) ); } + + // Below $g-bp2 the patterns move from beside the text to above/below it. Both + // sides keep their EXACT desktop tile/anchor (background-position-x untouched) - + // only where the strip itself is placed changes: it's now anchored against + // &__pattern-slot's inner edge (the 1/3 or 2/3 landmark) instead of "near text". + // That's what puts the clean shape there and pushes the arbitrary cut-off out to + // the true screen edge, and mirrored for --right - exactly the desktop mechanism, + // reused. (top/bottom: 0 is unconditional above, so unchanged here.) + @media (max-width: variables.$g-bp2) { + &--left { + right: 0; + left: auto; + } + + &--right { + left: 0; + right: auto; + } + } + } + + // No-op on desktop: an unpositioned, unsized inline box that &__pattern's + // position: absolute reaches past, straight to &__content. Below $g-bp2 it becomes the + // positioned, overflow-clipping box that caps &__pattern at exactly 1/3 (top) or 2/3 + // (bottom) of &__content's width. + // + // The cap MUST live on this separate wrapper rather than on &__pattern itself: capping + // &__pattern's own width there would make its rendered width a non-multiple of + // $pattern-cycle-width again, reintroducing the Chrome tearing bug described above. + // + // top: 0 / bottom: 0 here land inside the padding &__content reserves above/below the + // title and text (see there) - not against &__content's content-box directly, which is + // what makes this safe: those are literal zero offsets, not percentages, so (unlike + // the desktop right/left: calc(100% + 4rem) trick) they don't depend on &__content + // having a definite height to resolve correctly. + // + // The slot also overshoots &__content's left/right edge by $pattern-mobile-overshoot: + // &__content sits inside avo2's , which still applies its own (smaller, + // but non-zero) gutter below $g-bp2 - without this overshoot the pattern could only + // ever reach &__content's edge, not the true screen edge. The overshoot is compensated + // for in the width so the INNER edge (the anchor landmark) stays exactly at 1/3 / 2/3. + &__pattern-slot { + @media (max-width: variables.$g-bp2) { + position: absolute; + overflow: hidden; + height: 8rem; + + &--top { + top: 0; + left: -$pattern-mobile-overshoot; + width: calc(33.333% + #{$pattern-mobile-overshoot}); + } + + &--bottom { + bottom: 0; + right: -$pattern-mobile-overshoot; + width: calc(66.667% + #{$pattern-mobile-overshoot}); + } + } } } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx index 265d1320..e7311c55 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHomepageBanner/BlockHomepageBanner.tsx @@ -49,18 +49,32 @@ export const BlockHomepageBanner: FunctionComponent = `c-block-homepage-banner__content--${textAlign}` )} > + {/* + On desktop these patterns sit beside the text and this wrapper is a + no-op (unpositioned, so the pattern's own position: absolute reaches + past it to __content). On mobile the wrapper becomes the positioned, + overflow-clipping "slot" that caps the pattern at max 1/3 or 2/3 of the + screen width - see BlockHomepageBanner.scss for why the clipping can't + happen on the pattern element itself. It's placed here, before the + title, purely for source order (mobile stacks it above the text); the + desktop position is entirely controlled by CSS. + */} +
+ {title} -