Redesign
This commit is contained in:
parent
3f178f8795
commit
c6e6c5ca48
20 changed files with 664 additions and 277 deletions
|
|
@ -32,12 +32,12 @@ function tryPretty(text: string): string {
|
|||
|
||||
function methodBg(method: string): string {
|
||||
switch (method) {
|
||||
case "GET": return "#2563eb";
|
||||
case "POST": return "#16a34a";
|
||||
case "PUT": return "#d97706";
|
||||
case "GET": return "#428ce2";
|
||||
case "POST": return "#5dd776";
|
||||
case "PUT": return "#f59e0b";
|
||||
case "PATCH": return "#7c3aed";
|
||||
case "DELETE": return "#dc2626";
|
||||
default: return "#6b7280";
|
||||
case "DELETE": return "#ef4444";
|
||||
default: return "#7b899a";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +124,8 @@ export default function DevConsole({
|
|||
zIndex: 50,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "#ffffff",
|
||||
borderLeft: "1px solid #e5e7eb",
|
||||
background: "var(--color-primary)",
|
||||
borderLeft: "1px solid var(--color-secondary)",
|
||||
boxShadow: "-4px 0 24px rgba(0,0,0,0.06)",
|
||||
transform: open ? "translateX(0)" : "translateX(100%)",
|
||||
transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)",
|
||||
|
|
@ -158,17 +158,17 @@ export default function DevConsole({
|
|||
alignItems: "center",
|
||||
gap: "8px",
|
||||
padding: "10px 14px",
|
||||
borderBottom: "1px solid #e5e7eb",
|
||||
borderBottom: "1px solid var(--color-secondary)",
|
||||
flexShrink: 0,
|
||||
background: "#ffffff",
|
||||
background: "var(--color-primary)",
|
||||
}}>
|
||||
<LuTerminal size={13} style={{ color: "#9ca3af" }} />
|
||||
<LuTerminal size={13} style={{ color: "var(--color-foreground-sec)" }} />
|
||||
<span style={{
|
||||
fontSize: "0.65rem",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.1em",
|
||||
textTransform: "uppercase",
|
||||
color: "#9ca3af",
|
||||
color: "var(--color-foreground-sec)",
|
||||
}}>
|
||||
Dev Console
|
||||
</span>
|
||||
|
|
@ -185,8 +185,8 @@ export default function DevConsole({
|
|||
borderRadius: "6px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
background: activeTab === tab ? "#f3f4f6" : "transparent",
|
||||
color: activeTab === tab ? "#111827" : "#9ca3af",
|
||||
background: activeTab === tab ? "var(--color-secondary)" : "transparent",
|
||||
color: activeTab === tab ? "var(--color-foreground)" : "var(--color-foreground-sec)",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
|
|
@ -200,7 +200,7 @@ export default function DevConsole({
|
|||
background: "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "#9ca3af",
|
||||
color: "var(--color-foreground-sec)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "2px",
|
||||
|
|
@ -213,9 +213,9 @@ export default function DevConsole({
|
|||
|
||||
{/* Logs tab */}
|
||||
{activeTab === "logs" && (
|
||||
<div style={{ flex: 1, overflowY: "auto", fontFamily: "ui-monospace, 'Cascadia Code', monospace" }}>
|
||||
<div style={{ flex: 1, overflowY: "auto", fontFamily: "inherit" }}>
|
||||
{logs.length === 0 ? (
|
||||
<div style={{ padding: "32px 16px", textAlign: "center", color: "#d1d5db", fontSize: "0.75rem" }}>
|
||||
<div style={{ padding: "32px 16px", textAlign: "center", color: "var(--color-foreground-sec)", fontSize: "0.75rem" }}>
|
||||
No requests captured yet
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -234,20 +234,20 @@ export default function DevConsole({
|
|||
{/* Request tab */}
|
||||
{activeTab === "request" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<div style={{ padding: "12px", borderBottom: "1px solid #e5e7eb", flexShrink: 0 }}>
|
||||
<div style={{ padding: "12px", borderBottom: "1px solid var(--color-secondary)", flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", gap: "6px", marginBottom: "8px" }}>
|
||||
<select
|
||||
value={reqMethod}
|
||||
onChange={(e) => setReqMethod(e.target.value)}
|
||||
style={{
|
||||
background: "#f9fafb",
|
||||
border: "1px solid #e5e7eb",
|
||||
background: "var(--color-secondary)",
|
||||
border: "1px solid var(--color-secondary)",
|
||||
borderRadius: "8px",
|
||||
color: "#111827",
|
||||
color: "var(--color-foreground)",
|
||||
fontSize: "0.68rem",
|
||||
padding: "5px 6px",
|
||||
cursor: "pointer",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontFamily: "inherit",
|
||||
outline: "none",
|
||||
}}
|
||||
>
|
||||
|
|
@ -262,13 +262,13 @@ export default function DevConsole({
|
|||
placeholder="/api/power"
|
||||
style={{
|
||||
flex: 1,
|
||||
background: "#f9fafb",
|
||||
border: "1px solid #e5e7eb",
|
||||
background: "var(--color-secondary)",
|
||||
border: "1px solid var(--color-secondary)",
|
||||
borderRadius: "8px",
|
||||
color: "#111827",
|
||||
color: "var(--color-foreground)",
|
||||
fontSize: "0.68rem",
|
||||
padding: "5px 8px",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontFamily: "inherit",
|
||||
outline: "none",
|
||||
minWidth: 0,
|
||||
}}
|
||||
|
|
@ -277,7 +277,7 @@ export default function DevConsole({
|
|||
onClick={sendRequest}
|
||||
disabled={reqLoading}
|
||||
style={{
|
||||
background: "#2563eb",
|
||||
background: "#428ce2",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
color: "white",
|
||||
|
|
@ -305,13 +305,13 @@ export default function DevConsole({
|
|||
rows={4}
|
||||
style={{
|
||||
width: "100%",
|
||||
background: "#f9fafb",
|
||||
border: "1px solid #e5e7eb",
|
||||
background: "var(--color-secondary)",
|
||||
border: "1px solid var(--color-secondary)",
|
||||
borderRadius: "8px",
|
||||
color: "#111827",
|
||||
color: "var(--color-foreground)",
|
||||
fontSize: "0.68rem",
|
||||
padding: "6px 8px",
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
fontFamily: "inherit",
|
||||
resize: "vertical",
|
||||
outline: "none",
|
||||
boxSizing: "border-box",
|
||||
|
|
@ -320,11 +320,11 @@ export default function DevConsole({
|
|||
/>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: "0.6rem", color: "#d1d5db" }}>
|
||||
<div style={{ fontSize: "0.6rem", color: "var(--color-foreground-sec)" }}>
|
||||
Bearer token attached via cookie ·{" "}
|
||||
<span style={{ color: "#9ca3af" }}>use</span>{" "}
|
||||
<code style={{ color: "#6b7280" }}>http://localhost:3001/path</code>{" "}
|
||||
<span style={{ color: "#9ca3af" }}>to bypass Next.js proxy</span>
|
||||
<span style={{ color: "var(--color-foreground-sec)" }}>use</span>{" "}
|
||||
<code style={{ color: "var(--color-blue, #428ce2)" }}>http://localhost:3001/path</code>{" "}
|
||||
<span style={{ color: "var(--color-foreground-sec)" }}>to bypass Next.js proxy</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -332,11 +332,11 @@ export default function DevConsole({
|
|||
flex: 1,
|
||||
overflow: "auto",
|
||||
padding: "12px",
|
||||
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
||||
background: "#f9fafb",
|
||||
fontFamily: "inherit",
|
||||
background: "var(--color-secondary)",
|
||||
}}>
|
||||
{reqLoading && (
|
||||
<div style={{ fontSize: "0.7rem", color: "#9ca3af" }}>Sending…</div>
|
||||
<div style={{ fontSize: "0.7rem", color: "var(--color-foreground-sec)" }}>Sending…</div>
|
||||
)}
|
||||
{!reqLoading && reqResponse && (
|
||||
<>
|
||||
|
|
@ -344,13 +344,13 @@ export default function DevConsole({
|
|||
fontSize: "0.65rem",
|
||||
fontWeight: 600,
|
||||
marginBottom: "8px",
|
||||
color: reqResponse.status >= 200 && reqResponse.status < 300 ? "#16a34a" : "#dc2626",
|
||||
color: reqResponse.status >= 200 && reqResponse.status < 300 ? "#5dd776" : "#ef4444",
|
||||
}}>
|
||||
HTTP {reqResponse.status}
|
||||
</div>
|
||||
<pre style={{
|
||||
fontSize: "0.65rem",
|
||||
color: "#6b7280",
|
||||
color: "var(--color-foreground-sec)",
|
||||
margin: 0,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
|
|
@ -361,7 +361,7 @@ export default function DevConsole({
|
|||
</>
|
||||
)}
|
||||
{!reqLoading && !reqResponse && (
|
||||
<div style={{ fontSize: "0.7rem", color: "#d1d5db" }}>
|
||||
<div style={{ fontSize: "0.7rem", color: "var(--color-foreground-sec)" }}>
|
||||
Response will appear here
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -384,13 +384,13 @@ function LogRow({
|
|||
const [hovered, setHovered] = useState(false);
|
||||
const statusColor =
|
||||
entry.status === null
|
||||
? "#9ca3af"
|
||||
? "var(--color-foreground-sec)"
|
||||
: entry.status >= 200 && entry.status < 300
|
||||
? "#16a34a"
|
||||
: "#dc2626";
|
||||
? "#5dd776"
|
||||
: "#ef4444";
|
||||
|
||||
return (
|
||||
<div style={{ borderBottom: "1px solid #f3f4f6" }}>
|
||||
<div style={{ borderBottom: "1px solid var(--color-secondary)" }}>
|
||||
<div
|
||||
onClick={onToggle}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
|
|
@ -401,7 +401,7 @@ function LogRow({
|
|||
gap: "8px",
|
||||
padding: "6px 12px",
|
||||
cursor: "pointer",
|
||||
background: hovered ? "#f9fafb" : "transparent",
|
||||
background: hovered ? "var(--color-secondary)" : "transparent",
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
|
|
@ -430,7 +430,7 @@ function LogRow({
|
|||
</span>
|
||||
<span style={{
|
||||
fontSize: "0.68rem",
|
||||
color: "#6b7280",
|
||||
color: "var(--color-foreground-sec)",
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
|
|
@ -438,23 +438,23 @@ function LogRow({
|
|||
}}>
|
||||
{entry.path}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.6rem", color: "#9ca3af", flexShrink: 0 }}>
|
||||
<span style={{ fontSize: "0.6rem", color: "var(--color-foreground-sec)", flexShrink: 0 }}>
|
||||
{entry.duration !== null ? `${entry.duration}ms` : ""}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.6rem", color: "#d1d5db", flexShrink: 0 }}>
|
||||
<span style={{ fontSize: "0.6rem", color: "var(--color-foreground-sec)", flexShrink: 0, opacity: 0.6 }}>
|
||||
{entry.timestamp}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div style={{
|
||||
background: "#f9fafb",
|
||||
background: "var(--color-secondary)",
|
||||
padding: "8px 12px 10px",
|
||||
borderTop: "1px solid #e5e7eb",
|
||||
borderTop: "1px solid var(--color-secondary)",
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: "0.6rem",
|
||||
color: "#9ca3af",
|
||||
color: "var(--color-foreground-sec)",
|
||||
marginBottom: "6px",
|
||||
wordBreak: "break-all",
|
||||
}}>
|
||||
|
|
@ -463,7 +463,7 @@ function LogRow({
|
|||
{entry.response !== null && (
|
||||
<pre style={{
|
||||
fontSize: "0.63rem",
|
||||
color: "#6b7280",
|
||||
color: "var(--color-foreground-sec)",
|
||||
margin: 0,
|
||||
overflow: "auto",
|
||||
maxHeight: "220px",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function Hero({ lastUpdated }: HeroProps) {
|
|||
<ControlPanel onClose={() => setMenuOpen(false)} />,
|
||||
document.body,
|
||||
)}
|
||||
<p className="text-xs font-medium tracking-widest uppercase text-blue-500 mb-3">
|
||||
<p className="text-xs font-medium tracking-widest uppercase text-blue mb-3">
|
||||
dell-xps-nixos-serv
|
||||
</p>
|
||||
<div className="flex gap-[10px] items-center w-full">
|
||||
|
|
@ -57,28 +57,27 @@ export default function Hero({ lastUpdated }: HeroProps) {
|
|||
/>
|
||||
</svg>
|
||||
<h1
|
||||
className="text-4xl md:text-5xl font-normal leading-tight tracking-tight text-gray-900 mb-2"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}
|
||||
className="text-4xl md:text-5xl font-normal leading-tight tracking-tight text-foreground mb-2"
|
||||
>
|
||||
Home server
|
||||
</h1>
|
||||
{authed ? (
|
||||
<div
|
||||
onClick={() => setMenuOpen(true)}
|
||||
className="align-self-end ml-auto px-[20px] py-[20px] rounded-2xl hover:bg-gray-200 cursor-pointer duration-[200ms] text-gray-600 hover:shadow-sm font-[600]"
|
||||
className="align-self-end ml-auto px-[20px] py-[20px] rounded-2xl hover:bg-secondary cursor-pointer duration-[200ms] text-foreground-sec font-[600]"
|
||||
>
|
||||
<LuSettings2 />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => router.push("/auth")}
|
||||
className="align-self-end ml-auto px-[18px] py-[5px] rounded-xl hover:bg-blue-500 shadow-sm bg-white border-blue-300 hover:border-blue-400 border cursor-pointer duration-[200ms] text-blue-400 hover:shadow-sm hover:text-blue-100 text-[11pt] font-[600]"
|
||||
className="align-self-end ml-auto px-[18px] py-[5px] rounded-xl hover:bg-blue shadow-sm bg-primary border-blue/30 hover:border-blue border cursor-pointer duration-[200ms] text-blue hover:text-primary text-[11pt] font-[600]"
|
||||
>
|
||||
Authenticate
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className="text-sm text-foreground-sec font-light">
|
||||
{lastUpdated
|
||||
? `Last updated ${new Date(lastUpdated).toLocaleTimeString()}`
|
||||
: "Fetching system stats..."}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ export default function LinkCard({ link, delay = 0 }: LinkCardProps) {
|
|||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm flex flex-col gap-2 hover:shadow-md hover:-translate-y-1 hover:border-blue-200 transition-all duration-200 animate-fade-up no-underline text-inherit"
|
||||
className="bg-primary border border-secondary rounded-2xl p-6 flex flex-col gap-2 hover:-translate-y-1 hover:border-blue/30 transition-all duration-200 animate-fade-up no-underline text-inherit"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<div className="w-11 h-11 bg-blue-50 rounded-xl flex items-center justify-center text-2xl mb-1">
|
||||
<div className="w-11 h-11 bg-blue/10 rounded-xl flex items-center justify-center text-2xl mb-1">
|
||||
{link.icon}
|
||||
</div>
|
||||
<span className="text-base font-medium text-gray-900">{link.name}</span>
|
||||
<span className="text-sm text-gray-400 font-light">{link.description}</span>
|
||||
<span className="mt-auto pt-2 text-sm font-medium text-blue-500">
|
||||
<span className="text-base font-medium text-foreground">{link.name}</span>
|
||||
<span className="text-sm text-foreground-sec font-light">{link.description}</span>
|
||||
<span className="mt-auto pt-2 text-sm font-medium text-blue">
|
||||
Open app →
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default function LinksGrid() {
|
|||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between mb-5">
|
||||
<h2 className="text-lg font-medium tracking-tight text-gray-900">
|
||||
<h2 className="text-lg font-medium tracking-tight text-foreground" style={{ lineHeight: "normal", marginTop: 0 }}>
|
||||
Services & Apps
|
||||
</h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LuCode } from "react-icons/lu";
|
||||
|
||||
interface NavBarProps {
|
||||
online: boolean;
|
||||
devConsoleOpen: boolean;
|
||||
onToggleDevConsole: () => void;
|
||||
}
|
||||
|
||||
export default function NavBar({ online, devConsoleOpen, onToggleDevConsole }: NavBarProps) {
|
||||
const router = useRouter();
|
||||
const [auth, setAuth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/auth/check")
|
||||
.then((r) => setAuth(r.ok))
|
||||
.catch(() => setAuth(false));
|
||||
}, []);
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
router.push("/auth");
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between pt-7 pb-6 mb-13 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
{auth && (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 text-xs text-red-700 bg-red-50 border border-red-200 px-3 py-1.5 rounded-full cursor-pointer"
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onToggleDevConsole}
|
||||
className={`flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full border cursor-pointer transition-colors ${
|
||||
devConsoleOpen
|
||||
? "text-blue-700 bg-blue-50 border-blue-200"
|
||||
: "text-gray-500 bg-gray-50 border-gray-200 hover:border-gray-300 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<LuCode size={12} />
|
||||
Dev
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`flex items-center gap-2 text-xs font-medium px-3 py-1.5 rounded-full border ${
|
||||
online
|
||||
? "text-green-700 bg-green-50 border-green-200"
|
||||
: "text-gray-500 bg-gray-50 border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{
|
||||
background: online ? "#22c55e" : "#d1d5db",
|
||||
animation: online ? "pulse-dot 2s infinite" : "none",
|
||||
}}
|
||||
/>
|
||||
{online ? "Online" : "Connecting..."}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,31 +9,31 @@ interface NetworkCardProps {
|
|||
export default function NetworkCard({ iface, speed, delay = 0 }: NetworkCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm animate-fade-up"
|
||||
className="bg-primary border border-secondary rounded-2xl p-6 animate-fade-up"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400 mb-4">
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec mb-4">
|
||||
Network
|
||||
</p>
|
||||
{iface && speed ? (
|
||||
<>
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400 mb-3">
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec mb-3">
|
||||
{iface}
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-lg font-medium text-blue-500">
|
||||
<span className="text-lg font-medium text-blue">
|
||||
↓ {formatBytes(speed.rx)}/s
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec">
|
||||
Download
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-lg font-medium text-violet-500">
|
||||
<span className="text-lg font-medium text-blue/70">
|
||||
↑ {formatBytes(speed.tx)}/s
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec">
|
||||
Upload
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface PowerCardProps {
|
|||
function powerColor(watts: number): string {
|
||||
if (watts > 400) return "#ef4444";
|
||||
if (watts > 200) return "#f59e0b";
|
||||
return "#3b82f6";
|
||||
return "#428ce2";
|
||||
}
|
||||
|
||||
export default function PowerCard({ device, label, delay = 0, toggling = false, onToggle }: PowerCardProps) {
|
||||
|
|
@ -23,23 +23,23 @@ export default function PowerCard({ device, label, delay = 0, toggling = false,
|
|||
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl p-5 flex flex-col shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 animate-fade-up"
|
||||
className="bg-primary border border-secondary rounded-2xl p-5 flex flex-col hover:-translate-y-0.5 transition-all duration-200 animate-fade-up"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400">
|
||||
<span className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec">
|
||||
{label}
|
||||
</span>
|
||||
{device ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`flex items-center gap-1.5 text-[0.62rem] font-medium uppercase tracking-widest ${
|
||||
device.on ? "text-emerald-500" : "text-gray-400"
|
||||
device.on ? "text-green" : "text-foreground-sec"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
device.on ? "bg-emerald-400" : "bg-gray-300"
|
||||
device.on ? "bg-green" : "bg-foreground-sec/40"
|
||||
}`}
|
||||
/>
|
||||
{device.on ? "On" : "Off"}
|
||||
|
|
@ -50,8 +50,8 @@ export default function PowerCard({ device, label, delay = 0, toggling = false,
|
|||
disabled={toggling}
|
||||
className={`text-[0.6rem] font-medium uppercase tracking-widest px-2 py-0.5 rounded-full border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
||||
device.on
|
||||
? "border-red-200 text-red-400 hover:bg-red-50"
|
||||
: "border-emerald-200 text-emerald-500 hover:bg-emerald-50"
|
||||
? "border-red-500/20 text-red-400 hover:bg-red-500/10"
|
||||
: "border-green/20 text-green hover:bg-green/10"
|
||||
}`}
|
||||
>
|
||||
{toggling ? "···" : device.on ? "Turn off" : "Turn on"}
|
||||
|
|
@ -64,16 +64,16 @@ export default function PowerCard({ device, label, delay = 0, toggling = false,
|
|||
{device ? (
|
||||
<>
|
||||
<div className="flex items-baseline gap-1.5 mt-0.5">
|
||||
<span className="text-3xl font-medium tracking-tight text-gray-900 leading-none">
|
||||
<span className="text-3xl font-medium tracking-tight text-foreground leading-none">
|
||||
{device.current_power_w.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-base text-gray-400 font-medium">W</span>
|
||||
<span className="text-base text-foreground-sec font-medium">W</span>
|
||||
</div>
|
||||
<span className="text-[0.7rem] text-gray-400 mt-1 truncate">
|
||||
<span className="text-[0.7rem] text-foreground-sec mt-1 truncate">
|
||||
{device.alias} · {device.model}
|
||||
</span>
|
||||
|
||||
<div className="h-[3px] bg-gray-100 rounded-full mt-4 overflow-hidden">
|
||||
<div className="h-[3px] bg-secondary rounded-full mt-4 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-700"
|
||||
style={{
|
||||
|
|
@ -83,30 +83,30 @@ export default function PowerCard({ device, label, delay = 0, toggling = false,
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="grid grid-cols-3 gap-2 mt-4 pt-4 border-t border-secondary">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{(device.today_energy_wh / 1000).toFixed(3)}
|
||||
<span className="text-gray-400 text-xs ml-0.5">kWh</span>
|
||||
<span className="text-foreground-sec text-xs ml-0.5">kWh</span>
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec">
|
||||
Today
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{(device.month_energy_wh / 1000).toFixed(2)}
|
||||
<span className="text-gray-400 text-xs ml-0.5">kWh</span>
|
||||
<span className="text-foreground-sec text-xs ml-0.5">kWh</span>
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec">
|
||||
Month
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{runtimeHours}h {runtimeMins}m
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec">
|
||||
Runtime
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ export default function ServicePill({ name, status }: ServicePillProps) {
|
|||
<div
|
||||
className={`flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border text-sm ${
|
||||
active
|
||||
? "bg-green-50 border-green-200"
|
||||
: "bg-gray-50 border-gray-200 text-gray-400"
|
||||
? "bg-green/10 border-green/20"
|
||||
: "bg-secondary/30 border-secondary text-foreground-sec"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
||||
active ? "bg-green-500" : "bg-gray-300"
|
||||
active ? "bg-green" : "bg-foreground-sec/40"
|
||||
}`}
|
||||
/>
|
||||
<span className="flex-1 font-normal">{name}</span>
|
||||
<span
|
||||
className={`text-[0.65rem] uppercase tracking-widest font-medium ${
|
||||
active ? "text-green-600" : "text-gray-400"
|
||||
active ? "text-green" : "text-foreground-sec"
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ interface ServicesCardProps {
|
|||
export default function ServicesCard({ services, delay = 0 }: ServicesCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm animate-fade-up"
|
||||
className="bg-primary border border-secondary rounded-2xl p-6 animate-fade-up"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400 mb-4">
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec mb-4">
|
||||
Services
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
|
|||
299
app/components/SideNav.tsx
Normal file
299
app/components/SideNav.tsx
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
IconHome2,
|
||||
IconMoon,
|
||||
IconSun,
|
||||
IconChevronsLeft,
|
||||
IconChevronsRight,
|
||||
IconMenu2,
|
||||
IconX,
|
||||
IconCode,
|
||||
IconKey,
|
||||
IconLogout,
|
||||
} from "@tabler/icons-react";
|
||||
import { useSetTheme } from "@/stores/useThemeStore";
|
||||
|
||||
const LINKS = [
|
||||
{ href: "/", label: "Dashboard", icon: IconHome2 },
|
||||
{ href: "/auth", label: "Auth", icon: IconKey },
|
||||
];
|
||||
|
||||
const COLLAPSED_W = 52;
|
||||
|
||||
interface SideNavProps {
|
||||
online: boolean;
|
||||
devConsoleOpen: boolean;
|
||||
onToggleDevConsole: () => void;
|
||||
}
|
||||
|
||||
const SideNav = ({ online, devConsoleOpen, onToggleDevConsole }: SideNavProps) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const setTheme = useSetTheme();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(168);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [auth, setAuth] = useState(false);
|
||||
const isDragging = useRef(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/auth/check")
|
||||
.then((r) => setAuth(r.ok))
|
||||
.catch(() => setAuth(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
if (!isDragging.current || !wrapperRef.current) return;
|
||||
const left = wrapperRef.current.getBoundingClientRect().left;
|
||||
setSidebarWidth(Math.max(120, Math.min(320, e.clientX - left)));
|
||||
};
|
||||
const onUp = () => { isDragging.current = false; };
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
router.push("/auth");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar + drag handle */}
|
||||
<div ref={wrapperRef} className="hidden lg:flex flex-row shrink-0 select-none">
|
||||
<div
|
||||
style={{ width: collapsed ? COLLAPSED_W : sidebarWidth }}
|
||||
className="flex flex-col py-[16px] overflow-hidden transition-[width] duration-200"
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className={collapsed ? "flex justify-center mb-[16px] shrink-0" : "px-[16px] mb-[24px] shrink-0"}>
|
||||
<Link href="/">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="logo"
|
||||
className={collapsed ? "max-h-[24px]" : "max-h-[36px]"}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Nav links */}
|
||||
<nav className="flex flex-col gap-[2px] px-[8px] flex-1">
|
||||
{LINKS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href || (href !== "/" && pathname?.startsWith(href));
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
title={collapsed ? label : undefined}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] transition-colors cursor-pointer " +
|
||||
(collapsed
|
||||
? "justify-center py-[7px] "
|
||||
: "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap ") +
|
||||
(active
|
||||
? "bg-blue/10 text-blue font-semibold"
|
||||
: "text-foreground-sec hover:bg-secondary/50 hover:text-foreground font-medium")
|
||||
}
|
||||
>
|
||||
<Icon size={16} strokeWidth={active ? 2.5 : 2} className="shrink-0" />
|
||||
{!collapsed && label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Online status */}
|
||||
<div className="px-[8px] mb-[2px] shrink-0">
|
||||
<div
|
||||
title={collapsed ? (online ? "Online" : "Connecting...") : undefined}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] font-medium text-foreground-sec " +
|
||||
(collapsed ? "justify-center py-[7px]" : "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap")
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="w-[7px] h-[7px] rounded-full shrink-0"
|
||||
style={{
|
||||
background: online ? "#5dd776" : "#7b899a",
|
||||
animation: online ? "pulse-dot 2s infinite" : "none",
|
||||
}}
|
||||
/>
|
||||
{!collapsed && (online ? "Online" : "Connecting...")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dev console toggle */}
|
||||
<div className="px-[8px] shrink-0">
|
||||
<button
|
||||
onClick={onToggleDevConsole}
|
||||
title={collapsed ? "Dev Console" : undefined}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] transition-colors cursor-pointer font-medium " +
|
||||
(collapsed ? "justify-center py-[7px]" : "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap ") +
|
||||
(devConsoleOpen
|
||||
? "bg-blue/10 text-blue"
|
||||
: "text-foreground-sec hover:bg-secondary/50 hover:text-foreground")
|
||||
}
|
||||
>
|
||||
<IconCode size={16} className="shrink-0" />
|
||||
{!collapsed && "Dev Console"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
{auth && (
|
||||
<div className="px-[8px] shrink-0">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
title={collapsed ? "Log out" : undefined}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] transition-colors cursor-pointer font-medium text-red-400 hover:bg-red-500/10 " +
|
||||
(collapsed ? "justify-center py-[7px]" : "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap")
|
||||
}
|
||||
>
|
||||
<IconLogout size={16} className="shrink-0" />
|
||||
{!collapsed && "Log out"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="px-[8px] mt-[4px] shrink-0">
|
||||
<button
|
||||
onClick={setTheme}
|
||||
title={collapsed ? "Toggle theme" : undefined}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] transition-colors cursor-pointer text-foreground-sec hover:bg-secondary/50 hover:text-foreground font-medium " +
|
||||
(collapsed ? "justify-center py-[7px]" : "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap")
|
||||
}
|
||||
>
|
||||
<IconMoon size={16} className="shrink-0 dark-theme:hidden" />
|
||||
<IconSun size={16} className="shrink-0 hidden dark-theme:block" />
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="dark-theme:hidden">Dark mode</span>
|
||||
<span className="hidden dark-theme:block">Light mode</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Divider + collapse toggle */}
|
||||
<div className="mx-[8px] my-[8px] border-t border-secondary shrink-0" />
|
||||
<div className="px-[8px] shrink-0">
|
||||
<button
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
className={
|
||||
"w-full flex items-center rounded-[8px] transition-colors cursor-pointer text-foreground-sec hover:bg-secondary/50 hover:text-foreground font-medium " +
|
||||
(collapsed ? "justify-center py-[7px]" : "gap-[10px] px-[10px] py-[7px] text-[13px] whitespace-nowrap")
|
||||
}
|
||||
>
|
||||
{collapsed
|
||||
? <IconChevronsRight size={16} className="shrink-0" />
|
||||
: <IconChevronsLeft size={16} className="shrink-0" />}
|
||||
{!collapsed && "Collapse"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drag handle */}
|
||||
{!collapsed && (
|
||||
<div
|
||||
onMouseDown={(e) => { isDragging.current = true; e.preventDefault(); }}
|
||||
className="w-[10px] shrink-0 flex items-center justify-center cursor-col-resize group"
|
||||
>
|
||||
<div className="w-[3px] h-[40px] rounded-full bg-blue/20 transition-colors" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile header */}
|
||||
<div className="lg:hidden fixed top-0 left-0 right-0 z-[998] h-[52px] bg-primary border-b border-secondary flex items-center px-[16px]">
|
||||
<Link href="/" onClick={() => setMenuOpen(false)}>
|
||||
<img src="/logo.svg" alt="logo" className="max-h-[22px]" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMenuOpen((o) => !o)}
|
||||
className="ml-auto p-[7px] rounded-[8px] text-foreground-sec hover:bg-secondary/50 hover:text-foreground transition-colors cursor-pointer"
|
||||
>
|
||||
{menuOpen ? <IconX size={18} /> : <IconMenu2 size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile dropdown menu */}
|
||||
{menuOpen && (
|
||||
<div className="lg:hidden fixed top-[52px] left-0 right-0 z-[997] bg-primary border-b border-secondary shadow-xl">
|
||||
<nav className="flex flex-col gap-[2px] p-[8px]">
|
||||
{LINKS.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href || (href !== "/" && pathname?.startsWith(href));
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={
|
||||
"w-full flex items-center gap-[10px] px-[10px] py-[7px] text-[13px] rounded-[8px] transition-colors cursor-pointer " +
|
||||
(active
|
||||
? "bg-blue/10 text-blue font-semibold"
|
||||
: "text-foreground-sec hover:bg-secondary/50 hover:text-foreground font-medium")
|
||||
}
|
||||
>
|
||||
<Icon size={16} strokeWidth={active ? 2.5 : 2} className="shrink-0" />
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="mx-[8px] border-t border-secondary" />
|
||||
<div className="p-[8px] flex flex-col gap-[2px]">
|
||||
<button
|
||||
onClick={() => { onToggleDevConsole(); setMenuOpen(false); }}
|
||||
className={
|
||||
"w-full flex items-center gap-[10px] px-[10px] py-[7px] text-[13px] rounded-[8px] transition-colors cursor-pointer " +
|
||||
(devConsoleOpen ? "bg-blue/10 text-blue font-medium" : "text-foreground-sec hover:bg-secondary/50 hover:text-foreground font-medium")
|
||||
}
|
||||
>
|
||||
<IconCode size={16} className="shrink-0" />
|
||||
Dev Console
|
||||
</button>
|
||||
{auth && (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-[10px] px-[10px] py-[7px] text-[13px] rounded-[8px] text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer font-medium"
|
||||
>
|
||||
<IconLogout size={16} className="shrink-0" />
|
||||
Log out
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={setTheme}
|
||||
className="w-full flex items-center gap-[10px] px-[10px] py-[7px] text-[13px] rounded-[8px] text-foreground-sec hover:bg-secondary/50 hover:text-foreground transition-colors cursor-pointer font-medium"
|
||||
>
|
||||
<IconMoon size={16} className="shrink-0 dark-theme:hidden" />
|
||||
<IconSun size={16} className="shrink-0 hidden dark-theme:block" />
|
||||
<span className="dark-theme:hidden">Dark mode</span>
|
||||
<span className="hidden dark-theme:block">Light mode</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideNav;
|
||||
|
|
@ -14,20 +14,20 @@ export default function StatCard({ label, value, sub, percent, delay = 0 }: Stat
|
|||
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl p-5 flex flex-col gap-1 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 animate-fade-up"
|
||||
className="bg-primary border border-secondary rounded-2xl p-5 flex flex-col gap-1 hover:-translate-y-0.5 transition-all duration-200 animate-fade-up"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<span className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400">
|
||||
<span className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-3xl font-medium tracking-tight text-gray-900 leading-none mt-1">
|
||||
<span className="text-3xl font-medium tracking-tight text-foreground leading-none mt-1">
|
||||
{value}
|
||||
</span>
|
||||
{sub && (
|
||||
<span className="text-[0.7rem] text-gray-400 mt-0.5 truncate">{sub}</span>
|
||||
<span className="text-[0.7rem] text-foreground-sec mt-0.5 truncate">{sub}</span>
|
||||
)}
|
||||
{percent !== undefined && (
|
||||
<div className="h-[3px] bg-gray-100 rounded-full mt-3 overflow-hidden">
|
||||
<div className="h-[3px] bg-secondary rounded-full mt-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-700"
|
||||
style={{ width: `${pct}%`, background: color }}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ interface UptimeCardProps {
|
|||
export default function UptimeCard({ uptime, delay = 0 }: UptimeCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl p-6 shadow-sm animate-fade-up"
|
||||
className="bg-primary border border-secondary rounded-2xl p-6 animate-fade-up"
|
||||
style={{ animationDelay: `${delay}ms` }}
|
||||
>
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-gray-400 mb-4">
|
||||
<p className="text-[0.68rem] font-medium tracking-widest uppercase text-foreground-sec mb-4">
|
||||
Uptime
|
||||
</p>
|
||||
{uptime ? (
|
||||
|
|
@ -25,13 +25,13 @@ export default function UptimeCard({ uptime, delay = 0 }: UptimeCardProps) {
|
|||
<div
|
||||
key={unit}
|
||||
className={`flex flex-col items-center flex-1 ${
|
||||
i < 2 ? "border-r border-gray-200" : ""
|
||||
i < 2 ? "border-r border-secondary" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="text-3xl font-medium tracking-tight text-gray-900 leading-none">
|
||||
<span className="text-3xl font-medium tracking-tight text-foreground leading-none">
|
||||
{pad(val)}
|
||||
</span>
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-gray-400 mt-1.5">
|
||||
<span className="text-[0.62rem] uppercase tracking-widest text-foreground-sec mt-1.5">
|
||||
{unit}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue