Skip to content

feat: add flex layout support to renderV2Message - #1339

Open
sailaya99 wants to merge 6 commits into
paypal:developfrom
sailaya99:DTCRCMERC-5374-flex-layout-render-v2-message
Open

feat: add flex layout support to renderV2Message#1339
sailaya99 wants to merge 6 commits into
paypal:developfrom
sailaya99:DTCRCMERC-5374-flex-layout-render-v2-message

Conversation

@sailaya99

Copy link
Copy Markdown
Contributor

Description

Adds flex layout support to renderV2Message (DTCRCMERC-5374), following the same static CSS/class-based approach used by the text layout renderer. The flex renderer does not depend on getMutations, applyCascade, locale/product mutation files, or any v5 mutation cascade logic.

Changes:

  • src/server/v2/message.jsx — adds FlexMessage component that extracts logo from main_items and renders pp-flex__logo-container, pp-flex__messaging, pp-flex__main, pp-flex__action, and pp-flex__disclaimer sections directly from CPS v2 content blocks
  • src/server/v2/styles.js — adds flexStyles() covering all 7 color themes (blue, black, white, white-no-border, gray, monochrome, grayscale) and all 4 ratio layouts (1x1, 1x4, 8x1, 20x1) with responsive media queries; color and ratio are applied via static CSS class selectors, not dynamic injection
  • src/server/v2/getParentStyles.js — existing ratioMap already handles flex ratio outer iframe sizing; no changes needed
  • src/server/message/font.js — adds shared buildFontRules utility used by v2 stylesheets (text and flex)
  • src/server/v2/utils/buildLogoConfiguration.js — updates to accept logoType and correctly handle inline logo extraction
  • Tests and snapshots for all new flex behavior

Color/ratio values are normalized before reaching renderV2Message by the existing validateStyle logic (greygray, greyscalegrayscale).

Screenshots / Videos

N/A

Testing instructions

  • npm test — 152 unit tests pass covering all color and ratio combinations, content block structure, stylesheet isolation, alias normalization, and snapshots
  • Flex rendering can be exercised locally via node scripts/preview-v2-render.js with style.layout: "flex" options

User added 4 commits June 10, 2026 11:15
…sage

- Add body reset and scoped .pp-message stylesheet (display:block, width:100%)
- Map text.color to CSS classes on .main/.action spans with vertical-align:middle
- Add CSS filters on .logo img for white (invert), monochrome (grayscale+black), grayscale variants
- Handle logo positions: left (default), right (margin swap), top (block display)
- Implement fontSource @font-face generation with URL/name security validation
- Wire fontSource through message.jsx into styles()
- Add data-pp-style-* root attributes for layout, logo-type, logo-position, text-align, text-color, text-size
- Add greyscale alias pipeline test (validateStyle -> render -> class)
- Expand snapshot coverage to all 7 text sizes (10-16) and full logo/color/align matrix (21 snapshots)
- Separate logoType/logoPosition in message.jsx; buildLogoConfiguration
  now branches on logoType=inline without using it as a pseudo-position
- Replace full-matrix snapshots with one representative render snapshot,
  one stylesheet snapshot, and test.each matrix assertions (119 lines vs ~1240)
- Add stylesheet isolation tests (inline embed + selector scope check)
- Simplify redundant selector-parsing filter in isolation test
Implements FlexMessage component and flex stylesheet in src/server/v2,
using a static CSS/class-based approach with no dependency on the v5
mutation cascade (getMutations, applyCascade, or locale mutation files).

- FlexMessage maps color and ratio to CSS classes and data attributes
- flexStyles() covers all 7 color themes and 4 ratio layouts with
  responsive media queries
- getParentStyles ratioMap drives outer iframe/container aspect-ratio
  sizing for flex ratios
- grey/greyscale aliases normalize via existing validateStyle logic
- Unit tests cover all color/ratio combinations, structure, stylesheet
  isolation, alias normalization, and snapshots for representative cases

@Braluna-pp Braluna-pp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thermo-nuclear code quality review focused on maintainability, structure, and reviewability. These are not behavior blockers from a test run; they are structural review comments from the PR diff and Jira scope.

Comment thread src/server/v2/styles.js Outdated
});
}

