Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -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