Field groups: tighter style + apply to report
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.
This commit is contained in:
+93
-10
@@ -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) => (
|
||||
<Fragment key={f.name}>
|
||||
|
||||
// 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<string, { id: string; label?: string }>();
|
||||
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<string>();
|
||||
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(
|
||||
<FieldHistoryGroup
|
||||
key={`group-${grp.id}`}
|
||||
label={grp.label}
|
||||
fields={groupedFields}
|
||||
history={history}
|
||||
options={options}
|
||||
/>,
|
||||
);
|
||||
} else if (!grp) {
|
||||
out.push(
|
||||
<FieldHistoryRow
|
||||
key={f.name}
|
||||
field={f}
|
||||
entries={history[f.name] ?? []}
|
||||
options={options}
|
||||
/>
|
||||
{i === chartIdx && chartIdx >= 0 && (
|
||||
<div className="bg-paper-2/20 px-5 py-5 sm:px-7 sm:py-6">
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if (i === chartIdx && chartIdx >= 0) {
|
||||
out.push(
|
||||
<div key={`chart-${f.name}`} className="bg-paper-2/20 px-5 py-5 sm:px-7 sm:py-6">
|
||||
<MembershipChart
|
||||
membersField={membersFieldInSection}
|
||||
goalField={goalFieldInSection}
|
||||
membersHistory={history[MEMBERS_ACTUAL_NAME]}
|
||||
goalHistory={history[MEMBERS_GOAL_NAME]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
));
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string, FieldHistoryEntry[]>;
|
||||
options: Record<number, SelectOption[]>;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-l-2 border-leaf-300/60">
|
||||
{label && (
|
||||
<p className="px-5 pt-3 pb-1 pl-7 text-[10px] uppercase tracking-[0.1em] font-medium text-ink-soft sm:px-7 sm:pl-9">
|
||||
{label}
|
||||
</p>
|
||||
)}
|
||||
<div className="divide-y divide-rule-soft">
|
||||
{fields.map((f) => (
|
||||
<FieldHistoryRow
|
||||
key={f.name}
|
||||
field={f}
|
||||
entries={history[f.name] ?? []}
|
||||
options={options}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StageRankMark({ rank, isCurrent }: { rank: number; isCurrent: boolean }) {
|
||||
return (
|
||||
<span aria-hidden className="relative inline-flex h-12 w-12 flex-shrink-0 items-center justify-center">
|
||||
|
||||
@@ -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 (
|
||||
<div className="rounded-md border border-rule-soft bg-paper-2/30 px-4 py-4 sm:px-5 sm:py-5">
|
||||
<div className="border-l-2 border-leaf-300/60 pl-3 sm:pl-4">
|
||||
{label && (
|
||||
<h3 className="font-display text-sm font-medium uppercase tracking-[0.08em] text-ink-soft">
|
||||
<h3 className="font-display text-[11px] font-medium uppercase tracking-[0.1em] text-ink-soft">
|
||||
{label}
|
||||
</h3>
|
||||
)}
|
||||
{intro && (
|
||||
<p className={"max-w-prose text-xs leading-relaxed text-ink-mute " + (label ? "mt-1" : "")}>
|
||||
<p
|
||||
className={
|
||||
"max-w-prose text-xs leading-relaxed text-ink-mute " +
|
||||
(label ? "mt-0.5" : "")
|
||||
}
|
||||
>
|
||||
{intro}
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 gap-x-7 gap-y-5 md:grid-cols-2 " +
|
||||
(label || intro ? "mt-3" : "")
|
||||
(label || intro ? "mt-2" : "")
|
||||
}
|
||||
>
|
||||
{fields.map((f) => {
|
||||
|
||||
Reference in New Issue
Block a user