Import source: web/src/components/Header

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:20:01 +03:30
parent 07f8e82242
commit 2a03adf447
+212
View File
@@ -0,0 +1,212 @@
import { useEffect, useState } from "react";
import { motion } from "motion/react";
import type { LiveTarget, TargetState } from "../lib/types";
import { fmtClock, fmtNumber } from "../lib/format";
import { springs, STATE_COLOR, useUI, type ViewMode, type Window } from "../lib/ui";
function LiveClock() {
const { lang } = useUI();
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
return (
<span className="tabular text-paper-dim text-[13px] font-mono">
{fmtClock(lang, now)}
</span>
);
}
/** Live fleet summary: 24 up · 2 degraded · 1 down. Zero counts hide. */
function StatusSummary({ targets }: { targets: LiveTarget[] }) {
const { t, lang } = useUI();
const counts = new Map<TargetState, number>();
targets.forEach((x) => counts.set(x.state, (counts.get(x.state) ?? 0) + 1));
const order: TargetState[] = ["up", "degraded", "down", "unknown"];
const parts = order.filter((s) => (counts.get(s) ?? 0) > 0);
if (parts.length === 0) return null;
return (
<div className="hidden md:flex items-center gap-3 font-mono text-[10px]">
{parts.map((s) => (
<span key={s} className="flex items-center gap-1.5">
<span className="size-[5px] rounded-full" style={{ background: STATE_COLOR[s] }} />
<span className="tabular text-paper-dim">{fmtNumber(lang, counts.get(s)!)}</span>
<span className="text-paper-faint">{t.state[s]}</span>
</span>
))}
</div>
);
}
function Segmented<T extends string>({
value,
options,
onChange,
ariaLabel,
}: {
value: T;
options: { value: T; label: string }[];
onChange: (v: T) => void;
ariaLabel: string;
}) {
const { reduced } = useUI();
return (
<div
role="radiogroup"
aria-label={ariaLabel}
className="flex items-center border border-ink-700/60 rounded-full p-[3px] gap-[2px]"
>
{options.map((opt) => {
const active = opt.value === value;
return (
<button
key={opt.value}
role="radio"
aria-checked={active}
onClick={() => onChange(opt.value)}
className={`relative px-3 py-1 rounded-full text-[10px] font-mono tracking-[0.14em] uppercase transition-colors cursor-pointer ${
active ? "text-ink-950" : "text-paper-faint hover:text-paper-dim"
}`}
>
{active && (
<motion.span
layoutId={`seg-${ariaLabel}`}
className="absolute inset-0 rounded-full bg-brass"
transition={reduced ? { duration: 0 } : springs.snappy}
/>
)}
<span className="relative z-10">{opt.label}</span>
</button>
);
})}
</div>
);
}
function ThemeToggle() {
const { t, theme, setTheme, reduced } = useUI();
const next = theme === "dark" ? "light" : "dark";
return (
<motion.button
onClick={() => setTheme(next)}
aria-label={theme === "dark" ? t.themeLight : t.themeDark}
title={theme === "dark" ? t.themeLight : t.themeDark}
className="size-[30px] rounded-full border border-ink-700/60 text-paper-dim hover:text-paper hover:border-ink-700 cursor-pointer flex items-center justify-center"
whileTap={reduced ? undefined : { scale: 0.88, rotate: 40 }}
transition={springs.snappy}
>
<svg viewBox="0 0 16 16" width={13} height={13} aria-hidden>
{theme === "dark" ? (
// sun
<g stroke="currentColor" strokeWidth={1.2} strokeLinecap="round">
<circle cx={8} cy={8} r={3.1} fill="none" />
{Array.from({ length: 8 }, (_, i) => {
const a = (i * Math.PI) / 4;
return (
<line
key={i}
x1={8 + Math.cos(a) * 5}
y1={8 + Math.sin(a) * 5}
x2={8 + Math.cos(a) * 6.6}
y2={8 + Math.sin(a) * 6.6}
/>
);
})}
</g>
) : (
// moon
<path
d="M 13.2 9.8 A 5.6 5.6 0 1 1 6.2 2.8 A 4.6 4.6 0 0 0 13.2 9.8 Z"
fill="currentColor"
/>
)}
</svg>
</motion.button>
);
}
export function Header({
connected,
targets,
}: {
connected: boolean;
targets: LiveTarget[];
}) {
const { t, lang, mode, window, setLang, setMode, setWindow, reduced } =
useUI();
return (
<header className="relative z-10 flex flex-wrap items-center gap-x-6 gap-y-4 px-6 sm:px-10 pt-7 pb-5">
{/* wordmark */}
<div className="flex items-baseline gap-3 me-auto">
<span
className="text-[28px] leading-none text-paper"
style={{ fontFamily: "var(--font-display)" }}
>
Dash
</span>
<span className="microlabel hidden sm:inline">{t.tagline}</span>
</div>
<StatusSummary targets={targets} />
<div className="flex items-center gap-4 flex-wrap">
{/* connection state */}
<div className="flex items-center gap-2" title={connected ? t.live : t.connecting}>
<span className="relative flex size-[7px]">
{connected && !reduced && (
<motion.span
className="absolute inset-0 rounded-full bg-up"
animate={{ scale: [1, 2.4], opacity: [0.6, 0] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeOut" }}
/>
)}
<span
className={`relative inline-flex size-[7px] rounded-full ${
connected ? "bg-up" : "bg-warn"
}`}
/>
</span>
<span className="microlabel">{connected ? t.live : t.connecting}</span>
</div>
<LiveClock />
{mode === "internal" && (
<Segmented<Window>
ariaLabel="window"
value={window}
onChange={setWindow}
options={[
{ value: "24h", label: lang === "fa" ? "۲۴س" : "24H" },
{ value: "7d", label: lang === "fa" ? "۷ر" : "7D" },
]}
/>
)}
<Segmented<ViewMode>
ariaLabel="mode"
value={mode}
onChange={setMode}
options={[
{ value: "internal", label: t.internalMode },
{ value: "public", label: t.publicMode },
]}
/>
<Segmented<"en" | "fa">
ariaLabel="language"
value={lang}
onChange={setLang}
options={[
{ value: "en", label: "EN" },
{ value: "fa", label: "فا" },
]}
/>
<ThemeToggle />
</div>
</header>
);
}