From 9c01fd235f519aa9538ab120df081adce15fba94 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 22 Apr 2026 11:00:24 +0000 Subject: [PATCH] fix: native button toggles, scroll enabled, remove whileHover interference --- app/page.tsx | 8 +- components/cards/AirPurifierCard.tsx | 201 +++++++++++---------------- components/cards/LightCard.tsx | 170 ++++++++++------------ 3 files changed, 155 insertions(+), 224 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 35b3ec5..94ab9a3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -64,7 +64,7 @@ export default function Home() { return (
{/* Ambient orbs */} @@ -128,14 +128,13 @@ export default function Home() { {/* Content area — fills remaining space */} -
+
{/* ═══════════════ HOME TAB ═══════════════ */} {activeTab === "home" && ( {/* Свет Гостиная */} diff --git a/components/cards/AirPurifierCard.tsx b/components/cards/AirPurifierCard.tsx index a7f6a60..b46c3f3 100644 --- a/components/cards/AirPurifierCard.tsx +++ b/components/cards/AirPurifierCard.tsx @@ -18,165 +18,124 @@ const MODES = [ { id: "High", label: "Турбо", color: "#f59e0b" }, ]; -export default function AirPurifierCard({ - entityId, - state, - presetMode, - onUpdate, -}: Props) { - const isOn = state === "on"; +export default function AirPurifierCard({ entityId, state, presetMode, onUpdate }: Props) { + const [localOn, setLocalOn] = useState(state === "on"); const [currentMode, setCurrentMode] = useState(presetMode || "Auto"); const [pending, setPending] = useState(false); const handleToggle = useCallback(async () => { if (pending) return; + const newState = !localOn; + setLocalOn(newState); setPending(true); - await toggleFan(entityId, !isOn); - onUpdate(); - setPending(false); - }, [entityId, isOn, pending, onUpdate]); - - const handleMode = useCallback( - async (mode: string) => { - setCurrentMode(mode); - await setFanPreset(entityId, mode); + try { + await toggleFan(entityId, newState); onUpdate(); - }, - [entityId, onUpdate] - ); + } catch (e) { + setLocalOn(!newState); + } + setPending(false); + }, [entityId, localOn, pending, onUpdate]); + const handleMode = useCallback(async (mode: string) => { + setCurrentMode(mode); + await setFanPreset(entityId, mode); + onUpdate(); + }, [entityId, onUpdate]); + + const isOn = localOn; const activeMode = MODES.find((m) => m.id === currentMode) || MODES[0]; const accentColor = isOn ? activeMode.color : "rgba(255,255,255,0.3)"; return ( - {/* Top row: icon + name + toggle */} + {/* Top row */}
- {/* Animated icon */} -
- - + }}> + +
-
- Очиститель воздуха -
-
+
Очиститель
+
{isOn ? activeMode.label : "Выключен"}
- {/* Toggle */} - -
- + +
{/* Mode buttons */} - - {isOn && ( - - {MODES.map((mode) => { - const isActive = currentMode === mode.id; - return ( - handleMode(mode.id)} - className="flex-1 py-2.5 rounded-2xl text-sm font-semibold" - style={ - isActive - ? { - background: `${mode.color}22`, - border: `1.5px solid ${mode.color}60`, - color: mode.color, - boxShadow: `0 0 14px ${mode.color}30`, - } - : { - background: "rgba(255,255,255,0.05)", - border: "1px solid rgba(255,255,255,0.08)", - color: "var(--text-secondary)", - } - } - whileTap={{ scale: 0.88 }} - > - {mode.label} - - ); - })} - - )} - - - {/* Offline state bottom fill */} - {!isOn && ( -
- {MODES.map((mode) => ( -
+ {MODES.map((mode) => { + const isActive = currentMode === mode.id && isOn; + return ( +
- ))} -
- )} + + ); + })} +
); } diff --git a/components/cards/LightCard.tsx b/components/cards/LightCard.tsx index ce3d387..73fe924 100644 --- a/components/cards/LightCard.tsx +++ b/components/cards/LightCard.tsx @@ -15,142 +15,116 @@ interface Props { onUpdate: () => void; } -export default function LightCard({ - entityId, - name, - state, - brightness, - showSlider = false, - onUpdate, -}: Props) { - const isOn = state === "on"; +export default function LightCard({ entityId, name, state, brightness, showSlider = false, onUpdate }: Props) { + // Optimistic local state — не зависим от HA mock + const [localOn, setLocalOn] = useState(state === "on"); const brightPct = getBrightnessPct(brightness); const [localBrightness, setLocalBrightness] = useState(brightPct || 70); const [pending, setPending] = useState(false); const handleToggle = useCallback(async () => { if (pending) return; + const newState = !localOn; + setLocalOn(newState); // optimistic setPending(true); - await toggleLight(entityId, !isOn); - onUpdate(); - setPending(false); - }, [entityId, isOn, pending, onUpdate]); - - const handleBrightnessChange = useCallback( - async (val: number) => { - setLocalBrightness(val); - await setLightBrightness(entityId, pctToBrightness(val)); + try { + await toggleLight(entityId, newState); onUpdate(); - }, - [entityId, onUpdate] - ); + } catch (e) { + setLocalOn(!newState); // rollback on real error + } + setPending(false); + }, [entityId, localOn, pending, onUpdate]); + + const handleBrightnessChange = useCallback(async (val: number) => { + setLocalBrightness(val); + await setLightBrightness(entityId, pctToBrightness(val)); + onUpdate(); + }, [entityId, onUpdate]); + + const isOn = localOn; return ( {/* Top row: icon + toggle */}
- {/* Icon */} - + - {/* Toggle */} - -
- + +
{/* Name + state */} -
-
+
+
{name}
-
+
{isOn ? (showSlider ? `Яркость ${localBrightness}%` : "Включён") : "Выключен"}
- {/* Brightness slider */} {showSlider && isOn && ( - -
-
- setLocalBrightness(parseInt(e.target.value))} - onMouseUp={(e) => - handleBrightnessChange(parseInt((e.target as HTMLInputElement).value)) - } - onTouchEnd={(e) => - handleBrightnessChange(parseInt((e.target as HTMLInputElement).value)) - } - className="w-full relative z-10" - style={{ background: "transparent" }} - /> -
+ + setLocalBrightness(parseInt(e.target.value))} + onMouseUp={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))} + onTouchEnd={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))} + className="w-full" + style={{ accentColor: "#f59e0b" }} + /> )}