2026-07-12 13:16:53 -07:00
|
|
|
import React, { CSSProperties } from 'react';
|
2026-04-05 18:31:16 -07:00
|
|
|
import { useNode, Element, UserComponent } from '@craftjs/core';
|
|
|
|
|
import { Container } from './Container';
|
|
|
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
2026-07-12 11:49:10 -07:00
|
|
|
import { escapeAttr } from '../../utils/escape';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
type SplitOption =
|
|
|
|
|
| '100'
|
|
|
|
|
| '50-50' | '30-70' | '70-30' | '40-60' | '60-40'
|
|
|
|
|
| '33-33-33' | '25-50-25'
|
|
|
|
|
| '25-25-25-25'
|
|
|
|
|
| '20-20-20-20-20'
|
|
|
|
|
| '16-16-16-16-16-16'
|
|
|
|
|
| 'equal';
|
|
|
|
|
|
|
|
|
|
interface ColumnLayoutProps {
|
|
|
|
|
columns?: number;
|
|
|
|
|
split?: SplitOption;
|
|
|
|
|
gap?: string;
|
|
|
|
|
style?: CSSProperties;
|
|
|
|
|
children?: React.ReactNode;
|
2026-05-25 12:43:28 -07:00
|
|
|
anchorId?: string;
|
2026-04-05 18:31:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const splitToWidths: Record<string, string[]> = {
|
|
|
|
|
'100': ['100%'],
|
|
|
|
|
'50-50': ['50%', '50%'],
|
|
|
|
|
'30-70': ['30%', '70%'],
|
|
|
|
|
'70-30': ['70%', '30%'],
|
|
|
|
|
'40-60': ['40%', '60%'],
|
|
|
|
|
'60-40': ['60%', '40%'],
|
|
|
|
|
'33-33-33': ['33.333%', '33.333%', '33.333%'],
|
|
|
|
|
'25-50-25': ['25%', '50%', '25%'],
|
|
|
|
|
'25-25-25-25': ['25%', '25%', '25%', '25%'],
|
|
|
|
|
'20-20-20-20-20': ['20%', '20%', '20%', '20%', '20%'],
|
|
|
|
|
'16-16-16-16-16-16': ['16.666%', '16.666%', '16.666%', '16.666%', '16.666%', '16.666%'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function getWidths(split: SplitOption, columns: number): string[] {
|
|
|
|
|
// Check predefined splits first
|
|
|
|
|
if (split !== 'equal') {
|
|
|
|
|
const defined = splitToWidths[split];
|
|
|
|
|
if (defined && defined.length === columns) return defined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try parsing custom split string (e.g., "35-65" or "25-50-25")
|
|
|
|
|
if (split && split !== 'equal' && split.includes('-')) {
|
|
|
|
|
const parts = split.split('-').map(Number);
|
|
|
|
|
if (parts.length === columns && parts.every(n => !isNaN(n) && n > 0)) {
|
|
|
|
|
return parts.map(n => `${n}%`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: equal widths
|
|
|
|
|
const w = `${(100 / columns).toFixed(3)}%`;
|
|
|
|
|
return Array.from({ length: columns }, () => w);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ColumnLayout: UserComponent<ColumnLayoutProps> = ({
|
|
|
|
|
columns = 2,
|
|
|
|
|
split = '50-50',
|
|
|
|
|
gap = '16px',
|
|
|
|
|
style = {},
|
2026-05-25 12:43:28 -07:00
|
|
|
anchorId,
|
2026-04-05 18:31:16 -07:00
|
|
|
}) => {
|
|
|
|
|
const { connectors: { connect, drag } } = useNode();
|
|
|
|
|
const widths = getWidths(split, columns);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
2026-05-25 12:43:28 -07:00
|
|
|
id={anchorId || undefined}
|
2026-04-05 18:31:16 -07:00
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
gap,
|
|
|
|
|
width: '100%',
|
|
|
|
|
minHeight: '60px',
|
|
|
|
|
...style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{widths.map((w, i) => (
|
|
|
|
|
<Element
|
|
|
|
|
key={`col-${i}`}
|
|
|
|
|
id={`col-${i}`}
|
|
|
|
|
is={Container}
|
|
|
|
|
canvas
|
|
|
|
|
custom={{ className: 'craft-column' }}
|
|
|
|
|
style={{ flex: `0 0 calc(${w} - ${gap})`, minHeight: '60px', padding: '8px' }}
|
|
|
|
|
tag="div"
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
|
|
|
|
|
|
ColumnLayout.craft = {
|
|
|
|
|
displayName: 'Columns',
|
|
|
|
|
props: {
|
|
|
|
|
columns: 2,
|
|
|
|
|
split: '50-50',
|
|
|
|
|
gap: '16px',
|
|
|
|
|
style: {},
|
2026-05-25 12:43:28 -07:00
|
|
|
anchorId: '',
|
2026-04-05 18:31:16 -07:00
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
canDrag: () => true,
|
|
|
|
|
canMoveIn: () => false,
|
|
|
|
|
canMoveOut: () => true,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
|
|
|
|
|
|
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
|
|
|
|
const gap = props.gap || '16px';
|
|
|
|
|
const outerStyle = cssPropsToString({
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
gap,
|
|
|
|
|
width: '100%',
|
|
|
|
|
...props.style,
|
|
|
|
|
});
|
2026-07-12 11:49:10 -07:00
|
|
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
2026-04-05 18:31:16 -07:00
|
|
|
return {
|
2026-05-25 12:43:28 -07:00
|
|
|
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
2026-04-05 18:31:16 -07:00
|
|
|
};
|
|
|
|
|
};
|