mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
feat(reporting): restyle scenario filter descriptions to match
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
Apply the new structured tooltip treatment to the scenario filters, so their descriptions read like the cost figures': a summary line plus a subtle footnote, in the same larger, rounded ⓘ card. - Extract the tooltip body into a shared `tipContent.tsx` (TipBody + TipSpec); costHelp now consumes it instead of its own copy. - FilterChips: the two toggle chips use the shared `InfoDot`; the "skip already-compliant" popover reuses the same structured copy. Filter help now lives in one `FILTER_HELP` map. Copy/present only — no filter behaviour changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8dd2036fa5
commit
32da5bf079
3 changed files with 88 additions and 81 deletions
|
|
@ -1,12 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { X, Info, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/app/shadcn_components/ui/tooltip";
|
||||
import { X, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
|
|
@ -20,6 +14,30 @@ import {
|
|||
SelectValue,
|
||||
} from "@/app/shadcn_components/ui/select";
|
||||
import { EPC_BANDS } from "@/lib/epc/bands";
|
||||
import { InfoDot } from "./primitives";
|
||||
import { TipBody, type TipSpec } from "./tipContent";
|
||||
|
||||
/** Structured "what this filter does" copy — same shape/look as the cost ⓘs. */
|
||||
const FILTER_HELP: Record<
|
||||
"reachTarget" | "lodgedBaseline" | "complianceWindow",
|
||||
TipSpec
|
||||
> = {
|
||||
reachTarget: {
|
||||
summary:
|
||||
"Counts only homes whose modelled result reaches this scenario's target band.",
|
||||
note: "Homes still short of target after the works drop out of every figure, so costs and outcomes cover the fully-compliant homes only.",
|
||||
},
|
||||
lodgedBaseline: {
|
||||
summary:
|
||||
"Decides which homes need work from their official lodged EPC band on the government register, not Domna's modelled current band.",
|
||||
note: "Use it to match what the register says a home needs.",
|
||||
},
|
||||
complianceWindow: {
|
||||
summary:
|
||||
"Leaves out homes whose official (lodged) EPC already meets the band below and stays valid past the date.",
|
||||
note: "The government counts these as compliant beyond then, so costs and outcomes show only the works on the remaining homes.",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Inline scenario filter chips (Screen B). State is always visible and
|
||||
|
|
@ -49,12 +67,12 @@ function ToggleChip({
|
|||
on,
|
||||
onToggle,
|
||||
label,
|
||||
help,
|
||||
tip,
|
||||
}: {
|
||||
on: boolean;
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
help: string;
|
||||
tip: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
|
|
@ -73,22 +91,7 @@ function ToggleChip({
|
|||
{label}
|
||||
{on && <X className="h-3 w-3 text-gray-600" />}
|
||||
</button>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`About: ${label}`}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[280px] text-xs leading-relaxed">
|
||||
{help}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<InfoDot label={label} tip={tip} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -118,7 +121,7 @@ export function FilterChips({
|
|||
onChange({ ...filters, hideNonCompliant: !filters.hideNonCompliant })
|
||||
}
|
||||
label="Only homes that reach target"
|
||||
help="Counts only the homes whose modelled result actually reaches this scenario's target band. Homes still short of target after the works drop out of every figure — so costs and outcomes cover the fully-compliant homes only."
|
||||
tip={<TipBody spec={FILTER_HELP.reachTarget} />}
|
||||
/>
|
||||
|
||||
{/* Compliance window — editable band + date */}
|
||||
|
|
@ -142,12 +145,9 @@ export function FilterChips({
|
|||
<div className="text-[0.82rem] font-semibold text-brandblue">
|
||||
Skip already-compliant homes
|
||||
</div>
|
||||
<p className="mt-1 text-[0.76rem] leading-relaxed text-gray-500">
|
||||
Leave out homes whose official (lodged) EPC already meets the band
|
||||
below and stays valid past the date — homes the government counts as
|
||||
compliant beyond then. The costs and outcomes then show only the
|
||||
works on the remaining homes.
|
||||
</p>
|
||||
<div className="mt-1.5 text-[0.78rem] leading-relaxed">
|
||||
<TipBody spec={FILTER_HELP.complianceWindow} />
|
||||
</div>
|
||||
|
||||
<label className="mt-3 flex items-center gap-2 text-[0.8rem] text-gray-700">
|
||||
<input
|
||||
|
|
@ -224,7 +224,7 @@ export function FilterChips({
|
|||
})
|
||||
}
|
||||
label="Judge need by lodged EPC"
|
||||
help="Decides which homes need work from their official lodged EPC band on the government register, rather than Domna's modelled current band. Use it to match what the register says a home needs."
|
||||
tip={<TipBody spec={FILTER_HELP.lodgedBaseline} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { Check, Minus } from "lucide-react";
|
||||
import { PROJECT_DELIVERY_RATE } from "@/lib/reporting/model";
|
||||
import { TipBody, type TipSpec } from "./tipContent";
|
||||
|
||||
const deliveryPct = Math.round(PROJECT_DELIVERY_RATE * 100);
|
||||
|
||||
|
|
@ -12,51 +12,6 @@ const deliveryPct = Math.round(PROJECT_DELIVERY_RATE * 100);
|
|||
* (CONTEXT.md → Reporting): Gross = construction works + project delivery +
|
||||
* contingency; Net = gross − secured funding.
|
||||
*/
|
||||
interface HelpSpec {
|
||||
summary: string;
|
||||
includes?: string[];
|
||||
excludes?: string[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
function Bullets({ items, kind }: { items: string[]; kind: "in" | "out" }) {
|
||||
const Icon = kind === "in" ? Check : Minus;
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[0.64rem] font-bold uppercase tracking-wide text-gray-400">
|
||||
{kind === "in" ? "Includes" : "Excludes"}
|
||||
</div>
|
||||
<ul className="mt-1 space-y-1">
|
||||
{items.map((it) => (
|
||||
<li key={it} className="flex items-start gap-1.5">
|
||||
<Icon
|
||||
className={`mt-[3px] h-3 w-3 flex-none ${
|
||||
kind === "in" ? "text-[#0c6b4a]" : "text-gray-400"
|
||||
}`}
|
||||
/>
|
||||
<span>{it}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HelpBody({ spec }: { spec: HelpSpec }) {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-medium text-gray-800">{spec.summary}</p>
|
||||
{spec.includes && <Bullets items={spec.includes} kind="in" />}
|
||||
{spec.excludes && <Bullets items={spec.excludes} kind="out" />}
|
||||
{spec.note && (
|
||||
<p className="border-t border-gray-100 pt-2 text-gray-500">
|
||||
{spec.note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GROSS_PARTS = ["Construction works", "Project delivery", "Contingency"];
|
||||
|
||||
const SPECS = {
|
||||
|
|
@ -112,12 +67,12 @@ const SPECS = {
|
|||
excludes: ["Project delivery", "Contingency"],
|
||||
note: 'Totals the "Construction works" ledger line, not gross cost.',
|
||||
},
|
||||
} satisfies Record<string, HelpSpec>;
|
||||
} satisfies Record<string, TipSpec>;
|
||||
|
||||
/** Rendered tooltip bodies, keyed by figure. `tip={COST_HELP.net}` etc. */
|
||||
export const COST_HELP = Object.fromEntries(
|
||||
Object.entries(SPECS).map(([key, spec]) => [
|
||||
key,
|
||||
<HelpBody key={key} spec={spec} />,
|
||||
<TipBody key={key} spec={spec} />,
|
||||
]),
|
||||
) as Record<keyof typeof SPECS, ReactNode>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { Check, Minus } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Shared shape + renderer for the reporting help tooltips (cost figures and
|
||||
* scenario filters alike): a one-line summary, optional scannable
|
||||
* Includes/Excludes lists, and an optional footnote — so every ⓘ across the
|
||||
* reporting view reads the same way.
|
||||
*/
|
||||
export interface TipSpec {
|
||||
summary: string;
|
||||
includes?: string[];
|
||||
excludes?: string[];
|
||||
note?: string;
|
||||
}
|
||||
|
||||
function Bullets({ items, kind }: { items: string[]; kind: "in" | "out" }) {
|
||||
const Icon = kind === "in" ? Check : Minus;
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[0.64rem] font-bold uppercase tracking-wide text-gray-400">
|
||||
{kind === "in" ? "Includes" : "Excludes"}
|
||||
</div>
|
||||
<ul className="mt-1 space-y-1">
|
||||
{items.map((it) => (
|
||||
<li key={it} className="flex items-start gap-1.5">
|
||||
<Icon
|
||||
className={`mt-[3px] h-3 w-3 flex-none ${
|
||||
kind === "in" ? "text-[#0c6b4a]" : "text-gray-400"
|
||||
}`}
|
||||
/>
|
||||
<span>{it}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TipBody({ spec }: { spec: TipSpec }) {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-medium text-gray-800">{spec.summary}</p>
|
||||
{spec.includes && <Bullets items={spec.includes} kind="in" />}
|
||||
{spec.excludes && <Bullets items={spec.excludes} kind="out" />}
|
||||
{spec.note && (
|
||||
<p className="border-t border-gray-100 pt-2 text-gray-500">
|
||||
{spec.note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue