spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { Loader2, X } from "lucide-react";3import { useEffect, useId, type ReactNode } from "react";45export const cx = (...a: (string | false | null | undefined)[]) => a.filter(Boolean).join(" ");67export function Card({ title, subtitle, actions, children, className, id }: { title?: ReactNode; subtitle?: ReactNode; actions?: ReactNode; children: ReactNode; className?: string; id?: string }) {8 return (9 <section id={id} className={cx("rounded-lg border border-line bg-bg-elev print-card", className)}>10 {(title || actions) && (11 <header className="flex items-start justify-between gap-3 border-b border-line px-4 py-3">12 <div>13 {title && <h2 className="text-sm font-semibold text-fg">{title}</h2>}14 {subtitle && <p className="mt-0.5 text-xs text-fg-muted">{subtitle}</p>}15 </div>16 {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}17 </header>18 )}19 <div className="p-4">{children}</div>20 </section>21 );22}2324export function Tile({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone?: "up" | "down" | "warn" | "muted" }) {25 const t = tone === "up" ? "text-up" : tone === "down" ? "text-down" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-fg-muted" : "text-fg";26 return (27 <div className="rounded-lg border border-line bg-bg-elev px-4 py-3">28 <div className="text-xs uppercase tracking-wide text-fg-dim">{label}</div>29 <div className={cx("mt-1 text-xl font-semibold num", t)}>{value}</div>30 {sub && <div className="mt-0.5 text-xs text-fg-muted">{sub}</div>}31 </div>32 );33}3435type BtnProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: "primary" | "ghost" | "danger" | "outline"; size?: "sm" | "md"; loading?: boolean };36export function Button({ variant = "outline", size = "md", loading, className, children, disabled, ...rest }: BtnProps) {37 const v =38 variant === "primary" ? "bg-accent text-white hover:bg-accent/90 border-transparent" :39 variant === "danger" ? "bg-down text-white hover:bg-down/90 border-transparent" :40 variant === "ghost" ? "border-transparent hover:bg-bg-sub text-fg-muted hover:text-fg" :41 "border-line-strong bg-bg-sub hover:bg-line text-fg";42 const s = size === "sm" ? "h-7 px-2.5 text-xs" : "h-9 px-3.5 text-sm";43 return (44 <button {...rest} disabled={disabled || loading} className={cx("inline-flex items-center justify-center gap-1.5 rounded-md border font-medium transition disabled:cursor-not-allowed disabled:opacity-50", v, s, className)}>45 {loading && <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />}46 {children}47 </button>48 );49}5051export function Field({ label, hint, children, htmlFor }: { label: ReactNode; hint?: ReactNode; children: ReactNode; htmlFor?: string }) {52 return (53 <label htmlFor={htmlFor} className="block text-xs">54 <span className="mb-1 block font-medium text-fg-muted">{label}</span>55 {children}56 {hint && <span className="mt-1 block text-[11px] text-fg-dim">{hint}</span>}57 </label>58 );59}6061const inputCls = "h-9 w-full rounded-md border border-line-strong bg-bg px-2.5 text-sm text-fg placeholder:text-fg-dim focus:border-accent";62export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {63 return <input {...props} className={cx(inputCls, props.type === "number" && "num", props.className)} />;64}65export function Select(props: React.SelectHTMLAttributes<HTMLSelectElement>) {66 return <select {...props} className={cx(inputCls, props.className)} />;67}68export function Textarea(props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {69 return <textarea {...props} className={cx("w-full rounded-md border border-line-strong bg-bg px-2.5 py-2 font-mono text-xs text-fg focus:border-accent", props.className)} />;70}7172export function Badge({ children, tone = "muted", className }: { children: ReactNode; tone?: "up" | "down" | "warn" | "muted" | "accent" | "live"; className?: string }) {73 const t = {74 up: "bg-up/15 text-up border-up/30",75 down: "bg-down/15 text-down border-down/30",76 warn: "bg-warn/15 text-warn border-warn/30",77 muted: "bg-bg-sub text-fg-muted border-line-strong",78 accent: "bg-accent-soft text-accent border-accent/40",79 live: "bg-live text-white border-live",80 }[tone];81 return <span className={cx("inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[11px] font-medium uppercase tracking-wide", t, className)}>{children}</span>;82}8384export function Dot({ ok, className }: { ok: boolean | null | undefined; className?: string }) {85 return <span aria-hidden className={cx("inline-block h-2 w-2 rounded-full", ok === true ? "bg-up" : ok === false ? "bg-down" : "bg-fg-dim", className)} />;86}8788export function Spinner({ label = "Loading…" }: { label?: string }) {89 return (90 <div className="flex items-center gap-2 py-6 text-sm text-fg-muted" role="status">91 <Loader2 className="h-4 w-4 animate-spin" aria-hidden /> {label}92 </div>93 );94}9596export function Empty({ title, children }: { title: string; children?: ReactNode }) {97 return (98 <div className="rounded-md border border-dashed border-line-strong px-4 py-6 text-center">99 <div className="text-sm font-medium text-fg-muted">{title}</div>100 {children && <div className="mt-1 text-xs text-fg-dim">{children}</div>}101 </div>102 );103}104105export function ErrorBox({ error }: { error: unknown }) {106 const msg = error instanceof Error ? error.message : String(error);107 return <div role="alert" className="rounded-md border border-down/40 bg-down/10 px-3 py-2 text-xs text-down">{msg}</div>;108}109110export function Table({ children, className }: { children: ReactNode; className?: string }) {111 return (112 <div className={cx("overflow-x-auto scroll-thin", className)}>113 <table className="w-full border-collapse text-xs">{children}</table>114 </div>115 );116}117export function Th({ children, className, align = "left", scope = "col" }: { children?: ReactNode; className?: string; align?: "left" | "right" | "center"; scope?: "col" | "row" }) {118 return <th scope={scope} className={cx("border-b border-line px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-fg-dim", align === "right" ? "text-right" : align === "center" ? "text-center" : "text-left", className)}>{children}</th>;119}120export function Td({ children, className, align = "left", num, title }: { children?: ReactNode; className?: string; align?: "left" | "right" | "center"; num?: boolean; title?: string }) {121 return <td title={title} className={cx("border-b border-line/60 px-2 py-1.5 align-middle", align === "right" ? "text-right" : align === "center" ? "text-center" : "text-left", num && "num", className)}>{children}</td>;122}123124export function Dialog({ open, onClose, title, children, footer }: { open: boolean; onClose: () => void; title: string; children: ReactNode; footer?: ReactNode }) {125 const id = useId();126 useEffect(() => {127 if (!open) return;128 const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();129 window.addEventListener("keydown", onKey);130 return () => window.removeEventListener("keydown", onKey);131 }, [open, onClose]);132 if (!open) return null;133 return (134 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>135 <div role="dialog" aria-modal="true" aria-labelledby={id} className="w-full max-w-md rounded-lg border border-line-strong bg-bg-elev shadow-xl" onClick={(e) => e.stopPropagation()}>136 <header className="flex items-center justify-between border-b border-line px-4 py-3">137 <h2 id={id} className="text-sm font-semibold">{title}</h2>138 <button onClick={onClose} aria-label="Close" className="rounded p-1 text-fg-muted hover:bg-bg-sub hover:text-fg"><X className="h-4 w-4" /></button>139 </header>140 <div className="px-4 py-3 text-sm">{children}</div>141 {footer && <footer className="flex justify-end gap-2 border-t border-line px-4 py-3">{footer}</footer>}142 </div>143 </div>144 );145}146147export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: ReactNode; actions?: ReactNode }) {148 return (149 <div className="mb-4 flex flex-wrap items-end justify-between gap-3">150 <div>151 <h1 className="text-lg font-semibold text-fg">{title}</h1>152 {subtitle && <p className="mt-0.5 text-xs text-fg-muted">{subtitle}</p>}153 </div>154 {actions && <div className="flex items-center gap-2">{actions}</div>}155 </div>156 );157}158159export function KV({ items }: { items: [ReactNode, ReactNode][] }) {160 return (161 <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs">162 {items.map(([k, v], i) => (163 <div key={i} className="contents">164 <dt className="text-fg-dim">{k}</dt>165 <dd className="num break-all text-fg">{v}</dd>166 </div>167 ))}168 </dl>169 );170}171