redesign: glassmorphism UI with big cards, 3-col layout, ambient orbs
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Lightbulb } from "lucide-react";
|
||||
import { toggleLight, setLightBrightness } from "@/lib/api";
|
||||
import { getBrightnessPct, pctToBrightness } from "@/lib/ha";
|
||||
@@ -47,50 +47,42 @@ export default function LightCard({
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="glass-card p-5 h-full flex flex-col justify-between"
|
||||
className="glass-card p-6 h-full flex flex-col justify-between"
|
||||
style={
|
||||
isOn
|
||||
? {
|
||||
background: "rgba(245,158,11,0.08)",
|
||||
border: "1px solid rgba(245,158,11,0.2)",
|
||||
boxShadow: "0 0 30px rgba(245,158,11,0.1)",
|
||||
background: "rgba(245,158,11,0.07)",
|
||||
border: "1px solid rgba(245,158,11,0.22)",
|
||||
boxShadow: "0 0 40px rgba(245,158,11,0.08), inset 0 1px 0 rgba(245,158,11,0.1)",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35 }}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
>
|
||||
{/* Top row: icon + toggle */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
|
||||
style={{
|
||||
background: isOn
|
||||
? "rgba(245,158,11,0.2)"
|
||||
: "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<Lightbulb
|
||||
size={20}
|
||||
color={isOn ? "#f59e0b" : "var(--text-secondary)"}
|
||||
fill={isOn ? "#f59e0b" : "none"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: isOn ? "#f59e0b" : "var(--text-secondary)" }}
|
||||
>
|
||||
{isOn ? (showSlider ? `${localBrightness}%` : "Включён") : "Выключен"}
|
||||
</div>
|
||||
</div>
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="w-16 h-16 rounded-2xl flex items-center justify-center"
|
||||
style={{
|
||||
background: isOn
|
||||
? "rgba(245,158,11,0.18)"
|
||||
: "rgba(255,255,255,0.06)",
|
||||
boxShadow: isOn ? "0 0 28px rgba(245,158,11,0.35)" : "none",
|
||||
}}
|
||||
animate={{ scale: isOn ? 1 : 0.97 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Lightbulb
|
||||
size={32}
|
||||
color={isOn ? "#f59e0b" : "rgba(255,255,255,0.35)"}
|
||||
fill={isOn ? "#f59e0b" : "none"}
|
||||
strokeWidth={isOn ? 1.5 : 2}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Toggle */}
|
||||
<motion.div
|
||||
@@ -99,52 +91,70 @@ export default function LightCard({
|
||||
background: isOn
|
||||
? "#f59e0b"
|
||||
: "rgba(255,255,255,0.1)",
|
||||
boxShadow: isOn ? "0 0 16px rgba(245,158,11,0.5)" : "none",
|
||||
}}
|
||||
onClick={handleToggle}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
whileTap={{ scale: 0.88 }}
|
||||
>
|
||||
<div className="toggle-thumb" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{showSlider && isOn && (
|
||||
<motion.div
|
||||
className="mt-4"
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
{/* Name + state */}
|
||||
<div className="mt-auto">
|
||||
<div
|
||||
className="text-xl font-bold leading-tight"
|
||||
style={{ color: isOn ? "#f59e0b" : "var(--text-primary)" }}
|
||||
>
|
||||
<div
|
||||
className="text-xs mb-2 flex justify-between"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
<span>Яркость</span>
|
||||
<span>{localBrightness}%</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-l-full pointer-events-none"
|
||||
style={{
|
||||
width: `${localBrightness}%`,
|
||||
background: "linear-gradient(90deg, rgba(245,158,11,0.4), rgba(245,158,11,0.8))",
|
||||
height: "6px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min={5}
|
||||
max={100}
|
||||
value={localBrightness}
|
||||
onChange={(e) => 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" }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm mt-1 font-medium"
|
||||
style={{ color: isOn ? "rgba(245,158,11,0.7)" : "var(--text-secondary)" }}
|
||||
>
|
||||
{isOn ? (showSlider ? `Яркость ${localBrightness}%` : "Включён") : "Выключен"}
|
||||
</div>
|
||||
|
||||
{/* Brightness slider */}
|
||||
<AnimatePresence>
|
||||
{showSlider && isOn && (
|
||||
<motion.div
|
||||
className="mt-4"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<div className="relative pt-1">
|
||||
<div
|
||||
className="absolute left-0 rounded-full pointer-events-none"
|
||||
style={{
|
||||
width: `${localBrightness}%`,
|
||||
height: "6px",
|
||||
top: "calc(50% + 4px)",
|
||||
background: "linear-gradient(90deg, rgba(245,158,11,0.5), rgba(245,158,11,0.9))",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min={5}
|
||||
max={100}
|
||||
value={localBrightness}
|
||||
onChange={(e) => 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" }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user