This commit is contained in:
Andras Schmelczer 2026-09-11 16:09:28 +01:00
commit d8f2407ddf
44 changed files with 10377 additions and 0 deletions

View file

@ -0,0 +1,178 @@
import { Plus, Trash2, ArrowRight, ArrowDown } from 'lucide-react';
import type { Project, Room, FlooringSettings, SkirtingSettings } from '../lib/types';
import { NumberField } from './Fields';
interface Props {
project: Project;
room: Room;
changeProject: (fn: (p: Project) => Project) => 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, changeProject, changeRoom }: Props) {
const f = project.flooring;
const s = project.skirting;
const floor = (key: keyof FlooringSettings, value: number | string) =>
changeProject((p) => ({ ...p, flooring: { ...p.flooring, [key]: value } }));
const skirt = (key: keyof SkirtingSettings, value: number | number[]) =>
changeProject((p) => ({ ...p, skirting: { ...p.skirting, [key]: value } }));
return (
<>
<div className="editor-heading">
<h2>Materials</h2>
<span>Shared across rooms · mm</span>
</div>
<section className="material-section flooring" aria-labelledby="flooring-materials">
<h3 id="flooring-materials" className="material-heading">
Floorboards
</h3>
<div className="field-grid">
<NumberField
label="Board length"
value={f.length}
min={1}
onChange={(n) => floor('length', n)}
/>
<NumberField
label="Board width"
value={f.width}
min={1}
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 === 'brick' ? 'active' : ''}
aria-pressed={f.pattern === 'brick'}
onClick={() => floor('pattern', 'brick')}
>
Brick
</button>
<button
className={f.pattern === 'random' ? 'active' : ''}
aria-pressed={f.pattern === 'random'}
onClick={() => floor('pattern', 'random')}
>
Random
</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>
<div className="field-grid">
<NumberField label="Wall spacer" value={f.gap} onChange={(n) => floor('gap', n)} />
<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}
min={1}
onChange={(n) => floor('minLength', n)}
/>
<NumberField
label="Narrowest edge row"
value={f.minWidth}
min={1}
onChange={(n) => floor('minWidth', n)}
/>
</div>
</section>
<section className="material-section skirting" aria-labelledby="skirting-materials">
<h3 id="skirting-materials" className="material-heading">
Skirting boards
</h3>
{s.stockLengths.map((length, i) => (
<div className="stock-input" key={i}>
<NumberField
label={`Stock length ${i + 1}`}
value={length}
min={1}
onChange={(n) =>
skirt(
'stockLengths',
s.stockLengths.map((v, j) => (i === j ? n : v)),
)
}
/>
<button
className="icon-button"
aria-label={`Remove stock length ${i + 1}`}
disabled={s.stockLengths.length === 1}
onClick={() =>
skirt(
'stockLengths',
s.stockLengths.filter((_, j) => i !== j),
)
}
>
<Trash2 size={16} />
</button>
</div>
))}
<button
className="text-button"
onClick={() => skirt('stockLengths', [...s.stockLengths, 3600])}
disabled={s.stockLengths.length >= 10}
>
<Plus size={16} /> Add a stock length
</button>
<div className="field-grid">
<NumberField
label="Skirting thickness"
value={s.thickness}
onChange={(n) => skirt('thickness', n)}
/>
<NumberField
label="Minimum piece"
value={s.minLength}
min={1}
onChange={(n) => skirt('minLength', n)}
/>
<NumberField
label="Skirting saw kerf"
value={s.kerf}
onChange={(n) => skirt('kerf', n)}
tooltip={kerfHelp}
/>
</div>
</section>
</>
);
}