Implement Smart Timers, Power & Water Schedule Dashboard, and tighten UI layout spacing
This commit is contained in:
@@ -9,6 +9,22 @@ import { ColorSwatchRow } from './ColorPopover';
|
||||
const rad2deg = (r: number) => Math.round((r * 180) / Math.PI);
|
||||
const deg2rad = (d: number) => (d * Math.PI) / 180;
|
||||
|
||||
const ELECTRICAL_PARTS = new Set<string>([
|
||||
'pump',
|
||||
'airCompressor',
|
||||
'waterChiller',
|
||||
'growLight',
|
||||
'inlineFan',
|
||||
'circulationFan',
|
||||
]);
|
||||
|
||||
function formatDurationHelper(mins: number) {
|
||||
if (mins < 60) return `${mins}m`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
const remaining = mins % 60;
|
||||
return remaining > 0 ? `${hrs}h ${remaining}m` : `${hrs}h`;
|
||||
}
|
||||
|
||||
/**
|
||||
* PropertiesPanel — right sidebar inspector.
|
||||
* One part selected: full inspector (pump quick-settings first for pumps).
|
||||
@@ -105,7 +121,7 @@ function MultiInspector({ ids }: { ids: string[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto px-4 py-3">
|
||||
<div className="flex-1 space-y-3 overflow-y-auto px-3.5 py-2.5">
|
||||
<Section title="Selection">
|
||||
<ul className="space-y-1">
|
||||
{[...counts.entries()].map(([label, n]) => (
|
||||
@@ -128,14 +144,14 @@ function MultiInspector({ ids }: { ids: string[] }) {
|
||||
<button
|
||||
onClick={() => duplicateParts(ids)}
|
||||
title="Duplicate the whole selection (D)"
|
||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-xs font-medium text-zinc-200 hover:bg-zinc-700"
|
||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1 text-[11px] font-medium text-zinc-200 hover:bg-zinc-700"
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeParts(ids)}
|
||||
title="Delete every selected part (⌫)"
|
||||
className="flex-1 rounded-md border border-rose-900 bg-rose-950 px-2 py-1.5 text-xs font-medium text-rose-300 hover:bg-rose-900"
|
||||
className="flex-1 rounded-md border border-rose-900 bg-rose-950 px-2 py-1 text-[11px] font-medium text-rose-300 hover:bg-rose-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -709,7 +725,7 @@ function PartInspector({ partId }: { partId: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto px-4 py-3">
|
||||
<div className="flex-1 space-y-3 overflow-y-auto px-3.5 py-2.5">
|
||||
<div>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span
|
||||
@@ -814,7 +830,7 @@ function PartInspector({ partId }: { partId: string }) {
|
||||
<select
|
||||
value={String(snapPipeSize(part.params[key] ?? def.defaults[key]))}
|
||||
onChange={(e) => setParam(partId, key, parseFloat(e.target.value))}
|
||||
className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-2 py-1.5 text-[11px] text-zinc-200 outline-none focus:border-sky-600"
|
||||
className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-2 py-1 text-[11px] text-zinc-200 outline-none focus:border-sky-600"
|
||||
>
|
||||
{STANDARD_PIPE_SIZES.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
@@ -839,6 +855,65 @@ function PartInspector({ partId }: { partId: string }) {
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{ELECTRICAL_PARTS.has(part.type) && (
|
||||
<Section title="Smart Timer Schedule">
|
||||
<div className="space-y-2 rounded-lg border border-zinc-800 bg-zinc-900/20 p-2.5">
|
||||
<label className="flex items-center justify-between text-[11px] cursor-pointer">
|
||||
<span className="text-zinc-400 font-semibold">Enable Timer</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(part.params.timerEnabled ?? 0) === 1}
|
||||
onChange={(e) => setParam(partId, 'timerEnabled', e.target.checked ? 1 : 0)}
|
||||
className="h-3.5 w-3.5 accent-sky-500 rounded border-zinc-800 bg-zinc-900 cursor-pointer"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{(part.params.timerEnabled ?? 0) === 1 && (
|
||||
<div className="grid grid-cols-2 gap-2 pt-1 border-t border-zinc-900/50">
|
||||
<div>
|
||||
<div className="mb-1 text-[9px] font-bold text-zinc-500 uppercase">ON Duration</div>
|
||||
<label className="flex items-center gap-1 rounded-md border border-zinc-800 bg-zinc-900 px-1.5 py-0.5">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={part.params.timerOnMin ?? 15}
|
||||
onChange={(e) => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (!Number.isNaN(v) && v > 0) setParam(partId, 'timerOnMin', v);
|
||||
}}
|
||||
className="w-full bg-transparent text-left font-mono text-[11px] text-zinc-200 outline-none"
|
||||
/>
|
||||
<span className="text-[10px] text-zinc-500">min</span>
|
||||
</label>
|
||||
<span className="text-[9px] text-zinc-500 block mt-0.5">
|
||||
{formatDurationHelper(part.params.timerOnMin ?? 15)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-[9px] font-bold text-zinc-500 uppercase">OFF Duration</div>
|
||||
<label className="flex items-center gap-1 rounded-md border border-zinc-800 bg-zinc-900 px-1.5 py-0.5">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={part.params.timerOffMin ?? 45}
|
||||
onChange={(e) => {
|
||||
const v = parseInt(e.target.value, 10);
|
||||
if (!Number.isNaN(v) && v > 0) setParam(partId, 'timerOffMin', v);
|
||||
}}
|
||||
className="w-full bg-transparent text-left font-mono text-[11px] text-zinc-200 outline-none"
|
||||
/>
|
||||
<span className="text-[10px] text-zinc-500">min</span>
|
||||
</label>
|
||||
<span className="text-[9px] text-zinc-500 block mt-0.5">
|
||||
{formatDurationHelper(part.params.timerOffMin ?? 45)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section title="Color">
|
||||
<ColorSwatchRow ids={[partId]} />
|
||||
</Section>
|
||||
@@ -847,14 +922,14 @@ function PartInspector({ partId }: { partId: string }) {
|
||||
<button
|
||||
onClick={() => duplicateParts([partId])}
|
||||
title="Duplicate this part (D)"
|
||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-xs font-medium text-zinc-200 hover:bg-zinc-700"
|
||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1 text-[11px] font-medium text-zinc-200 hover:bg-zinc-700"
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeParts([partId])}
|
||||
title="Delete this part (⌫)"
|
||||
className="flex-1 rounded-md border border-rose-900 bg-rose-950 px-2 py-1.5 text-xs font-medium text-rose-300 hover:bg-rose-900"
|
||||
className="flex-1 rounded-md border border-rose-900 bg-rose-950 px-2 py-1 text-[11px] font-medium text-rose-300 hover:bg-rose-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -896,7 +971,7 @@ function NumberField({
|
||||
const v = parseFloat(e.target.value);
|
||||
if (!Number.isNaN(v)) onChange(v);
|
||||
}}
|
||||
className="w-full bg-transparent py-1.5 text-right font-mono text-[11px] text-zinc-200 outline-none"
|
||||
className="w-full bg-transparent py-1 text-right font-mono text-[11px] text-zinc-200 outline-none"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
@@ -906,7 +981,7 @@ function MiniBtn({ children, onClick }: { children: React.ReactNode; onClick: ()
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex-1 rounded border border-zinc-800 bg-zinc-900 px-1.5 py-1 text-[10px] text-zinc-300 hover:bg-zinc-800"
|
||||
className="flex-1 rounded border border-zinc-800 bg-zinc-900 px-1 py-0.5 text-[10px] text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user