180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
import { Card, Col, Row } from "antd";
|
|
import { Children, isValidElement } from "react";
|
|
import { INLINE_FORM_ROW_WRAP_TITLE_STYLES } from "./inline-form-row-title.utils.js";
|
|
import "./layout-form-row.styles.scss";
|
|
|
|
export default function LayoutFormRow({
|
|
header,
|
|
children,
|
|
grow = false,
|
|
noDivider = false,
|
|
titleOnly = false,
|
|
wrapTitle = false,
|
|
gutter,
|
|
rowProps,
|
|
|
|
// Optional overrides if you ever need per-section customization
|
|
surface = true,
|
|
surfaceBg,
|
|
surfaceHeaderBg,
|
|
surfaceBorderColor,
|
|
|
|
...cardProps
|
|
}) {
|
|
const items = Children.toArray(children).filter(Boolean);
|
|
const isCompactRow = noDivider;
|
|
|
|
const title = !noDivider && header ? header : undefined;
|
|
const resolvedTitle = cardProps.title ?? title;
|
|
const isHeaderOnly = titleOnly || items.length === 0;
|
|
const hideBody = isHeaderOnly;
|
|
|
|
if (items.length === 0 && !resolvedTitle) return null;
|
|
const resolvedGutter = gutter ?? [16, isCompactRow ? 8 : 16];
|
|
|
|
const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined);
|
|
const headBg = surfaceHeaderBg ?? (surface ? "var(--imex-form-surface-head)" : undefined);
|
|
const borderColor = surfaceBorderColor ?? (surface ? "var(--imex-form-surface-border)" : undefined);
|
|
|
|
const mergedStyles = mergeSemanticStyles(
|
|
{
|
|
...(wrapTitle ? INLINE_FORM_ROW_WRAP_TITLE_STYLES : null),
|
|
header: {
|
|
paddingInline: isHeaderOnly ? 8 : isCompactRow ? 12 : 16,
|
|
background: headBg,
|
|
borderBottomColor: borderColor
|
|
},
|
|
body: {
|
|
padding: hideBody ? 0 : isCompactRow ? 12 : 16,
|
|
display: hideBody ? "none" : undefined,
|
|
background: bg
|
|
}
|
|
},
|
|
cardProps.styles
|
|
);
|
|
|
|
const baseCardStyle = {
|
|
marginBottom: isHeaderOnly ? "0" : isCompactRow ? "8px" : ".8rem",
|
|
...(bg ? { background: bg } : null), // ensures the “circled area” is tinted
|
|
...(borderColor ? { borderColor } : null),
|
|
...cardProps.style
|
|
};
|
|
|
|
const count = items.length;
|
|
|
|
// Modern responsive strategy leveraging Ant Design 6:
|
|
// - xs (phone <576px): Always stack vertically
|
|
// - sm (tablet 576-768px): 2 columns for better readability
|
|
// - md (tablet 768-992px): 3 columns for tablets
|
|
// - lg+ (desktop >992px): Dynamic flex-based columns that adapt to screen width
|
|
// Target: 1366px → 4 cols, 1920px → 6 cols, 2560px → 8 cols, 4K → capped at 8-9 cols
|
|
// Note: xxl uses higher min-width to naturally cap maximum columns
|
|
const baseCol = (() => {
|
|
if (grow) {
|
|
// Grow mode: use flex with reasonable min-widths
|
|
return {
|
|
xs: 24,
|
|
sm: 12, // Fixed 2 cols on small tablets
|
|
md: 8, // Fixed 3 cols on large tablets
|
|
lg: { flex: `1 1 320px` }, // Dynamic: ~3 cols at 1200px
|
|
xl: { flex: `1 1 280px` }, // Dynamic: ~4 cols at 1366px, ~6 cols at 1920px
|
|
xxl: { flex: `1 1 380px` } // Dynamic: ~6 cols at 2560px, ~9 cols at 4K (capped)
|
|
};
|
|
} else {
|
|
// Fixed mode: Use flex without grow to maintain uniform widths across rows
|
|
// xxl uses larger min-width to cap maximum columns on 4K/ultrawide
|
|
const minWidthLg = count <= 2 ? 500 : count === 3 ? 380 : 320; // 3 cols at 1200px
|
|
const minWidthXl = count <= 2 ? 480 : count === 3 ? 360 : 280; // 4 cols at 1366px, ~6 at 1920px
|
|
const minWidthXxl = count <= 2 ? 500 : count === 3 ? 400 : 380; // ~6 cols at 2560px, ~9 at 4K (natural cap)
|
|
|
|
return {
|
|
xs: 24,
|
|
sm: 12, // Fixed 2 columns on tablet
|
|
md: count === 1 ? 24 : count === 2 ? 12 : 8, // Fixed 1-3 cols on large tablet
|
|
lg: count === 1 ? 24 : { flex: `0 0 ${minWidthLg}px` }, // Fixed width on desktop
|
|
xl: count === 1 ? 24 : { flex: `0 0 ${minWidthXl}px` }, // Fixed width on large desktop
|
|
xxl: count === 1 ? 24 : { flex: `0 0 ${minWidthXxl}px` } // Fixed width on ultrawide
|
|
};
|
|
}
|
|
})();
|
|
|
|
const getColPropsForChild = (child) => {
|
|
if (!isValidElement(child)) return baseCol;
|
|
|
|
// Back-compat: child.props.span can override Col sizing
|
|
const spanOverride = child.props?.span;
|
|
if (typeof spanOverride === "number") return { span: spanOverride };
|
|
if (spanOverride && typeof spanOverride === "object") return spanOverride;
|
|
|
|
// Explicit override: <Field col={{ md:{span:12}, ... }} />
|
|
const colOverride = child.props?.col;
|
|
if (colOverride && typeof colOverride === "object") {
|
|
// Deep merge: allow partial overrides while keeping baseCol defaults
|
|
const merged = { ...baseCol };
|
|
Object.keys(colOverride).forEach((bp) => {
|
|
merged[bp] = typeof colOverride[bp] === "object" ? { ...baseCol[bp], ...colOverride[bp] } : colOverride[bp];
|
|
});
|
|
return merged;
|
|
}
|
|
|
|
return baseCol;
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
{...cardProps}
|
|
title={resolvedTitle}
|
|
size={cardProps.size ?? "small"}
|
|
variant={cardProps.variant ?? "outlined"}
|
|
className={[
|
|
"imex-form-row",
|
|
isCompactRow ? "imex-form-row--compact" : null,
|
|
isHeaderOnly ? "imex-form-row--title-only" : null,
|
|
cardProps.className
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
style={baseCardStyle}
|
|
styles={mergedStyles}
|
|
>
|
|
{!isHeaderOnly &&
|
|
(items.length === 1 ? (
|
|
items[0]
|
|
) : (
|
|
<Row gutter={resolvedGutter} wrap {...rowProps}>
|
|
{items.map((child, idx) => (
|
|
<Col key={child?.key ?? idx} {...getColPropsForChild(child)}>
|
|
{child}
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
))}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function mergeSemanticStyles(defaults, userStyles) {
|
|
if (!userStyles) return defaults;
|
|
|
|
if (typeof userStyles === "function") {
|
|
return (info) => {
|
|
const computed = userStyles(info) || {};
|
|
return {
|
|
...defaults,
|
|
...computed,
|
|
title: { ...(defaults.title || {}), ...(computed.title || {}) },
|
|
header: { ...defaults.header, ...(computed.header || {}) },
|
|
body: { ...defaults.body, ...(computed.body || {}) }
|
|
};
|
|
};
|
|
}
|
|
|
|
return {
|
|
...defaults,
|
|
...userStyles,
|
|
title: { ...(defaults.title || {}), ...(userStyles.title || {}) },
|
|
header: { ...defaults.header, ...(userStyles.header || {}) },
|
|
body: { ...defaults.body, ...(userStyles.body || {}) }
|
|
};
|
|
}
|