From 6948956ea4a8a0dd6db246f7775b071682382d79 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Tue, 4 Aug 2026 13:15:30 +0200 Subject: [PATCH 1/7] ARC-3819: Start adding copyright attribution configs to each content block --- ...ArchiefImageTextBackground.editorconfig.ts | 4 ++ .../BlockHetArchiefImageTextBackground.scss | 1 - .../BlockHetArchiefImageTextBackground.tsx | 38 ++++++++---- .../BlockImage/BlockImage.editorconfig.ts | 23 +++++--- .../blocks/BlockImage/BlockImage.tsx | 15 +++-- .../BlockImageGrid.editorconfig.ts | 11 +++- .../blocks/BlockImageGrid/BlockImageGrid.tsx | 6 ++ .../BlockImageGrid/BlockImageGrid.types.tsx | 3 + .../BlockUspGrid.editorconfig.ts | 11 +++- .../BlockRichText.editorconfig.ts | 10 +++- .../blocks/BlockRichText/BlockRichText.tsx | 15 ++++- .../BlockRichTextTwoColumns.editorconfig.ts | 11 +++- .../components/blocks/defaults.ts | 58 ++++++++++++++++--- .../content-page/types/content-block.types.ts | 15 +++-- .../CopyrightAttribution.scss | 6 ++ .../CopyrightAttribution.tsx | 42 ++++++++++++++ .../components/CopyrightAttribution/index.ts | 1 + 17 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss create mode 100644 ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx create mode 100644 ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts index 9a54e2ec9..6205f6eac 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts @@ -25,6 +25,8 @@ import { Color, ContentBlockEditor, ContentBlockType } from '../../../types/cont import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, PADDING_SINGLE_VALUE_FIELD, TEXT_FIELD, } from '../defaults'; @@ -42,6 +44,7 @@ export const INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_COMPONENTS_STATE = imageAlignment: 'left-inside-page', buttonLabel: '', buttonIconAlignment: 'left', + ...COPYRIGHT_STATE(), }); export const INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_STATE = @@ -155,6 +158,7 @@ export const HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_CONFIG = ( validator: undefined, }, }, + ...COPYRIGHT_FIELDS(), buttonType: { label: tText( 'admin/content-block/helpers/image-text-background/image-text-background___knop-type' diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss index 3ee274650..72bbfdeba 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss @@ -165,7 +165,6 @@ .c-block-het-archief-image-text-background__image-wrapper { @media (min-width: variables.$g-bp2) { - position: absolute; inset: 0; } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx index cb47c6e29..2233ee4bf 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx @@ -1,7 +1,7 @@ import type { ButtonAction, ButtonType, IconName } from '@viaa/avo2-components'; import { Button, Image } from '@viaa/avo2-components'; import clsx from 'clsx'; -import type { FunctionComponent, ReactElement } from 'react'; +import React, { type FunctionComponent, type ReactElement } from 'react'; import type { BackgroundAlignOption, Color, @@ -15,6 +15,7 @@ import { BlockHeading } from '../BlockHeading/BlockHeading'; import './BlockHetArchiefImageTextBackground.scss'; import type { ContentPageWidth } from '~modules/content-page/types/content-pages.types'; import { CONTENT_PAGE_WIDTH_TO_REM } from '~modules/content-page/types/content-pages.types'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; export interface BlockHetArchiefImageTextBackgroundProps extends DefaultComponentProps { heading: string; @@ -25,6 +26,9 @@ export interface BlockHetArchiefImageTextBackgroundProps extends DefaultComponen image?: string; imageAlignment?: BackgroundAlignOption; imageAltText?: string; + copyrightTitle: string; + copyrightIconVisible: boolean; + copyrightText: string; buttonAction?: ButtonAction; buttonAltTitle?: string; buttonLabel: string; @@ -46,6 +50,9 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< image, imageAlignment = 'left-screen', imageAltText, + copyrightTitle, + copyrightIconVisible, + copyrightText, buttonAction, buttonAltTitle, buttonLabel, @@ -93,18 +100,25 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< buttonAltTitle || buttonLabel )} - {image && ( - {imageAltText} + {image && ( + {imageAltText} + )} + - )} + ); }; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts index 2fab0ef41..cb6f82ca4 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts @@ -10,10 +10,17 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + FILE_FIELD, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_IMAGE_COMPONENTS_STATE = (): ImageBlockComponentState => ({ title: '', + copyrightIconVisible: true, text: '', imageSource: '', imageAlt: '', @@ -37,13 +44,13 @@ export const IMAGE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ components: { state: INITIAL_IMAGE_COMPONENTS_STATE(), fields: { - title: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/image___bijschift-titel'), - validator: undefined, - }), - text: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/image___bijschrift-beschrijving'), - validator: undefined, + ...COPYRIGHT_FIELDS({ + title: { + fieldName: 'title', + }, + text: { + fieldName: 'text', + }, }), imageSource: FILE_FIELD( tText('admin/content-block/helpers/generators/image___een-afbeelding-is-verplicht'), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx index 19d921b2f..37d71e108 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx @@ -6,6 +6,7 @@ import React from 'react'; import type { AlignOption } from '~modules/content-page/types/content-block.types'; import './BlockImage.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; import { generateSmartLink, SmartLink } from '~shared/components/SmartLink/SmartLink'; export interface BlockImageProps extends DefaultProps { @@ -14,6 +15,7 @@ export interface BlockImageProps extends DefaultProps { imageAction?: ButtonAction; imageAlt?: string; title?: string; + copyrightIconVisible?: boolean; text?: string; width?: 'page-header' | 'full-width' | string; align?: AlignOptions; @@ -31,6 +33,7 @@ export const BlockImage: FunctionComponent = ({ imageAction, imageAlt, title = '', + copyrightIconVisible = false, text = '', width = '100%', align = 'center', @@ -95,12 +98,12 @@ export const BlockImage: FunctionComponent = ({ {/* image author attribution */} - {(!!title || !!text) && ( -
- {title &&

© {title}

} - {text &&

{text}

} -
- )} + ); }; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts index f8fe26295..dca287c51 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts @@ -15,7 +15,14 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + FILE_FIELD, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_IMAGE_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentStateFields[] => [ { @@ -26,6 +33,7 @@ export const INITIAL_IMAGE_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentSt buttonType: 'primary', buttonTitle: '', action: undefined, + ...COPYRIGHT_STATE(), } as unknown as ImageGridBlockComponentStateFields, ]; @@ -64,6 +72,7 @@ export const IMAGE_GRID_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ } as FileUploadProps, } ), + ...COPYRIGHT_FIELDS(), title: TEXT_FIELD({ label: tText('admin/content-block/helpers/generators/image-grid___titel'), validator: undefined, diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx index a14e5051d..a2193426d 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx @@ -9,6 +9,7 @@ import type { import { defaultRenderLinkFunction } from '~shared/helpers/routing/link'; import './BlockImageGrid.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; export const BlockImageGrid: FunctionComponent = ({ elements = [], @@ -48,6 +49,11 @@ export const BlockImageGrid: FunctionComponent = ({ backgroundPosition: imageItemAlignment || 'center', }} /> +
[ { @@ -22,6 +29,7 @@ export const INITIAL_USP_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentStat title: '', text: '', action: undefined, + ...COPYRIGHT_STATE(), } as unknown as ImageGridBlockComponentStateFields, ]; @@ -58,6 +66,7 @@ export const USP_GRID_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ } as FileUploadProps, } ), + ...COPYRIGHT_FIELDS(), title: TEXT_FIELD({ label: tText('admin/content-block/helpers/generators/image-grid___titel'), }), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts index 5afac4a06..3b771479a 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts @@ -9,10 +9,17 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_RICH_TEXT_COMPONENTS_STATE = (): RichTextBlockComponentState => ({ content: '', + ...COPYRIGHT_STATE(), }); export const INITIAL_RICH_TEXT_BLOCK_STATE = (): DefaultContentBlockState => @@ -43,6 +50,7 @@ export const RICH_TEXT_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ }, tText('admin/content-block/helpers/generators/rich-text___tekst-is-verplicht') ), + ...COPYRIGHT_FIELDS(), }, }, block: { diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx index ffcf1ba36..7676fa846 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx @@ -17,9 +17,13 @@ import { defaultRenderLinkFunction } from '~shared/helpers/routing/link'; import { SanitizePreset } from '~shared/helpers/sanitize/presets'; import './BlockRichText.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; interface BlockRichTextElement { content: string; + copyrightTitle: string; + copyrightIconVisible: boolean; + copyrightText: string; buttons?: (ButtonProps & { buttonAction: ButtonAction })[]; color?: string; } @@ -35,6 +39,9 @@ export const BlockRichText: FunctionComponent = ({ elements = [ { content: '', + copyrightTitle: '', + copyrightIconVisible: false, + copyrightText: '', }, ], maxTextWidth, @@ -69,7 +76,8 @@ export const BlockRichText: FunctionComponent = ({ }; const renderContent = (contentElem: BlockRichTextElement, columnIndex = 0) => { - const { content, color, buttons } = contentElem; + const { content, copyrightTitle, copyrightIconVisible, copyrightText, color, buttons } = + contentElem; return ( <> @@ -83,6 +91,11 @@ export const BlockRichText: FunctionComponent = ({ }} type="div" /> + {buttons && !!buttons.length && renderButtons(columnIndex, buttons)} ); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts index ec42a0195..5442bf09a 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts @@ -17,15 +17,23 @@ import { DEFAULT_BUTTON_PROPS, } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_RICH_TEXT_TWO_COLUMNS_COMPONENTS_STATE = (): RichTextBlockComponentState[] => [ { content: '', + ...COPYRIGHT_STATE(), buttons: [], }, { content: '', + ...COPYRIGHT_STATE(), buttons: [], }, ]; @@ -60,6 +68,7 @@ export const RICH_TEXT_TWO_COLUMNS_BLOCK_CONFIG = (position = 0): ContentBlockCo }, tText('admin/content-block/helpers/generators/rich-text-two-columns___tekst-is-verplicht') ), + ...COPYRIGHT_FIELDS(), buttons: { label: tText('admin/content-block/helpers/generators/rich-text-two-columns___knop'), fields: { diff --git a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts index bd5fe4469..bd4c8b506 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts @@ -1,4 +1,4 @@ -import type { SelectOption } from '@viaa/avo2-components'; +import type { CheckboxProps, SelectOption } from '@viaa/avo2-components'; import { GET_ALIGN_OPTIONS } from '~modules/content-page/const/get-align-options'; import { GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF, @@ -16,14 +16,17 @@ import { isAvo } from '~shared/helpers/is-avo'; import { tText } from '~shared/helpers/translation-functions'; import { validateRequiredValue } from '~shared/helpers/validation.ts'; import { SpecialUserGroups } from '~shared/types/authentication.types'; -import type { - ContentBlockField, - CustomBackground, - DefaultContentBlockState, - GradientColor, - PaddingFieldState, +import { + Color, + type ContentBlockComponentsConfig, + ContentBlockEditor, + type ContentBlockField, + type CopyrightComponentState, + type CustomBackground, + type DefaultContentBlockState, + type GradientColor, + type PaddingFieldState, } from '../../types/content-block.types'; -import { Color, ContentBlockEditor } from '../../types/content-block.types'; // Block config defaults export const BLOCK_STATE_DEFAULTS = ( @@ -197,3 +200,42 @@ export const CONTENT_TYPE_AND_LABELS_INPUT = ( validator: undefined, ...propOverride, }); + +export const COPYRIGHT_FIELDS = (overrides?: { + title?: { + fieldName?: string; + overrides?: Partial; + }; + showIcon?: { + fieldName?: string; + overrides?: Partial; + }; + text?: { + fieldName?: string; + overrides?: Partial; + }; +}): ContentBlockComponentsConfig['fields'] => ({ + [overrides?.title?.fieldName || 'copyrightTitle']: TEXT_FIELD({ + label: tText('bijschrift titel'), + validator: undefined, + ...overrides?.title?.overrides, + }), + [overrides?.showIcon?.fieldName || 'copyrightIconVisible']: { + editorType: ContentBlockEditor.Checkbox, + editorProps: { + label: tText('Toon bijschrift icoon'), + } as CheckboxProps, + ...overrides?.showIcon?.overrides, + }, + [overrides?.text?.fieldName || 'copyrightText']: TEXT_FIELD({ + label: tText('bijschrift beschrijving'), + validator: undefined, + ...overrides?.text?.overrides, + }), +}); + +export const COPYRIGHT_STATE = (): CopyrightComponentState => ({ + copyrightTitle: '', + copyrightIconVisible: true, + copyrightText: '', +}); 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 b8462c62b..e91339747 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 @@ -283,7 +283,14 @@ export interface HeadingBlockComponentState { align: AlignOption; } -export interface ImageBlockComponentState { +export interface CopyrightComponentState { + copyrightTitle: string; + copyrightIconVisible: boolean; + copyrightText: string; +} + +export interface ImageBlockComponentState + extends Omit { title: string; text: string; imageSource: string; @@ -298,7 +305,7 @@ export interface ImageBlockComponentState { buttonAlign?: AlignOption; } -export interface ImageGridBlockComponentStateFields { +export interface ImageGridBlockComponentStateFields extends CopyrightComponentState { source: string; title?: string; text?: string; @@ -331,7 +338,7 @@ export interface ButtonsBlockComponentState { navigate?: (buttonAction: ButtonAction) => void; } -export interface RichTextBlockComponentState { +export interface RichTextBlockComponentState extends CopyrightComponentState { content: string; buttons?: ButtonsBlockComponentState[]; } @@ -559,7 +566,7 @@ export interface AvoImageTextBackgroundBlockComponentState { buttonIconAlignment?: SimpleAlignOption; } -export interface HetArchiefImageTextBackgroundBlockComponentState { +export interface HetArchiefImageTextBackgroundBlockComponentState extends CopyrightComponentState { heading: string; headingType: HeadingTypeOption; headingSize: HeadingSizeOption; diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss new file mode 100644 index 000000000..ba2ecf0aa --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss @@ -0,0 +1,6 @@ +@use "../../styles/settings/colors" as colors; +@use "../../styles/settings/variables" as variables; + +.a-copyright-attribution__text { + color: colors.$color-gray-200; +} diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx new file mode 100644 index 000000000..2a610f13a --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx @@ -0,0 +1,42 @@ +import type { DefaultProps } from '@viaa/avo2-components'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import './CopyrightAttribution.scss'; +import clsx from 'clsx'; + +export interface BlockImageProps extends DefaultProps { + title?: string; + showIcon?: boolean; + text?: string; +} + +export const CopyrightAttribution: FunctionComponent = ({ + className, + title = '', + showIcon = false, + text = '', +}) => { + if (!title && !text) { + return null; + } + + const renderTitle = () => { + if (!title && !showIcon) { + return null; + } + + return ( +

+ {showIcon && <>©} {title} +

+ ); + }; + + return ( +
+ {renderTitle()} + {text &&

{text}

} +
+ ); +}; diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts new file mode 100644 index 000000000..034f4f4cb --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts @@ -0,0 +1 @@ +export * from './CopyrightAttribution.tsx'; From 3ddd4ced156bff346be1fd2938abad2b5f822099 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Tue, 4 Aug 2026 13:33:51 +0200 Subject: [PATCH 2/7] ARC-3819: Style attribution --- .../CopyrightAttribution.scss | 22 ++++++++++++++++--- .../CopyrightAttribution.tsx | 8 +++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss index ba2ecf0aa..da5fab27b 100644 --- a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss @@ -1,6 +1,22 @@ @use "../../styles/settings/colors" as colors; -@use "../../styles/settings/variables" as variables; +@use "../../styles/mixins/typography" as typography; -.a-copyright-attribution__text { - color: colors.$color-gray-200; +.a-copyright-attribution { + display: flex; + flex-direction: column; + width: 100%; + + &__annotation { + color: colors.$neutral; + @include typography.sofia-pro-body-sm(true); + font-size: 1.4rem; + line-height: 2rem; + } + + &__text { + color: colors.$color-gray-1000; + @include typography.sofia-pro-body-sm(true); + font-size: 1.4rem; + line-height: 2rem; + } } diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx index 2a610f13a..ee78f7da1 100644 --- a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx @@ -27,16 +27,16 @@ export const CopyrightAttribution: FunctionComponent = ({ } return ( -

+ {showIcon && <>©} {title} -

+ ); }; return ( -
+
{renderTitle()} - {text &&

{text}

} + {text && {text}}
); }; From 97646db330a1a95a905a13e874713e35805326b9 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 09:42:27 +0200 Subject: [PATCH 3/7] ARC-3791: fix build errors --- .../blocks/BlockImageGrid/BlockImageGrid.types.tsx | 6 +++--- .../components/blocks/BlockRichText/BlockRichText.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.types.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.types.tsx index a45b92f13..4eae8b65c 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.types.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.types.tsx @@ -9,9 +9,9 @@ import type { ReactNode } from 'react'; export interface GridItem { source: string; - copyrightTitle: string; - copyrightIconVisible: boolean; - copyrightText: string; + copyrightTitle?: string; + copyrightIconVisible?: boolean; + copyrightText?: string; titleAbove?: string; title?: string; textAbove?: string | ReactNode; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx index 7676fa846..0d88565fe 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx @@ -21,9 +21,9 @@ import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; interface BlockRichTextElement { content: string; - copyrightTitle: string; - copyrightIconVisible: boolean; - copyrightText: string; + copyrightTitle?: string; + copyrightIconVisible?: boolean; + copyrightText?: string; buttons?: (ButtonProps & { buttonAction: ButtonAction })[]; color?: string; } From 3e7f1895557b66a37fc8da686d09b048c0e590e3 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 17:43:19 +0200 Subject: [PATCH 4/7] ARC-3819: Fix props interface --- .../components/CopyrightAttribution/CopyrightAttribution.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx index ee78f7da1..31bed4980 100644 --- a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx @@ -5,13 +5,13 @@ import React from 'react'; import './CopyrightAttribution.scss'; import clsx from 'clsx'; -export interface BlockImageProps extends DefaultProps { +export interface CopyrightAttributionProps extends DefaultProps { title?: string; showIcon?: boolean; text?: string; } -export const CopyrightAttribution: FunctionComponent = ({ +export const CopyrightAttribution: FunctionComponent = ({ className, title = '', showIcon = false, From a3a3046910e8de197891ea92f4430df094e256ca Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Thu, 6 Aug 2026 07:23:26 +0200 Subject: [PATCH 5/7] ARC-3819: Render image-wrapper conditional --- .../BlockHetArchiefImageTextBackground.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx index 2233ee4bf..1778ea335 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx @@ -100,25 +100,27 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< buttonAltTitle || buttonLabel )}
-
- {image && ( - {imageAltText} + {image && ( + {imageAltText} + )} + - )} - -
+
+ )} ); }; From 62e7fa24dac71494c5e86172952cc5e38c3641d0 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Thu, 6 Aug 2026 10:48:19 +0200 Subject: [PATCH 6/7] ARC-3819: Adjust image text background --- .../BlockHetArchiefImageTextBackground.scss | 83 ++++++++++++++----- .../BlockHetArchiefImageTextBackground.tsx | 78 ++++++++++++----- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss index 72bbfdeba..81955d3c4 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss @@ -2,12 +2,11 @@ @use "../../../../shared/styles/settings/variables" as variables; .c-block-het-archief-image-text-background { + border: 1px solid black; width: 100%; display: grid; - // Add margin right by adding an extra column - // Also make the middle column a bit smaller so the image is a bit smaller than 66% width - grid-template-columns: 1fr 0.6fr 1fr 20px; + grid-template-columns: 1fr 0.6fr 1fr; gap: variables.$g-spacer-unit * 3; min-height: 34rem; margin: 0 auto; @@ -18,7 +17,7 @@ } &.c-block-het-archief-image-text-background--left-screen { - position: initial; + position: relative; .c-block-het-archief-image-text-background__content-wrapper { grid-row: 1; @@ -34,19 +33,23 @@ grid-column: 1/3; grid-row: 1; - img { - margin: 0 auto 0 0; - } - @media (max-width: variables.$g-bp2) { grid-column: 1; grid-row: 1; } + + @media (min-width: variables.$g-bp2) { + img, + .c-block-het-archief-image-text-background__copyright { + margin-right: auto; + margin-left: 0; + } + } } } &.c-block-het-archief-image-text-background--right-screen { - position: initial; + position: relative; .c-block-het-archief-image-text-background__content-wrapper { grid-row: 1; @@ -67,8 +70,12 @@ grid-row: 2; } - img { - margin: 0 0 0 auto; + @media (min-width: variables.$g-bp2) { + img, + .c-block-het-archief-image-text-background__copyright { + margin-left: auto; + margin-right: 0; + } } } } @@ -95,8 +102,12 @@ grid-row: 1; } - img { - margin: 0 auto 0 0; + @media (min-width: variables.$g-bp2) { + img, + .c-block-het-archief-image-text-background__copyright { + margin-right: auto; + margin-left: 0; + } } } } @@ -123,9 +134,12 @@ grid-row: 2; } - &.c-image img { - margin-right: 0 !important; - margin-left: auto !important; + @media (min-width: variables.$g-bp2) { + img, + .c-block-het-archief-image-text-background__copyright { + margin-left: auto; + margin-right: 0; + } } } } @@ -165,17 +179,44 @@ .c-block-het-archief-image-text-background__image-wrapper { @media (min-width: variables.$g-bp2) { + position: absolute; inset: 0; } - img { - max-width: 100%; + .c-block-het-archief-image-text-background__media { + display: flex; + flex-direction: column; height: 100%; - object-fit: contain; + justify-content: center; + } - @media (max-width: variables.$g-bp2) { - width: 100%; + .c-block-het-archief-image-text-background__image { + min-width: 0; + + @media (min-width: variables.$g-bp2) { + min-height: 0; + } + + img { + display: block; + max-width: 100%; + height: 100%; + object-fit: contain; + + @media (max-width: variables.$g-bp2) { + width: 100%; + } } } + + .c-block-het-archief-image-text-background__copyright { + flex: 0 0 auto; + margin-top: variables.$g-spacer-unit; + max-width: 100%; + + &:empty{ + display: none; + } + } } } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx index 1778ea335..2c8760b0a 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx @@ -1,7 +1,13 @@ import type { ButtonAction, ButtonType, IconName } from '@viaa/avo2-components'; -import { Button, Image } from '@viaa/avo2-components'; +import { Button } from '@viaa/avo2-components'; import clsx from 'clsx'; -import React, { type FunctionComponent, type ReactElement } from 'react'; +import React, { + type FunctionComponent, + type ReactElement, + useEffect, + useRef, + useState, +} from 'react'; import type { BackgroundAlignOption, Color, @@ -61,6 +67,33 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< buttonIconAlignment = 'left', pageWidth, }): ReactElement => { + const imgRef = useRef(null); + // The copyright caption must always line up with the image's actual rendered + // left edge, even when the image itself is right-aligned and narrower than its + // column (height-constrained, not full width). CSS can't reliably shrink-wrap a + // container around a height-constrained while also letting it share space + // dynamically with an arbitrary-length caption (no fixed height to reserve), so + // the image's rendered width is measured directly and applied to the caption. + const [copyrightWidth, setCopyrightWidth] = useState(); + + // `image` is not read in the effect body, but is kept as a dependency to + // re-attach the observer when the element is conditionally + // (re)mounted, e.g. in a live content-page preview. + // biome-ignore lint/correctness/useExhaustiveDependencies: see above + useEffect(() => { + const imgEl = imgRef.current; + if (!imgEl) { + return; + } + + const observer = new ResizeObserver(() => { + setCopyrightWidth(imgEl.getBoundingClientRect().width); + }); + observer.observe(imgEl); + + return () => observer.disconnect(); + }, [image]); + return (
{(image || copyrightTitle || copyrightText) && ( -
- {image && ( - {imageAltText} - )} - +
+
+
+ {imageAltText} +
+
+ +
+
)}
From 36911638df73de1f67417a837a732ebb598203ca Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Mon, 10 Aug 2026 12:40:23 +0200 Subject: [PATCH 7/7] ARC-3819: Fix styling --- .../BlockHetArchiefImageTextBackground.scss | 382 +++++++++--------- .../BlockHetArchiefImageTextBackground.tsx | 129 ++++-- 2 files changed, 299 insertions(+), 212 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss index 81955d3c4..9b8528afa 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss @@ -2,221 +2,229 @@ @use "../../../../shared/styles/settings/variables" as variables; .c-block-het-archief-image-text-background { - border: 1px solid black; width: 100%; display: grid; - grid-template-columns: 1fr 0.6fr 1fr; gap: variables.$g-spacer-unit * 3; - min-height: 34rem; margin: 0 auto; - @media (max-width: variables.$g-bp2) { - padding-left: variables.$g-spacer-unit; - padding-right: variables.$g-spacer-unit; - } - - &.c-block-het-archief-image-text-background--left-screen { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 3/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-column: 1/3; - grid-row: 1; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - - @media (min-width: variables.$g-bp2) { - img, - .c-block-het-archief-image-text-background__copyright { - margin-right: auto; - margin-left: 0; - } - } - } + // The image column is absolutely positioned (see __image-wrapper) so it can't grow + // this row's height. Reserve room for the copyright's real rendered height here so + // it isn't clipped by a consuming app's `.c-content-block { overflow: hidden }`. + min-height: max(34rem, calc(var(--copyright-height, 0px) + #{variables.$g-spacer-unit})); + + &.c-block-het-archief-image-text-background--has-image { + // Also reserve the image's own 21.7rem floor (or its actual height if a + // tall/portrait source needs more, see __image below). + min-height: max( + 34rem, + calc(max(21.7rem, var(--image-height, 0px)) + var(--copyright-height, 0px) + #{variables.$g-spacer-unit}) + ); } - &.c-block-het-archief-image-text-background--right-screen { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 1/1; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 2/4; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - - @media (min-width: variables.$g-bp2) { - img, - .c-block-het-archief-image-text-background__copyright { - margin-left: auto; - margin-right: 0; - } - } - } + @media (max-width: variables.$g-bp2) { + padding-left: variables.$g-spacer-unit; + padding-right: variables.$g-spacer-unit; } + &.c-block-het-archief-image-text-background--left-screen, &.c-block-het-archief-image-text-background--left-inside-page { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 3/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 1/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - - @media (min-width: variables.$g-bp2) { - img, - .c-block-het-archief-image-text-background__copyright { - margin-right: auto; - margin-left: 0; - } - } - } + position: relative; + + .c-block-het-archief-image-text-background__content-wrapper { + grid-row: 1; + grid-column: 3/3; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 2; + } + } + + .c-block-het-archief-image-text-background__image-wrapper { + grid-column: 1/3; + grid-row: 1; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 1; + } + } } + &.c-block-het-archief-image-text-background--right-screen, &.c-block-het-archief-image-text-background--right-inside-page { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 1/1; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 2/4; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - - @media (min-width: variables.$g-bp2) { - img, - .c-block-het-archief-image-text-background__copyright { - margin-left: auto; - margin-right: 0; - } - } - } + position: relative; + + .c-block-het-archief-image-text-background__content-wrapper { + grid-row: 1; + grid-column: 1/1; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 1; + } + } + + .c-block-het-archief-image-text-background__image-wrapper { + grid-row: 1; + grid-column: 2/4; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 2; + } + } + } + + // `.media`'s width is set from JS to match its content (see __image-wrapper), so + // pushing it — rather than the image/copyright individually — aligns the group to + // the screen/page edge. Only with an actual image: a caption-only block has + // nothing to align against and just sits at the start. + &.c-block-het-archief-image-text-background--left-screen.c-block-het-archief-image-text-background--has-image, + &.c-block-het-archief-image-text-background--left-inside-page.c-block-het-archief-image-text-background--has-image { + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__media { + @media (min-width: variables.$g-bp2) { + margin-right: auto; + margin-left: 0; + } + } + } + + &.c-block-het-archief-image-text-background--right-screen.c-block-het-archief-image-text-background--has-image, + &.c-block-het-archief-image-text-background--right-inside-page.c-block-het-archief-image-text-background--has-image { + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__media { + @media (min-width: variables.$g-bp2) { + margin-left: auto; + margin-right: 0; + } + } + + // The only ever renders at its own width, left-aligned by default. When a + // wide copyright makes `.media` wider than the image, pin the image to the end so + // it stays flush with the block's right edge regardless of how wide `.media` grows. + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__image { + @media (min-width: variables.$g-bp2) { + justify-content: flex-end; + } + } } @media (max-width: variables.$g-bp2) { - grid-template-rows: repeat(2, auto); - grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(2, auto); + grid-template-columns: repeat(1, 1fr); } .c-block-het-archief-image-text-background__content-wrapper { - width: 100%; - height: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - flex-direction: column; - padding: variables.$g-spacer-unit * 4; - z-index: 1; - - @media (max-width: variables.$g-bp2) { - padding: 0; - } - - p { - padding: unset !important; - } + width: 100%; + height: 100%; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + padding: variables.$g-spacer-unit * 4; + z-index: 1; + + @media (max-width: variables.$g-bp2) { + padding: 0; + } + + p { + padding: unset !important; + } } .c-block-het-archief-image-text-background__content, .c-block-het-archief-image-text-background__heading { - margin-bottom: variables.$g-spacer-unit !important; + margin-bottom: variables.$g-spacer-unit !important; } .c-block-het-archief-image-text-background__button { - margin-top: math.div(variables.$g-spacer-unit, 2); + margin-top: math.div(variables.$g-spacer-unit, 2); } .c-block-het-archief-image-text-background__image-wrapper { - @media (min-width: variables.$g-bp2) { - position: absolute; - inset: 0; - } - - .c-block-het-archief-image-text-background__media { - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - } - - .c-block-het-archief-image-text-background__image { - min-width: 0; - - @media (min-width: variables.$g-bp2) { - min-height: 0; - } - - img { - display: block; - max-width: 100%; - height: 100%; - object-fit: contain; - - @media (max-width: variables.$g-bp2) { - width: 100%; - } - } - } - - .c-block-het-archief-image-text-background__copyright { - flex: 0 0 auto; - margin-top: variables.$g-spacer-unit; - max-width: 100%; - - &:empty{ - display: none; - } - } + // Below $g-bp2 this stays a normal in-flow grid item, so the default + // `min-width: auto` would let `.media`'s JS-applied width (sized off the + // copyright's unwrapped width) drag this track — and the image — past the + // viewport. `min-width: 0` keeps it bound to the actual available width. + min-width: 0; + + @media (min-width: variables.$g-bp2) { + position: absolute; + inset: 0; + } + + .c-block-het-archief-image-text-background__media { + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + + // Width is set inline from JS (image's rendered width, or the caption's + // natural width if wider — see component) so both share one width and their + // left edges align. `max-width` here is just the safety net beyond that. + max-width: 100%; + } + + .c-block-het-archief-image-text-background__image { + min-width: 0; + min-height: 21.7rem; + display: flex; + align-items: center; + + // Capped, not just floored, on mobile: a tall source image should crop + // instead of growing the whole single-column block, unlike on desktop. + @media (max-width: variables.$g-bp2) { + height: 21.7rem; + overflow: hidden; + } + + img { + display: block; + max-width: 100%; + // `max-height`, not `height`, keeps the box aspect-correct so the + // width-alignment above matches the image's actual visible content. + max-height: 100%; + height: auto; + object-fit: contain; + + @media (max-width: variables.$g-bp2) { + width: 100%; + height: 100%; + } + } + } + + .c-block-het-archief-image-text-background__copyright { + flex: 0 0 auto; + margin-top: variables.$g-spacer-unit; + max-width: 100%; + + &:empty { + display: none; + } + } + + // Off-screen, single-line clone of the caption, used only to measure its + // natural (unwrapped) width in JS — see the component. `inline-block` + // shrink-wraps it; `white-space: nowrap` is what keeps it unwrapped. + .c-block-het-archief-image-text-background__copyright-measure { + position: absolute; + top: 0; + left: 0; + visibility: hidden; + white-space: nowrap; + pointer-events: none; + width: auto; + display: inline-block; + + // Override `.a-copyright-attribution`'s own `width: 100%` — otherwise this + // clone fills the column instead of shrinking to its content, breaking the + // natural-width measurement above. + .a-copyright-attribution { + width: max-content; + } + } } -} +} \ No newline at end of file diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx index 2c8760b0a..0adc663a1 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx @@ -2,8 +2,10 @@ import type { ButtonAction, ButtonType, IconName } from '@viaa/avo2-components'; import { Button } from '@viaa/avo2-components'; import clsx from 'clsx'; import React, { + type CSSProperties, type FunctionComponent, type ReactElement, + useCallback, useEffect, useRef, useState, @@ -68,44 +70,100 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< pageWidth, }): ReactElement => { const imgRef = useRef(null); - // The copyright caption must always line up with the image's actual rendered - // left edge, even when the image itself is right-aligned and narrower than its - // column (height-constrained, not full width). CSS can't reliably shrink-wrap a - // container around a height-constrained while also letting it share space - // dynamically with an arbitrary-length caption (no fixed height to reserve), so - // the image's rendered width is measured directly and applied to the caption. - const [copyrightWidth, setCopyrightWidth] = useState(); + const copyrightRef = useRef(null); + // Hidden, unwrapped clone of the caption, used only to measure its natural + // (single-line) width in JS — see updateMediaMeasurements below. Can't be done in + // pure CSS: a flex/grid container's intrinsic-size computation uses the 's + // intrinsic pixel dimensions, not its rendered size, so `width: fit-content` on + // `.media` collapses to the full column width for any normal-sized image. + const copyrightMeasureRef = useRef(null); + // Copyright's rendered height, fed into --copyright-height (see scss). The image + // column is absolutely positioned so it can't grow the block's height itself, so + // this is what makes the block reserve enough room for a tall caption instead of + // letting it overflow into a consuming app's `.c-content-block { overflow: hidden }`. + const [copyrightHeight, setCopyrightHeight] = useState(); + // Image's own rendered width, or the caption's natural (unwrapped) width if that's + // wider — applied to `.media` below so both share exactly that width and align + // their left edges, while letting the caption grow past the image instead of + // wrapping when it would otherwise need to. + const [mediaWidth, setMediaWidth] = useState(); + // Image's rendered height, which can exceed its 21.7rem floor (see scss) for a + // tall/portrait source. Fed into --image-height for the same reason as + // copyrightHeight: without it such an image could get squeezed or overflow. + const [imageHeight, setImageHeight] = useState(); - // `image` is not read in the effect body, but is kept as a dependency to - // re-attach the observer when the element is conditionally - // (re)mounted, e.g. in a live content-page preview. + // Uses the image's *rendered* width, not `naturalWidth` (its raw file resolution): + // a grid/flex container's intrinsic-size computation falls back to the raw pixel + // dimensions whenever height isn't a definite value, which would always win the + // `max()` below and pin `.media` to the full column regardless of the copyright. + // Only reads it once the image has finished loading — reading mid-load measures + // ~0px, which would shrink `.media` (and the image with it) with no way to + // recover, since a later re-measure would just report that same self-inflicted size. + // biome-ignore lint/correctness/useExhaustiveDependencies: refs are stable + const updateMediaMeasurements = useCallback(() => { + const imgEl = imgRef.current; + const measureEl = copyrightMeasureRef.current; + const naturalCopyrightWidth = measureEl?.getBoundingClientRect().width ?? 0; + + if (imgEl && !imgEl.complete) { + return; + } + + const imageWidth = imgEl?.getBoundingClientRect().width ?? 0; + setMediaWidth(Math.max(imageWidth, naturalCopyrightWidth) || undefined); + setImageHeight(imgEl?.getBoundingClientRect().height || undefined); + }, []); + + // Deps aren't read in the effect body; they re-attach the observers when the + // /measuring clone are conditionally (re)mounted, e.g. in a live preview. // biome-ignore lint/correctness/useExhaustiveDependencies: see above useEffect(() => { const imgEl = imgRef.current; - if (!imgEl) { + const measureEl = copyrightMeasureRef.current; + + const observer = new ResizeObserver(updateMediaMeasurements); + if (imgEl) { + observer.observe(imgEl); + } + if (measureEl) { + observer.observe(measureEl); + } + updateMediaMeasurements(); + + return () => observer.disconnect(); + }, [image, copyrightTitle, copyrightText, copyrightIconVisible, updateMediaMeasurements]); + + useEffect(() => { + const copyrightEl = copyrightRef.current; + if (!copyrightEl) { return; } const observer = new ResizeObserver(() => { - setCopyrightWidth(imgEl.getBoundingClientRect().width); + setCopyrightHeight(copyrightEl.getBoundingClientRect().height); }); - observer.observe(imgEl); + observer.observe(copyrightEl); return () => observer.disconnect(); - }, [image]); + }, []); return (
-
-
- {imageAltText} -
+
+ {image && ( +
+ {imageAltText} +
+ )}
+ +
+