first commit
This commit is contained in:
21
frontend/src/components/common/Badge.tsx
Normal file
21
frontend/src/components/common/Badge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import clsx from "clsx";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
interface BadgeProps extends PropsWithChildren {
|
||||
tone?: "ok" | "warn" | "critical" | "neutral";
|
||||
}
|
||||
|
||||
export function Badge({ tone = "neutral", children }: BadgeProps) {
|
||||
const palette = {
|
||||
ok: "border-emerald-400/30 bg-emerald-500/10 text-emerald-200",
|
||||
warn: "border-amber-400/30 bg-amber-500/10 text-amber-200",
|
||||
critical: "border-rose-400/30 bg-rose-500/10 text-rose-200",
|
||||
neutral: "border-white/10 bg-white/5 text-slate-200",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={clsx("inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium", palette[tone])}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
31
frontend/src/components/common/Card.tsx
Normal file
31
frontend/src/components/common/Card.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import clsx from "clsx";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
interface CardProps extends PropsWithChildren {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
action?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Card({ title, subtitle, action, className, children }: CardProps) {
|
||||
return (
|
||||
<section
|
||||
className={clsx(
|
||||
"rounded-3xl border border-white/10 bg-white/5 p-5 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(title || subtitle || action) && (
|
||||
<header className="mb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
{title && <h3 className="text-base font-semibold text-white">{title}</h3>}
|
||||
{subtitle && <p className="mt-1 text-sm text-slate-400">{subtitle}</p>}
|
||||
</div>
|
||||
{action}
|
||||
</header>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
31
frontend/src/components/common/EChart.tsx
Normal file
31
frontend/src/components/common/EChart.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as echarts from "echarts";
|
||||
import type { EChartsOption } from "echarts";
|
||||
|
||||
interface EChartProps {
|
||||
option: EChartsOption;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function EChart({ option, className = "h-80 w-full" }: EChartProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chart = echarts.init(ref.current);
|
||||
chart.setOption(option);
|
||||
|
||||
const observer = new ResizeObserver(() => chart.resize());
|
||||
observer.observe(ref.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
chart.dispose();
|
||||
};
|
||||
}, [option]);
|
||||
|
||||
return <div ref={ref} className={className} />;
|
||||
}
|
||||
13
frontend/src/components/common/EmptyState.tsx
Normal file
13
frontend/src/components/common/EmptyState.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function EmptyState({ title, description }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="rounded-3xl border border-dashed border-white/10 bg-white/3 p-10 text-center">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
<p className="mt-2 text-sm text-slate-400">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
frontend/src/components/common/Icons.tsx
Normal file
84
frontend/src/components/common/Icons.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
type IconProps = SVGProps<SVGSVGElement> & { size?: number };
|
||||
|
||||
function BaseIcon({ size = 18, children, ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBolt(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M13 2L5 14h6l-1 8 8-12h-6l1-8z" /></BaseIcon>;
|
||||
}
|
||||
export function IconChartBar(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M4 20h16" /><path d="M7 16V8" /><path d="M12 16V4" /><path d="M17 16v-6" /></BaseIcon>;
|
||||
}
|
||||
export function IconChecklist(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M9 6h11" /><path d="M9 12h11" /><path d="M9 18h11" /><path d="M4 6l1.5 1.5L7.5 5" /><path d="M4 12l1.5 1.5L7.5 11" /><path d="M4 18l1.5 1.5L7.5 17" /></BaseIcon>;
|
||||
}
|
||||
export function IconClockHour4(props: IconProps) {
|
||||
return <BaseIcon {...props}><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></BaseIcon>;
|
||||
}
|
||||
export function IconDatabaseImport(props: IconProps) {
|
||||
return <BaseIcon {...props}><ellipse cx="12" cy="5" rx="7" ry="3" /><path d="M5 5v6c0 1.7 3.1 3 7 3s7-1.3 7-3V5" /><path d="M12 14v8" /><path d="M9 19l3 3 3-3" /></BaseIcon>;
|
||||
}
|
||||
export function IconDeviceDesktop(props: IconProps) {
|
||||
return <BaseIcon {...props}><rect x="3" y="4" width="18" height="12" rx="2" /><path d="M8 20h8" /><path d="M12 16v4" /></BaseIcon>;
|
||||
}
|
||||
export function IconHistory(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v5h5" /><path d="M12 7v5l3 2" /></BaseIcon>;
|
||||
}
|
||||
export function IconLanguage(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M4 5h10" /><path d="M9 3c0 6-2 10-5 12" /><path d="M7 13c1.5 2.5 3.5 4.5 6 6" /><path d="M14 10h6" /><path d="M17 7l3 10" /><path d="M14 17h6" /></BaseIcon>;
|
||||
}
|
||||
export function IconLayoutDashboard(props: IconProps) {
|
||||
return <BaseIcon {...props}><rect x="3" y="3" width="8" height="8" rx="1" /><rect x="13" y="3" width="8" height="5" rx="1" /><rect x="13" y="10" width="8" height="11" rx="1" /><rect x="3" y="13" width="8" height="8" rx="1" /></BaseIcon>;
|
||||
}
|
||||
export function IconLock(props: IconProps) {
|
||||
return <BaseIcon {...props}><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" /></BaseIcon>;
|
||||
}
|
||||
export function IconLogin2(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" /><path d="M10 17l5-5-5-5" /><path d="M15 12H3" /></BaseIcon>;
|
||||
}
|
||||
export function IconLogout(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><path d="M16 17l5-5-5-5" /><path d="M21 12H9" /></BaseIcon>;
|
||||
}
|
||||
export function IconMoon(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M12 3a7 7 0 1 0 9 9 9 9 0 1 1-9-9z" /></BaseIcon>;
|
||||
}
|
||||
export function IconPlayerPlay(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M8 5v14l11-7z" /></BaseIcon>;
|
||||
}
|
||||
export function IconRefresh(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M20 11a8 8 0 0 0-14-5l-2 2" /><path d="M4 3v5h5" /><path d="M4 13a8 8 0 0 0 14 5l2-2" /><path d="M20 21v-5h-5" /></BaseIcon>;
|
||||
}
|
||||
export function IconSettings(props: IconProps) {
|
||||
return <BaseIcon {...props}><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.2a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.2a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3h0A1.6 1.6 0 0 0 10 3.2V3a2 2 0 1 1 4 0v.2a1.6 1.6 0 0 0 1 1.5h0a1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8v0a1.6 1.6 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.2a1.6 1.6 0 0 0-1.4 1z" /></BaseIcon>;
|
||||
}
|
||||
export function IconSun(props: IconProps) {
|
||||
return <BaseIcon {...props}><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="M4.9 4.9l1.4 1.4" /><path d="M17.7 17.7l1.4 1.4" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="M4.9 19.1l1.4-1.4" /><path d="M17.7 6.3l1.4-1.4" /></BaseIcon>;
|
||||
}
|
||||
export function IconTemperature(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M14 14.76V5a2 2 0 0 0-4 0v9.76a4 4 0 1 0 4 0z" /></BaseIcon>;
|
||||
}
|
||||
export function IconX(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M18 6L6 18" /><path d="M6 6l12 12" /></BaseIcon>;
|
||||
}
|
||||
export function IconArrowsMove(props: IconProps) {
|
||||
return <BaseIcon {...props}><path d="M12 2v20" /><path d="M2 12h20" /><path d="M7 7l5-5 5 5" /><path d="M7 17l5 5 5-5" /></BaseIcon>;
|
||||
}
|
||||
17
frontend/src/components/common/ValuePair.tsx
Normal file
17
frontend/src/components/common/ValuePair.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { formatValue } from "../../lib/format";
|
||||
import type { MetricValue } from "../../types";
|
||||
|
||||
interface ValuePairProps {
|
||||
metric?: MetricValue;
|
||||
}
|
||||
|
||||
export function ValuePair({ metric }: ValuePairProps) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/8 bg-slate-950/40 p-3">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">{metric?.label ?? "--"}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">
|
||||
{metric ? formatValue(metric.value, metric.unit, metric.precision) : "--"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user