873 lines
35 KiB
TypeScript
873 lines
35 KiB
TypeScript
export type SkinThemeMode = "light" | "dark";
|
|
export type SkinShadowStyle = "none" | "soft" | "strong";
|
|
|
|
export type SkinButtonFill = {
|
|
mode: "solid" | "gradient";
|
|
angle: number;
|
|
start: string;
|
|
end: string;
|
|
hoverStart: string;
|
|
hoverEnd: string;
|
|
activeStart: string;
|
|
activeEnd: string;
|
|
foreground: string;
|
|
};
|
|
|
|
export const DEFAULT_BUTTON_GRADIENT: SkinButtonFill = {
|
|
mode: "gradient", angle: 115,
|
|
start: "#6554df", end: "#386fbc",
|
|
hoverStart: "#5744cf", hoverEnd: "#356bbb",
|
|
activeStart: "#4938b8", activeEnd: "#2c5da5", foreground: "#ffffff",
|
|
};
|
|
|
|
export const SKIN_BUTTON_COLOR_FIELDS = [
|
|
{ key: "start", label: "渐变起色" }, { key: "end", label: "渐变终色" },
|
|
{ key: "hoverStart", label: "悬停起色" }, { key: "hoverEnd", label: "悬停终色" },
|
|
{ key: "activeStart", label: "按下起色" }, { key: "activeEnd", label: "按下终色" },
|
|
{ key: "foreground", label: "渐变按钮文字" },
|
|
] as const;
|
|
|
|
export type SkinModeTokens = {
|
|
canvas: string;
|
|
surface: string;
|
|
surfaceSubtle: string;
|
|
surfaceRaised: string;
|
|
overlay: string;
|
|
text: string;
|
|
textMuted: string;
|
|
border: string;
|
|
control: string;
|
|
controlHover: string;
|
|
controlActive: string;
|
|
controlBorder: string;
|
|
controlFocus: string;
|
|
controlDisabledBackground: string;
|
|
controlDisabledForeground: string;
|
|
switchChecked: string;
|
|
switchCheckedHover: string;
|
|
switchCheckedHandle: string;
|
|
switchUnchecked: string;
|
|
switchUncheckedHover: string;
|
|
switchUncheckedHandle: string;
|
|
primary: string;
|
|
primaryHover: string;
|
|
primaryActive: string;
|
|
primaryForeground: string;
|
|
selected: string;
|
|
selectedHover: string;
|
|
selectedActive: string;
|
|
selectedForeground: string;
|
|
icon: string;
|
|
iconMuted: string;
|
|
iconActive: string;
|
|
success: string;
|
|
warning: string;
|
|
danger: string;
|
|
dangerHover: string;
|
|
dangerActive: string;
|
|
dangerForeground: string;
|
|
info: string;
|
|
workspace: string;
|
|
workspaceGrid: string;
|
|
adminBackground: string;
|
|
adminSurface: string;
|
|
adminSubtle: string;
|
|
adminStrong: string;
|
|
authBackground: string;
|
|
authPanel: string;
|
|
authCard: string;
|
|
authAccent: string;
|
|
authMuted: string;
|
|
};
|
|
|
|
export type SkinComponentTokens = {
|
|
buttonRadius: number;
|
|
inputRadius: number;
|
|
cardRadius: number;
|
|
overlayRadius: number;
|
|
menuRadius: number;
|
|
checkboxRadius: number;
|
|
controlHeight: number;
|
|
controlHeightSmall: number;
|
|
controlHeightLarge: number;
|
|
borderWidth: number;
|
|
focusRingWidth: number;
|
|
iconSize: number;
|
|
buttonFontWeight: number;
|
|
hoverLift: number;
|
|
motionFast: number;
|
|
motionNormal: number;
|
|
shadowStyle: SkinShadowStyle;
|
|
};
|
|
|
|
export type SkinTokens = {
|
|
light: SkinModeTokens;
|
|
dark: SkinModeTokens;
|
|
components: SkinComponentTokens;
|
|
buttons: Record<SkinThemeMode, SkinButtonFill>;
|
|
};
|
|
|
|
export type SkinDefinition = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
locked: boolean;
|
|
tokens: SkinTokens;
|
|
};
|
|
|
|
export type SkinAntOverrides = {
|
|
text: string;
|
|
textMuted: string;
|
|
primary: string;
|
|
primaryHover: string;
|
|
primaryActive: string;
|
|
primaryForeground: string;
|
|
selected: string;
|
|
selectedHover: string;
|
|
selectedActive: string;
|
|
selectedForeground: string;
|
|
controlSurface: string;
|
|
controlHover: string;
|
|
controlActive: string;
|
|
controlBorder: string;
|
|
controlFocus: string;
|
|
controlDisabledBackground: string;
|
|
controlDisabledForeground: string;
|
|
switchChecked: string;
|
|
switchCheckedHover: string;
|
|
switchCheckedHandle: string;
|
|
switchUnchecked: string;
|
|
switchUncheckedHover: string;
|
|
switchUncheckedHandle: string;
|
|
elevatedBackground: string;
|
|
subtleBackground: string;
|
|
menuBackground: string;
|
|
menuForeground: string;
|
|
icon: string;
|
|
iconHover: string;
|
|
success: string;
|
|
warning: string;
|
|
danger: string;
|
|
dangerHover: string;
|
|
dangerActive: string;
|
|
dangerForeground: string;
|
|
info: string;
|
|
borderRadius: number;
|
|
borderRadiusLG: number;
|
|
borderRadiusSM: number;
|
|
buttonRadius: number;
|
|
inputRadius: number;
|
|
overlayRadius: number;
|
|
menuRadius: number;
|
|
checkboxRadius: number;
|
|
controlHeight: number;
|
|
controlHeightSmall: number;
|
|
controlHeightLarge: number;
|
|
borderWidth: number;
|
|
iconSize: number;
|
|
buttonFontWeight: number;
|
|
motionFast: number;
|
|
motionNormal: number;
|
|
shadowStyle: SkinShadowStyle;
|
|
};
|
|
|
|
export type SkinColorField = {
|
|
key: keyof SkinModeTokens;
|
|
label: string;
|
|
help: string;
|
|
};
|
|
|
|
export type SkinColorGroup = {
|
|
key: string;
|
|
label: string;
|
|
fields: readonly SkinColorField[];
|
|
};
|
|
|
|
export type SkinComponentNumberField = {
|
|
key: Exclude<keyof SkinComponentTokens, "shadowStyle">;
|
|
label: string;
|
|
suffix: string;
|
|
min: number;
|
|
max: number;
|
|
step?: number;
|
|
};
|
|
|
|
export const SKIN_COLOR_GROUPS: readonly SkinColorGroup[] = [
|
|
{
|
|
key: "foundation",
|
|
label: "基础表面与文字",
|
|
fields: [
|
|
{ key: "canvas", label: "页面背景", help: "全站页面与画布底色" },
|
|
{ key: "surface", label: "基础表面", help: "卡片、常规容器与输入区" },
|
|
{ key: "surfaceSubtle", label: "次级表面", help: "弱分区、表头与悬停底色" },
|
|
{ key: "surfaceRaised", label: "强调表面", help: "高层级卡片与激活容器" },
|
|
{ key: "overlay", label: "浮层表面", help: "下拉、弹窗和气泡菜单" },
|
|
{ key: "text", label: "主要文字", help: "标题和正文" },
|
|
{ key: "textMuted", label: "次要文字", help: "说明、占位和元数据" },
|
|
{ key: "border", label: "分隔与边界", help: "普通描边和分隔线" },
|
|
],
|
|
},
|
|
{
|
|
key: "controls",
|
|
label: "控件与交互状态",
|
|
fields: [
|
|
{ key: "control", label: "控件背景", help: "输入框、复选框和普通按钮" },
|
|
{ key: "controlHover", label: "控件悬停", help: "普通控件 hover 背景" },
|
|
{ key: "controlActive", label: "控件按下", help: "普通控件 active 背景" },
|
|
{ key: "controlBorder", label: "控件边框", help: "输入与选择控件边界" },
|
|
{ key: "controlFocus", label: "键盘焦点", help: "focus-visible 与输入焦点" },
|
|
{ key: "controlDisabledBackground", label: "禁用背景", help: "不可操作控件背景" },
|
|
{ key: "controlDisabledForeground", label: "禁用前景", help: "不可操作文字和图标" },
|
|
{ key: "switchChecked", label: "开关开启", help: "Switch 开启轨道,须与关闭态明显区分" },
|
|
{ key: "switchCheckedHover", label: "开启悬停", help: "开启状态 hover 轨道" },
|
|
{ key: "switchCheckedHandle", label: "开启滑块", help: "开启状态圆形滑块" },
|
|
{ key: "switchUnchecked", label: "开关关闭", help: "Switch 关闭轨道" },
|
|
{ key: "switchUncheckedHover", label: "关闭悬停", help: "关闭状态 hover 轨道" },
|
|
{ key: "switchUncheckedHandle", label: "关闭滑块", help: "关闭状态圆形滑块" },
|
|
{ key: "primary", label: "主操作", help: "纯色主按钮与链接;开关使用独立颜色" },
|
|
{ key: "primaryHover", label: "主操作悬停", help: "主要操作 hover" },
|
|
{ key: "primaryActive", label: "主操作按下", help: "主要操作 active" },
|
|
{ key: "primaryForeground", label: "主操作前景", help: "主按钮上的文字和图标" },
|
|
{ key: "selected", label: "选中背景", help: "菜单、单选与分段控件" },
|
|
{ key: "selectedHover", label: "选中悬停", help: "已选项 hover" },
|
|
{ key: "selectedActive", label: "选中按下", help: "已选项 active" },
|
|
{ key: "selectedForeground", label: "选中前景", help: "已选项文字和图标" },
|
|
],
|
|
},
|
|
{
|
|
key: "signals",
|
|
label: "图标与状态",
|
|
fields: [
|
|
{ key: "icon", label: "普通图标", help: "工具栏和普通功能图标" },
|
|
{ key: "iconMuted", label: "弱化图标", help: "低优先级和占位图标" },
|
|
{ key: "iconActive", label: "激活图标", help: "当前工具和强调图标" },
|
|
{ key: "success", label: "成功", help: "完成与可用状态" },
|
|
{ key: "warning", label: "警告", help: "待处理与风险提示" },
|
|
{ key: "danger", label: "危险", help: "错误、删除与破坏性操作" },
|
|
{ key: "dangerHover", label: "危险悬停", help: "破坏性操作 hover" },
|
|
{ key: "dangerActive", label: "危险按下", help: "破坏性操作 active" },
|
|
{ key: "dangerForeground", label: "危险前景", help: "实心危险按钮文字和图标" },
|
|
{ key: "info", label: "信息", help: "普通说明和处理中状态" },
|
|
],
|
|
},
|
|
{
|
|
key: "scenes",
|
|
label: "业务场景表面",
|
|
fields: [
|
|
{ key: "workspace", label: "创作工作区", help: "工作区卡片和画布承载面" },
|
|
{ key: "workspaceGrid", label: "画布网格", help: "画布网格与空间辅助线" },
|
|
{ key: "adminBackground", label: "后台底层", help: "管理后台页面底色" },
|
|
{ key: "adminSurface", label: "后台卡片", help: "管理后台主卡片" },
|
|
{ key: "adminSubtle", label: "后台次级层", help: "后台弱分区" },
|
|
{ key: "adminStrong", label: "后台强调层", help: "后台高对比区块" },
|
|
{ key: "authBackground", label: "登录页背景", help: "登录页整体底色" },
|
|
{ key: "authPanel", label: "登录表单区", help: "登录页右侧表单面板" },
|
|
{ key: "authCard", label: "登录卡片", help: "登录表单卡片" },
|
|
{ key: "authAccent", label: "登录页强调", help: "登录页品牌强调文字" },
|
|
{ key: "authMuted", label: "登录页次要文字", help: "登录页说明和辅助信息" },
|
|
],
|
|
},
|
|
] as const;
|
|
|
|
export const SKIN_COMPONENT_NUMBER_FIELDS: readonly SkinComponentNumberField[] = [
|
|
{ key: "buttonRadius", label: "按钮圆角", suffix: "px", min: 0, max: 32 },
|
|
{ key: "inputRadius", label: "输入框圆角", suffix: "px", min: 0, max: 32 },
|
|
{ key: "cardRadius", label: "卡片圆角", suffix: "px", min: 0, max: 40 },
|
|
{ key: "overlayRadius", label: "弹窗与浮层圆角", suffix: "px", min: 0, max: 40 },
|
|
{ key: "menuRadius", label: "菜单圆角", suffix: "px", min: 0, max: 32 },
|
|
{ key: "checkboxRadius", label: "复选框圆角", suffix: "px", min: 0, max: 12 },
|
|
{ key: "controlHeight", label: "标准控件高度", suffix: "px", min: 30, max: 48 },
|
|
{ key: "controlHeightSmall", label: "小号控件高度", suffix: "px", min: 24, max: 40 },
|
|
{ key: "controlHeightLarge", label: "大号控件高度", suffix: "px", min: 36, max: 56 },
|
|
{ key: "borderWidth", label: "控件描边", suffix: "px", min: 1, max: 3 },
|
|
{ key: "focusRingWidth", label: "键盘焦点环", suffix: "px", min: 1, max: 4 },
|
|
{ key: "iconSize", label: "标准图标尺寸", suffix: "px", min: 12, max: 24 },
|
|
{ key: "buttonFontWeight", label: "按钮字重", suffix: "", min: 400, max: 700, step: 50 },
|
|
{ key: "hoverLift", label: "卡片悬停抬升", suffix: "px", min: 0, max: 4 },
|
|
{ key: "motionFast", label: "快速反馈时长", suffix: "ms", min: 0, max: 400, step: 10 },
|
|
{ key: "motionNormal", label: "常规过渡时长", suffix: "ms", min: 0, max: 800, step: 10 },
|
|
] as const;
|
|
|
|
export const DEFAULT_CLASSIC_SKIN: SkinDefinition = {
|
|
id: "classic",
|
|
name: "经典黑白",
|
|
description: "项目原始样式 · 不可修改",
|
|
locked: true,
|
|
tokens: {
|
|
light: {
|
|
canvas: "#ffffff",
|
|
surface: "#ffffff",
|
|
surfaceSubtle: "#f7f7f7",
|
|
surfaceRaised: "#ececec",
|
|
overlay: "#ffffff",
|
|
text: "#171717",
|
|
textMuted: "#737373",
|
|
border: "#e5e5e5",
|
|
control: "#ffffff",
|
|
controlHover: "#f5f5f5",
|
|
controlActive: "#ececec",
|
|
controlBorder: "#d1d1d1",
|
|
controlFocus: "#171717",
|
|
controlDisabledBackground: "#f2f2f2",
|
|
controlDisabledForeground: "#a3a3a3",
|
|
switchChecked: "#16a34a",
|
|
switchCheckedHover: "#15803d",
|
|
switchCheckedHandle: "#ffffff",
|
|
switchUnchecked: "#b8b8b8",
|
|
switchUncheckedHover: "#9f9f9f",
|
|
switchUncheckedHandle: "#ffffff",
|
|
primary: "#171717",
|
|
primaryHover: "#303030",
|
|
primaryActive: "#404040",
|
|
primaryForeground: "#ffffff",
|
|
selected: "#e8e8e8",
|
|
selectedHover: "#dedede",
|
|
selectedActive: "#d5d5d5",
|
|
selectedForeground: "#171717",
|
|
icon: "#3f3f46",
|
|
iconMuted: "#a1a1aa",
|
|
iconActive: "#171717",
|
|
success: "#16a34a",
|
|
warning: "#d97706",
|
|
danger: "#dc2626",
|
|
dangerHover: "#b91c1c",
|
|
dangerActive: "#991b1b",
|
|
dangerForeground: "#ffffff",
|
|
info: "#2563eb",
|
|
workspace: "#ffffff",
|
|
workspaceGrid: "#f3f3f3",
|
|
adminBackground: "#f3f4f6",
|
|
adminSurface: "#ffffff",
|
|
adminSubtle: "#f7f8fa",
|
|
adminStrong: "#eceff3",
|
|
authBackground: "#ffffff",
|
|
authPanel: "#f7f7f7",
|
|
authCard: "#ffffff",
|
|
authAccent: "#2563eb",
|
|
authMuted: "#737373",
|
|
},
|
|
dark: {
|
|
canvas: "#0a0a0a",
|
|
surface: "#181818",
|
|
surfaceSubtle: "#202020",
|
|
surfaceRaised: "#2a2a2a",
|
|
overlay: "#1f1f20",
|
|
text: "#f5f5f5",
|
|
textMuted: "#a3a3a3",
|
|
border: "#2d2d2d",
|
|
control: "#202020",
|
|
controlHover: "#292929",
|
|
controlActive: "#333333",
|
|
controlBorder: "#4a4a4a",
|
|
controlFocus: "#f5f5f5",
|
|
controlDisabledBackground: "#252525",
|
|
controlDisabledForeground: "#737373",
|
|
switchChecked: "#22c55e",
|
|
switchCheckedHover: "#4ade80",
|
|
switchCheckedHandle: "#071a0f",
|
|
switchUnchecked: "#525252",
|
|
switchUncheckedHover: "#686868",
|
|
switchUncheckedHandle: "#f5f5f5",
|
|
primary: "#f5f5f5",
|
|
primaryHover: "#ffffff",
|
|
primaryActive: "#e5e5e5",
|
|
primaryForeground: "#171717",
|
|
selected: "#2b2b2b",
|
|
selectedHover: "#343434",
|
|
selectedActive: "#3d3d3d",
|
|
selectedForeground: "#f5f5f5",
|
|
icon: "#d4d4d8",
|
|
iconMuted: "#71717a",
|
|
iconActive: "#ffffff",
|
|
success: "#4ade80",
|
|
warning: "#fbbf24",
|
|
danger: "#f87171",
|
|
dangerHover: "#fca5a5",
|
|
dangerActive: "#ef4444",
|
|
dangerForeground: "#2b0808",
|
|
info: "#60a5fa",
|
|
workspace: "#181818",
|
|
workspaceGrid: "#222222",
|
|
adminBackground: "#101010",
|
|
adminSurface: "#181818",
|
|
adminSubtle: "#202020",
|
|
adminStrong: "#2a2a2a",
|
|
authBackground: "#08090c",
|
|
authPanel: "#0b0c10",
|
|
authCard: "#121318",
|
|
authAccent: "#93c5fd",
|
|
authMuted: "#8a8b91",
|
|
},
|
|
buttons: { light: { ...DEFAULT_BUTTON_GRADIENT }, dark: { ...DEFAULT_BUTTON_GRADIENT } },
|
|
components: {
|
|
buttonRadius: 6,
|
|
inputRadius: 6,
|
|
cardRadius: 12,
|
|
overlayRadius: 12,
|
|
menuRadius: 8,
|
|
checkboxRadius: 4,
|
|
controlHeight: 36,
|
|
controlHeightSmall: 30,
|
|
controlHeightLarge: 42,
|
|
borderWidth: 1,
|
|
focusRingWidth: 2,
|
|
iconSize: 16,
|
|
buttonFontWeight: 500,
|
|
hoverLift: 1,
|
|
motionFast: 120,
|
|
motionNormal: 180,
|
|
shadowStyle: "soft",
|
|
},
|
|
},
|
|
};
|
|
|
|
const HEX_COLOR = /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i;
|
|
const MANAGED_VARIABLES = [
|
|
"--button-primary-bg", "--button-primary-hover-bg", "--button-primary-active-bg", "--button-primary-fg",
|
|
"--background",
|
|
"--foreground",
|
|
"--card",
|
|
"--card-foreground",
|
|
"--popover",
|
|
"--popover-foreground",
|
|
"--primary",
|
|
"--primary-foreground",
|
|
"--secondary",
|
|
"--secondary-foreground",
|
|
"--muted",
|
|
"--muted-foreground",
|
|
"--accent",
|
|
"--accent-foreground",
|
|
"--destructive",
|
|
"--destructive-hover",
|
|
"--destructive-active",
|
|
"--destructive-foreground",
|
|
"--border",
|
|
"--input",
|
|
"--ring",
|
|
"--surface",
|
|
"--surface-hover",
|
|
"--surface-active",
|
|
"--surface-card",
|
|
"--surface-card-hover",
|
|
"--card-surface",
|
|
"--card-surface-hover",
|
|
"--card-surface-raised",
|
|
"--btn-solid-bg",
|
|
"--btn-solid-hover-bg",
|
|
"--btn-solid-active-bg",
|
|
"--btn-solid-fg",
|
|
"--control-selected-bg",
|
|
"--control-selected-hover-bg",
|
|
"--control-selected-fg",
|
|
"--control-selected-border",
|
|
"--control-check-bg",
|
|
"--control-check-fg",
|
|
"--control-switch-checked-bg",
|
|
"--control-switch-checked-hover-bg",
|
|
"--control-switch-checked-handle",
|
|
"--control-switch-off-bg",
|
|
"--control-switch-off-hover-bg",
|
|
"--control-switch-off-handle",
|
|
"--control-disabled-bg",
|
|
"--control-disabled-fg",
|
|
"--control-focus-ring",
|
|
"--workspace-surface",
|
|
"--workspace-surface-strong",
|
|
"--workspace-border",
|
|
"--workspace-border-strong",
|
|
"--workspace-grid-line",
|
|
"--workspace-accent",
|
|
"--workspace-accent-soft",
|
|
"--skin-admin-layer-0",
|
|
"--skin-admin-layer-1",
|
|
"--skin-admin-layer-2",
|
|
"--skin-admin-layer-3",
|
|
"--auth-page-bg",
|
|
"--auth-panel-bg",
|
|
"--auth-card-bg",
|
|
"--auth-accent",
|
|
"--auth-muted",
|
|
"--palette-status-success",
|
|
"--palette-status-error",
|
|
"--palette-status-loading",
|
|
"--palette-status-warning",
|
|
"--icon-foreground",
|
|
"--icon-muted",
|
|
"--icon-active",
|
|
"--r-sm",
|
|
"--r-md",
|
|
"--r-lg",
|
|
"--r-xl",
|
|
"--r-2xl",
|
|
"--radius",
|
|
"--control-radius",
|
|
"--button-radius",
|
|
"--input-radius",
|
|
"--card-radius",
|
|
"--overlay-radius",
|
|
"--menu-radius",
|
|
"--checkbox-radius",
|
|
"--control-border-width",
|
|
"--focus-ring-width",
|
|
"--icon-size",
|
|
"--theme-hover-lift",
|
|
"--motion-instant",
|
|
"--motion-state",
|
|
"--elevation-card",
|
|
"--elevation-card-hover",
|
|
"--elevation-overlay",
|
|
"--theme-admin-card-shadow",
|
|
] as const;
|
|
|
|
export function normalizeSkinDefinition(value: unknown, fallback: SkinDefinition = DEFAULT_CLASSIC_SKIN): SkinDefinition {
|
|
if (!value || typeof value !== "object") return cloneSkinDefinition(fallback);
|
|
const candidate = value as Partial<SkinDefinition>;
|
|
const id = typeof candidate.id === "string" && /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(candidate.id) ? candidate.id : fallback.id;
|
|
const modes = candidate.tokens;
|
|
const light = normalizeSkinMode(modes?.light, fallback.tokens.light);
|
|
const dark = normalizeSkinMode(modes?.dark, fallback.tokens.dark);
|
|
if (!modes || !light || !dark || !isSkinComponents(modes.components)) return cloneSkinDefinition(fallback);
|
|
return {
|
|
id,
|
|
name: typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim().slice(0, 40) : fallback.name,
|
|
description: typeof candidate.description === "string" ? candidate.description.trim().slice(0, 100) : fallback.description,
|
|
locked: id === "classic",
|
|
tokens: { light, dark, components: { ...modes.components }, buttons: {
|
|
light: normalizeButtonFill(modes.buttons?.light, id),
|
|
dark: normalizeButtonFill(modes.buttons?.dark, id),
|
|
} },
|
|
};
|
|
}
|
|
|
|
export function cloneSkinDefinition(source: SkinDefinition): SkinDefinition {
|
|
return { ...source, tokens: cloneSkinTokens(source.tokens) };
|
|
}
|
|
|
|
export function createSkinThemeID(existingIDs: Iterable<string>) {
|
|
const existing = new Set(existingIDs);
|
|
const base = `custom-${Date.now().toString(36)}`;
|
|
let candidate = base;
|
|
let suffix = 2;
|
|
while (existing.has(candidate)) candidate = `${base}-${suffix++}`;
|
|
return candidate;
|
|
}
|
|
|
|
export function duplicateSkinDefinition(source: SkinDefinition, existingIDs: Iterable<string>, name?: string): SkinDefinition {
|
|
const copy = cloneSkinDefinition(source);
|
|
copy.id = createSkinThemeID(existingIDs);
|
|
copy.name = (name || `${source.name} 副本`).slice(0, 40);
|
|
copy.description = source.id === "classic" ? "从经典黑白参数复制,可自由调整" : `复制自 ${source.name}`;
|
|
copy.locked = false;
|
|
return copy;
|
|
}
|
|
|
|
export function skinSwatches(skin: SkinDefinition) {
|
|
const frequencyOrder: readonly (keyof SkinModeTokens)[] = [
|
|
"canvas",
|
|
"surface",
|
|
"text",
|
|
"textMuted",
|
|
"border",
|
|
"surfaceSubtle",
|
|
"control",
|
|
"controlHover",
|
|
"primary",
|
|
"primaryHover",
|
|
"selected",
|
|
"selectedForeground",
|
|
"surfaceRaised",
|
|
"overlay",
|
|
"controlBorder",
|
|
"switchChecked",
|
|
"switchUnchecked",
|
|
"icon",
|
|
"iconMuted",
|
|
"iconActive",
|
|
"success",
|
|
"warning",
|
|
"danger",
|
|
"info",
|
|
"workspace",
|
|
"workspaceGrid",
|
|
"adminBackground",
|
|
"adminSurface",
|
|
"adminSubtle",
|
|
"adminStrong",
|
|
"authBackground",
|
|
"authPanel",
|
|
"authCard",
|
|
"authAccent",
|
|
"authMuted",
|
|
"controlActive",
|
|
"controlFocus",
|
|
"controlDisabledBackground",
|
|
"controlDisabledForeground",
|
|
"primaryActive",
|
|
"primaryForeground",
|
|
"selectedHover",
|
|
"selectedActive",
|
|
"switchCheckedHover",
|
|
"switchCheckedHandle",
|
|
"switchUncheckedHover",
|
|
"switchUncheckedHandle",
|
|
"dangerHover",
|
|
"dangerActive",
|
|
"dangerForeground",
|
|
];
|
|
const colors = frequencyOrder.flatMap((key) => [skin.tokens.light[key], skin.tokens.dark[key]]);
|
|
return [...new Set(colors)];
|
|
}
|
|
|
|
export function normalizeSkinID(value: unknown) {
|
|
return normalizeSkinDefinition(value).id;
|
|
}
|
|
|
|
export function getSkinAntOverrides(value: unknown, mode: SkinThemeMode): Partial<SkinAntOverrides> {
|
|
const skin = normalizeSkinDefinition(value);
|
|
if (skin.id === "classic") return {};
|
|
const color = skin.tokens[mode];
|
|
const component = skin.tokens.components;
|
|
return {
|
|
text: color.text,
|
|
textMuted: color.textMuted,
|
|
primary: color.primary,
|
|
primaryHover: color.primaryHover,
|
|
primaryActive: color.primaryActive,
|
|
primaryForeground: color.primaryForeground,
|
|
selected: color.selected,
|
|
selectedHover: color.selectedHover,
|
|
selectedActive: color.selectedActive,
|
|
selectedForeground: color.selectedForeground,
|
|
controlSurface: color.control,
|
|
controlHover: color.controlHover,
|
|
controlActive: color.controlActive,
|
|
controlBorder: color.controlBorder,
|
|
controlFocus: color.controlFocus,
|
|
controlDisabledBackground: color.controlDisabledBackground,
|
|
controlDisabledForeground: color.controlDisabledForeground,
|
|
switchChecked: color.switchChecked,
|
|
switchCheckedHover: color.switchCheckedHover,
|
|
switchCheckedHandle: color.switchCheckedHandle,
|
|
switchUnchecked: color.switchUnchecked,
|
|
switchUncheckedHover: color.switchUncheckedHover,
|
|
switchUncheckedHandle: color.switchUncheckedHandle,
|
|
elevatedBackground: color.overlay,
|
|
subtleBackground: color.surfaceSubtle,
|
|
menuBackground: color.selected,
|
|
menuForeground: color.selectedForeground,
|
|
icon: color.icon,
|
|
iconHover: color.iconActive,
|
|
success: color.success,
|
|
warning: color.warning,
|
|
danger: color.danger,
|
|
dangerHover: color.dangerHover,
|
|
dangerActive: color.dangerActive,
|
|
dangerForeground: color.dangerForeground,
|
|
info: color.info,
|
|
borderRadius: component.buttonRadius,
|
|
borderRadiusLG: component.cardRadius,
|
|
borderRadiusSM: component.inputRadius,
|
|
buttonRadius: component.buttonRadius,
|
|
inputRadius: component.inputRadius,
|
|
overlayRadius: component.overlayRadius,
|
|
menuRadius: component.menuRadius,
|
|
checkboxRadius: component.checkboxRadius,
|
|
controlHeight: component.controlHeight,
|
|
controlHeightSmall: component.controlHeightSmall,
|
|
controlHeightLarge: component.controlHeightLarge,
|
|
borderWidth: component.borderWidth,
|
|
iconSize: component.iconSize,
|
|
buttonFontWeight: component.buttonFontWeight,
|
|
motionFast: component.motionFast,
|
|
motionNormal: component.motionNormal,
|
|
shadowStyle: component.shadowStyle,
|
|
};
|
|
}
|
|
|
|
export function applySkinTheme(skinValue: unknown, mode: SkinThemeMode, targetDocument: Document | undefined = typeof document === "undefined" ? undefined : document) {
|
|
if (!targetDocument) return;
|
|
const skin = normalizeSkinDefinition(skinValue);
|
|
const root = targetDocument.documentElement;
|
|
for (const property of MANAGED_VARIABLES) root.style.removeProperty(property);
|
|
if (skin.id !== "classic") {
|
|
const values = skinCSSVariables(skin, mode);
|
|
for (const [property, value] of Object.entries(values)) root.style.setProperty(property, value);
|
|
}
|
|
for (const [property, value] of Object.entries(skinButtonCSSVariables(skin, mode))) root.style.setProperty(property, value);
|
|
root.dataset.skin = skin.id;
|
|
}
|
|
|
|
export function isSkinButtonFill(value: unknown): value is SkinButtonFill {
|
|
if (!value || typeof value !== "object") return false;
|
|
const fill = value as SkinButtonFill;
|
|
return ["solid", "gradient"].includes(fill.mode) && Number.isInteger(fill.angle) && fill.angle >= 0 && fill.angle <= 360
|
|
&& SKIN_BUTTON_COLOR_FIELDS.every(({ key }) => typeof fill[key] === "string" && HEX_COLOR.test(fill[key]));
|
|
}
|
|
|
|
function normalizeButtonFill(value: unknown, skinID: string): SkinButtonFill {
|
|
return isSkinButtonFill(value) ? { ...value } : { ...DEFAULT_BUTTON_GRADIENT, mode: skinID === "classic" ? "gradient" : "solid" };
|
|
}
|
|
|
|
export function getSkinButtonAppearance(skin: SkinDefinition, mode: SkinThemeMode) {
|
|
const fill = normalizeButtonFill(skin.tokens.buttons?.[mode], skin.id);
|
|
const color = skin.tokens[mode];
|
|
const gradient = (start: string, end: string) => `linear-gradient(${fill.angle}deg, ${start}, ${end})`;
|
|
return fill.mode === "gradient" ? {
|
|
background: gradient(fill.start, fill.end), hover: gradient(fill.hoverStart, fill.hoverEnd),
|
|
active: gradient(fill.activeStart, fill.activeEnd), foreground: fill.foreground,
|
|
} : { background: color.primary, hover: color.primaryHover, active: color.primaryActive, foreground: color.primaryForeground };
|
|
}
|
|
|
|
function skinButtonCSSVariables(skin: SkinDefinition, mode: SkinThemeMode) {
|
|
const button = getSkinButtonAppearance(skin, mode);
|
|
return { "--button-primary-bg": button.background, "--button-primary-hover-bg": button.hover,
|
|
"--button-primary-active-bg": button.active, "--button-primary-fg": button.foreground };
|
|
}
|
|
|
|
export function skinCSSVariables(skin: SkinDefinition, mode: SkinThemeMode): Record<string, string> {
|
|
const color = skin.tokens[mode];
|
|
const component = skin.tokens.components;
|
|
const shadows = skinShadowVariables(component.shadowStyle, mode);
|
|
return {
|
|
"--background": color.canvas,
|
|
"--foreground": color.text,
|
|
"--card": color.surface,
|
|
"--card-foreground": color.text,
|
|
"--popover": color.overlay,
|
|
"--popover-foreground": color.text,
|
|
"--primary": color.primary,
|
|
"--primary-foreground": color.primaryForeground,
|
|
"--secondary": color.surfaceSubtle,
|
|
"--secondary-foreground": color.text,
|
|
"--muted": color.surfaceSubtle,
|
|
"--muted-foreground": color.textMuted,
|
|
"--accent": color.selected,
|
|
"--accent-foreground": color.selectedForeground,
|
|
"--destructive": color.danger,
|
|
"--destructive-hover": color.dangerHover,
|
|
"--destructive-active": color.dangerActive,
|
|
"--destructive-foreground": color.dangerForeground,
|
|
"--border": color.border,
|
|
"--input": color.controlBorder,
|
|
"--ring": color.controlFocus,
|
|
"--surface": color.surface,
|
|
"--surface-hover": color.controlHover,
|
|
"--surface-active": color.controlActive,
|
|
"--surface-card": color.surfaceSubtle,
|
|
"--surface-card-hover": color.controlHover,
|
|
"--card-surface": color.surfaceSubtle,
|
|
"--card-surface-hover": color.controlHover,
|
|
"--card-surface-raised": color.surfaceRaised,
|
|
"--btn-solid-bg": color.primary,
|
|
"--btn-solid-hover-bg": color.primaryHover,
|
|
"--btn-solid-active-bg": color.primaryActive,
|
|
"--btn-solid-fg": color.primaryForeground,
|
|
"--control-selected-bg": color.selected,
|
|
"--control-selected-hover-bg": color.selectedHover,
|
|
"--control-selected-fg": color.selectedForeground,
|
|
"--control-selected-border": color.controlBorder,
|
|
"--control-check-bg": color.primary,
|
|
"--control-check-fg": color.primaryForeground,
|
|
"--control-switch-checked-bg": color.switchChecked,
|
|
"--control-switch-checked-hover-bg": color.switchCheckedHover,
|
|
"--control-switch-checked-handle": color.switchCheckedHandle,
|
|
"--control-switch-off-bg": color.switchUnchecked,
|
|
"--control-switch-off-hover-bg": color.switchUncheckedHover,
|
|
"--control-switch-off-handle": color.switchUncheckedHandle,
|
|
"--control-disabled-bg": color.controlDisabledBackground,
|
|
"--control-disabled-fg": color.controlDisabledForeground,
|
|
"--control-focus-ring": color.controlFocus,
|
|
"--workspace-surface": color.workspace,
|
|
"--workspace-surface-strong": color.surfaceRaised,
|
|
"--workspace-border": color.border,
|
|
"--workspace-border-strong": color.controlBorder,
|
|
"--workspace-grid-line": color.workspaceGrid,
|
|
"--workspace-accent": color.primary,
|
|
"--workspace-accent-soft": `color-mix(in srgb, ${color.primary} 12%, transparent)`,
|
|
"--skin-admin-layer-0": color.adminBackground,
|
|
"--skin-admin-layer-1": color.adminSurface,
|
|
"--skin-admin-layer-2": color.adminSubtle,
|
|
"--skin-admin-layer-3": color.adminStrong,
|
|
"--auth-page-bg": color.authBackground,
|
|
"--auth-panel-bg": color.authPanel,
|
|
"--auth-card-bg": color.authCard,
|
|
"--auth-accent": color.authAccent,
|
|
"--auth-muted": color.authMuted,
|
|
"--palette-status-success": color.success,
|
|
"--palette-status-error": color.danger,
|
|
"--palette-status-loading": color.info,
|
|
"--palette-status-warning": color.warning,
|
|
"--icon-foreground": color.icon,
|
|
"--icon-muted": color.iconMuted,
|
|
"--icon-active": color.iconActive,
|
|
"--r-sm": `${component.inputRadius}px`,
|
|
"--r-md": `${component.buttonRadius}px`,
|
|
"--r-lg": `${component.cardRadius}px`,
|
|
"--r-xl": `${component.overlayRadius}px`,
|
|
"--r-2xl": `${component.overlayRadius}px`,
|
|
"--radius": `${component.buttonRadius / 16}rem`,
|
|
"--control-radius": `${component.inputRadius}px`,
|
|
"--button-radius": `${component.buttonRadius}px`,
|
|
"--input-radius": `${component.inputRadius}px`,
|
|
"--card-radius": `${component.cardRadius}px`,
|
|
"--overlay-radius": `${component.overlayRadius}px`,
|
|
"--menu-radius": `${component.menuRadius}px`,
|
|
"--checkbox-radius": `${component.checkboxRadius}px`,
|
|
"--control-border-width": `${component.borderWidth}px`,
|
|
"--focus-ring-width": `${component.focusRingWidth}px`,
|
|
"--icon-size": `${component.iconSize}px`,
|
|
"--theme-hover-lift": `${component.hoverLift}px`,
|
|
"--motion-instant": `${component.motionFast}ms`,
|
|
"--motion-state": `${component.motionNormal}ms`,
|
|
...shadows,
|
|
};
|
|
}
|
|
|
|
function skinShadowVariables(style: SkinShadowStyle, mode: SkinThemeMode) {
|
|
if (style === "none") return { "--elevation-card": "none", "--elevation-card-hover": "none", "--elevation-overlay": "none", "--theme-admin-card-shadow": "none" };
|
|
if (style === "strong") {
|
|
return mode === "dark"
|
|
? { "--elevation-card": "0 5px 16px #00000080", "--elevation-card-hover": "0 18px 44px #000000a6", "--elevation-overlay": "0 30px 90px #000000c2", "--theme-admin-card-shadow": "0 5px 16px #00000070" }
|
|
: { "--elevation-card": "0 5px 16px #0f172a29", "--elevation-card-hover": "0 18px 44px #0f172a3d", "--elevation-overlay": "0 30px 90px #0f172a52", "--theme-admin-card-shadow": "0 5px 16px #11182724" };
|
|
}
|
|
return mode === "dark"
|
|
? { "--elevation-card": "0 2px 7px #00000061", "--elevation-card-hover": "0 12px 30px #0000007a", "--elevation-overlay": "0 26px 72px #0000009e", "--theme-admin-card-shadow": "0 2px 7px #00000052" }
|
|
: { "--elevation-card": "0 2px 7px #0f172a14", "--elevation-card-hover": "0 12px 30px #0f172a24", "--elevation-overlay": "0 26px 72px #0f172a33", "--theme-admin-card-shadow": "0 2px 7px #1118270f" };
|
|
}
|
|
|
|
function cloneSkinTokens(tokens: SkinTokens): SkinTokens {
|
|
return { light: { ...tokens.light }, dark: { ...tokens.dark }, components: { ...tokens.components },
|
|
buttons: { light: { ...tokens.buttons.light }, dark: { ...tokens.buttons.dark } } };
|
|
}
|
|
|
|
function isSkinMode(value: unknown): value is SkinModeTokens {
|
|
if (!value || typeof value !== "object") return false;
|
|
return SKIN_COLOR_GROUPS.flatMap((group) => group.fields).every((field) => HEX_COLOR.test(String((value as Record<string, unknown>)[field.key] || "")));
|
|
}
|
|
|
|
function normalizeSkinMode(value: unknown, fallback: SkinModeTokens): SkinModeTokens | null {
|
|
if (!value || typeof value !== "object") return null;
|
|
const candidate = value as Partial<SkinModeTokens>;
|
|
const result: SkinModeTokens = {
|
|
...fallback,
|
|
...candidate,
|
|
switchChecked: candidate.switchChecked || candidate.primary || fallback.switchChecked,
|
|
switchCheckedHover: candidate.switchCheckedHover || candidate.primaryHover || fallback.switchCheckedHover,
|
|
switchCheckedHandle: candidate.switchCheckedHandle || candidate.primaryForeground || fallback.switchCheckedHandle,
|
|
switchUnchecked: candidate.switchUnchecked || candidate.controlBorder || fallback.switchUnchecked,
|
|
switchUncheckedHover: candidate.switchUncheckedHover || candidate.controlActive || fallback.switchUncheckedHover,
|
|
switchUncheckedHandle: candidate.switchUncheckedHandle || candidate.selectedForeground || fallback.switchUncheckedHandle,
|
|
dangerHover: candidate.dangerHover || candidate.danger || fallback.dangerHover,
|
|
dangerActive: candidate.dangerActive || candidate.danger || fallback.dangerActive,
|
|
dangerForeground: candidate.dangerForeground || candidate.primaryForeground || fallback.dangerForeground,
|
|
};
|
|
return isSkinMode(result) ? result : null;
|
|
}
|
|
|
|
function isSkinComponents(value: unknown): value is SkinComponentTokens {
|
|
if (!value || typeof value !== "object") return false;
|
|
const candidate = value as Record<string, unknown>;
|
|
return SKIN_COMPONENT_NUMBER_FIELDS.every((field) => typeof candidate[field.key] === "number" && Number.isFinite(candidate[field.key])) && ["none", "soft", "strong"].includes(String(candidate.shadowStyle));
|
|
}
|