function flexStyles({ fontFamily, fontSource } = {}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a large hand-written flex stylesheet directly into the shared v2 styles module. The file now mixes font setup, text styles, seven color themes, four ratio layouts, and responsive behavior in one template string, which makes future flex changes harder to review because a small theme or ratio change means scanning the whole CSS blob.

Problematic shape:

function flexStyles({ fontFamily, fontSource } = {}) {
    // themes, ratios, media queries, and font setup all live here
}

Can we split flex into its own module and make the themes/ratios table-driven? For example, keep textStyles and flexStyles in separate files, define FLEX_THEMES and FLEX_RATIOS, then render the selectors from those maps. Same output, but fewer repeated selectors and a clearer owner for flex styling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. flexStyles.js now has FLEX_THEMES, PORTRAIT_RATIOS, and LANDSCAPE_RATIOS data tables. A lscpSel() helper expands LANDSCAPE_RATIOS into grouped selectors everywhere, including inside @media blocks. CSS output is unchanged; all 95 tests pass.

Comment thread src/server/message/font.js Outdated
const isSafeFontName = value => typeof value === 'string' && /^[\w\s-]+$/.test(value.trim());
const isSafeFontSource = value => typeof value === 'string' && /^https:\/\/[^'")\s]+$/i.test(value);

export function buildFontRules({ fontSource, fontFamily, fallbackStack, defaultFontFamily, fontNamePrefix }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puts a second font-generation path into the legacy message font module, but the existing getFontRules path above still uses different naming, fallback, and validation behavior. That leaves one file owning two contracts that can drift.

Problematic shape:

export function buildFontRules(...) { ... }

Can we either move this v2-only helper under src/server/v2 or fully refactor getFontRules to use the same shared primitive? Right now the abstraction looks shared, but only v2 uses it, so it adds cross-layer coupling without deleting the old complexity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildFontRules is in src/server/v2/font.js. The only change to src/server/message/font.js on this branch is a trailing-newline removal — no new exports were added there.

['gray', '1x4'],
['monochrome', '20x1']
])('full render snapshot for flex color=%s ratio=%s', (color, ratio) => {
const options = { style: { layout: 'flex', color, ratio } };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These full render snapshots add a new 1,235-line snapshot file. That is too much generated output for this change, and it makes the review depend on scanning huge snapshots instead of clear expectations. It also duplicates many string assertions already covered above.

Problematic shape:

expect(render(options, flexContentWithLogo)).toMatchSnapshot();

Can we shrink this to one representative HTML snapshot plus semantic assertions for the full color/ratio matrix? The ratio/theme coverage can stay table-driven, but the tests should assert the meaningful contract directly: root classes, data attributes, key selectors, and logo/action/disclaimer structure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot file is 491 lines. The color/ratio matrix uses semantic toContain assertions; only two flex snapshots exist: one full HTML render (blue/8x1) and one for the stylesheet.

Comment thread src/server/v2/message.jsx Outdated
);
}

function FlexMessage({ style, v2Content }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the whole flex renderer inside message.jsx, while the same file still owns the text renderer and shared block rendering. The branch at style.layout === 'flex' is clean today, but this file is now the place where every future layout can grow more renderer-specific logic.

Problematic shape:

function FlexMessage({ style, v2Content }) { ... }

Can we move FlexMessage into a dedicated v2 flex module and keep message.jsx as a small dispatcher? The shared helpers like renderBlock, renderLogo, and content extraction can stay reusable, but the layout-specific structure should live behind its own component so this does not become a multi-layout grab bag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlexMessage is in src/server/v2/flex.jsx. message.jsx just imports and dispatches to it.

Comment thread src/server/v2/flex.jsx Outdated
Comment on lines +10 to +11
const color = style.color ?? 'black';
const ratio = style.ratio ?? '8x1';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a correctness edge case: the direct flex renderer defaults do not match the validated flex defaults. validOptions defaults flex to color: "blue" and ratio: "1x1" because those are the first valid values, but this lower-level render path falls back to black and 8x1.

That means render({ style: { layout: "flex" } }) can produce class="pp-message pp-flex black r-8x1", while the validated path produces blue r-1x1. Since render, validateStyle, and getParentStyles are all exported from src/server/v2/index.js, callers can hit this mismatch and end up with a rendered message that does not match the wrapper/default style contract.

Can we make these defaults come from the same source? At minimum this should use blue and 1x1 here, and ideally the flex defaults should live in one shared constant used by both validOptions and FlexMessage. Please also add a small test for render({ style: { layout: "flex" } }) so this stays aligned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added FLEX_DEFAULTS = { color: 'blue', ratio: '1x1' } to constants.js and updated flex.jsx to use it instead of the hardcoded black/8x1. Added a test for render({ style: { layout: 'flex' } }) that asserts the output matches validateStyle's defaults — any future drift will break the test.

Adds FLEX_DEFAULTS to constants.js so flex.jsx and validateStyle share
the same color/ratio defaults, preventing render/validation mismatch.
Adds a test that asserts render({ style: { layout: 'flex' } }) produces
the same output as rendering the style returned by validateStyle.
@surekhaw surekhaw changed the title feat(DTCRCMERC-5374): add flex layout support to renderV2Message feat: add flex layout support to renderV2Message Jul 16, 2026

@surrsurus surrsurus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this visually with the harness that Jeremy put together - piped it through the local bundle built from this PR instead of the v5 stage bundle. Looks good, tested it with a variety of options. Just a few really minor things I noticed.

I noticed the width of the 1x1 and 1x4 didn't match but this might be due to the size of the zoid in the window - can you confirm if that should be the case?

I also noticed the 20x1 text doesn't look fully aligned with the logo, if this is intentional then feel free to ignore:

Image

Before (cdn-v5_rewrite bundle)

1x1 1x4 8x1 20x1
Image Image Image Image

After (This bundle)

1x1 1x4 8x1 20x1
Image Image Image Image

@surrsurus

Copy link
Copy Markdown
Contributor

Ignore my comment about the ratios - I'm now seeing where the widths are coming from in the ratioMap, these dimensions make more sense to me now:

  const ratioMap = {
    '1x1': [
        {
            ratio: '1x1',
            width: [120, 300]
        }
    ],
    '1x4': [
        {
            ratio: '1x2',
            width: [160, 160]
        },
        {
            ratio: '1x4',
            breakpoint: 768
        }
    ],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants