This commit is contained in:
Dave
2026-02-06 10:43:58 -05:00
parent 56559dd3ff
commit 5ae0e8e4d5
2 changed files with 183 additions and 61 deletions

View File

@@ -1,59 +1,137 @@
import { Col, Divider, Row } from "antd";
import { Card, Col, Row } from "antd";
import { Children, isValidElement } from "react";
import "./layout-form-row.styles.scss";
export default function LayoutFormRow({ header, children, grow = false, noDivider = false, ...restProps }) {
const DividerHeader = () =>
!noDivider && (
<Divider titlePlacement="left" orientation="horizontal" style={{ marginTop: ".8rem" }}>
{header}
</Divider>
);
export default function LayoutFormRow({
header,
children,
grow = false,
noDivider = false,
gutter = [16, 16],
rowProps,
if (!children.length) {
//We have only one element. It's going to get the whole thing.
// Optional overrides if you ever need per-section customization
surface = true,
surfaceBg,
surfaceHeaderBg,
...cardProps
}) {
const items = Children.toArray(children).filter(Boolean);
if (items.length === 0) return null;
const title = !noDivider && header ? header : undefined;
const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined);
const headBg = surfaceHeaderBg ?? (surface ? "var(--imex-form-surface-head)" : undefined);
const mergedStyles = mergeSemanticStyles(
{
header: {
paddingInline: 16,
background: headBg
},
body: {
padding: 16,
background: bg
}
},
cardProps.styles
);
const baseCardStyle = {
marginBottom: ".8rem",
...(bg ? { background: bg } : null), // ensures the “circled area” is tinted
...cardProps.style
};
// single child => just render it
if (items.length === 1) {
return (
<div className="imex-form-row" {...restProps} style={{ marginBottom: ".8rem", ...restProps.style }}>
<DividerHeader />
{children}
</div>
<Card
{...cardProps}
title={cardProps.title ?? title}
size={cardProps.size ?? "small"}
variant={cardProps.variant ?? "outlined"}
className={["imex-form-row", cardProps.className].filter(Boolean).join(" ")}
style={baseCardStyle}
styles={mergedStyles}
>
{items[0]}
</Card>
);
}
const rowGutter = { gutter: [16, 16] };
const colSpan = (spanOverride) => {
if (spanOverride) return { span: spanOverride };
return {
xs: {
span: !grow ? 24 : Math.max(12, 24 / children.length)
},
sm: {
span: !grow ? 12 : Math.max(12, 24 / children.length)
},
md: {
span: !grow ? 8 : Math.max(8, 24 / children.length)
},
lg: {
span: !grow ? 6 : Math.max(6, 24 / children.length)
},
xl: {
span: !grow ? 4 : Math.max(4, 24 / children.length)
}
};
const count = items.length;
const spanFor = (min) => Math.max(min, Math.floor(24 / count));
// Mobile-first: stack on xs
const baseCol = {
xs: { span: 24 },
sm: { span: grow ? spanFor(12) : 12 },
md: { span: grow ? spanFor(8) : 8 },
lg: { span: grow ? spanFor(6) : 6 },
xl: { span: grow ? spanFor(4) : 4 },
xxl: { span: grow ? spanFor(4) : 4 }
};
//{header ? <Typography.Title level={4}>{header}</Typography.Title> : null}
const getColPropsForChild = (child) => {
if (!isValidElement(child)) return baseCol;
// Back-compat with old pattern: 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;
// Optional explicit override: <Field col={{ md:{span:12}, ... }} />
const colOverride = child.props?.col;
if (colOverride && typeof colOverride === "object") {
return { ...baseCol, ...colOverride };
}
return baseCol;
};
return (
<div className="imex-form-row" {...restProps} style={{ marginBottom: ".8rem", ...restProps.style }}>
<DividerHeader />
<Row {...rowGutter}>
{children.map(
(c, idx) =>
c && (
<Col key={idx} {...colSpan(c && c.props && c.props.span)}>
{c}
</Col>
)
)}
<Card
{...cardProps}
title={cardProps.title ?? title}
size={cardProps.size ?? "small"}
variant={cardProps.variant ?? "outlined"}
className={["imex-form-row", cardProps.className].filter(Boolean).join(" ")}
style={baseCardStyle}
styles={mergedStyles}
>
<Row gutter={gutter} wrap {...rowProps}>
{items.map((child, idx) => (
<Col key={child?.key ?? idx} {...getColPropsForChild(child)}>
{child}
</Col>
))}
</Row>
</div>
</Card>
);
}
function mergeSemanticStyles(defaults, userStyles) {
if (!userStyles) return defaults;
if (typeof userStyles === "function") {
return (info) => {
const computed = userStyles(info) || {};
return {
...defaults,
...computed,
header: { ...defaults.header, ...(computed.header || {}) },
body: { ...defaults.body, ...(computed.body || {}) }
};
};
}
return {
...defaults,
...userStyles,
header: { ...defaults.header, ...(userStyles.header || {}) },
body: { ...defaults.body, ...(userStyles.body || {}) }
};
}

View File

@@ -1,20 +1,64 @@
//Override Antd Row margin to save space on forms.
/* layout-form-row.styles.scss */
/**
* Surface tokens (light/dark)
* - --imex-form-surface: the main section background (circled area)
* - --imex-form-surface-head: slightly different strip for the Card header
* - --imex-form-surface-border: border color for the card
*
* Update the dark selector(s) to match whatever you use (body class, data-theme, etc).
*/
:root {
--imex-form-surface: #fafafa; /* subtle contrast vs white page */
--imex-form-surface-head: #f5f5f5; /* header strip */
--imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */
}
/* Pick the selector that matches your app and remove the rest */
html[data-theme="dark"] {
--imex-form-surface: rgba(255, 255, 255, 0.01); /* subtle lift off page bg */
--imex-form-surface-head: rgba(255, 255, 255, 0.06); /* slightly stronger for header strip */
--imex-form-surface-border: rgba(5, 5, 5, 0.12);
}
.imex-form-row {
.ant-row {
margin-bottom: 0rem;
width: 100%;
/* Match old Divider title typography */
.ant-card-head-title {
font-weight: 500;
font-size: var(--ant-font-size-lg);
line-height: 1.2; /* optional, makes it feel like a section header */
}
.ant-form-item-label {
padding: 0rem;
/* Make the whole section read as its own surface */
&.ant-card {
background: var(--imex-form-surface);
border-color: var(--imex-form-surface-border);
}
.ant-card-head {
background: var(--imex-form-surface-head);
border-bottom-color: var(--imex-form-surface-border);
}
.ant-card-body {
background: var(--imex-form-surface);
}
/* Optional: slightly tighter on phones */
@media (max-width: 575px) {
.ant-card-head {
padding-inline: 12px;
}
label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
padding: 0rem;
margin: 0rem;
.ant-card-body {
padding: 12px;
}
}
/* Common form nicety */
.ant-col > * {
width: 100%;
}
}