diff --git a/ui/src/client.ts b/ui/src/client.ts index 1588c6387..5a8f90954 100644 --- a/ui/src/client.ts +++ b/ui/src/client.ts @@ -1,5 +1,6 @@ export { AvoHeroWrapper } from '~content-blocks/BlockAvoHero/AvoHeroWrapper'; export { BlockAvoImageTextBackground } from '~content-blocks/BlockAvoImageTextBackground'; +export { BlockAvoQuote } from '~content-blocks/BlockAvoQuote'; export { BlockBreadcrumbs } from '~content-blocks/BlockBreadcrumbs'; export { BlockButtonsWrapper } from '~content-blocks/BlockButtons/BlockButtons.wrapper'; export { BlockCardsWithoutDescription } from '~content-blocks/BlockCardsWithoutDescription'; @@ -9,6 +10,7 @@ export { BlockEventbrite } from '~content-blocks/BlockEventbrite'; export { BlockHeading } from '~content-blocks/BlockHeading/BlockHeading'; export { BlockHetArchiefHeaderSearch } from '~content-blocks/BlockHetArchiefHeaderSearch'; export { BlockHetArchiefImageTextBackground } from '~content-blocks/BlockHetArchiefImageTextBackground'; +export { BlockHetArchiefQuote } from '~content-blocks/BlockHetArchiefQuote'; export { BlockIFrame } from '~content-blocks/BlockIFrame'; export { BlockImage } from '~content-blocks/BlockImage'; export { BlockImageGridWrapper } from '~content-blocks/BlockImageGrid/BlockImageGrid.wrapper'; @@ -21,7 +23,6 @@ export { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid'; export { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles'; export { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel'; export { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview/BlockPageOverview.wrapper'; -export { BlockQuote } from '~content-blocks/BlockQuote'; export { BlockRichText } from '~content-blocks/BlockRichText'; export { BlockRichTextWrapper } from '~content-blocks/BlockRichText/BlockRichText.wrapper'; export { BlockScrollDownNudge } from '~content-blocks/BlockScrollDownNudge'; diff --git a/ui/src/react-admin/core/config/config.types.ts b/ui/src/react-admin/core/config/config.types.ts index 0b00f6779..c64025e3b 100644 --- a/ui/src/react-admin/core/config/config.types.ts +++ b/ui/src/react-admin/core/config/config.types.ts @@ -195,6 +195,7 @@ export interface IconConfig { newspaper: IconComponentProps; noAudio: IconComponentProps; noVideo: IconComponentProps; + quotes: IconComponentProps; }; list: () => { label: string; value: string }[]; alerts: () => { key: string; label: string; value: string }[]; 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 dc4d6e65b..c516db2d2 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 @@ -1,6 +1,7 @@ import type { FC, FunctionComponent } from 'react'; import { AvoHeroWrapper } from '~content-blocks/BlockAvoHero/AvoHeroWrapper'; import { BlockAvoImageTextBackground } from '~content-blocks/BlockAvoImageTextBackground'; +import { BlockAvoQuote } from '~content-blocks/BlockAvoQuote'; import { BlockBreadcrumbs } from '~content-blocks/BlockBreadcrumbs'; import { BlockButtonsWrapper } from '~content-blocks/BlockButtons'; import { BlockCardsWithoutDescription } from '~content-blocks/BlockCardsWithoutDescription'; @@ -10,6 +11,7 @@ import { BlockEventbrite } from '~content-blocks/BlockEventbrite'; import { BlockHeading } from '~content-blocks/BlockHeading'; import { BlockHetArchiefHeaderSearch } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch'; import { BlockHetArchiefImageTextBackground } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { BlockHetArchiefQuote } from '~content-blocks/BlockHetArchiefQuote'; import { BlockHighlightText } from '~content-blocks/BlockHighlightText'; import { BlockHomepageBanner } from '~content-blocks/BlockHomepageBanner'; import { BlockIFrame } from '~content-blocks/BlockIFrame'; @@ -26,7 +28,6 @@ import { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid'; import { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles'; import { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel/BlockOverviewWithCarousel.tsx'; import { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview'; -import { BlockQuote } from '~content-blocks/BlockQuote'; import { BlockRichTextWrapper } from '~content-blocks/BlockRichText'; import { BlockScrollDownNudge } from '~content-blocks/BlockScrollDownNudge'; import { BlockProjectSpotlightWrapper, BlockSpotlight } from '~content-blocks/BlockSpotlight'; @@ -64,7 +65,7 @@ export function GET_BLOCK_COMPONENT( [ContentBlockType.MediaPlayer]: BlockVideoWrapper, [ContentBlockType.PageOverview]: BlockPageOverviewWrapper, [ContentBlockType.ProjectsSpotlight]: BlockProjectSpotlightWrapper, - [ContentBlockType.Quote]: BlockQuote, + [ContentBlockType.AvoQuote]: BlockAvoQuote, [ContentBlockType.RichTextTwoColumns]: BlockRichTextWrapper, [ContentBlockType.RichText]: BlockRichTextWrapper, [ContentBlockType.Spotlight]: BlockSpotlight, @@ -90,6 +91,7 @@ export function GET_BLOCK_COMPONENT( [ContentBlockType.Breadcrumbs]: BlockBreadcrumbs, [ContentBlockType.HetArchiefImageTextBackground]: BlockHetArchiefImageTextBackground, [ContentBlockType.OverviewWithCarousel]: BlockOverviewWithCarousel, + [ContentBlockType.HetArchiefQuote]: BlockHetArchiefQuote, [ContentBlockType.HomepageBanner]: BlockHomepageBanner, [ContentBlockType.HighlightText]: BlockHighlightText, [ContentBlockType.ThemeReels]: BlockThemeReels, diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.editorconfig.ts similarity index 67% rename from ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.editorconfig.ts rename to ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.editorconfig.ts index b03e416c0..fb3ab35fd 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.editorconfig.ts @@ -1,21 +1,22 @@ import type { FileUploadProps } from '~shared/components/FileUpload/FileUpload'; import { tText } from '~shared/helpers/translation-functions'; +import { AVO, HET_ARCHIEF } from '~shared/types'; import type { + AvoQuoteBlockComponentState, ContentBlockConfig, DefaultContentBlockState, - QuoteBlockComponentState, } from '../../../types/content-block.types'; import { ContentBlockType } from '../../../types/content-block.types'; import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults'; -export const INITIAL_QUOTE_COMPONENTS_STATE = (): QuoteBlockComponentState => ({ +export const INITIAL_AVO_QUOTE_COMPONENTS_STATE = (): AvoQuoteBlockComponentState => ({ quote: '', authorName: '', authorInitials: '', }); -export const INITIAL_QUOTE_BLOCK_STATE = (): DefaultContentBlockState => +export const INITIAL_AVO_QUOTE_BLOCK_STATE = (): DefaultContentBlockState => BLOCK_STATE_DEFAULTS({ padding: { top: 'top-small', @@ -23,25 +24,37 @@ export const INITIAL_QUOTE_BLOCK_STATE = (): DefaultContentBlockState => }, }); -export const QUOTE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ +export const AVO_QUOTE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ position, - name: tText('admin/content-block/helpers/generators/quote___quote'), - type: ContentBlockType.Quote, + name: tText('admin/content-block/helpers/generators/quote___quote', {}, [AVO, HET_ARCHIEF]), + type: ContentBlockType.AvoQuote, components: { - state: INITIAL_QUOTE_COMPONENTS_STATE(), + state: INITIAL_AVO_QUOTE_COMPONENTS_STATE(), fields: { quote: TEXT_FIELD( { - label: tText('admin/content-block/helpers/generators/quote___quote'), + label: tText('admin/content-block/helpers/generators/quote___quote', {}, [ + AVO, + HET_ARCHIEF, + ]), }, - tText('admin/content-block/helpers/generators/quote___quote-is-verplicht') + tText('admin/content-block/helpers/generators/quote___quote-is-verplicht', {}, [ + AVO, + HET_ARCHIEF, + ]) ), authorName: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/quote___auteur'), + label: tText('admin/content-block/helpers/generators/quote___auteur', {}, [ + AVO, + HET_ARCHIEF, + ]), validator: undefined, }), authorInitials: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/quote___initialen'), + label: tText('admin/content-block/helpers/generators/quote___initialen', {}, [ + AVO, + HET_ARCHIEF, + ]), validator: undefined, }), authorImage: FILE_FIELD( @@ -54,7 +67,7 @@ export const QUOTE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ }, }, block: { - state: INITIAL_QUOTE_BLOCK_STATE(), + state: INITIAL_AVO_QUOTE_BLOCK_STATE(), fields: BLOCK_FIELD_DEFAULTS(), }, }); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.scss similarity index 100% rename from ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.scss rename to ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.scss diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.test.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.test.tsx similarity index 86% rename from ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.test.tsx rename to ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.test.tsx index 2384f83a6..332acf546 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.test.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.test.tsx @@ -3,7 +3,7 @@ import { loremIpsum } from 'lorem-ipsum'; import React from 'react'; import { afterEach, describe, it } from 'vitest'; -import { BlockQuote } from './BlockQuote'; +import { BlockAvoQuote } from './BlockAvoQuote'; const customClass = 'c-block-custom'; const quote = loremIpsum({ count: 10 }); @@ -12,7 +12,7 @@ const authorName = loremIpsum({ count: 2 }); const authorInitials = 'AE'; const quoteExample = ( -
{ cleanup(); }); -describe('
', () => { +describe('', () => { it('Should be able to render', () => { render(quoteExample); }); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.tsx similarity index 76% rename from ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.tsx rename to ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.tsx index 96c8e2961..27aa6c9de 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/BlockQuote.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/BlockAvoQuote.tsx @@ -4,16 +4,16 @@ import clsx from 'clsx'; import type { FunctionComponent } from 'react'; import React from 'react'; -import './BlockQuote.scss'; +import './BlockAvoQuote.scss'; -export interface BlockQuoteProps extends DefaultProps { +export interface BlockAvoQuoteProps extends DefaultProps { quote: string; authorImage?: string; authorName: string; authorInitials?: string; } -export const BlockQuote: FunctionComponent = ({ +export const BlockAvoQuote: FunctionComponent = ({ className, quote, authorImage, diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/index.ts new file mode 100644 index 000000000..cc549247d --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockAvoQuote/index.ts @@ -0,0 +1,2 @@ +export * from './BlockAvoQuote'; +export * from './BlockAvoQuote.editorconfig'; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.editorconfig.ts new file mode 100644 index 000000000..c352237de --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.editorconfig.ts @@ -0,0 +1,82 @@ +import { + GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF, + GET_FOREGROUND_COLOR_OPTIONS_ARCHIEF, +} from '~modules/content-page/const/get-color-options'; +import { tText } from '~shared/helpers/translation-functions'; +import { HET_ARCHIEF } from '~shared/types'; +import type { + ContentBlockConfig, + DefaultContentBlockState, + HetArchiefQuoteBlockComponentState, +} from '../../../types/content-block.types'; +import { Color, ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; + +import { + BACKGROUND_COLOR_FIELD, + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + FOREGROUND_COLOR_FIELD, + TEXT_FIELD, +} from '../defaults'; + +export const INITIAL_HET_ARCHIEF_QUOTE_COMPONENTS_STATE = + (): HetArchiefQuoteBlockComponentState => ({ + quote: '', + authorName: '', + textColor: Color.White, + frameColor: Color.Black, + }); + +export const INITIAL_HET_ARCHIEF_QUOTE_BLOCK_STATE = (): DefaultContentBlockState => + BLOCK_STATE_DEFAULTS({ + padding: { + top: 'top-small', + bottom: 'bottom-small', + }, + }); + +export const HET_ARCHIEF_QUOTE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ + position, + name: tText('admin/content-block/helpers/generators/hetarchief-quote___quote-kader', {}, [ + HET_ARCHIEF, + ]), + type: ContentBlockType.HetArchiefQuote, + components: { + state: INITIAL_HET_ARCHIEF_QUOTE_COMPONENTS_STATE(), + fields: { + quote: TEXT_FIELD( + { + label: tText('admin/content-block/helpers/generators/hetarchief-quote___quote', {}, [ + HET_ARCHIEF, + ]), + editorType: ContentBlockEditor.TextArea, + }, + tText('admin/content-block/helpers/generators/hetarchief-quote___quote-is-verplicht', {}, [ + HET_ARCHIEF, + ]) + ), + authorName: TEXT_FIELD({ + label: tText('admin/content-block/helpers/generators/hetarchief-quote___auteur', {}, [ + HET_ARCHIEF, + ]), + validator: undefined, + }), + textColor: FOREGROUND_COLOR_FIELD( + tText('admin/content-block/helpers/generators/hetarchief-quote___tekstkleur', {}, [ + HET_ARCHIEF, + ]), + GET_FOREGROUND_COLOR_OPTIONS_ARCHIEF()[1] + ), + frameColor: BACKGROUND_COLOR_FIELD( + tText('admin/content-block/helpers/generators/hetarchief-quote___kleur-kader', {}, [ + HET_ARCHIEF, + ]), + GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF()[5] + ), + }, + }, + block: { + state: INITIAL_HET_ARCHIEF_QUOTE_BLOCK_STATE(), + fields: BLOCK_FIELD_DEFAULTS(), + }, +}); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.scss new file mode 100644 index 000000000..96af282a9 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.scss @@ -0,0 +1,101 @@ +@use "../../../../shared/styles/settings/variables" as variables; +@use "../../../../shared/styles/mixins/typography" as typography; + +// Width of the column holding the quote mark (Figma node 455:5722). +$mark-column-width: variables.$g-spacer-unit * 13; + +// Space between the end of the text column and the frame edge, so the text stays clear of the +// rounded right side. 24 units is the gap Figma leaves at the design's 1008px frame width. +$text-inset-right: variables.$g-spacer-unit * 24; +$text-inset-right-tablet: variables.$g-spacer-unit * 8; + +// Figma node 455:5722: rounded-tr/br only, square on the left. At the design's 228px height CSS +// clamps both radii to height/2 so this reads as a pill; past ~800px of height it stops growing, +// which is the point - an unbounded 9999px radius would keep eating into a long quote. +$frame-radius: 40rem; +$frame-radius-mobile: 12rem; + +$mark-font-size: variables.$g-spacer-unit * 8; +$mark-line-height: variables.$g-spacer-unit * 9; + +.c-block-het-archief-quote { + display: grid; + grid-template-columns: $mark-column-width 1fr; + grid-template-areas: + 'mark quote' + '. author'; + margin: 0; + padding: variables.$g-spacer-unit * 5 $text-inset-right variables.$g-spacer-unit * 5 0; + background: var(--frame-color); + color: var(--text-color); + border-radius: 0 $frame-radius $frame-radius 0; + + // Figma emits the deprecated `word-break: break-word`; this is its modern equivalent. + overflow-wrap: break-word; + + &__mark { + grid-area: mark; + + // Figma node 455:5723 sits at top 28 while the quote sits at top 40, so the mark is + // raised out of the shared grid row and centred in its own column. + margin-top: -(variables.$g-spacer-unit * 1.5); + text-align: center; + font-size: $mark-font-size; + line-height: $mark-line-height; + + @media (max-width: variables.$g-bp2) { + // Figma node 455:7187: in the stacked layout the mark is in normal flow and + // left-aligned with the text below it. + margin-top: 0; + text-align: left; + } + } + + &__quote { + grid-area: quote; + margin: 0; + white-space: pre-line; + + // Figma node 455:5724, style "P - XL". + @include typography.sofia-pro-body-xl(); + + @media (max-width: variables.$g-bp2) { + // Figma node 455:7186: gap below the mark, plus the padding on the text group that + // holds the quote and the author. + margin-top: variables.$g-spacer-unit * 2; + margin-left: variables.$g-spacer-unit; + } + } + + &__author { + grid-area: author; + margin-top: variables.$g-spacer-unit; + + // Figma node 455:5725, style "P - S". + @include typography.sofia-pro-body-xs(); + + @media (max-width: variables.$g-bp2) { + margin-bottom: variables.$g-spacer-unit; + margin-left: variables.$g-spacer-unit; + } + } + + // Below the design's 1008px frame width the desktop inset eats too much of the text column, + // so it scales back to a third while the pill shape is kept. + @media (max-width: variables.$g-bp4) { + padding-right: $text-inset-right-tablet; + } + + @media (max-width: variables.$g-bp2) { + grid-template-columns: 1fr; + grid-template-areas: + 'mark' + 'quote' + 'author'; + + // Figma node 455:7186: a single top-right radius - the desktop's two-corner pill does + // not carry over to mobile. + padding: variables.$g-spacer-unit * 3 variables.$g-spacer-unit * 2.5; + border-radius: 0 $frame-radius-mobile 0 0; + } +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.test.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.test.tsx new file mode 100644 index 000000000..5fe9d7c8c --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.test.tsx @@ -0,0 +1,66 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +// The real Icon reads the injected client config, which a unit test has no business setting up. +vi.mock('~shared/components/Icon/Icon', () => ({ + Icon: ({ name }: { name: string }) => , +})); + +import { Color } from '../../../types/content-block.types'; +import { BlockHetArchiefQuote } from './BlockHetArchiefQuote'; + +afterEach(() => { + cleanup(); +}); + +describe('', () => { + it('renders the quote and the author', () => { + render( + + ); + + expect(screen.getByText('Een citaat')).toBeTruthy(); + expect(screen.getByText('John Doe')).toBeTruthy(); + }); + + it('exposes the chosen colours as custom properties', () => { + const { container } = render( + + ); + + const figure = container.querySelector('.c-block-het-archief-quote') as HTMLElement; + + expect(figure.style.getPropertyValue('--text-color')).toEqual(Color.White); + expect(figure.style.getPropertyValue('--frame-color')).toEqual(Color.OceanGreen); + }); + + it('omits the caption when there is no author', () => { + const { container } = render( + + ); + + expect(container.querySelector('figcaption')).toBeNull(); + }); + + it('renders the quote mark as a decorative icon, hidden from assistive tech', () => { + const { container } = render( + + ); + + const mark = container.querySelector('.c-block-het-archief-quote__mark'); + + expect(mark?.getAttribute('aria-hidden')).toEqual('true'); + expect(mark?.querySelector('[data-icon="quotes"]')).toBeTruthy(); + }); +}); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.tsx new file mode 100644 index 000000000..e4e606e76 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote.tsx @@ -0,0 +1,43 @@ +import type { DefaultProps } from '@viaa/avo2-components'; +import clsx from 'clsx'; +import type { CSSProperties, FunctionComponent } from 'react'; +import React from 'react'; + +import { Icon } from '~shared/components/Icon/Icon'; +import type { Color, CustomBackground, GradientColor } from '../../../types/content-block.types'; + +import './BlockHetArchiefQuote.scss'; + +export interface BlockHetArchiefQuoteProps extends DefaultProps { + quote: string; + authorName?: string; + textColor: Color; + frameColor: Color | GradientColor | CustomBackground; +} + +export const BlockHetArchiefQuote: FunctionComponent = ({ + className, + quote, + authorName, + textColor, + frameColor, +}) => ( +
+ {/* Decorative: the quote itself carries the meaning, so keep it out of the a11y tree */} + +
{quote}
+ {authorName && ( +
{authorName}
+ )} +
+); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/index.ts new file mode 100644 index 000000000..27ff5a9f4 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefQuote/index.ts @@ -0,0 +1,2 @@ +export * from './BlockHetArchiefQuote'; +export * from './BlockHetArchiefQuote.editorconfig'; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/index.ts deleted file mode 100644 index a03d999c1..000000000 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockQuote/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './BlockQuote'; -export * from './BlockQuote.editorconfig'; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/README.md b/ui/src/react-admin/modules/content-page/components/blocks/README.md index ebdc28287..6a427b45e 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/README.md +++ b/ui/src/react-admin/modules/content-page/components/blocks/README.md @@ -120,6 +120,55 @@ component. --- +## Colour fields + +Colours come from the shared lists in +[`../../const/get-color-options.ts`](../../const/get-color-options.ts), through the helpers in +[`defaults.ts`](./defaults.ts): `FOREGROUND_COLOR_FIELD` for text colours, +`BACKGROUND_COLOR_FIELD` / `BACKGROUND_COLOR_EXTENDED_FIELD` for backgrounds. + +- **Don't build a filtered variant of a list.** Some entries are not a plain CSS colour + (`Color.Transparent`, the `GradientColor.BlackWhite` gradient string, and the + `CustomBackground.MeemooLogo` sentinel that only `ContentBlockRenderer` expands). Whether a + given combination is legible or useful is the editor's call, not something we narrow down in + code — pick the list that fits (`GET_FOREGROUND_COLOR_OPTIONS_*` is already gradient-free) and + pass it through unchanged. +- **Take defaults out of the list, never hand-write them.** `GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF()[5]`, + not `{ label: tText('…___zwart'), value: Color.Black }` — a literal copy silently drifts when + the label behind that translation key changes. +- Use the same option for the field default and for `INITIAL_…_COMPONENTS_STATE`, so the form and + the stored state agree. + +## Styling + +Block styles are a plain `Block.scss` next to the component, imported by it. Reuse what +`~shared/styles` already provides instead of re-deriving values out of Figma: + +```scss +@use "../../../../shared/styles/settings/variables" as variables; +@use "../../../../shared/styles/mixins/typography" as typography; +``` + +- **Spacing** is `variables.$g-spacer-unit` (0.8rem) multiples — `variables.$g-spacer-unit * 3`, + not `2.4rem`. +- **Breakpoints** are `variables.$g-bp1`…`$g-bp4`, written as + `@media (max-width: variables.$g-bp2)`. +- **Type** goes through [`mixins/_typography.scss`](../../../shared/styles/mixins/_typography.scss): + a preset (`sofia-pro-body()`, `sofia-pro-heading-lg()`, …) when the size/weight matches, otherwise + `sofia-pro($size, $weight, $line-height)`. Don't re-declare `font-family`/`font-size`/`font-weight` + by hand. +- **Colours** come from `settings/colors`, or from a CSS custom property when the editor picks them + (`background: var(--frame-color)`). +- A value that genuinely is design-specific (a frame radius, an icon-column width) becomes a named + `$variable` at the top of the file with a comment saying where it comes from — no bare literals + in the rules. +- Check the block at tablet and mobile widths before opening the PR; review asks about it every + time. + +`BlockOverviewWithCarousel.scss` and `BlockHetArchiefQuote.scss` are reasonable reference files. + +--- + ## Key files - [`../ContentBlockForm/ContentBlockForm.tsx`](../ContentBlockForm/ContentBlockForm.tsx) — diff --git a/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts b/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts index 4d12d7ff5..ecd938c91 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 @@ -1,6 +1,7 @@ import { ANCHOR_LINKS_BLOCK_CONFIG } from '~content-blocks/anchor-links.ts'; import { AVO_HERO_BLOCK_CONFIG } from '~content-blocks/BlockAvoHero'; import { AVO_IMAGE_TEXT_BACKGROUND_BLOCK_CONFIG } from '~content-blocks/BlockAvoImageTextBackground'; +import { AVO_QUOTE_BLOCK_CONFIG } from '~content-blocks/BlockAvoQuote'; import { CONTENT_BREADCRUMBS_CONFIG } from '~content-blocks/BlockBreadcrumbs'; import { BUTTONS_BLOCK_CONFIG } from '~content-blocks/BlockButtons'; import { CARDS_WITHOUT_DESCRIPTION_BLOCK_CONFIG } from '~content-blocks/BlockCardsWithoutDescription'; @@ -11,6 +12,7 @@ import { EVENTBRITE_BLOCK_CONFIG } from '~content-blocks/BlockEventbrite'; import { HEADING_BLOCK_CONFIG } from '~content-blocks/BlockHeading'; import { HET_ARCHIEF_HEADER_SEARCH_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig'; import { HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { HET_ARCHIEF_QUOTE_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefQuote'; import { CONTENT_HIGHLIGHT_TEXT_CONFIG } from '~content-blocks/BlockHighlightText'; import { CONTENT_HOMEPAGE_BANNER_CONFIG } from '~content-blocks/BlockHomepageBanner'; import { IFRAME_BLOCK_CONFIG } from '~content-blocks/BlockIFrame'; @@ -27,7 +29,6 @@ import { MAINTAINERS_GRID_BLOCK_CONFIG } from '~content-blocks/BlockMaintainersG import { MEDIA_GRID_BLOCK_CONFIG } from '~content-blocks/BlockMediaGrid'; import { OVERVIEW_NEWSPAPER_TITLES_BLOCK_CONFIG } from '~content-blocks/BlockOverviewNewspaperTitles'; import { OVERVIEW_WITH_CAROUSEL_CONFIG } from '~content-blocks/BlockOverviewWithCarousel'; -import { QUOTE_BLOCK_CONFIG } from '~content-blocks/BlockQuote'; import { RICH_TEXT_BLOCK_CONFIG, RICH_TEXT_TWO_COLUMNS_BLOCK_CONFIG, @@ -65,7 +66,8 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record< [ContentBlockType.MediaGrid]: MEDIA_GRID_BLOCK_CONFIG, [ContentBlockType.MediaPlayer]: MEDIA_PLAYER_BLOCK_CONFIG, [ContentBlockType.MediaPlayerTitleTextButton]: MEDIA_PLAYER_TITLE_TEXT_BUTTON_BLOCK_CONFIG, - [ContentBlockType.Quote]: QUOTE_BLOCK_CONFIG, + [ContentBlockType.AvoQuote]: AVO_QUOTE_BLOCK_CONFIG, + [ContentBlockType.HetArchiefQuote]: HET_ARCHIEF_QUOTE_BLOCK_CONFIG, [ContentBlockType.PageOverview]: PAGE_OVERVIEW_BLOCK_CONFIG, [ContentBlockType.ProjectsSpotlight]: PROJECTS_SPOTLIGHT_BLOCK_CONFIG, [ContentBlockType.Spotlight]: SPOTLIGHT_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 47dd3bb3f..5206b8644 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 @@ -1,6 +1,7 @@ import { INITIAL_ANCHOR_LINKS_COMPONENTS_STATE } from '~content-blocks/anchor-links'; import { INITIAL_AVO_HERO_COMPONENTS_STATE } from '~content-blocks/BlockAvoHero'; import { INITIAL_AVO_IMAGE_TEXT_BACKGROUND_BLOCK_STATE } from '~content-blocks/BlockAvoImageTextBackground'; +import { INITIAL_AVO_QUOTE_COMPONENTS_STATE } from '~content-blocks/BlockAvoQuote'; import { INITIAL_BREADCRUMBS_BLOCK_STATE } from '~content-blocks/BlockBreadcrumbs/BlockBreadcrumbs.editorconfig'; import { INITIAL_BUTTONS_COMPONENTS_STATE } from '~content-blocks/BlockButtons'; import { INITIAL_CARDS_WITHOUT_DESCRIPTION_COMPONENTS_STATE } from '~content-blocks/BlockCardsWithoutDescription'; @@ -11,6 +12,7 @@ import { INITIAL_EVENTBRITE_COMPONENTS_STATE } from '~content-blocks/BlockEventb import { INITIAL_HEADING_COMPONENTS_STATE } from '~content-blocks/BlockHeading'; import { INITIAL_HET_ARCHIEF_HEADER_SEARCH_BLOCK_STATE } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig'; import { INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_STATE } from '~content-blocks/BlockHetArchiefImageTextBackground'; +import { INITIAL_HET_ARCHIEF_QUOTE_COMPONENTS_STATE } from '~content-blocks/BlockHetArchiefQuote'; import { INITIAL_CONTENT_HIGHLIGHT_TEXT_BLOCK_STATE } from '~content-blocks/BlockHighlightText'; import { INITIAL_CONTENT_HOMEPAGE_BANNER_BLOCK_STATE } from '~content-blocks/BlockHomepageBanner'; import { INITIAL_IFRAME_COMPONENTS_STATE } from '~content-blocks/BlockIFrame'; @@ -28,7 +30,6 @@ import { INITIAL_MEDIA_GRID_COMPONENTS_STATE } from '~content-blocks/BlockMediaG import { INITIAL_OVERVIEW_NEWSPAPER_TITLES_BLOCK_STATE } from '~content-blocks/BlockOverviewNewspaperTitles'; import { INITIAL_OVERVIEW_WITH_CAROUSEL_BLOCK_STATE } from '~content-blocks/BlockOverviewWithCarousel/BlockOverviewWithCarousel.editorconfig.ts'; import { INITIAL_PAGE_OVERVIEW_COMPONENTS_STATE } from '~content-blocks/BlockPageOverview'; -import { INITIAL_QUOTE_COMPONENTS_STATE } from '~content-blocks/BlockQuote'; import { INITIAL_RICH_TEXT_COMPONENTS_STATE, INITIAL_RICH_TEXT_TWO_COLUMNS_COMPONENTS_STATE, @@ -70,7 +71,8 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: { [ContentBlockType.PageOverview]: INITIAL_PAGE_OVERVIEW_COMPONENTS_STATE, [ContentBlockType.ProjectsSpotlight]: INITIAL_PROJECTS_SPOTLIGHT_COMPONENTS_STATE, [ContentBlockType.Spotlight]: INITIAL_SPOTLIGHT_COMPONENTS_STATE, - [ContentBlockType.Quote]: INITIAL_QUOTE_COMPONENTS_STATE, + [ContentBlockType.AvoQuote]: INITIAL_AVO_QUOTE_COMPONENTS_STATE, + [ContentBlockType.HetArchiefQuote]: INITIAL_HET_ARCHIEF_QUOTE_COMPONENTS_STATE, [ContentBlockType.RichText]: INITIAL_RICH_TEXT_COMPONENTS_STATE, [ContentBlockType.RichTextTwoColumns]: INITIAL_RICH_TEXT_TWO_COLUMNS_COMPONENTS_STATE, [ContentBlockType.Search]: INITIAL_SEARCH_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 f138c03af..4e656cd23 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 @@ -79,8 +79,8 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () = value: ContentBlockType.Spotlight, }, { - label: tText('admin/content-block/content-block___quote'), - value: ContentBlockType.Quote, + label: tText('admin/content-block/content-block___quote', {}, [AVO, HET_ARCHIEF]), + value: ContentBlockType.AvoQuote, }, { label: tText('admin/content-block/helpers/generators/anchor-links___links'), @@ -184,6 +184,12 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () = ), value: ContentBlockType.OverviewWithCarousel, }, + { + label: tText('modules/content-page/const/get-content-block-type-options___quote-kader', {}, [ + HET_ARCHIEF, + ]), + value: ContentBlockType.HetArchiefQuote, + }, { label: tText( 'modules/content-page/const/get-content-block-type-options___homepage-banner', 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..0244e003c 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 @@ -168,7 +168,7 @@ export enum ContentBlockType { MediaPlayerTitleTextButton = 'MEDIA_PLAYER_TITLE_TEXT_BUTTON', PageOverview = 'PAGE_OVERVIEW', ProjectsSpotlight = 'PROJECTS_SPOTLIGHT', - Quote = 'QUOTE', + AvoQuote = 'QUOTE', RichText = 'RICH_TEXT', RichTextTwoColumns = 'RICH_TEXT_TWO_COLUMNS', Search = 'SEARCH', @@ -181,6 +181,7 @@ export enum ContentBlockType { HetArchiefImageTextBackground = 'IMAGE_TEXT_BACKGROUND', // Hetarchief MaintainersGrid = 'MAINTAINERS_GRID', HetArchiefHeaderSearch = 'HETARCHIEF__HEADER_SEARCH', + HetArchiefQuote = 'HETARCHIEF_QUOTE', OverviewNewspaperTitles = 'OVERVIEW_NEWSPAPER_TITLES', ContentEncloseGrid = 'CONTENT_ENCLOSE_GRID', Breadcrumbs = 'BREADCRUMBS', @@ -361,7 +362,7 @@ export interface IFrameBlockComponentState { src: string; } -export interface QuoteBlockComponentState { +export interface AvoQuoteBlockComponentState { quote: string; authorName: string; authorInitials: string; @@ -459,7 +460,8 @@ export type SingleContentBlockComponentState = | MediaPlayerBlockComponentState | MediaPlayerTitleTextButtonBlockComponentState | PageOverviewBlockComponentStateFields - | QuoteBlockComponentState + | AvoQuoteBlockComponentState + | HetArchiefQuoteBlockComponentState | RichTextBlockComponentState // biome-ignore lint/complexity/noBannedTypes: todo | {}; // Search block & content page meta @@ -594,6 +596,13 @@ export interface HetArchiefHeaderSearchBlockComponentState { textBelowSearch?: string; } +export interface HetArchiefQuoteBlockComponentState { + quote: string; + authorName: string; + textColor: Color; + frameColor: Color | GradientColor | CustomBackground; +} + export interface HetArchiefIeObject { name: string; schemaIdentifier: string; diff --git a/ui/src/react-admin/modules/index.ts b/ui/src/react-admin/modules/index.ts index 540ced09a..0440c6394 100644 --- a/ui/src/react-admin/modules/index.ts +++ b/ui/src/react-admin/modules/index.ts @@ -98,6 +98,7 @@ export * from './content-page/components/blocks/anchor-links'; export * from './content-page/components/blocks/BlockAccordions/BlockAccordions'; export * from './content-page/components/blocks/BlockAvoHero/BlockAvoHero'; export * from './content-page/components/blocks/BlockAvoImageTextBackground/BlockAvoImageTextBackground'; +export * from './content-page/components/blocks/BlockAvoQuote/BlockAvoQuote'; export * from './content-page/components/blocks/BlockBreadcrumbs/BlockBreadcrumbs'; export * from './content-page/components/blocks/BlockButtons/BlockButtons'; export * from './content-page/components/blocks/BlockCardsWithoutDescription/BlockCardsWithoutDescription'; @@ -108,6 +109,7 @@ export * from './content-page/components/blocks/BlockEventbrite/BlockEventbrite' export * from './content-page/components/blocks/BlockHeading/BlockHeading'; export * from './content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch'; export * from './content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground'; +export * from './content-page/components/blocks/BlockHetArchiefQuote/BlockHetArchiefQuote'; export * from './content-page/components/blocks/BlockIFrame/BlockIFrame'; export * from './content-page/components/blocks/BlockImage/BlockImage'; export * from './content-page/components/blocks/BlockImageGrid/BlockImageGrid'; @@ -118,7 +120,6 @@ export * from './content-page/components/blocks/BlockMaintainersGrid/BlockMainta export * from './content-page/components/blocks/BlockMediaGrid/BlockMediaGrid.editorconfig'; export * from './content-page/components/blocks/BlockOverviewNewspaperTitles/BlockOverviewNewspaperTitles'; export * from './content-page/components/blocks/BlockPageOverview/BlockPageOverview'; -export * from './content-page/components/blocks/BlockQuote/BlockQuote'; export * from './content-page/components/blocks/BlockRichText/BlockRichText'; export * from './content-page/components/blocks/BlockSpotlight/BlockSpotlight'; export * from './content-page/components/blocks/BlockTagsWithLink/BlockTagsWithLink'; diff --git a/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss b/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss index f048a56cd..e18bf1df2 100644 --- a/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss +++ b/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss @@ -37,6 +37,10 @@ $font-family: "SofiaPro", sans-serif; @include sofia-pro(1.8rem, 800, 2.8rem, $important); } +@mixin sofia-pro-body-xl($important: false) { + @include sofia-pro(3.2rem, 500, 4rem, $important); +} + @mixin sofia-pro-body($important: false) { @include sofia-pro(1.8rem, 500, 2.8rem, $important); } @@ -45,6 +49,10 @@ $font-family: "SofiaPro", sans-serif; @include sofia-pro(1.6rem, 500, 2.4rem, $important); } +@mixin sofia-pro-body-xs($important: false) { + @include sofia-pro(1.4rem, 500, 2rem, $important); +} + // Single-line truncation with an ellipsis - shared with .u-truncate (utilities/_text.scss). @mixin truncate() { white-space: nowrap; diff --git a/ui/src/shared/helpers/admin-core-config.tsx b/ui/src/shared/helpers/admin-core-config.tsx index c9e68078d..d8dc95bd4 100644 --- a/ui/src/shared/helpers/admin-core-config.tsx +++ b/ui/src/shared/helpers/admin-core-config.tsx @@ -78,7 +78,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction ContentBlockType.ImageGrid, ContentBlockType.PageOverview, ContentBlockType.UspGrid, - ContentBlockType.Quote, + ContentBlockType.AvoQuote, ContentBlockType.CTAs, // AVO ContentBlockType.AnchorLinks, @@ -97,6 +97,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction // ContentBlockType.ContentEncloseGrid, // ContentBlockType.Breadcrumbs, ContentBlockType.OverviewWithCarousel, + ContentBlockType.HetArchiefQuote, ], defaultPageWidth: ContentPageWidth.LARGE, onSaveContentPage: async (contentPageInfo: ContentPageInfo) => { @@ -155,6 +156,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction newspaper: { name: 'newspaper' }, noAudio: { name: 'no-audio' }, noVideo: { name: 'no-video' }, + quotes: { name: 'quotes' }, }, list: (): { value: IconName; label: string }[] => { return Object.values(IconName).map((iconName: IconName) => ({ diff --git a/ui/src/shared/translations/hetArchief/nl.json b/ui/src/shared/translations/hetArchief/nl.json index 81ff17162..e62a4f2ea 100644 --- a/ui/src/shared/translations/hetArchief/nl.json +++ b/ui/src/shared/translations/hetArchief/nl.json @@ -137,6 +137,12 @@ "admin/content-block/helpers/generators/heading___titel-kleur": "Titel kleur", "admin/content-block/helpers/generators/heading___type": "Type", "admin/content-block/helpers/generators/hero___alt-title-text": "Alt. titel tekst", + "admin/content-block/helpers/generators/hetarchief-quote___auteur": "Auteur", + "admin/content-block/helpers/generators/hetarchief-quote___kleur-kader": "Kleur kader", + "admin/content-block/helpers/generators/hetarchief-quote___quote": "Quote", + "admin/content-block/helpers/generators/hetarchief-quote___quote-is-verplicht": "Quote is verplicht", + "admin/content-block/helpers/generators/hetarchief-quote___quote-kader": "Quote kader", + "admin/content-block/helpers/generators/hetarchief-quote___tekstkleur": "Tekstkleur", "admin/content-block/helpers/generators/iframe___i-frame": "iFrame", "admin/content-block/helpers/generators/iframe___titel": "Alt-tekst", "admin/content-block/helpers/generators/iframe___titel-is-verplicht": "Alt-tekst is verplicht voor SEO en slechtzienden.", @@ -805,6 +811,7 @@ "modules/content-page/const/get-content-block-type-options___homepage-banner": "Homepage banner", "modules/content-page/const/get-content-block-type-options___overzicht-krantentitels": "Overzicht krantentitels", "modules/content-page/const/get-content-block-type-options___overzicht-met-carousel": "Overzicht met carousel", + "modules/content-page/const/get-content-block-type-options___quote-kader": "Quote kader", "modules/content-page/const/get-content-block-type-options___scroll-down-nudge": "Nudge om te scrollen", "modules/content-page/const/get-content-block-type-options___thema-reels": "Thema reels", "modules/content-page/const/get-heading-type-options___groot": "Groot",