feat(frontend): M9 模态 UI(mode 系统+共享 store+WS 客户端 + chat(思维链/工具回显/情绪徽标)/code/Agent 监控/后台思考页 + Miuix 设计 token + PWA);tsc+vite build 通过

This commit is contained in:
Aska committed 2026-09-06 07:44:40 +08:00
1 parent b3ffd51736
commit 4c48e94659
40 files changed
+5321

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
/** Miuix-style primitives: Button / Tag / Switch / Spinner / Surface / Empty.
* Springs are applied only to interactive elements (cubic-bezier(0.34,1.56,0.64,1)). */
type ButtonVariant = "primary" | "soft" | "ghost" | "outline" | "danger";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: "sm" | "md";
children: ReactNode;
}
const buttonVariant: Record<ButtonVariant, string> = {
primary: "bg-[var(--mu-accent)] text-white hover:bg-[var(--mu-accent-hi)] shadow-[var(--mu-shadow-2)]",
soft: "bg-[var(--mu-surface-4)] text-[var(--mu-text-hi)] hover:bg-[var(--mu-surface-5)]",
ghost: "text-[var(--mu-text-mid)] hover:bg-[var(--mu-surface-4)]",
outline:
"border border-[var(--mu-glass-border)] text-[var(--mu-text-hi)] hover:bg-[var(--mu-surface-4)]",
danger: "bg-[var(--mu-err)] text-white",
};
export function Button({
variant = "soft",
size = "md",
className = "",
children,
...rest
}: ButtonProps): React.ReactNode {
const pad = size === "sm" ? "px-2.5 py-1 text-xs" : "px-4 py-2 text-sm";
return (
<button
className={`inline-flex items-center justify-center gap-1.5 font-medium transition-all duration-200 ease-[var(--mu-spring)] active:scale-[0.97] disabled:opacity-50 disabled:pointer-events-none ${pad} ${buttonVariant[variant]} ${className}`}
{...rest}
>
{children}
</button>
);
}
export type TagTone = "accent" | "ok" | "warn" | "err" | "neutral";
export interface TagProps {
tone?: TagTone;
children: ReactNode;
className?: string;
}
const tagTone: Record<TagTone, string> = {
accent: "bg-[var(--mu-accent)]/15 text-[var(--mu-accent-hi)]",
ok: "bg-[var(--mu-ok)]/15 text-[var(--mu-ok)]",
warn: "bg-[var(--mu-warn)]/15 text-[var(--mu-warn)]",
err: "bg-[var(--mu-err)]/15 text-[var(--mu-err)]",
neutral: "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)]",
};
export function Tag({ tone = "neutral", children, className = "" }: TagProps): React.ReactNode {
return (
<span
className={`inline-flex items-center gap-1 rounded-[var(--mu-radius-sm)] px-2 py-0.5 text-xs font-medium ${tagTone[tone]} ${className}`}
>
{children}
</span>
);
}
export interface SwitchProps {
checked: boolean;
onChange: (next: boolean) => void;
disabled?: boolean;
label?: string;
}
export function Switch({ checked, onChange, disabled, label }: SwitchProps): React.ReactNode {
return (
<label
className={`flex items-center gap-2 select-none ${disabled ? "opacity-50 pointer-events-none" : ""}`}
>
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
onClick={() => onChange(!checked)}
className={`relative h-6 w-11 shrink-0 rounded-[var(--mu-radius-pill)] transition-colors duration-200 ${
checked ? "bg-[var(--mu-accent)]" : "bg-[var(--mu-surface-5)]"
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow-md transition-transform duration-300 ease-[var(--mu-spring)] ${
checked ? "translate-x-[22px]" : "translate-x-0.5"
}`}
/>
</button>
{label ? <span className="text-sm text-[var(--mu-text-mid)]">{label}</span> : null}
</label>
);
}
export interface SpinnerProps {
size?: number;
label?: string;
}
export function Spinner({ size = 18, label }: SpinnerProps): React.ReactNode {
return (
<span className="inline-flex items-center gap-2 text-[var(--mu-text-lo)]">
<span
className="inline-block animate-spin rounded-full border-2 border-[var(--mu-surface-5)] border-t-[var(--mu-accent)]"
style={{ width: size, height: size }}
/>
{label ? <span className="text-xs">{label}</span> : null}
</span>
);
}
export interface SurfaceProps extends HTMLAttributes<HTMLDivElement> {
glass?: boolean;
liquid?: boolean;
gap?: "none" | "sm" | "md";
children: ReactNode;
}
export function Surface({
glass = false,
liquid = false,
gap = "md",
className = "",
children,
...rest
}: SurfaceProps): React.ReactNode {
const pad = gap === "none" ? "" : gap === "sm" ? "p-3" : "p-5";
const base = glass
? "glass"
: liquid
? "liquid-glass"
: "bg-[var(--mu-surface-3)] border border-[var(--mu-glass-border)] shadow-[var(--mu-shadow-1)]";
return (
<div className={`rounded-[var(--mu-radius-lg)] ${base} ${pad} ${className}`} {...rest}>
{children}
</div>
);
}
export interface EmptyStateProps {
icon?: ReactNode;
title: string;
hint?: string;
}
export function EmptyState({ icon, title, hint }: EmptyStateProps): React.ReactNode {
return (
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
{icon ? <div className="text-3xl opacity-60">{icon}</div> : null}
<div className="text-sm font-medium text-[var(--mu-text-mid)]">{title}</div>
{hint ? <div className="text-xs text-[var(--mu-text-lo)]">{hint}</div> : null}
</div>
);
}