Files
litoral-central-frontend/src/features/meteo/pages/MeteoPage.tsx
T
2026-05-25 16:37:00 +01:00

928 lines
36 KiB
TypeScript

import { useEffect, useState, type ReactNode } from "react";
import {
Activity,
ChartNoAxesColumnIncreasing,
CloudRain,
Droplets,
MoreHorizontal,
Sun,
Table2,
Thermometer,
TrendingUp,
Wind,
} from "lucide-react";
import { useMeteoModuleStream } from "../hooks/useMeteoModuleStream";
import { MeteoHistoryModal } from "../components/MeteoHistoryModal";
import type { ModuleSensorResponse } from "../../../types/meteo";
import { useMeteoHistory } from "../hooks/useMeteoHistory";
import { AccumulatedHistoryModal } from "../components/AccumulatedHistoryModal";
import {
useAccumulatedHistory,
type AccumulatedRange,
} from "../hooks/useAccumulatedHistory";
import { useWeatherForecast } from "../hooks/useWeatherForecast";
import { WeatherForecastCard } from "../components/WeatherForecastCard";
type MeteoPageProps = {
theme: "dark" | "light";
};
type HistoryMap = Record<string, number[]>;
type Accent = "amber" | "blue" | "cyan" | "emerald";
const MAX_HISTORY_POINTS = 34;
const RADIUS = "rounded-[5px]";
export function MeteoPage({ theme }: MeteoPageProps) {
const { sensors } = useMeteoModuleStream();
const [selectedSensor, setSelectedSensor] =
useState<ModuleSensorResponse | null>(null);
const [history, setHistory] = useState<HistoryMap>({});
const [openMenu, setOpenMenu] = useState<string | null>(null);
const [selectedAccumulated, setSelectedAccumulated] = useState<{
title: string;
sensor: ModuleSensorResponse | null;
} | null>(null);
const [accumulatedRange, setAccumulatedRange] =
useState<AccumulatedRange>("7d");
const temperature = findSensor(sensors, "temperatura.exterior");
const humidity = findSensor(sensors, "humidade.exterior");
const windDirection = findSensor(sensors, "direcao.vento");
const windSpeed = maxSensor(
sensors.filter((sensor) => sensor.key.startsWith("velocidade.vento.")),
);
const radiation = maxSensor(
sensors.filter((sensor) => sensor.key.startsWith("radiacao.")),
);
const rainSensors = sensors.filter((sensor) => sensor.key.startsWith("chuva."));
const rainSensor =
findSensor(sensors, "chuva.atual") ??
findSensor(sensors, "chuva.instantanea") ??
findSensor(sensors, "chuva.intensidade") ??
maxSensor(rainSensors);
const rainValue = numericValue(rainSensor);
const isRaining = rainValue !== null && rainValue > 0;
const meteoHistory = useMeteoHistory(selectedSensor);
const accumulatedHistory = useAccumulatedHistory(
selectedAccumulated?.sensor ?? null,
accumulatedRange,
);
const weatherForecast = useWeatherForecast();
useEffect(() => {
const samples: Array<[string, number | null]> = [
["temperatura.exterior", numericValue(temperature)],
["humidade.exterior", numericValue(humidity)],
["vento.velocidade", numericValue(windSpeed)],
["radiacao.solar", numericValue(radiation)],
["chuva.total", numericValue(rainSensor)],
];
setHistory((current) => {
const next = { ...current };
for (const [key, value] of samples) {
if (value === null || Number.isNaN(value)) continue;
const previous = next[key] ?? [];
const last = previous[previous.length - 1];
if (last === value && previous.length > 1) continue;
next[key] = [...previous, value].slice(-MAX_HISTORY_POINTS);
}
return next;
});
}, [
temperature?.value,
humidity?.value,
windSpeed?.value,
radiation?.value,
rainSensor?.value,
]);
return (
<div className="space-y-5 pb-6">
<WeatherForecastCard
theme={theme}
forecast={weatherForecast.forecast}
loading={weatherForecast.loading}
error={weatherForecast.error}
/>
<div className="grid gap-5 2xl:grid-cols-[minmax(0,1fr)_420px_minmax(0,1fr)]">
<div className="grid gap-5 md:grid-cols-2 2xl:grid-cols-1">
<MetricTile
theme={theme}
title="Temperatura"
subtitle="Temperatura exterior"
sensor={temperature}
icon={<Thermometer className="h-5 w-5" />}
accent="amber"
status={temperatureBadge(temperature)}
values={history["temperatura.exterior"]}
menuOpen={openMenu === "temperature"}
onMenuToggle={() =>
setOpenMenu(openMenu === "temperature" ? null : "temperature")
}
actions={[
{
label: "Ver gráfico",
icon: <ChartNoAxesColumnIncreasing className="h-4 w-4" />,
onClick: () => {
setSelectedSensor(temperature ?? null);
setOpenMenu(null);
},
},
]}
/>
<MetricTile
theme={theme}
title="Humidade"
subtitle="Humidade relativa"
sensor={humidity}
icon={<Droplets className="h-5 w-5" />}
accent="blue"
status={humidityBadge(humidity)}
values={history["humidade.exterior"]}
menuOpen={openMenu === "humidity"}
onMenuToggle={() =>
setOpenMenu(openMenu === "humidity" ? null : "humidity")
}
actions={[
{
label: "Ver gráfico",
icon: <ChartNoAxesColumnIncreasing className="h-4 w-4" />,
onClick: () => {
setSelectedSensor(humidity ?? null);
setOpenMenu(null);
},
},
]}
/>
<MetricTile
theme={theme}
title="Precipitação"
subtitle="Precipitação atual"
sensor={rainSensor}
icon={<CloudRain className="h-5 w-5" />}
accent="emerald"
status={isRaining ? "A chover" : "Sem chuva"}
values={history["chuva.total"]}
menuOpen={openMenu === "rain"}
onMenuToggle={() =>
setOpenMenu(openMenu === "rain" ? null : "rain")
}
actions={[
{
label: "Ver gráfico",
icon: <ChartNoAxesColumnIncreasing className="h-4 w-4" />,
onClick: () => {
setSelectedSensor(rainSensor ?? null);
setOpenMenu(null);
},
},
{
label: "Ver acumulado",
icon: <Table2 className="h-4 w-4" />,
onClick: () => {
setSelectedAccumulated({
title: "Precipitação acumulada",
sensor: rainSensor ?? null,
});
setOpenMenu(null);
},
},
]}
/>
</div>
<CompassPanel theme={theme} direction={numericValue(windDirection)} />
<div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-1">
<MetricTile
theme={theme}
title="Vento"
subtitle="Velocidade do vento"
sensor={windSpeed}
icon={<Wind className="h-5 w-5" />}
accent="cyan"
status={windBadge(windSpeed)}
values={history["vento.velocidade"]}
menuOpen={openMenu === "wind"}
onMenuToggle={() =>
setOpenMenu(openMenu === "wind" ? null : "wind")
}
actions={[
{
label: "Ver gráfico",
icon: <ChartNoAxesColumnIncreasing className="h-4 w-4" />,
onClick: () => {
setSelectedSensor(windSpeed ?? null);
setOpenMenu(null);
},
},
]}
/>
<MetricTile
theme={theme}
title="Radiação solar"
subtitle="Radiação instantânea"
sensor={radiation}
icon={<Sun className="h-5 w-5" />}
accent="amber"
status={radiationBadge(radiation)}
values={history["radiacao.solar"]}
menuOpen={openMenu === "radiation"}
onMenuToggle={() =>
setOpenMenu(openMenu === "radiation" ? null : "radiation")
}
actions={[
{
label: "Ver gráfico",
icon: <ChartNoAxesColumnIncreasing className="h-4 w-4" />,
onClick: () => {
setSelectedSensor(radiation ?? null);
setOpenMenu(null);
},
},
{
label: "Ver acumulado",
icon: <Table2 className="h-4 w-4" />,
onClick: () => {
setSelectedAccumulated({
title: "Radiação solar acumulada",
sensor: radiation ?? null,
});
setOpenMenu(null);
},
},
]}
/>
</div>
</div>
<MeteoHistoryModal
sensor={selectedSensor}
theme={theme}
points={meteoHistory.points}
loading={meteoHistory.loading}
hours={meteoHistory.hours}
onHoursChange={meteoHistory.setHours}
onClose={() => setSelectedSensor(null)}
/>
<AccumulatedHistoryModal
sensor={selectedAccumulated?.sensor ?? null}
title={selectedAccumulated?.title ?? ""}
theme={theme}
buckets={accumulatedHistory.buckets}
loading={accumulatedHistory.loading}
range={accumulatedRange}
onRangeChange={setAccumulatedRange}
onClose={() => setSelectedAccumulated(null)}
/>
</div>
);
}
function MetricTile({
theme,
title,
subtitle,
sensor,
customValue,
customUnit,
icon,
accent,
status,
values,
menuOpen,
onMenuToggle,
actions,
}: {
theme: "dark" | "light";
title: string;
subtitle: string;
sensor?: ModuleSensorResponse;
customValue?: string;
customUnit?: string;
icon: ReactNode;
accent: Accent;
status: string;
values?: number[];
menuOpen: boolean;
onMenuToggle: () => void;
actions: Array<{
label: string;
icon: ReactNode;
onClick: () => void;
}>;
}) {
const isDark = theme === "dark";
const colors = accentColors(accent, isDark);
const value = customValue ?? formatValue(sensor);
const unit = customUnit ?? sensor?.unit;
const trend = getTrend(values);
return (
<article
className={
isDark
? `${RADIUS} group relative min-h-[190px] overflow-hidden border border-white/10 bg-[#111827] p-5 text-left shadow-[0_12px_30px_rgba(0,0,0,0.22)] transition duration-200 hover:border-white/15 hover:bg-[#141d2b]`
: `${RADIUS} group relative min-h-[190px] overflow-hidden border border-slate-200 bg-white p-5 text-left shadow-[0_10px_26px_rgba(15,23,42,0.06)] transition duration-200 hover:border-slate-300 hover:shadow-[0_14px_34px_rgba(15,23,42,0.08)]`
}
>
<div className="relative z-20 flex items-start justify-between gap-4">
<div className="flex items-start gap-4">
<div
className={`grid h-11 w-11 shrink-0 place-items-center ${RADIUS} border ${colors.iconBox} ${colors.icon}`}
>
{icon}
</div>
<div>
<h2
className={
isDark
? "text-[15px] font-bold tracking-[-0.01em] text-slate-100"
: "text-[15px] font-bold tracking-[-0.01em] text-slate-950"
}
>
{title}
</h2>
<p
className={
isDark
? "mt-1 text-sm text-slate-400"
: "mt-1 text-sm text-slate-500"
}
>
{subtitle}
</p>
</div>
</div>
<div className="relative shrink-0">
<button
type="button"
onClick={(event) => {
event.stopPropagation();
onMenuToggle();
}}
className={
isDark
? `grid h-9 w-9 place-items-center ${RADIUS} text-slate-400 transition hover:bg-white/5 hover:text-slate-100`
: `grid h-9 w-9 place-items-center ${RADIUS} text-slate-400 transition hover:bg-slate-100 hover:text-slate-700`
}
aria-label={`Abrir ações de ${title}`}
>
<MoreHorizontal className="h-5 w-5" />
</button>
{menuOpen && (
<div
className={
isDark
? `absolute right-0 top-11 z-50 w-48 ${RADIUS} border border-white/10 bg-[#0f172a] p-1.5 shadow-xl`
: `absolute right-0 top-11 z-50 w-48 ${RADIUS} border border-slate-200 bg-white p-1.5 shadow-xl`
}
>
{actions.map((action) => (
<button
key={action.label}
type="button"
onClick={(event) => {
event.stopPropagation();
action.onClick();
}}
className={
isDark
? `flex w-full items-center gap-3 ${RADIUS} px-3 py-2.5 text-sm font-semibold text-slate-300 transition hover:bg-white/5 hover:text-white`
: `flex w-full items-center gap-3 ${RADIUS} px-3 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100 hover:text-slate-950`
}
>
{action.icon}
{action.label}
</button>
))}
</div>
)}
</div>
</div>
<div className="relative z-10 mt-8">
<div className="flex items-end justify-between gap-5">
<div>
<div
className={
isDark
? "flex items-end text-[50px] font-black leading-none tracking-[-0.07em] text-slate-100"
: "flex items-end text-[50px] font-black leading-none tracking-[-0.07em] text-slate-950"
}
>
{value}
{unit && (
<span
className={
isDark
? "mb-1 ml-2 text-base font-bold tracking-[-0.02em] text-slate-400"
: "mb-1 ml-2 text-base font-bold tracking-[-0.02em] text-slate-500"
}
>
{unit}
</span>
)}
</div>
<div className="mt-5 flex flex-wrap items-center gap-2">
<span
className={`inline-flex items-center justify-center ${RADIUS} border px-3 py-1.5 text-xs font-bold ${colors.badge}`}
>
{status}
</span>
<span
className={
isDark
? `inline-flex items-center gap-1.5 ${RADIUS} border border-white/10 bg-white/[0.03] px-3 py-1.5 text-xs font-semibold text-slate-400`
: `inline-flex items-center gap-1.5 ${RADIUS} border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-semibold text-slate-500`
}
>
<TrendingUp className="h-3.5 w-3.5" />
{trend}
</span>
</div>
</div>
</div>
</div>
<Sparkline
values={values}
className="absolute bottom-7 right-6 z-0 h-[62px] w-[220px]"
strokeClassName={colors.stroke}
glowClassName={colors.dot}
/>
</article>
);
}
function CompassPanel({
theme,
direction,
}: {
theme: "dark" | "light";
direction: number | null;
}) {
const isDark = theme === "dark";
const angle = direction ?? 0;
const cardinal = directionName(direction);
const degrees = direction !== null ? Math.round(direction) : null;
return (
<article
className={
isDark
? `${RADIUS} relative flex min-h-[620px] flex-col overflow-hidden border border-white/10 bg-[#111827] p-6 shadow-[0_12px_30px_rgba(0,0,0,0.22)]`
: `${RADIUS} relative flex min-h-[620px] flex-col overflow-hidden border border-slate-200 bg-white p-6 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`
}
>
<div className="relative z-10 flex flex-col items-center">
<span
className={
isDark
? `inline-flex items-center gap-2 ${RADIUS} border border-white/10 bg-white/[0.03] px-3 py-1 text-[11px] font-bold uppercase tracking-[0.16em] text-slate-400`
: `inline-flex items-center gap-2 ${RADIUS} border border-slate-200 bg-slate-50 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.16em] text-slate-500`
}
>
<Activity className="h-3.5 w-3.5" />
Direção do vento
</span>
<h2
className={
isDark
? "mt-4 text-[42px] font-black leading-none tracking-[-0.08em] text-slate-100"
: "mt-4 text-[42px] font-black leading-none tracking-[-0.08em] text-slate-950"
}
>
{cardinal}
</h2>
<div className="mt-5 grid w-full grid-cols-2 gap-3">
<CompassStat
theme={theme}
label="Graus"
value={degrees !== null ? `${degrees}°` : "--"}
/>
<CompassStat
theme={theme}
label="Quadrante"
value={directionQuadrant(direction)}
highlighted
/>
</div>
</div>
<div className="relative z-10 flex flex-1 items-center justify-center pt-6">
<div className="relative h-[340px] w-[340px] max-w-full">
<div
className={
isDark
? "absolute inset-0 rounded-full border border-white/10 bg-[#0b1220]"
: "absolute inset-0 rounded-full border border-slate-200 bg-slate-50"
}
/>
<div
className={
isDark
? "absolute inset-6 rounded-full border border-white/10"
: "absolute inset-6 rounded-full border border-slate-200"
}
/>
<div
className={
isDark
? "absolute inset-[78px] rounded-full border border-white/10 bg-white/[0.02]"
: "absolute inset-[78px] rounded-full border border-slate-200 bg-white"
}
/>
{Array.from({ length: 72 }).map((_, index) => {
const major = index % 6 === 0;
return (
<span
key={index}
className={`absolute left-1/2 top-1/2 w-px origin-[50%_158px] ${major ? "h-4" : "h-2"
} ${isDark ? "bg-slate-600/55" : "bg-slate-300"}`}
style={{
transform: `translate(-50%, -158px) rotate(${index * 5}deg)`,
}}
/>
);
})}
<CompassLabel label="N" isDark={isDark} className="left-1/2 top-[18px] -translate-x-1/2" />
<CompassLabel label="S" isDark={isDark} className="bottom-[18px] left-1/2 -translate-x-1/2" />
<CompassLabel label="W" isDark={isDark} className="left-[22px] top-1/2 -translate-y-1/2" />
<CompassLabel label="E" isDark={isDark} className="right-[22px] top-1/2 -translate-y-1/2" />
<div
className={
isDark
? "absolute inset-[116px] rounded-full border border-white/10 bg-[#111827]"
: "absolute inset-[116px] rounded-full border border-slate-200 bg-white"
}
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative h-28 w-28">
<div
className={
isDark
? "absolute left-1/2 top-1/2 h-[126px] w-[10px] origin-bottom rounded-full bg-slate-300"
: "absolute left-1/2 top-1/2 h-[126px] w-[10px] origin-bottom rounded-full bg-slate-700"
}
style={{
clipPath: "polygon(50% 0%, 100% 100%, 50% 84%, 0% 100%)",
transform: `translate(-50%, -100%) rotate(${angle}deg)`,
}}
/>
<div
className={
isDark
? "absolute left-1/2 top-1/2 h-[118px] w-[2px] origin-bottom rounded-full bg-white/60"
: "absolute left-1/2 top-1/2 h-[118px] w-[2px] origin-bottom rounded-full bg-white/90"
}
style={{
transform: `translate(-50%, -100%) rotate(${angle}deg)`,
}}
/>
<div
className={
isDark
? "absolute left-1/2 top-1/2 h-10 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10 bg-[#0b1220]"
: "absolute left-1/2 top-1/2 h-10 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full border border-slate-200 bg-white"
}
/>
<div
className={
isDark
? "absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-slate-300"
: "absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-slate-700"
}
/>
</div>
</div>
</div>
</div>
</article>
);
}
function CompassStat({
theme,
label,
value,
highlighted,
}: {
theme: "dark" | "light";
label: string;
value: string;
highlighted?: boolean;
}) {
const isDark = theme === "dark";
return (
<div
className={
isDark
? `${RADIUS} border border-white/10 bg-white/[0.03] px-4 py-3`
: `${RADIUS} border border-slate-200 bg-slate-50 px-4 py-3`
}
>
<p
className={
isDark
? "text-[10px] font-bold uppercase tracking-[0.16em] text-slate-500"
: "text-[10px] font-bold uppercase tracking-[0.16em] text-slate-400"
}
>
{label}
</p>
<p
className={
highlighted
? isDark
? "mt-1 text-xl font-black tracking-[-0.04em] text-slate-200"
: "mt-1 text-xl font-black tracking-[-0.04em] text-slate-800"
: isDark
? "mt-1 text-xl font-black tracking-[-0.04em] text-slate-100"
: "mt-1 text-xl font-black tracking-[-0.04em] text-slate-950"
}
>
{value}
</p>
</div>
);
}
function CompassLabel({
label,
className,
isDark,
}: {
label: string;
className: string;
isDark: boolean;
}) {
return (
<span
className={`absolute flex h-6 w-6 items-center justify-center rounded-full text-[13px] font-black leading-none tracking-[0.02em] ${isDark
? "bg-white/[0.03] text-slate-300"
: "bg-white text-slate-500 shadow-sm"
} ${className}`}
>
{label}
</span>
);
}
function Sparkline({
values,
className,
strokeClassName,
glowClassName,
}: {
values?: number[];
className?: string;
strokeClassName: string;
glowClassName: string;
}) {
if (!values || values.length < 2) return null;
const width = 220;
const height = 62;
const padding = 8;
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const points = values.map((value, index) => {
const x = padding + (index / (values.length - 1)) * (width - padding * 2);
const y = padding + (1 - (value - min) / range) * (height - padding * 2);
return `${x},${y}`;
});
const last = points[points.length - 1].split(",").map(Number);
return (
<svg
className={`pointer-events-none opacity-45 ${className}`}
viewBox={`0 0 ${width} ${height}`}
fill="none"
aria-hidden="true"
>
<polyline
points={points.join(" ")}
className={strokeClassName}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<circle cx={last[0]} cy={last[1]} r="3.5" className={glowClassName} />
</svg>
);
}
function findSensor(sensors: ModuleSensorResponse[], key: string) {
return sensors.find((sensor) => sensor.key === key);
}
function numericValue(sensor?: ModuleSensorResponse) {
return typeof sensor?.value === "number" ? sensor.value : null;
}
function maxSensor(sensors: ModuleSensorResponse[]) {
return sensors
.filter((sensor) => typeof sensor.value === "number")
.sort((a, b) => Number(b.value) - Number(a.value))[0];
}
function formatValue(sensor?: ModuleSensorResponse) {
if (!sensor) return "--";
if (typeof sensor.value === "number") {
return Number.isInteger(sensor.value)
? String(sensor.value)
: sensor.value.toFixed(1);
}
if (typeof sensor.value === "boolean") {
return sensor.value ? "On" : "Off";
}
if (sensor.value === null || sensor.value === undefined) {
return "--";
}
return String(sensor.value);
}
function directionName(direction: number | null) {
if (direction === null) return "--";
const labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
const index = Math.round(direction / 45) % 8;
return labels[index];
}
function directionQuadrant(direction: number | null) {
if (direction === null) return "--";
if (direction >= 315 || direction < 45) return "Norte";
if (direction >= 45 && direction < 135) return "Este";
if (direction >= 135 && direction < 225) return "Sul";
return "Oeste";
}
function getTrend(values?: number[]) {
if (!values || values.length < 2) return "Sem tendência";
const first = values[0];
const last = values[values.length - 1];
const delta = last - first;
if (Math.abs(delta) < 0.1) return "Estável";
if (delta > 0) return "A subir";
return "A descer";
}
function temperatureBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
if (value === null) return "Sem dados";
if (value >= 30) return "Quente";
if (value <= 10) return "Frio";
return "Normal";
}
function humidityBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
if (value === null) return "Sem dados";
if (value >= 80) return "Alta";
if (value <= 35) return "Baixa";
return "Normal";
}
function windBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
if (value === null) return "Sem dados";
if (value >= 30) return "Forte";
if (value >= 10) return "Moderado";
return "Fraco";
}
function radiationBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
if (value === null) return "Sem dados";
if (value >= 800) return "Alta";
if (value >= 400) return "Média";
return "Baixa";
}
function accentColors(accent: Accent, isDark: boolean) {
switch (accent) {
case "amber":
return isDark
? {
icon: "text-amber-200",
stroke: "stroke-amber-200",
dot: "fill-amber-200",
iconBox: "border-white/10 bg-white/[0.03]",
badge: "border-white/10 bg-white/[0.03] text-slate-300",
}
: {
icon: "text-amber-700",
stroke: "stroke-amber-500",
dot: "fill-amber-500",
iconBox: "border-slate-200 bg-slate-50",
badge: "border-slate-200 bg-slate-50 text-slate-600",
};
case "blue":
return isDark
? {
icon: "text-sky-200",
stroke: "stroke-sky-200",
dot: "fill-sky-200",
iconBox: "border-white/10 bg-white/[0.03]",
badge: "border-white/10 bg-white/[0.03] text-slate-300",
}
: {
icon: "text-sky-700",
stroke: "stroke-sky-500",
dot: "fill-sky-500",
iconBox: "border-slate-200 bg-slate-50",
badge: "border-slate-200 bg-slate-50 text-slate-600",
};
case "cyan":
return isDark
? {
icon: "text-cyan-200",
stroke: "stroke-cyan-200",
dot: "fill-cyan-200",
iconBox: "border-white/10 bg-white/[0.03]",
badge: "border-white/10 bg-white/[0.03] text-slate-300",
}
: {
icon: "text-cyan-700",
stroke: "stroke-cyan-500",
dot: "fill-cyan-500",
iconBox: "border-slate-200 bg-slate-50",
badge: "border-slate-200 bg-slate-50 text-slate-600",
};
case "emerald":
return isDark
? {
icon: "text-emerald-200",
stroke: "stroke-emerald-200",
dot: "fill-emerald-200",
iconBox: "border-white/10 bg-white/[0.03]",
badge: "border-white/10 bg-white/[0.03] text-slate-300",
}
: {
icon: "text-emerald-700",
stroke: "stroke-emerald-500",
dot: "fill-emerald-500",
iconBox: "border-slate-200 bg-slate-50",
badge: "border-slate-200 bg-slate-50 text-slate-600",
};
}
}