diff --git a/ui/all-translations-het-archief.json b/ui/all-translations-het-archief.json
index 5f03634d..0c4c7cb8 100644
--- a/ui/all-translations-het-archief.json
+++ b/ui/all-translations-het-archief.json
@@ -59198,5 +59198,325 @@
"language": "en",
"value": "You do not have the right permissions to call this route",
"value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "achtergrondkleur-tekstvak",
+ "language": "nl",
+ "value": "Achtergrondkleur tekstvak",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "achtergrondkleur-tekstvak",
+ "language": "en",
+ "value": "Text panel background colour",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "afbeelding",
+ "language": "nl",
+ "value": "Afbeelding",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "afbeelding",
+ "language": "en",
+ "value": "Image",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "afbeelding-is-verplicht",
+ "language": "nl",
+ "value": "Afbeelding is verplicht",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "afbeelding-is-verplicht",
+ "language": "en",
+ "value": "Image is required",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "bestemming",
+ "language": "nl",
+ "value": "Bestemming",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "bestemming",
+ "language": "en",
+ "value": "Destination",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "bestemming-is-verplicht",
+ "language": "nl",
+ "value": "Bestemming is verplicht",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "bestemming-is-verplicht",
+ "language": "en",
+ "value": "Destination is required",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "dubbele-banner",
+ "language": "nl",
+ "value": "Dubbele banner",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "dubbele-banner",
+ "language": "en",
+ "value": "Double banner",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "helft",
+ "language": "nl",
+ "value": "Helft",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "helft",
+ "language": "en",
+ "value": "Half",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-1",
+ "language": "nl",
+ "value": "Icoon 1",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-1",
+ "language": "en",
+ "value": "Icon 1",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-2",
+ "language": "nl",
+ "value": "Icoon 2",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-2",
+ "language": "en",
+ "value": "Icon 2",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-3",
+ "language": "nl",
+ "value": "Icoon 3",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "icoon-3",
+ "language": "en",
+ "value": "Icon 3",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "label",
+ "language": "nl",
+ "value": "Label",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "label",
+ "language": "en",
+ "value": "Label",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "label-is-verplicht",
+ "language": "nl",
+ "value": "Label is verplicht",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "label-is-verplicht",
+ "language": "en",
+ "value": "Label is required",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "tekstkleur",
+ "language": "nl",
+ "value": "Tekstkleur",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "tekstkleur",
+ "language": "en",
+ "value": "Text colour",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/const/get-content-block-type-options",
+ "key": "dubbele-banner",
+ "language": "nl",
+ "value": "Dubbele banner",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/const/get-content-block-type-options",
+ "key": "dubbele-banner",
+ "language": "en",
+ "value": "Double banner",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "achtergrondkleur-tekstvak-is-verplicht",
+ "language": "nl",
+ "value": "Achtergrondkleur tekstvak is verplicht",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "achtergrondkleur-tekstvak-is-verplicht",
+ "language": "en",
+ "value": "Text panel background colour is required",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "tekstkleur-is-verplicht",
+ "language": "nl",
+ "value": "Tekstkleur is verplicht",
+ "value_type": "TEXT"
+ },
+ {
+ "id": "",
+ "app": "HET_ARCHIEF",
+ "component": "ADMIN_CORE",
+ "location": "modules/content-page/components/blocks/block-double-banner/block-double-banner",
+ "key": "tekstkleur-is-verplicht",
+ "language": "en",
+ "value": "Text colour is required",
+ "value_type": "TEXT"
}
-]
\ No newline at end of file
+]
diff --git a/ui/src/client.ts b/ui/src/client.ts
index 739fca16..40978994 100644
--- a/ui/src/client.ts
+++ b/ui/src/client.ts
@@ -6,6 +6,7 @@ export { BlockButtonsWrapper } from '~content-blocks/BlockButtons/BlockButtons.w
export { BlockCardsWithoutDescription } from '~content-blocks/BlockCardsWithoutDescription';
export { BlockContentPageMeta } from '~content-blocks/BlockContentPageMeta';
export { BlockCTAsWrapper } from '~content-blocks/BlockCTAs/BlockCTAs.wrapper';
+export { BlockDoubleBanner } from '~content-blocks/BlockDoubleBanner';
export { BlockEventbrite } from '~content-blocks/BlockEventbrite';
export { BlockHeading } from '~content-blocks/BlockHeading/BlockHeading';
export { BlockHetArchiefHeaderSearch } from '~content-blocks/BlockHetArchiefHeaderSearch';
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 58286ba1..a75ede7b 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
@@ -7,6 +7,7 @@ import { BlockButtonsWrapper } from '~content-blocks/BlockButtons';
import { BlockCardsWithoutDescription } from '~content-blocks/BlockCardsWithoutDescription';
import { BlockContentPageMeta } from '~content-blocks/BlockContentPageMeta';
import { BlockCTAsWrapper } from '~content-blocks/BlockCTAs';
+import { BlockDoubleBanner } from '~content-blocks/BlockDoubleBanner';
import { BlockEventbrite } from '~content-blocks/BlockEventbrite';
import { BlockHeading } from '~content-blocks/BlockHeading';
import { BlockHetArchiefHeaderSearch } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch';
@@ -99,6 +100,7 @@ export function GET_BLOCK_COMPONENT(
[ContentBlockType.HighlightText]: BlockHighlightText,
[ContentBlockType.ThemeReels]: BlockThemeReels,
[ContentBlockType.OverviewThemes]: BlockOverviewThemes,
+ [ContentBlockType.DoubleBanner]: BlockDoubleBanner,
// Avo specific blocks
[ContentBlockType.MediaGrid]: loadComponentFromConfig(ContentBlockType.MediaGrid),
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.test.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.test.ts
new file mode 100644
index 00000000..f26a717b
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.test.ts
@@ -0,0 +1,84 @@
+import { AvoCoreContentPickerType } from '@viaa/avo2-types';
+import { describe, expect, it, vi } from 'vitest';
+import type {
+ ContentBlockField,
+ ContentBlockFieldGroup,
+} from '~modules/content-page/types/content-block.types';
+import { Color } from '~modules/content-page/types/content-block.types';
+import {
+ DOUBLE_BANNER_BLOCK_CONFIG,
+ INITIAL_DOUBLE_BANNER_COMPONENTS_STATE,
+} from './BlockDoubleBanner.editorconfig';
+
+vi.mock('~shared/helpers/translation-functions', () => ({
+ tText: (key: string) => key,
+}));
+
+vi.mock('~shared/helpers/is-avo', () => ({
+ isAvo: () => false,
+}));
+
+vi.mock('~shared/consts/icons.consts', () => ({
+ GET_ADMIN_ICON_OPTIONS: () => [{ label: 'Newspaper', value: 'newspaper' }],
+}));
+
+const config = DOUBLE_BANNER_BLOCK_CONFIG();
+const halvesField = config.components.fields.halves as ContentBlockFieldGroup;
+const fields = halvesField.fields;
+const field = (key: string): ContentBlockField => fields[key];
+
+describe('DOUBLE_BANNER_BLOCK_CONFIG', () => {
+ it('always starts with exactly two halves', () => {
+ const state = INITIAL_DOUBLE_BANNER_COMPONENTS_STATE();
+
+ expect(state.halves).toHaveLength(2);
+ expect(halvesField.min).toBe(2);
+ expect(halvesField.max).toBe(2);
+ });
+
+ it('keeps only the three icon fields optional', () => {
+ for (const key of ['label', 'link', 'image', 'textColor', 'backgroundColor']) {
+ expect(field(key).validator?.('')).not.toEqual([]);
+ expect(field(key).validator?.('#000')).toEqual([]);
+ }
+
+ for (const key of ['icon1', 'icon2', 'icon3']) {
+ expect(field(key).validator).toBeUndefined();
+ expect(field(key).editorProps.options).toEqual([{ label: 'Newspaper', value: 'newspaper' }]);
+ }
+ });
+
+ it('does not impose a maximum label length', () => {
+ expect(field('label').editorProps?.maxLength).toBeUndefined();
+ });
+
+ it('accepts one image per half', () => {
+ expect(field('image').editorProps).toMatchObject({
+ assetType: 'CONTENT_BLOCK_IMAGE',
+ allowMulti: false,
+ });
+ });
+
+ it('accepts URLs and internal destinations in the same tab', () => {
+ expect(field('link').editorProps).toMatchObject({
+ hideTargetSwitch: true,
+ allowedTypes: [
+ AvoCoreContentPickerType.CONTENT_PAGE,
+ AvoCoreContentPickerType.INTERNAL_LINK,
+ AvoCoreContentPickerType.EXTERNAL_LINK,
+ AvoCoreContentPickerType.ANCHOR_LINK,
+ ],
+ });
+ });
+
+ it('defaults to white text on a black background', () => {
+ const state = INITIAL_DOUBLE_BANNER_COMPONENTS_STATE();
+
+ expect(state.halves[0]).toMatchObject({
+ textColor: Color.White,
+ backgroundColor: Color.Black,
+ });
+ expect(field('textColor').editorProps.defaultValue).toBe(Color.White);
+ expect(field('backgroundColor').editorProps.defaultValue).toBe(Color.Black);
+ });
+});
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.ts
new file mode 100644
index 00000000..cc108ffa
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig.ts
@@ -0,0 +1,207 @@
+import { AvoCoreContentPickerType } from '@viaa/avo2-types';
+import {
+ GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF,
+ GET_FOREGROUND_COLOR_OPTIONS_ARCHIEF,
+} from '~modules/content-page/const/get-color-options';
+import {
+ Color,
+ type ContentBlockConfig,
+ ContentBlockEditor,
+ ContentBlockType,
+ type DefaultContentBlockState,
+} from '~modules/content-page/types/content-block.types';
+import { GET_ADMIN_ICON_OPTIONS } from '~shared/consts/icons.consts';
+import { tText } from '~shared/helpers/translation-functions';
+import { validateRequiredValue } from '~shared/helpers/validation';
+import { HET_ARCHIEF } from '~shared/types';
+import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults';
+
+const INITIAL_DOUBLE_BANNER_HALF_STATE = () => ({
+ label: '',
+ icon1: '',
+ icon2: '',
+ icon3: '',
+ link: undefined,
+ image: '',
+ textColor: Color.White,
+ backgroundColor: Color.Black,
+});
+
+/**
+ * The block always shows exactly two halves, so `halves` starts with two entries and the group is
+ * pinned to min = max = 2: `FieldGenerator` then hides both the add and the delete button, which is
+ * how the FA requirement "beide helften moeten volledig ingevuld worden" is enforced in the editor.
+ * https://meemoo.atlassian.net/browse/ARC-3833
+ */
+export const INITIAL_DOUBLE_BANNER_COMPONENTS_STATE = () => ({
+ halves: [INITIAL_DOUBLE_BANNER_HALF_STATE(), INITIAL_DOUBLE_BANNER_HALF_STATE()],
+});
+
+export const INITIAL_DOUBLE_BANNER_BLOCK_STATE = (): DefaultContentBlockState =>
+ BLOCK_STATE_DEFAULTS();
+
+const ICON_FIELD = (label: string) => ({
+ label,
+ editorType: ContentBlockEditor.IconPicker,
+ editorProps: {
+ options: GET_ADMIN_ICON_OPTIONS(),
+ },
+});
+
+export const DOUBLE_BANNER_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({
+ position,
+ name: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___dubbele-banner',
+ {},
+ [HET_ARCHIEF]
+ ),
+ type: ContentBlockType.DoubleBanner,
+ components: {
+ state: INITIAL_DOUBLE_BANNER_COMPONENTS_STATE(),
+ fields: {
+ halves: {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___helft',
+ {},
+ [HET_ARCHIEF]
+ ),
+ type: 'fieldGroup',
+ min: 2,
+ max: 2,
+ repeat: {
+ defaultState: INITIAL_DOUBLE_BANNER_HALF_STATE(),
+ },
+ fields: {
+ label: TEXT_FIELD(
+ {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___label',
+ {},
+ [HET_ARCHIEF]
+ ),
+ },
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___label-is-verplicht',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ icon1: ICON_FIELD(
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-1',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ icon2: ICON_FIELD(
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-2',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ icon3: ICON_FIELD(
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-3',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ link: {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming',
+ {},
+ [HET_ARCHIEF]
+ ),
+ editorType: ContentBlockEditor.ContentPicker,
+ editorProps: {
+ allowedTypes: [
+ AvoCoreContentPickerType.CONTENT_PAGE,
+ AvoCoreContentPickerType.INTERNAL_LINK,
+ AvoCoreContentPickerType.EXTERNAL_LINK,
+ AvoCoreContentPickerType.ANCHOR_LINK,
+ ],
+ // The FA requires the destination to always open in the same tab
+ hideTargetSwitch: true,
+ },
+ validator: (value: string) =>
+ validateRequiredValue(
+ value,
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming-is-verplicht',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ },
+ image: FILE_FIELD(
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding-is-verplicht',
+ {},
+ [HET_ARCHIEF]
+ ),
+ {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding',
+ {},
+ [HET_ARCHIEF]
+ ),
+ editorProps: {
+ assetType: 'CONTENT_BLOCK_IMAGE',
+ allowMulti: false,
+ },
+ }
+ ),
+ textColor: {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur',
+ {},
+ [HET_ARCHIEF]
+ ),
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: GET_FOREGROUND_COLOR_OPTIONS_ARCHIEF(),
+ defaultValue: Color.White,
+ },
+ validator: (value: string) =>
+ validateRequiredValue(
+ value,
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur-is-verplicht',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ },
+ backgroundColor: {
+ label: tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak',
+ {},
+ [HET_ARCHIEF]
+ ),
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF(),
+ defaultValue: Color.Black,
+ },
+ validator: (value: string) =>
+ validateRequiredValue(
+ value,
+ tText(
+ 'modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak-is-verplicht',
+ {},
+ [HET_ARCHIEF]
+ )
+ ),
+ },
+ },
+ },
+ },
+ },
+ block: {
+ state: INITIAL_DOUBLE_BANNER_BLOCK_STATE(),
+ fields: {
+ ...BLOCK_FIELD_DEFAULTS(),
+ },
+ },
+});
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.icons.test.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.icons.test.tsx
new file mode 100644
index 00000000..eba8fde5
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.icons.test.tsx
@@ -0,0 +1,43 @@
+import { render, screen } from '@testing-library/react';
+import { AvoCoreDatabaseType } from '@viaa/avo2-types';
+import type { ReactNode } from 'react';
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { AdminConfigManager } from '~core/config/config.class';
+import { BlockDoubleBanner } from './BlockDoubleBanner';
+
+// The real Icon is used on purpose here: the editor stores icons by their raw name
+// (eg: newspaper--light), not by their admin-core config key (eg: newspaper).
+vi.mock('~shared/components/SmartLink/SmartLink', () => ({
+ generateSmartLink: (_action: unknown, children: ReactNode) =>
{children}
,
+}));
+
+const half = (label: string, icon?: string) => ({
+ label,
+ icon1: icon,
+ link: { type: 'EXTERNAL_LINK', value: `/${label}` },
+ image: `/${label}.jpg`,
+ textColor: '#FFFFFF',
+ backgroundColor: '#000000',
+});
+
+describe('BlockDoubleBanner icons', () => {
+ it('renders an icon that the editor picked by its raw name', () => {
+ AdminConfigManager.setConfig({
+ icon: {
+ component: ({ name, className }: { name: string; className?: string }) => (
+
+ ),
+ componentProps: { arrowDownRight: { name: 'arrow-down-right--light' } },
+ list: () => [{ label: 'Newspaper light', value: 'newspaper--light' }],
+ },
+ env: { DATABASE_APPLICATION_TYPE: AvoCoreDatabaseType.hetArchief },
+ } as never);
+
+ render();
+
+ expect(screen.getByTestId('icon-newspaper--light')).toBeInTheDocument();
+ // Both halves keep their CTA arrow, which resolves through the config key.
+ expect(screen.getAllByTestId('icon-arrow-down-right--light')).toHaveLength(2);
+ });
+});
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.scss
new file mode 100644
index 00000000..21afcc77
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.scss
@@ -0,0 +1,136 @@
+@use "../../../../shared/styles/settings/variables" as variables;
+@use "../../../../shared/styles/mixins/typography" as typography;
+@use "../../../../shared/styles/mixins/animations" as animations;
+@use "../../../../shared/styles/mixins/focus" as focus;
+
+$panel-padding: variables.$g-spacer-unit * 2;
+$half-gap: variables.$g-spacer-unit * 2;
+$pill-radius: 999px;
+
+.c-block-double-banner {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: $half-gap;
+
+ @media (min-width: variables.$g-bp3) {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ &__half {
+ display: flex;
+ flex-direction: column-reverse;
+ overflow: hidden;
+ text-decoration: none;
+
+ @include focus.focus-outline;
+
+ @media (min-width: variables.$g-bp3) {
+ flex-direction: row;
+ min-height: 16rem;
+ }
+
+ &:hover .c-block-double-banner__image img {
+ @include animations.zoom-in-animation;
+ }
+
+ // Mirror image: the image moves to the other side and its rounded end flips with it.
+ &--mirrored {
+ .c-block-double-banner__image {
+ border-radius: $pill-radius 0 0 $pill-radius;
+ }
+
+ @media (min-width: variables.$g-bp3) {
+ flex-direction: row-reverse;
+ }
+ }
+ }
+
+ &__panel {
+ box-sizing: border-box;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: end;
+ gap: variables.$g-spacer-unit;
+ padding: $panel-padding $panel-padding * 1.5;
+
+ @media (min-width: variables.$g-bp3) {
+ flex: 0 0 50%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+ min-width: 0;
+ padding: $panel-padding * 1.5;
+ }
+ }
+
+ &__label {
+ @include typography.sofia-pro-heading-md;
+ }
+
+ // Mobile keeps only the arrow at the far edge; desktop groups the selected icons and arrow at
+ // the start of the row.
+ &__actions {
+ display: flex;
+ align-items: center;
+ gap: variables.$g-spacer-unit * 0.5;
+ justify-self: end;
+
+ @media (min-width: variables.$g-bp3) {
+ justify-self: auto;
+ }
+ }
+
+ &__icons {
+ display: none;
+ align-items: center;
+ gap: variables.$g-spacer-unit * 0.5;
+
+ @media (min-width: variables.$g-bp3) {
+ display: flex;
+ }
+ }
+
+ &__icon,
+ &__arrow {
+ font-size: 2.4rem;
+ line-height: 1;
+
+ @media (min-width: variables.$g-bp3) {
+ font-size: 3.2rem;
+ }
+ }
+
+ &__image {
+ position: relative;
+ flex: 0 0 auto;
+ width: 100%;
+ height: 9.6rem;
+ overflow: hidden;
+
+ // Percentage radii would create an ellipse on this rectangular image. CSS clamps this
+ // oversized radius to a circular pill cap, regardless of the rendered image height.
+ border-radius: 0 $pill-radius $pill-radius 0;
+
+ @media (min-width: variables.$g-bp3) {
+ box-sizing: border-box;
+ flex: 0 0 50%;
+ width: auto;
+ height: auto;
+ min-width: 0;
+ }
+
+ img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+
+ // Cover-scaling, centered both ways, as required by the FA.
+ object-fit: cover;
+ object-position: center;
+
+ @include animations.zoom-in-transition;
+ }
+ }
+}
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.test.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.test.tsx
new file mode 100644
index 00000000..88ecc942
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.test.tsx
@@ -0,0 +1,74 @@
+import { render, screen } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { BlockDoubleBanner } from './BlockDoubleBanner';
+
+vi.mock('@viaa/avo2-components', () => ({
+ LinkTarget: { Self: '_self' },
+ Image: ({ alt, src }: { alt: string; src: string }) =>
,
+}));
+
+vi.mock('~shared/components/Icon/Icon', () => ({
+ Icon: ({ name, ...props }: { name: string; 'aria-hidden'?: boolean }) => (
+
+ ),
+}));
+
+vi.mock('~shared/components/SmartLink/SmartLink', () => ({
+ generateSmartLink: (
+ action: { target: string; value: string },
+ children: ReactNode,
+ _title?: string,
+ className?: string
+ ) => (
+
+ {children}
+
+ ),
+}));
+
+describe('BlockDoubleBanner', () => {
+ it('wraps each complete half in a same-tab link and keeps its media decorative', () => {
+ render(
+
+ );
+
+ const links = screen.getAllByRole('link');
+ expect(links).toHaveLength(2);
+ expect(links[0]).toHaveAttribute('href', '/newspapers');
+ expect(links[0]).toHaveAttribute('target', '_self');
+ expect(links[1]).toHaveAttribute('href', '/av');
+ expect(links[1]).toHaveAttribute('target', '_self');
+
+ for (const link of links) {
+ expect(link.querySelector('img')).toHaveAttribute('alt', '');
+ expect(link.querySelector('.c-block-double-banner__actions')).toHaveAttribute(
+ 'aria-hidden',
+ 'true'
+ );
+ }
+ // The shared Icon resolves the config name itself, so the block passes the picked name on.
+ expect(links[0].querySelector('[data-icon="newspaper--light"]')).toBeInTheDocument();
+ expect(links[0].querySelector('[data-icon="video--light"]')).toBeInTheDocument();
+ });
+});
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.tsx
new file mode 100644
index 00000000..6ca3e0ec
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.tsx
@@ -0,0 +1,95 @@
+import { type IconName, Image, LinkTarget } from '@viaa/avo2-components';
+import clsx from 'clsx';
+import { compact } from 'es-toolkit';
+import type { CSSProperties, FunctionComponent, ReactElement } from 'react';
+import React from 'react';
+import type {
+ BlockDoubleBannerProps,
+ DoubleBannerHalf,
+} from '~content-blocks/BlockDoubleBanner/BlockDoubleBanner.types';
+import { Icon } from '~shared/components/Icon/Icon';
+import { generateSmartLink } from '~shared/components/SmartLink/SmartLink';
+import './BlockDoubleBanner.scss';
+
+/**
+ * Two mirrored halves, each one clickable path to a search page or content page.
+ * The first half puts its text panel on the left and its image on the right; the second is the
+ * mirror image of that. https://meemoo.atlassian.net/browse/ARC-3833
+ */
+export const BlockDoubleBanner: FunctionComponent = ({
+ className,
+ halves,
+}): ReactElement => {
+ const renderHalf = (half: DoubleBannerHalf, index: number) => {
+ const icons = compact([half.icon1, half.icon2, half.icon3]);
+
+ return (
+ <>
+
+ {half.label}
+
+
+ {icons.map((icon, iconIndex) => (
+
+ ))}
+
+
+
+
+
+ {/* Before an image is uploaded the value is empty, and an empty src makes the
+ browser refetch the page. */}
+ {half.image ? : null}
+
+ >
+ );
+ };
+
+ return (
+
+ {halves.map((half: DoubleBannerHalf, index: number) => {
+ const content = renderHalf(half, index);
+ // Every half after the first mirrors the previous one: image towards the middle.
+ const halfClassName = clsx('c-block-double-banner__half', {
+ 'c-block-double-banner__half--mirrored': index % 2 === 1,
+ });
+
+ // generateSmartLink returns a bare fragment for an empty action, which drops the
+ // halfClassName and with it the whole layout. Keep the wrapper in that case, so the
+ // editor sees the real layout before the destination is filled in.
+ if (!half.link?.value) {
+ return (
+ // biome-ignore lint/suspicious/noArrayIndexKey: the halves have no id of their own
+
+ {content}
+
+ );
+ }
+
+ return (
+ // biome-ignore lint/suspicious/noArrayIndexKey: the halves have no id of their own
+
+ {generateSmartLink(
+ { ...half.link, target: LinkTarget.Self },
+ content,
+ undefined,
+ halfClassName
+ )}
+
+ );
+ })}
+
+ );
+};
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts
new file mode 100644
index 00000000..459d3669
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts
@@ -0,0 +1,22 @@
+import type { ButtonAction } from '@viaa/avo2-components';
+import type { DefaultComponentProps } from '~shared/types';
+
+/**
+ * One of the two mirrored halves of the double banner.
+ * The FA requires every field except the icons to be filled in.
+ * https://meemoo.atlassian.net/browse/ARC-3833
+ */
+export interface DoubleBannerHalf {
+ label: string;
+ icon1?: string;
+ icon2?: string;
+ icon3?: string;
+ link: ButtonAction;
+ image: string;
+ textColor: string;
+ backgroundColor: string;
+}
+
+export interface BlockDoubleBannerProps extends DefaultComponentProps {
+ halves: [DoubleBannerHalf, DoubleBannerHalf];
+}
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts
new file mode 100644
index 00000000..cba2f2dc
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts
@@ -0,0 +1,6 @@
+export { BlockDoubleBanner } from './BlockDoubleBanner';
+export {
+ DOUBLE_BANNER_BLOCK_CONFIG,
+ INITIAL_DOUBLE_BANNER_BLOCK_STATE,
+ INITIAL_DOUBLE_BANNER_COMPONENTS_STATE,
+} from './BlockDoubleBanner.editorconfig';
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 6265d370..90458f75 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
@@ -8,6 +8,7 @@ import { CARDS_WITHOUT_DESCRIPTION_BLOCK_CONFIG } from '~content-blocks/BlockCar
import { CONTENT_ENCLOSE_BLOCK_CONFIG } from '~content-blocks/BlockContentEnclose';
import { CONTENT_PAGE_META_BLOCK_CONFIG } from '~content-blocks/BlockContentPageMeta';
import { CTAS_BLOCK_CONFIG } from '~content-blocks/BlockCTAs';
+import { DOUBLE_BANNER_BLOCK_CONFIG } from '~content-blocks/BlockDoubleBanner';
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';
@@ -99,4 +100,5 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record<
[ContentBlockType.ThemeReels]: THEME_REELS_BLOCK_CONFIG,
[ContentBlockType.ObjectsGrid]: OBJECTS_GRID_BLOCK_CONFIG,
[ContentBlockType.OverviewThemes]: OVERVIEW_THEMES_BLOCK_CONFIG,
+ [ContentBlockType.DoubleBanner]: DOUBLE_BANNER_BLOCK_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 05ecb74f..403e2c88 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
@@ -8,6 +8,7 @@ import { INITIAL_CARDS_WITHOUT_DESCRIPTION_COMPONENTS_STATE } from '~content-blo
import { INITIAL_CONTENT_ENCLOSE_BLOCK_STATE } from '~content-blocks/BlockContentEnclose/BlockContentEnclose.editorconfig';
import { INITIAL_CONTENT_PAGE_META_COMPONENTS_STATE } from '~content-blocks/BlockContentPageMeta';
import { INITIAL_CTAS_COMPONENTS_STATE } from '~content-blocks/BlockCTAs';
+import { INITIAL_DOUBLE_BANNER_COMPONENTS_STATE } from '~content-blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig';
import { INITIAL_EVENTBRITE_COMPONENTS_STATE } from '~content-blocks/BlockEventbrite';
import { INITIAL_HEADING_COMPONENTS_STATE } from '~content-blocks/BlockHeading';
import { INITIAL_HET_ARCHIEF_HEADER_SEARCH_BLOCK_STATE } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig';
@@ -102,4 +103,5 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: {
[ContentBlockType.ThemeReels]: INITIAL_THEME_REELS_COMPONENTS_STATE,
[ContentBlockType.ObjectsGrid]: INITIAL_OBJECTS_GRID_BLOCK_STATE,
[ContentBlockType.OverviewThemes]: INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE,
+ [ContentBlockType.DoubleBanner]: INITIAL_DOUBLE_BANNER_COMPONENTS_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 66da92a9..f68b1a4e 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
@@ -228,6 +228,14 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () =
),
value: ContentBlockType.OverviewThemes,
},
+ {
+ label: tText(
+ 'modules/content-page/const/get-content-block-type-options___dubbele-banner',
+ {},
+ [HET_ARCHIEF]
+ ),
+ value: ContentBlockType.DoubleBanner,
+ },
];
// Only show the content blocks that the client enabled through the config object
diff --git a/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts b/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts
new file mode 100644
index 00000000..d03bddcb
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts
@@ -0,0 +1,60 @@
+import type { SelectOption } from '@viaa/avo2-components';
+import { describe, expect, it, vi } from 'vitest';
+import { Color, ContentBlockEditor } from '~modules/content-page/types/content-block.types';
+import { generateFieldAttributes } from './field-attributes';
+
+interface ColorSelectAttributes {
+ defaultValue?: SelectOption;
+ onChange?: (option: SelectOption) => void;
+}
+
+describe('generateFieldAttributes', () => {
+ it('maps a Color enum default to the option expected by ColorSelect', () => {
+ const whiteOption: SelectOption = {
+ label: 'White',
+ value: Color.White,
+ };
+ const onChange = vi.fn();
+ const attributes = generateFieldAttributes(
+ {
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: [whiteOption],
+ defaultValue: Color.White,
+ },
+ },
+ onChange,
+ undefined,
+ 'color',
+ 'color',
+ {}
+ ) as ColorSelectAttributes;
+
+ expect(attributes.defaultValue).toBe(whiteOption);
+ attributes.onChange?.(whiteOption);
+ expect(onChange).toHaveBeenCalledWith(Color.White);
+ });
+
+ it('keeps existing option-object defaults compatible', () => {
+ const blackOption: SelectOption = {
+ label: 'Black',
+ value: Color.Black,
+ };
+ const attributes = generateFieldAttributes(
+ {
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: [blackOption],
+ defaultValue: blackOption,
+ },
+ },
+ vi.fn(),
+ undefined,
+ 'color',
+ 'color',
+ {}
+ ) as ColorSelectAttributes;
+
+ expect(attributes.defaultValue).toBe(blackOption);
+ });
+});
diff --git a/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts b/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
index bd688818..76a51df7 100644
--- a/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
+++ b/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
@@ -91,15 +91,31 @@ export const generateFieldAttributes = (
} as DatePickerProps;
case ContentBlockEditor.IconPicker:
- case ContentBlockEditor.ColorSelect:
- return {
+ case ContentBlockEditor.ColorSelect: {
+ const options = field.editorProps.options as SelectOption[];
+ const selectAttributes = {
onChange: ((option: SelectOption) => {
onChange(option?.value || '');
// biome-ignore lint/suspicious/noExplicitAny: todo investigate why this cast is needed
}) as any,
- value: field.editorProps.options.find((opt: SelectOption) => opt.value === value),
+ value: options.find((option) => option.value === value),
};
+ if (field.editorType === ContentBlockEditor.ColorSelect) {
+ const configuredDefaultValue = field.editorProps.defaultValue;
+
+ return {
+ ...selectAttributes,
+ defaultValue:
+ typeof configuredDefaultValue === 'string'
+ ? options.find((option) => option.value === configuredDefaultValue)
+ : configuredDefaultValue,
+ };
+ }
+
+ return selectAttributes;
+ }
+
case ContentBlockEditor.RICH_TEXT_EDITOR: {
// biome-ignore lint/suspicious/noExplicitAny: todo
const html = (state as any)[key] || '';
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 2967497e..9eb7d116 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
@@ -194,6 +194,7 @@ export enum ContentBlockType {
ThemeReels = 'THEME_REELS',
ObjectsGrid = 'OBJECTS_GRID',
OverviewThemes = 'OVERVIEW_THEMES',
+ DoubleBanner = 'DOUBLE_BANNER',
}
export enum ContentBlockEditor {
diff --git a/ui/src/shared/helpers/admin-core-config.tsx b/ui/src/shared/helpers/admin-core-config.tsx
index 0d8d0bed..7742d865 100644
--- a/ui/src/shared/helpers/admin-core-config.tsx
+++ b/ui/src/shared/helpers/admin-core-config.tsx
@@ -100,6 +100,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction
ContentBlockType.HetArchiefQuote,
ContentBlockType.ObjectsGrid,
ContentBlockType.OverviewThemes,
+ ContentBlockType.DoubleBanner,
],
defaultPageWidth: ContentPageWidth.LARGE,
onSaveContentPage: async (contentPageInfo: ContentPageInfo) => {
diff --git a/ui/src/shared/translations/hetArchief/nl.json b/ui/src/shared/translations/hetArchief/nl.json
index e8a5d34d..afbe4f6f 100644
--- a/ui/src/shared/translations/hetArchief/nl.json
+++ b/ui/src/shared/translations/hetArchief/nl.json
@@ -674,6 +674,21 @@
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___titletype": "titeltype",
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___verwijder-object": "verwijder object",
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___voeg-object-toe": "voeg object toe",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak": "Achtergrondkleur tekstvak",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak-is-verplicht": "Achtergrondkleur tekstvak is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding": "Afbeelding",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding-is-verplicht": "Afbeelding is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming": "Bestemming",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming-is-verplicht": "Bestemming is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___dubbele-banner": "Dubbele banner",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___helft": "Helft",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-1": "Icoon 1",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-2": "Icoon 2",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-3": "Icoon 3",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___label": "Label",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___label-is-verplicht": "Label is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur": "Tekstkleur",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur-is-verplicht": "Tekstkleur is verplicht",
"modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___aria-label-verplicht": "aria label verplicht",
"modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___aria-label-voor-zoekveld": "Aria label voor zoekveld",
"modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___zoek-in-de-publieke-catalogus-input-aria-label": "Hiermee kan je zoeken in de publieke catalogus ",
@@ -838,6 +853,7 @@
"modules/content-page/const/content-page___vertalingen": "Vertalingen",
"modules/content-page/const/get-content-block-type-options___breadcrumbs": "Breadcrumbs",
"modules/content-page/const/get-content-block-type-options___content-enclose-grid": "Content insluiten grid",
+ "modules/content-page/const/get-content-block-type-options___dubbele-banner": "Dubbele banner",
"modules/content-page/const/get-content-block-type-options___highlight-text": "Highlight text",
"modules/content-page/const/get-content-block-type-options___homepage-banner": "Homepage banner",
"modules/content-page/const/get-content-block-type-options___objecten-grid": "Objecten grid",