From 5ae0e8e4d528c4d509421349b670d016516ae643 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 6 Feb 2026 10:43:58 -0500 Subject: [PATCH] Initial --- .../layout-form-row.component.jsx | 172 +++++++++++++----- .../layout-form-row.styles.scss | 72 ++++++-- 2 files changed, 183 insertions(+), 61 deletions(-) diff --git a/client/src/components/layout-form-row/layout-form-row.component.jsx b/client/src/components/layout-form-row/layout-form-row.component.jsx index 16e338d5e..e756d272c 100644 --- a/client/src/components/layout-form-row/layout-form-row.component.jsx +++ b/client/src/components/layout-form-row/layout-form-row.component.jsx @@ -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 && ( - - {header} - - ); +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 ( -
- - {children} -
+ + {items[0]} + ); } - 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 ? {header} : 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: + const colOverride = child.props?.col; + if (colOverride && typeof colOverride === "object") { + return { ...baseCol, ...colOverride }; + } + + return baseCol; + }; + return ( -
- - - {children.map( - (c, idx) => - c && ( - - {c} - - ) - )} + + + {items.map((child, idx) => ( + + {child} + + ))} -
+ ); } + +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 || {}) } + }; +} diff --git a/client/src/components/layout-form-row/layout-form-row.styles.scss b/client/src/components/layout-form-row/layout-form-row.styles.scss index faf24ccf1..4cedb9b79 100644 --- a/client/src/components/layout-form-row/layout-form-row.styles.scss +++ b/client/src/components/layout-form-row/layout-form-row.styles.scss @@ -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%; + } }