diff --git a/craft/src/components/layout/ColumnLayout.toHtml.test.ts b/craft/src/components/layout/ColumnLayout.toHtml.test.ts
new file mode 100644
index 0000000..c5d6799
--- /dev/null
+++ b/craft/src/components/layout/ColumnLayout.toHtml.test.ts
@@ -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' }, '
A
B
');
+ // 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' }, 'A
B
');
+ 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' }, 'A
B
C
');
+ 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' }, 'A
B
');
+ expect(html).toContain('A
B
');
+ });
+});
diff --git a/craft/src/components/layout/ColumnLayout.tsx b/craft/src/components/layout/ColumnLayout.tsx
index 7593382..a27a500 100644
--- a/craft/src/components/layout/ColumnLayout.tsx
+++ b/craft/src/components/layout/ColumnLayout.tsx
@@ -115,7 +115,11 @@ ColumnLayout.craft = {
/* ---------- HTML export ---------- */
(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 widths = getWidths(split, columns);
+
const outerStyle = cssPropsToString({
display: 'flex',
flexWrap: 'wrap',
@@ -124,7 +128,20 @@ ColumnLayout.craft = {
...props.style,
});
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 \n${childrenHtml}
`,
};
};