fix(builder): ColumnLayout.toHtml derives column widths from split (D2b)
toHtml emitted only the wrapper's flex/gap style and passed childrenHtml through untouched, so a non-default split percentage never showed up in the export -- each column's width silently reverted to whatever was baked into its own inline style at node-creation time. toHtml has no direct handle on individual children to rewrite their styles (it only receives the already-concatenated childrenHtml string), so emit a scoped <style> block with nth-child rules (!important, to win over any stale per-child inline flex) computed via the same getWidths(split, columns) mapping the editor render uses. Precedent: Menu/Navbar toHtml already emit scoped <style> blocks for hover CSS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { ColumnLayout } from './ColumnLayout';
|
||||||
|
|
||||||
|
const toHtml = (ColumnLayout as any).toHtml;
|
||||||
|
|
||||||
|
describe('ColumnLayout.toHtml width export from split', () => {
|
||||||
|
test('non-default split (70-30) exports per-column width CSS matching each column', () => {
|
||||||
|
const { html } = toHtml({ columns: 2, split: '70-30', gap: '16px' }, '<div>A</div><div>B</div>');
|
||||||
|
// First column gets 70%, second gets 30% (same mapping as getWidths()).
|
||||||
|
expect(html).toMatch(/nth-child\(1\)[^}]*calc\(70% - 16px\)/);
|
||||||
|
expect(html).toMatch(/nth-child\(2\)[^}]*calc\(30% - 16px\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default 50-50 split still exports equal widths', () => {
|
||||||
|
const { html } = toHtml({ columns: 2, split: '50-50', gap: '16px' }, '<div>A</div><div>B</div>');
|
||||||
|
expect(html).toMatch(/nth-child\(1\)[^}]*calc\(50% - 16px\)/);
|
||||||
|
expect(html).toMatch(/nth-child\(2\)[^}]*calc\(50% - 16px\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3-column 33-33-33 split exports three width rules', () => {
|
||||||
|
const { html } = toHtml({ columns: 3, split: '33-33-33', gap: '16px' }, '<div>A</div><div>B</div><div>C</div>');
|
||||||
|
expect(html).toMatch(/nth-child\(1\)[^}]*calc\(33\.333% - 16px\)/);
|
||||||
|
expect(html).toMatch(/nth-child\(2\)[^}]*calc\(33\.333% - 16px\)/);
|
||||||
|
expect(html).toMatch(/nth-child\(3\)[^}]*calc\(33\.333% - 16px\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('childrenHtml is preserved in the output', () => {
|
||||||
|
const { html } = toHtml({ columns: 2, split: '70-30', gap: '16px' }, '<div>A</div><div>B</div>');
|
||||||
|
expect(html).toContain('<div>A</div><div>B</div>');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -115,7 +115,11 @@ ColumnLayout.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
||||||
|
const columns = props.columns || 2;
|
||||||
|
const split = props.split || '50-50';
|
||||||
const gap = props.gap || '16px';
|
const gap = props.gap || '16px';
|
||||||
|
const widths = getWidths(split, columns);
|
||||||
|
|
||||||
const outerStyle = cssPropsToString({
|
const outerStyle = cssPropsToString({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
@@ -124,7 +128,20 @@ ColumnLayout.craft = {
|
|||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
|
// Each column is exported as an independently-serialized child node, so
|
||||||
|
// toHtml has no direct handle on individual children to rewrite their
|
||||||
|
// inline flex-basis. Instead, scope an nth-child CSS rule (with
|
||||||
|
// !important, to win over any stale inline flex baked into a child at
|
||||||
|
// creation time) to a generated class -- same width mapping (getWidths)
|
||||||
|
// the editor render uses. Precedent: Menu/Navbar toHtml already emit
|
||||||
|
// scoped <style> blocks for hover CSS.
|
||||||
|
const scopeId = `cols-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const widthCss = widths
|
||||||
|
.map((w, i) => `.${scopeId} > :nth-child(${i + 1}) { flex: 0 0 calc(${w} - ${gap}) !important; }`)
|
||||||
|
.join('\n ');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
html: `<style>\n ${widthCss}\n</style>\n<div class="${scopeId}"${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user