import clsx from "clsx"; import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { BarChart3, Bell, Check, Clapperboard, Download, Home, ListVideo, Lock, MessageSquare, PlayCircle, Repeat, Settings, SlidersHorizontal, Tv, Users, Youtube, } from "lucide-react"; import { Btn } from "./batchA"; import { hexContrast, hueGap } from "../lib/colorMath"; import { Reference, Sample } from "./labKit"; import { FADE_ABOVE_PANEL, FADE_BELOW, FADE_BELOW_PANEL, LAYER } from "./labTokens"; import { Chip } from "../components/ui/Chip"; import { SURFACE } from "../components/ui/surfaces"; import { SelectTrigger } from "../components/ui/SelectTrigger"; import { SkeletonShapes } from "../components/ui/Skeleton"; import { Button } from "../components/ui/Button"; // E28 S2 · Batch B — surfaces, chrome and colour, DECIDED (owner, 2026-09-24): every recommendation, // except B7, where the owner combined the soft tint + edge bar with optional labels. B1 (density) and // B3 (state layers) render inside batch A's references. Only the chosen rendering of each stays. const ico = "w-4 h-4"; // ── B2 · the scheme set ────────────────────────────────────────────────────────────────────────── // A scheme that is not in index.css yet is rendered by scoping its tokens on a wrapper: every // utility resolves var(--…) at the use site (`@theme inline`), so the tile paints its own accent. type Pal = { bg: string; surface: string; border: string; fg: string; muted: string; accent: string; accentFg: string; }; type Mode = "dark" | "light"; type SchemeDef = { name: string; dark: Pal; light: Pal }; const STATUS: Record> = { dark: { danger: "#f87171", dangerFg: "#2a0a0a", warn: "#fbbf24", success: "#34d399" }, light: { danger: "#b91c1c", dangerFg: "#ffffff", warn: "#a14a07", success: "#047857" }, }; const pal = ( bg: string, surface: string, border: string, fg: string, muted: string, accent: string, accentFg: string, ): Pal => ({ bg, surface, border, fg, muted, accent, accentFg }); // A literal copy of index.css's scheme tokens, so the tiles render side by side (the live CSS // switches only one scheme at a time). index.css is the source; theme.contract.test.ts holds it. const SCHEMES: SchemeDef[] = [ { name: "Midnight · blue", dark: pal("#0b1020", "#121a2e", "#243049", "#e6e9f0", "#97a3c0", "#6d8cff", "#0b1020"), light: pal("#f4f6fc", "#ffffff", "#dde3f0", "#1a2238", "#5b6685", "#3b5bdb", "#ffffff"), }, { name: "Forest · cyan", dark: pal("#0a1512", "#0f1f1a", "#1f3a30", "#e6f0ec", "#90b1a4", "#22d3ee", "#04110d"), light: pal("#f1f7f4", "#ffffff", "#d6e7e0", "#102a22", "#4d6b60", "#0e7490", "#ffffff"), }, { name: "Slate · fuchsia", dark: pal("#15171c", "#1b1e25", "#2c313b", "#e7e9ee", "#9aa1ad", "#e879f9", "#1a0620"), light: pal("#f6f7f9", "#ffffff", "#e3e6eb", "#1c1f26", "#5d636e", "#a21caf", "#ffffff"), }, { name: "Matrix · moss", dark: pal("#0b0e0b", "#111510", "#2a3226", "#d9ded6", "#8f9a8a", "#86a05a", "#0c1208"), light: pal("#f4f5f0", "#ffffff", "#dfe3d8", "#161a14", "#5a6352", "#556b2f", "#ffffff"), }, { name: "Fjord · steel", dark: pal("#0d1225", "#121931", "#25506e", "#eef1f5", "#9fb0c4", "#5aa1cb", "#0d1225"), light: pal("#f3f6fa", "#ffffff", "#c9d6e3", "#152232", "#4f6278", "#256b8f", "#ffffff"), }, ]; function Metric({ ok, children }: { ok: boolean; children: ReactNode }) { return ( {children} ); } function SchemeTile({ s, mode }: { s: SchemeDef; mode: Mode }) { const p = s[mode]; const st = STATUS[mode]; const style = { "--bg": p.bg, "--surface": p.surface, "--card": p.surface, "--border": p.border, "--fg": p.fg, "--muted": p.muted, "--accent": p.accent, "--accent-fg": p.accentFg, "--danger": st.danger, "--danger-fg": st.dangerFg, "--warn": st.warn, "--success": st.success, } as CSSProperties; const worst = (["danger", "warn", "success"] as const) .map((k) => ({ k, g: Math.round(hueGap(p.accent, st[k])) })) .reduce((a, b) => (b.g < a.g ? b : a)); const text = hexContrast(p.accent, p.bg); const onFill = hexContrast(p.accentFg, p.accent); return (
{s.name} · {mode}
Save Delete
Filter Retrying Done
= 30}>{`closest status: ${worst.k} ${worst.g}°`} = 4.5}>{`accent as text ${text.toFixed(2)}:1`} = 4.5}>{`button text ${onFill.toFixed(2)}:1`}
); } // ── B4 · the page-chrome band ──────────────────────────────────────────────────────────────────── function Frame({ h, label, children }: { h: string; label: string; children: ReactNode }) { return (
{children}
); } function Cards() { return (
{Array.from({ length: 12 }, (_, i) => (
Video title number {i + 1} Channel · 2 days ago
))}
); } const tab = (on: boolean) => clsx( "rounded-lg px-2.5 py-1 text-xs", on ? "bg-accent text-accent-fg" : clsx("text-muted hover:text-fg", LAYER), ); function Tabs() { return (
Queue Library History
); } function Band({ collapsed }: { collapsed?: boolean }) { return (
{collapsed && ( } > Filters · 3 )}
{!collapsed && (
{[ ["State", "Unwatched"], ["Date", "Any"], ["Sort", "Newest"], ].map(([k, v]) => ( {v} ))}
)}
); } // ── B5 · surface per role ──────────────────────────────────────────────────────────────────────── function Surfaces() { return (
Dialog · .glass r16
Card · r12
Picker · r12
{["Watch later", "Add to playlist", "Copy link"].map((t) => ( {t} ))}
Tooltip · .glass-menu r8
); } // ── B6 · the public watch page ─────────────────────────────────────────────────────────────────── function WatchPage() { return (
Siftlode
the browser's own video controls
Building a home lab on a budget
Techno Tim
} className="font-medium"> Repeat }> Download
); } // ── B7 · the nav rail (the owner's combination) ────────────────────────────────────────────────── // A soft tint + an edge bar for "you are here"; labels optional (a per-user Settings switch). Slim: the // rail is 3.75rem wide in BOTH modes (labels add height, never width), so each module needs a short // rail label that fits the item; the full name stays in the hover tooltip. Each rail reports whether // all its labels fit, measured live (it re-measures on the lab's text-size slider). type NavDef = { icon: typeof Home; en: string; hu: string }; const NAV: NavDef[] = [ { icon: Home, en: "Home", hu: "Kezdőlap" }, { icon: Youtube, en: "Feed", hu: "Feed" }, { icon: Tv, en: "Channels", hu: "Csatornák" }, { icon: ListVideo, en: "Playlists", hu: "Listák" }, { icon: Clapperboard, en: "Plex", hu: "Plex" }, { icon: Bell, en: "Alerts", hu: "Értesítés" }, { icon: MessageSquare, en: "Messages", hu: "Üzenetek" }, { icon: Download, en: "Downloads", hu: "Letöltések" }, ]; const SYSTEM: NavDef[] = [ { icon: BarChart3, en: "Stats", hu: "Statisztika" }, { icon: Users, en: "Users", hu: "Fiókok" }, { icon: Settings, en: "Settings", hu: "Beállítások" }, ]; function RailItem({ d, on, lang }: { d: NavDef; on: boolean; lang?: "en" | "hu" }) { const I = d.icon; return ( {on && ( )} {lang && ( {d[lang]} )} ); } function Rail({ lang }: { lang?: "en" | "hu" }) { const ref = useRef(null); const [clipped, setClipped] = useState([]); useLayoutEffect(() => { const measure = () => setClipped( [...(ref.current?.querySelectorAll("[data-rail-label]") ?? [])] .filter((e) => e.scrollWidth > e.clientWidth) .map((e) => e.textContent ?? ""), ); measure(); const mo = new MutationObserver(measure); mo.observe(document.documentElement, { attributes: true }); return () => mo.disconnect(); }, []); return (
{NAV.map((d, i) => ( ))} {SYSTEM.map((d) => ( ))}
{lang && ( {clipped.length ? `clipped: ${clipped.join(", ")}` : "every label fits ✓"} )}
); } // ── B8 · loading ───────────────────────────────────────────────────────────────────────────────── function Loading() { return ( <> {(["grid", "poster", "list", "table"] as const).map((shape) => (
))} ); } // ── B9 · the fade edges (1–4 chosen) ───────────────────────────────────────────────────────────── const LINES = Array.from({ length: 14 }, (_, i) => `Row ${i + 1} · Some content that scrolls`); function ScrollBody() { return (
{LINES.map((l) => ( {l} ))}
); } const CHIPS = ["Unwatched", "Shorts", "Live", "Music", "Tech", "Science", "News", "Gaming"]; function Fades() { return (
Title · Channel · Date
Edit details
Cancel }> Save
{CHIPS.map((c) => ( {c} ))}
); } export function BatchB() { return (
{SCHEMES.flatMap((s) => (["dark", "light"] as const).map((m) => ), )}
); }