Files
next-tjwater-frontend/src/features/workbench/components/workbench-panel-ui.tsx
T
jiang 617ba4d87c feat(ui): refine gem-blue acrylic palette and focus borders
Derive the gem-blue palette with OKLCH tonal and complementary-color
rules while preserving acrylic opacity, blur and surface hierarchy.

Darker focus tokens exposed stacked field rings, fallback outlines and
persistent basemap selection rings. Keep fallback focus styles in the
base layer and use one themed field boundary or inset card stroke.
Preserve keyboard focus, invalid states and native input semantics.

Add browser regression coverage for pointer and keyboard focus, all
basemap presentations, narrow layouts and rendered contrast.

Validation in an isolated checkout: pnpm lint, pnpm typecheck,
pnpm test (393 tests), focused browser suites and pnpm build.
2026-10-09 10:57:06 +08:00

136 lines
4.4 KiB
TypeScript

import type { ComponentProps, ReactNode } from "react";
import { AlertCircle, CheckCircle2, CircleHelp, Info, TriangleAlert } from "lucide-react";
import { cn } from "@/shared/ui/cn";
export type WorkbenchTone = "neutral" | "info" | "success" | "warning" | "danger" | "teal";
const sectionTone: Record<WorkbenchTone, string> = {
neutral: "bg-[var(--surface-control)] text-slate-600",
info: "bg-[var(--action-soft)] text-[var(--action-blue-hover)]",
success: "bg-emerald-50 text-emerald-700",
warning: "bg-[var(--status-warning-soft)] text-[var(--status-warning-foreground)]",
danger: "bg-rose-50 text-rose-700",
teal: "bg-cyan-50 text-cyan-800"
};
const noticeTone: Record<Exclude<WorkbenchTone, "teal">, string> = {
neutral: "border-[var(--surface-border)] bg-[var(--surface-reading)] text-slate-700",
info: "border-[var(--action-blue)] bg-[var(--action-soft)]/80 text-[var(--action-blue-pressed)]",
success: "border-emerald-500 bg-emerald-50/80 text-emerald-950",
warning: "border-[var(--status-warning-mark)] bg-[var(--status-warning-soft)]/90 text-[var(--status-warning-foreground)]",
danger: "border-rose-500 bg-rose-50/90 text-rose-950"
};
const noticeIcon = {
neutral: CircleHelp,
info: Info,
success: CheckCircle2,
warning: TriangleAlert,
danger: AlertCircle
};
export function WorkbenchSection({
title,
description,
icon,
meta,
actions,
tone = "neutral",
bodyClassName,
className,
children,
...props
}: ComponentProps<"section"> & {
title: ReactNode;
description?: ReactNode;
icon?: ReactNode;
meta?: ReactNode;
actions?: ReactNode;
tone?: WorkbenchTone;
bodyClassName?: string;
}) {
return (
<section
className={cn(
"overflow-hidden rounded-[14px] border border-[var(--surface-divider)]/90 bg-[var(--surface-reading)] shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
className
)}
{...props}
>
<div className="flex min-h-14 items-start gap-3 border-b border-[var(--surface-divider)]/75 bg-[var(--surface-control)] px-3.5 py-3">
{icon ? (
<span className={cn("mt-0.5 grid size-7 shrink-0 place-items-center rounded-lg", sectionTone[tone])}>
{icon}
</span>
) : null}
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<h3 className="text-[13px] font-semibold leading-5 text-slate-900">{title}</h3>
{meta}
</div>
{description ? <p className="mt-0.5 text-xs leading-5 text-[var(--ink-muted)]">{description}</p> : null}
</div>
{actions ? <div className="flex shrink-0 items-center gap-1">{actions}</div> : null}
</div>
<div className={cn("p-3.5", bodyClassName)}>{children}</div>
</section>
);
}
export function WorkbenchNotice({
tone = "info",
title,
icon,
className,
children,
...props
}: ComponentProps<"div"> & {
tone?: Exclude<WorkbenchTone, "teal">;
title?: ReactNode;
icon?: ReactNode;
}) {
const Icon = noticeIcon[tone];
return (
<div
className={cn(
"flex min-w-0 gap-2.5 rounded-r-lg border-l-2 px-3 py-2.5 text-xs leading-5",
noticeTone[tone],
className
)}
{...props}
>
<span className="mt-0.5 grid size-4 shrink-0 place-items-center" aria-hidden="true">
{icon ?? <Icon size={15} />}
</span>
<div className="min-w-0 flex-1 break-words">
{title ? <p className="font-semibold">{title}</p> : null}
<div className={cn(title && "mt-0.5")}>{children}</div>
</div>
</div>
);
}
export function WorkbenchEmptyState({
icon,
title,
description,
action,
className
}: {
icon?: ReactNode;
title: ReactNode;
description?: ReactNode;
action?: ReactNode;
className?: string;
}) {
return (
<div className={cn("rounded-xl border border-dashed border-[var(--surface-border)] bg-[var(--surface-reading)]/70 px-4 py-5 text-center", className)}>
{icon ? <span className="mx-auto grid size-8 place-items-center rounded-full bg-[var(--surface-reading)] text-[var(--ink-muted)] shadow-xs">{icon}</span> : null}
<p className={cn("text-xs font-semibold text-slate-700", icon && "mt-2")}>{title}</p>
{description ? <p className="mx-auto mt-1 max-w-72 text-[11px] leading-5 text-[var(--ink-muted)]">{description}</p> : null}
{action ? <div className="mt-3 flex justify-center">{action}</div> : null}
</div>
);
}