@metron-labs/quote-react 0.1.58 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -598,17 +598,8 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
598
598
  onCartSeeded();
599
599
  // eslint-disable-next-line react-hooks/exhaustive-deps
600
600
  }, [cartSeeded]);
601
- // The cart is authoritative once a row has been added. Rebuilding an existing row from
602
- // scheduleResult can overwrite contextual quantities resolved in the basket when the
603
- // user navigates back here. Only seed rows that are not already present.
604
- useEffect(() => {
605
- for (const row of rows) {
606
- const existing = cart[row.item.line_id];
607
- if (!existing && !row.unmatchable && row.best)
608
- onAddToCart(toCartItem(row));
609
- }
610
- // eslint-disable-next-line react-hooks/exhaustive-deps
611
- }, [scheduleResult]);
601
+ // After seeding, the cart is authoritative: coming back here never re-adds a line the user
602
+ // removed in the basket, nor rebuilds one and overwrites quantities resolved there.
612
603
  useEffect(() => {
613
604
  if (!selectedReviewId) {
614
605
  setCatalogueSearchResults([]);
@@ -5,7 +5,14 @@ const BRICK_COVERAGE = 60;
5
5
  const BLOCK_COVERAGE = 10;
6
6
  const BRICK_MORTAR_M3_PER_M2 = 0.02;
7
7
  const BLOCK_MORTAR_M3_PER_M2 = 0.012;
8
- const MORTAR_MIX_YIELD_M3 = 0.01;
8
+ // Mortar is mixed on site from general purpose cement and building sand, 1:4 by volume. Dry
9
+ // materials bulk up by about a third before water goes in, so each m³ of mortar takes 1.33 m³ of
10
+ // dry mix. Both are asked for by weight; the basket turns that into the merchant's bags.
11
+ const MORTAR_CEMENT_PARTS = 1;
12
+ const MORTAR_SAND_PARTS = 4;
13
+ const MORTAR_DRY_VOLUME_FACTOR = 1.33;
14
+ const CEMENT_KG_PER_M3 = 1440;
15
+ const SAND_KG_PER_M3 = 1600;
9
16
  const toNumber = (value) => Number.parseFloat(value) || 0;
10
17
  const formatNumber = (value, decimals = 2) => value.toFixed(decimals);
11
18
  export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
@@ -20,7 +27,9 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
20
27
  const baseUnits = areaM2 * coverage;
21
28
  const units = Math.ceil(baseUnits);
22
29
  const mortarM3 = areaM2 * (masonryType === "brick" ? BRICK_MORTAR_M3_PER_M2 : BLOCK_MORTAR_M3_PER_M2);
23
- const mortarBags = Math.ceil(mortarM3 / MORTAR_MIX_YIELD_M3);
30
+ const dryMixM3 = (mortarM3 * MORTAR_DRY_VOLUME_FACTOR) / (MORTAR_CEMENT_PARTS + MORTAR_SAND_PARTS);
31
+ const cementKg = Math.ceil(dryMixM3 * MORTAR_CEMENT_PARTS * CEMENT_KG_PER_M3);
32
+ const sandKg = Math.ceil(dryMixM3 * MORTAR_SAND_PARTS * SAND_KG_PER_M3);
24
33
  const displayedArea = unit === "metric" ? areaM2 : areaM2 * 10.7639;
25
34
  // Masonry requested as wall area - units per m² is a property of the brick or block.
26
35
  const requirements = [
@@ -34,7 +43,12 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
34
43
  query: masonryType === "brick" ? "facing brick" : "concrete block",
35
44
  category: "masonry",
36
45
  },
37
- ...(addMortar ? [{ key: "mortar", label: "Blue Circle Quality Assured Mortar Mix 20kg", qty: mortarBags, unit: "bag", query: "ready mixed mortar", category: "mortar" }] : []),
46
+ ...(addMortar
47
+ ? [
48
+ { key: "mortar-cement", label: "General purpose cement", qty: cementKg, unit: "kg", query: "general purpose cement", category: "cement" },
49
+ { key: "mortar-sand", label: "Building sand", qty: sandKg, unit: "kg", query: "building sand", category: "aggregate" },
50
+ ]
51
+ : []),
38
52
  ];
39
53
  const changeUnit = (nextUnit) => {
40
54
  if (nextUnit === unit)
@@ -44,12 +58,12 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
44
58
  setWidth((toNumber(width) * factor).toFixed(2));
45
59
  setUnit(nextUnit);
46
60
  };
47
- return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Brick and Block Calculator" }), _jsx("p", { children: "Estimate masonry units, sand and cement for a wall." })] }), _jsxs("div", { className: "truss-estimator__card brick-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Wall area and masonry type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Pick brick or block and enter the wall dimensions for unit counts plus ready-mixed mortar." }), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Masonry type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
61
+ return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Brick and Block Calculator" }), _jsx("p", { children: "Estimate masonry units, sand and cement for a wall." })] }), _jsxs("div", { className: "truss-estimator__card brick-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Wall area and masonry type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Pick brick or block and enter the wall dimensions for unit counts plus the cement and sand for the mortar." }), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Masonry type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
48
62
  ["brick", "Brick"],
49
63
  ["block", "Block"],
50
64
  ].map(([id, label]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${masonryType === id ? " is-selected" : ""}`, onClick: () => setMasonryType(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsxs("div", { className: "truss-estimator__option-copy", children: ["Approx. ", id === "brick" ? BRICK_COVERAGE : BLOCK_COVERAGE, " per m\u00B2"] })] }, id))) })] }), masonryType === "brick" && (_jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Brick type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: [
51
65
  ["common", "Common concrete", "General-purpose grey concrete brick"],
52
66
  ["engineering-red", "Engineering red", "Class B perforated, 65mm"],
53
67
  ["engineering-blue", "Engineering blue", "Class B solid, 65mm"],
54
- ].map(([id, label, blurb]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${brickOption === id ? " is-selected" : ""}`, onClick: () => setBrickOption(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id))) })] })), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "metric" ? "is-selected" : "", onClick: () => changeUnit("metric"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "imperial" ? "is-selected" : "", onClick: () => changeUnit("imperial"), children: "Imperial" })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Height (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: height, onChange: (event) => setHeight(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsx("div", { className: "brick-calculator__options", children: _jsxs("label", { className: "brick-calculator__checkbox", children: [_jsx("input", { type: "checkbox", checked: addMortar, onChange: (event) => setAddMortar(event.target.checked) }), _jsx("span", { children: "Include ready-mixed mortar" })] }) }), _jsxs("div", { className: "brick-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "brick-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Wall area" }), _jsxs("strong", { children: [formatNumber(displayedArea), " ", unit === "metric" ? "m²" : "ft²"] })] }), _jsxs("div", { children: [_jsx("span", { children: masonryType === "brick" ? "Bricks" : "Blocks" }), _jsx("strong", { children: units.toLocaleString("en-GB") })] }), addMortar ? (_jsxs("div", { children: [_jsx("span", { children: "Ready-mixed mortar" }), _jsxs("strong", { children: [mortarBags, " x 20kg bags"] })] })) : null] }), _jsx("p", { className: "brick-calculator__note", children: "Estimate based on standard nominal sizes and 10mm joints. Mortar bags are approximate; confirm the required mortar specification before ordering." })] }), _jsxs("div", { className: "brick-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.(requirements), disabled: areaM2 <= 0, children: submitLabel })] })] })] }) }));
68
+ ].map(([id, label, blurb]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${brickOption === id ? " is-selected" : ""}`, onClick: () => setBrickOption(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id))) })] })), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "metric" ? "is-selected" : "", onClick: () => changeUnit("metric"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "imperial" ? "is-selected" : "", onClick: () => changeUnit("imperial"), children: "Imperial" })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Height (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: height, onChange: (event) => setHeight(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsx("div", { className: "brick-calculator__options", children: _jsxs("label", { className: "brick-calculator__checkbox", children: [_jsx("input", { type: "checkbox", checked: addMortar, onChange: (event) => setAddMortar(event.target.checked) }), _jsx("span", { children: "Include mortar (cement and building sand, mixed 1:4)" })] }) }), _jsxs("div", { className: "brick-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "brick-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Wall area" }), _jsxs("strong", { children: [formatNumber(displayedArea), " ", unit === "metric" ? "m²" : "ft²"] })] }), _jsxs("div", { children: [_jsx("span", { children: masonryType === "brick" ? "Bricks" : "Blocks" }), _jsx("strong", { children: units.toLocaleString("en-GB") })] }), addMortar ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: "Cement" }), _jsxs("strong", { children: [cementKg, " kg"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Building sand" }), _jsxs("strong", { children: [sandKg, " kg"] })] })] })) : null] }), _jsx("p", { className: "brick-calculator__note", children: "Estimate based on standard nominal sizes and 10mm joints. Mortar is a general purpose 1:4 cement and sand mix; quantities are approximate, so confirm the specification before ordering." })] }), _jsxs("div", { className: "brick-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.(requirements), disabled: areaM2 <= 0, children: submitLabel })] })] })] }) }));
55
69
  }
@@ -159,7 +159,7 @@ export default function DeckingEstimator({ onBack, onReview, submitLabel = "Add
159
159
  const results = computeResults();
160
160
  const requirements = results
161
161
  ? [
162
- { key: `boards-${state.range ?? "meridian"}`, label: `Deck boards (${results.range.label})`, qty: results.boardCount, unit: "each", query: "timber decking board", category: "decking", calculatorWasteIncluded: true },
162
+ { key: `boards-${state.range ?? "meridian"}`, label: `Deck boards (${results.range.label})`, qty: Math.round(results.boardCount * results.range.boardLengthM * 100) / 100, unit: "m", query: "timber decking board", category: "decking", calculatorWasteIncluded: true },
163
163
  { key: "joists", label: "Treated C24 joists", qty: results.joistMetres, unit: "m", query: "treated joist", category: "timber", calculatorWasteIncluded: true },
164
164
  { key: "screws", label: "Green decking screws (200/box)", qty: results.screwBoxes, unit: "box", query: "green decking screws", category: "fixings", calculatorWasteIncluded: true },
165
165
  ]
@@ -7,7 +7,7 @@ import ForwardArrow from "../ForwardArrow";
7
7
  import { renderDisplayUnit, formatPriceBasis } from "../../app/components/UnitDisplay";
8
8
  import { useOptionalCart } from "../../lib/cart/CartContext";
9
9
  import { candidateToCartItem } from "../../lib/boq/toCartItem";
10
- import { fetchCatalogueStatus, resolveRequirements, searchCatalogue } from "../../lib/estimators/resolveProducts";
10
+ import { estimateRequestUnit, fetchCatalogueStatus, resolveRequirements, searchCatalogue } from "../../lib/estimators/resolveProducts";
11
11
  import { fmtCurrency, fmtQty } from "../../lib/formatters";
12
12
  import { productImageSrc } from "../../lib/productImage";
13
13
  import { getPricingBreakdown } from "../../util/pricing";
@@ -64,7 +64,7 @@ export default function EstimatorResults({ toolId, requirements, ready, onBack,
64
64
  const toCartLine = (row) => candidateToCartItem(row.chosen, {
65
65
  lineId: lineIdFor(row.requirement.key),
66
66
  description: row.requirement.label,
67
- unit: row.requirement.unit,
67
+ unit: estimateRequestUnit(row.requirement.unit, row.chosen),
68
68
  qty: row.requirement.qty,
69
69
  });
70
70
  // Requirements are rebuilt on every render; only their content should retrigger a lookup.
@@ -23,7 +23,7 @@ const CONFIG = {
23
23
  },
24
24
  heights: ["0.9", "1.2", "1.5", "1.8"],
25
25
  gate: {
26
- single: { label: "Single lap gate", widthDefault: 0.9 },
26
+ single: { label: "Single gate", widthDefault: 0.9 },
27
27
  },
28
28
  mounting: {
29
29
  concrete: { label: "Concrete posts", blurb: "Slotted concrete posts with matching panels." },
@@ -212,7 +212,8 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
212
212
  { key: `timber-post-${postLength}`, label: "Timber fence posts", qty: results.totalPostCount, unit: "each", query: "timber fence post", category: "fencing" },
213
213
  { key: "postcrete", label: "Postcrete", qty: results.bagsCount, unit: "bag", query: "postcrete", category: "cement" },
214
214
  ]),
215
- ...(gateLabel ? [{ key: "gate", label: gateLabel, qty: 1, unit: "each", query: "lap gate", category: "fencing" }] : []),
215
+ // The gate matches the panels: a lap gate in a lap fence, a feather edge gate in a feather edge one.
216
+ ...(gateLabel ? [{ key: `gate-${state.range ?? "boundary"}`, label: state.range === "skyline" ? "Single feather edge gate" : "Single lap gate", qty: 1, unit: "each", query: state.range === "skyline" ? "feather edge gate" : "lap gate", category: "fencing" }] : []),
216
217
  ...(results.gravelCount > 0
217
218
  ? [{ key: "gravel-boards", label: "Concrete gravel boards", qty: results.gravelCount, unit: "each", query: "gravel board", category: "fencing" }]
218
219
  : []),
@@ -1,16 +1,15 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- // byArea: ask for the area and let the matched product's own coverage turn it into packs, which
5
- // is right whichever client's product it is. Floor and wall still ask for packs worked out from
6
- // the coverage here.
7
- // ponytail: floor/wall stay on packs because FORT's catalogue coverage for its floor roll and wall
8
- // slab is wrong (0.06 and 0.546 m2); switch them to byArea once that data is fixed.
4
+ // The basket is asked for the area; the matched product's own coverage turns it into packs, which
5
+ // is right whichever merchant's product it is. coverage here only feeds the on-screen pack estimate.
9
6
  const APPLICATIONS = {
10
- floor: { label: "Floor", product: "Knauf Acoustic Roll", thickness: "100mm", coverage: 12.36, salesUnit: "roll" },
11
- wall: { label: "Cavity wall", product: "Celotex CW4100", thickness: "100mm", coverage: 3.24, salesUnit: "pack" },
12
- roof: { label: "Pitched roof", product: "Kingspan K107", thickness: "100mm", coverage: 8.64, salesUnit: "pack", byArea: true },
13
- loft: { label: "Loft", product: "Knauf Loft Roll 44", thickness: "100mm", coverage: 13.89, salesUnit: "roll", byArea: true },
7
+ // Between joists of an upstairs floor, where the job is sound; a ground floor needs heat kept in.
8
+ floor: { label: "Floor between rooms", product: "Knauf Acoustic Roll", thickness: "100mm", coverage: 12.36 },
9
+ ground: { label: "Ground floor", product: "Rigid PIR insulation board", thickness: "100mm", coverage: 2.88 },
10
+ wall: { label: "Cavity wall", product: "Celotex CW4100", thickness: "100mm", coverage: 3.24 },
11
+ roof: { label: "Pitched roof", product: "Kingspan K107", thickness: "100mm", coverage: 8.64 },
12
+ loft: { label: "Loft", product: "Knauf Loft Roll 44", thickness: "100mm", coverage: 13.89 },
14
13
  };
15
14
  export default function InsulationEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
16
15
  const [application, setApplication] = useState("floor");
@@ -23,12 +22,12 @@ export default function InsulationEstimator({ onBack, onReview, submitLabel = "A
23
22
  {
24
23
  key: `insulation-${application}`,
25
24
  label: `${selected.product} ${selected.thickness}`,
26
- qty: selected.byArea ? Math.round(area * 100) / 100 : packs,
27
- unit: selected.byArea ? "m2" : selected.salesUnit,
25
+ qty: Math.round(area * 100) / 100,
26
+ unit: "m2",
28
27
  query: selected.product,
29
28
  category: "insulation",
30
29
  },
31
30
  ];
32
31
  const applications = Object.entries(APPLICATIONS);
33
- return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Insulation Calculator" }), _jsx("p", { children: "Estimate insulation coverage and pack quantities for floors, walls, roofs and lofts." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Where and what you are insulating" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the application and product, and enter the area, for coverage and pack quantities." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Application" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: applications.map(([id, option]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${application === id ? " is-selected" : ""}`, onClick: () => setApplication(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsxs("div", { className: "truss-estimator__option-copy", children: [option.product, ", ", option.thickness] })] }, id)) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area" }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Insulated area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: selected.product }), _jsxs("strong", { children: [packs, " packs"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "This is a simple product choice, not thermal or Building Regulations advice. Confirm the specification before ordering." })] }), _jsxs("div", { className: "materials-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.(requirements), disabled: area <= 0, children: submitLabel })] })] })] }) }));
32
+ return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Insulation Calculator" }), _jsx("p", { children: "Estimate insulation coverage and pack quantities for floors, walls, roofs and lofts." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Where and what you are insulating" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the application and product, and enter the area, for coverage and pack quantities." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Application" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: applications.map(([id, option]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${application === id ? " is-selected" : ""}`, onClick: () => setApplication(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsxs("div", { className: "truss-estimator__option-copy", children: [option.product, ", ", option.thickness] })] }, id)) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area" }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Insulated area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: selected.product }), _jsxs("strong", { children: [packs, " packs"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "This is a simple product choice, not thermal or Building Regulations advice. Confirm the specification before ordering." })] }), _jsxs("div", { className: "materials-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.(requirements), disabled: area <= 0, children: submitLabel })] })] })] }) }));
34
33
  }