feat(web): W3-1 shared console routes, nav, api/sse/query-keys and shared components
- routes.ts registers all workspaces under /console (+ detail routes, engine, audit, login) - api.ts: cookie session first (credentials include, CSRF header), Bearer only with stored key, 401 -> /login - sse.ts: shared EventSource with ?after=seq catch-up, backoff reconnect, per-type subscribe - shared components: data-table, filter-bar, detail-sheet, timeline, json-view, confirm-dialog, form-field, kv-list, stat-tile, error/loading states; ui tabs and textarea Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a24bda952b
commit
e5e42b1f0d
32 files changed
+1286
-71
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import type { StatusTone } from "./status-badge.tsx";
|
||||
|
||||
const TONE_TEXT: Record<StatusTone, string> = {
|
||||
success: "text-success",
|
||||
warning: "text-warning",
|
||||
destructive: "text-destructive",
|
||||
neutral: "text-foreground",
|
||||
};
|
||||
|
||||
/**
|
||||
* 数字磁贴:标签 + 大号数值 + 说明。tone 只给数值上色,标签/说明仍要用文字说清含义。
|
||||
* plain 用于已在卡片内(不再叠 pg-slab)。onClick 存在时整块可点(筛选联动)。
|
||||
*/
|
||||
export function StatTile({
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
icon: Icon,
|
||||
tone = "neutral",
|
||||
active = false,
|
||||
onClick,
|
||||
plain = false,
|
||||
className,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
hint?: ReactNode;
|
||||
icon?: LucideIcon;
|
||||
tone?: StatusTone;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
plain?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 text-meta text-muted-foreground">
|
||||
{Icon ? <Icon className="h-3.5 w-3.5" /> : null}
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
<div className={cn("mt-1 text-title-sm font-semibold tabular-nums", TONE_TEXT[tone])}>{value}</div>
|
||||
{hint ? <div className="mt-0.5 truncate text-meta text-muted-foreground">{hint}</div> : null}
|
||||
</>
|
||||
);
|
||||
const base = cn(
|
||||
"min-w-0 rounded-xl px-4 py-3 text-left",
|
||||
plain ? "border border-border/70" : "pg-slab",
|
||||
active && "ring-2 ring-ring/40",
|
||||
className,
|
||||
);
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={cn(base, "cursor-pointer transition-colors duration-micro hover:bg-foreground/[0.04]")}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <div className={base}>{content}</div>;
|
||||
}
|
||||
Reference in new issue
Block a user