From 9bc4bbb732c4a8963042f8713350109d8601d7b6 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 21 May 2026 16:51:00 -0700 Subject: [PATCH] Field groups: tighter style + apply to report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Form: dropped the boxed card treatment for FieldGroupCard in favor of a leaf-tinted left rule + small uppercase mini-label. Eats only ~14px of horizontal space (border + pl-3/sm:pl-4) instead of ~32-40px for the previous bg-tinted card with px-4/sm:px-5 on both sides, so the inner 2-col grid keeps more breathing room for the fields themselves. Report: same field-group concept now applies to ReportSection. Grouped FieldHistoryRows render together inside a leaf-tinted left rule with a small label above. Walk preserves the declared field order — a group is emitted at its first member's position; the other members are skipped when the loop later reaches them. Mixes cleanly with the existing MembershipChart inline insertion and the divide-y rhythm of standalone rows. --- components/ReportView.tsx | 111 +++++++++++++++++++++++++++++++----- components/StageSection.tsx | 20 ++++--- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/components/ReportView.tsx b/components/ReportView.tsx index 855b190..b9d99f2 100644 --- a/components/ReportView.tsx +++ b/components/ReportView.tsx @@ -1,6 +1,6 @@ "use client"; -import { Fragment, useEffect, useId, useMemo, useState } from "react"; +import { useEffect, useId, useMemo, useState } from "react"; import type { FieldConfig, FieldHistoryEntry, @@ -1006,25 +1006,70 @@ function ReportSection({ const chartIdx = Math.max(memberIdx, goalIdx); const membersFieldInSection = memberIdx >= 0 ? fields[memberIdx] : undefined; const goalFieldInSection = goalIdx >= 0 ? fields[goalIdx] : undefined; - return fields.map((f, i) => ( - - - {i === chartIdx && chartIdx >= 0 && ( -
+ + // Build a map from field name → its group config (if any). A + // field referenced in multiple groups belongs to the first one. + const groupByField = new Map(); + for (const g of section.fieldGroups ?? []) { + for (const fname of g.fields) { + if (!groupByField.has(fname)) { + groupByField.set(fname, { id: g.id, label: g.label }); + } + } + } + + // Walk fields in declared order; whenever we hit one that + // belongs to a group we haven't emitted yet, collect every + // visible field from that group (those with history) and emit + // them as one bordered cluster. Other group members get + // skipped when we encounter them later in the loop. + const fieldsWithHistorySet = new Set(fields.map((f) => f.name)); + const emittedGroups = new Set(); + const out: React.ReactNode[] = []; + + fields.forEach((f, i) => { + const grp = groupByField.get(f.name); + if (grp && !emittedGroups.has(grp.id)) { + emittedGroups.add(grp.id); + const sectionGroup = (section.fieldGroups ?? []).find((g) => g.id === grp.id)!; + const groupedFields = sectionGroup.fields + .map((name) => fields.find((ff) => ff.name === name)) + .filter((ff): ff is FieldConfig => !!ff && fieldsWithHistorySet.has(ff.name)); + if (groupedFields.length === 0) return; + out.push( + , + ); + } else if (!grp) { + out.push( + , + ); + } + if (i === chartIdx && chartIdx >= 0) { + out.push( +
-
- )} - - )); +
, + ); + } + }); + + return out; })()} @@ -1032,6 +1077,44 @@ function ReportSection({ ); } +/** + * Mirrors the form's FieldGroupCard: a leaf-tinted left rule + small + * uppercase mini-label, with the grouped history rows stacked beneath + * and separated by the same divide-y as the standalone rows. Eats only + * ~14px of horizontal space (vs ~40px for a fully-boxed treatment). + */ +function FieldHistoryGroup({ + label, + fields, + history, + options, +}: { + label?: string; + fields: FieldConfig[]; + history: Record; + options: Record; +}) { + return ( +
+ {label && ( +

+ {label} +

+ )} +
+ {fields.map((f) => ( + + ))} +
+
+ ); +} + function StageRankMark({ rank, isCurrent }: { rank: number; isCurrent: boolean }) { return ( diff --git a/components/StageSection.tsx b/components/StageSection.tsx index b0a0b59..72dae78 100644 --- a/components/StageSection.tsx +++ b/components/StageSection.tsx @@ -348,9 +348,10 @@ function LockedBanner() { /** * Visual cluster of related fields inside a section (e.g. "Market Study" - * grouping its date + upload fields). Renders as a quiet bordered card - * with an optional heading; the fields inside use the same two-column - * grid rules as the standalone per-field grid above/below. + * grouping its date + upload fields). Lightweight treatment: a leaf-tinted + * left rule and an optional uppercase mini-label. No boxed background or + * heavy padding — preserves the horizontal space the inner 2-col grid has + * to work with, while still signaling "these belong together." */ function FieldGroupCard({ label, @@ -372,21 +373,26 @@ function FieldGroupCard({ errors: FieldErrors; }) { return ( -
+
{label && ( -

+

{label}

)} {intro && ( -

+

{intro}

)}
{fields.map((f) => {