floor-planner/src/components/MaterialsEditor.tsx
Andras Schmelczer 8bf3e3b705
Some checks failed
Deploy to Pages / build (push) Failing after 10m45s
Simplify
2026-09-11 23:27:29 +01:00

200 lines
7.4 KiB
TypeScript

import { ArrowRight, ArrowDown } from 'lucide-react';
import type { Project, Room, FlooringSettings, SkirtingSettings } from '../lib/types';
import { NumberField } from './Fields';
import type { FlooringOptions } from '../lib/flooring';
import { mm } from '../lib/format';
interface Props {
project: Project;
room: Room;
flooringOptions: FlooringOptions;
setFlooring: (patch: Partial<FlooringSettings>) => void;
setSkirting: (patch: Partial<SkirtingSettings>) => void;
changeRoom: (fn: (r: Room) => Room) => void;
}
const kerfHelp =
'Width of material removed by the saw blade. A 3 mm blade turns a 1,000 mm board into 600 mm + 397 mm after one cut.';
export function MaterialsEditor({
project,
room,
flooringOptions,
setFlooring,
setSkirting,
changeRoom,
}: Props) {
const f = project.flooring;
const s = project.skirting;
const floor = <K extends keyof FlooringSettings>(key: K, value: FlooringSettings[K]) =>
setFlooring({ [key]: value });
const skirt = (key: keyof SkirtingSettings, value: number) => setSkirting({ [key]: value });
return (
<>
<div className="editor-heading">
<h2>Materials</h2>
<span>Shared across rooms</span>
</div>
<section className="material-section flooring" aria-labelledby="flooring-materials">
<h3 id="flooring-materials" className="material-heading">
Floorboards
</h3>
<p className="helper material-help">
Use the length and width printed on your flooring pack.
</p>
<div className="field-grid">
<NumberField label="Board length" value={f.length} onChange={(n) => floor('length', n)} />
<NumberField label="Board width" value={f.width} onChange={(n) => floor('width', n)} />
</div>
<div className="field">
<span>Laying pattern</span>
<div className="segmented" role="group" aria-label="Laying pattern">
<button
className={f.pattern === 'random' ? 'active' : ''}
aria-pressed={f.pattern === 'random'}
onClick={() => floor('pattern', 'random')}
>
Random
</button>
<button
className={f.pattern === 'brick' ? 'active' : ''}
aria-pressed={f.pattern === 'brick'}
onClick={() => floor('pattern', 'brick')}
>
Brick
</button>
</div>
<p className="helper">
{f.pattern === 'random'
? 'Maximise randomness spreads out joints to avoid H shapes. Minimise cuts can repeat joint positions.'
: 'A regular pattern with each row staggered by half a board.'}
</p>
</div>
{f.pattern === 'random' && (
<div className="field">
<span>What matters most? · all rooms</span>
<div className="optimisation-options" role="group" aria-label="Flooring optimisation">
{(['cuts', 'random'] as const).map((priority) => {
const plan = flooringOptions[priority];
const valid = !plan.issues.some((issue) => issue.severity === 'error');
const selected = f.optimisation === priority;
return (
<button
key={priority}
type="button"
className={selected ? 'active' : ''}
aria-pressed={selected}
onClick={() => floor('optimisation', priority)}
>
<span>{priority === 'cuts' ? 'Minimise cuts' : 'Maximise randomness'}</span>
<small>
{valid
? `${plan.cuts} cuts · ${mm(plan.wastePercent)}% waste`
: 'Check measurements'}
</small>
</button>
);
})}
</div>
</div>
)}
<div className="field">
<span>Board direction · {room.name}</span>
<div className="segmented" role="group" aria-label="Board direction">
<button
className={room.direction === 0 ? 'active' : ''}
aria-pressed={room.direction === 0}
onClick={() => changeRoom((r) => ({ ...r, direction: 0 }))}
>
<ArrowRight size={16} aria-hidden="true" /> Along first wall
</button>
<button
className={room.direction === 90 ? 'active' : ''}
aria-pressed={room.direction === 90}
onClick={() => changeRoom((r) => ({ ...r, direction: 90 }))}
>
<ArrowDown size={16} aria-hidden="true" /> Across first wall
</button>
</div>
</div>
<NumberField
label="Wall spacer"
value={f.gap}
onChange={(n) => floor('gap', n)}
hint="Expansion gap between the floorboards and walls. Check your flooring instructions."
/>
<details className="advanced-settings">
<summary>Advanced flooring settings</summary>
<p className="helper material-help">
Adjust the saw blade width, joint spacing and smallest pieces to suit your flooring.
</p>
<div className="field-grid">
<NumberField
label="Saw kerf"
value={f.kerf}
onChange={(n) => floor('kerf', n)}
tooltip={kerfHelp}
/>
<NumberField
label="Minimum joint offset"
value={f.minOffset}
onChange={(n) => floor('minOffset', n)}
/>
<NumberField
label="Maximum joint offset"
value={f.maxOffset}
onChange={(n) => floor('maxOffset', n)}
/>
<NumberField
label="Shortest end piece"
value={f.minLength}
onChange={(n) => floor('minLength', n)}
/>
<NumberField
label="Narrowest edge row"
value={f.minWidth}
onChange={(n) => floor('minWidth', n)}
/>
</div>
</details>
</section>
<section className="material-section skirting" aria-labelledby="skirting-materials">
<h3 id="skirting-materials" className="material-heading">
Skirting boards
</h3>
<p className="helper material-help">
Enter one purchased board length. Door openings are left clear automatically.
</p>
<div className="field-grid">
<NumberField
label="Skirting board length"
value={s.stockLength}
onChange={(n) => skirt('stockLength', n)}
/>
<NumberField
label="Skirting thickness"
value={s.thickness}
onChange={(n) => skirt('thickness', n)}
/>
</div>
<details className="advanced-settings">
<summary>Advanced skirting settings</summary>
<div className="field-grid">
<NumberField
label="Minimum piece"
value={s.minLength}
onChange={(n) => skirt('minLength', n)}
/>
<NumberField
label="Skirting saw kerf"
value={s.kerf}
onChange={(n) => skirt('kerf', n)}
tooltip={kerfHelp}
/>
</div>
</details>
</section>
</>
);
}