SnowCat 雪猫底层全源码:NapCat 4.18.28 底座 + SnowLuma 能力移植 + V8 通道适配 9.9.37
26 包 monorepo(TS 全源码): - snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包 - snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线 - snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI) - snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports) - snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时 - 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏) - 工具件: 解包器/启动器/便携制作器/doctor GitHub 关联已全量指向 DandanLLab/SnowCatQQ
This commit is contained in:
commit
b9655a5ab1
1048 files changed
+130851
No files matched your search
@@ -0,0 +1,586 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||
import { Select, SelectItem } from '@heroui/select';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Tab, Tabs } from '@heroui/tabs';
|
||||
import { useRequest } from 'ahooks';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { FaFont, FaUserAstronaut, FaCheck } from 'react-icons/fa';
|
||||
import { FaPaintbrush } from 'react-icons/fa6';
|
||||
import { IoIosColorPalette, IoMdRefresh } from 'react-icons/io';
|
||||
import { MdDarkMode, MdLightMode } from 'react-icons/md';
|
||||
|
||||
import themes from '@/const/themes';
|
||||
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import FileInput from '@/components/input/file_input';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
|
||||
import FileManager from '@/controllers/file_manager';
|
||||
import { applyFont, colorKeys, generateTheme, loadTheme, updateFontCache } from '@/utils/theme';
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager';
|
||||
|
||||
export type PreviewThemeCardProps = {
|
||||
theme: ThemeInfo;
|
||||
onPreview: () => void;
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
const values = [
|
||||
'',
|
||||
'-50',
|
||||
'-100',
|
||||
'-200',
|
||||
'-300',
|
||||
'-400',
|
||||
'-500',
|
||||
'-600',
|
||||
'-700',
|
||||
'-800',
|
||||
'-900',
|
||||
];
|
||||
const colors = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'success',
|
||||
'danger',
|
||||
'warning',
|
||||
'default',
|
||||
];
|
||||
|
||||
function PreviewThemeCard ({ theme, onPreview, isSelected }: PreviewThemeCardProps) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = generateTheme(theme.theme, theme.name);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
document.head.appendChild(style);
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Card
|
||||
ref={cardRef}
|
||||
shadow='sm'
|
||||
radius='sm'
|
||||
isPressable
|
||||
onPress={onPreview}
|
||||
className={clsx(
|
||||
'text-primary bg-primary-50 relative transition-all',
|
||||
theme.name,
|
||||
isSelected && 'ring-2 ring-primary ring-offset-2'
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className='absolute top-1 right-1 z-10'>
|
||||
<Chip size='sm' color='primary' variant='solid'>
|
||||
<FaCheck size={10} />
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className='pb-0 flex flex-col items-start gap-1'>
|
||||
<div className='px-1 rounded-md bg-primary text-primary-foreground'>
|
||||
{theme.name}
|
||||
</div>
|
||||
<div className='text-xs flex items-center gap-1 text-primary-300'>
|
||||
<FaUserAstronaut />
|
||||
{theme.author ?? '未知'}
|
||||
</div>
|
||||
<div className='text-xs text-primary-200 whitespace-nowrap overflow-hidden text-ellipsis w-full'>{theme.description}</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className='flex flex-col gap-1'>
|
||||
{colors.map((color) => (
|
||||
<div className='flex gap-1 items-center flex-nowrap' key={color}>
|
||||
<div className='text-xs w-4 text-right flex-shrink-0'>
|
||||
{color[0].toUpperCase()}
|
||||
</div>
|
||||
{values.map((value) => (
|
||||
<div
|
||||
key={value}
|
||||
className={clsx(
|
||||
'w-2 h-2 rounded-full shadow-small flex-shrink-0',
|
||||
`bg-${color}${value}`
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// 比较两个主题配置是否相同(不比较 fontMode)
|
||||
const isThemeColorsEqual = (a: ThemeConfig, b: ThemeConfig): boolean => {
|
||||
if (!a || !b) return false;
|
||||
const aKeys = [...Object.keys(a.light || {}), ...Object.keys(a.dark || {})];
|
||||
const bKeys = [...Object.keys(b.light || {}), ...Object.keys(b.dark || {})];
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
|
||||
for (const key of Object.keys(a.light || {})) {
|
||||
if (a.light?.[key as keyof ThemeConfigItem] !== b.light?.[key as keyof ThemeConfigItem]) return false;
|
||||
}
|
||||
for (const key of Object.keys(a.dark || {})) {
|
||||
if (a.dark?.[key as keyof ThemeConfigItem] !== b.dark?.[key as keyof ThemeConfigItem]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 字体模式显示名称映射
|
||||
const fontModeNames: Record<string, string> = {
|
||||
aacute: 'Aa 偷吃可爱长大的',
|
||||
system: '系统默认',
|
||||
custom: '自定义字体',
|
||||
};
|
||||
|
||||
const ThemeConfigCard = () => {
|
||||
const { data, loading, error, refreshAsync } = useRequest(
|
||||
WebUIManager.getThemeConfig
|
||||
);
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleOnebotSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue: setOnebotValue,
|
||||
} = useForm<{
|
||||
theme: ThemeConfig;
|
||||
}>({
|
||||
defaultValues: {
|
||||
theme: {
|
||||
dark: {},
|
||||
light: {},
|
||||
fontMode: 'aacute',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [customFontExists, setCustomFontExists] = useState(false);
|
||||
|
||||
// 使用 useRef 存储 style 标签引用和状态
|
||||
const styleTagRef = useRef<HTMLStyleElement | null>(null);
|
||||
const originalDataRef = useRef<ThemeConfig | null>(null);
|
||||
const hasUnsavedChangesRef = useRef<boolean>(false);
|
||||
|
||||
// 同步 hasUnsavedChanges 到 ref,供 cleanup 函数使用
|
||||
useEffect(() => {
|
||||
hasUnsavedChangesRef.current = hasUnsavedChanges;
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
// 在组件挂载时创建 style 标签,并在卸载时清理
|
||||
// 同时在卸载时恢复字体到已保存的状态(避免"伪自动保存"问题)
|
||||
useEffect(() => {
|
||||
const styleTag = document.createElement('style');
|
||||
document.head.appendChild(styleTag);
|
||||
styleTagRef.current = styleTag;
|
||||
return () => {
|
||||
// 组件卸载时,只有在有未保存更改时才恢复到已保存的字体设置
|
||||
// 避免在刷新页面后字体被意外重置
|
||||
if (hasUnsavedChangesRef.current && originalDataRef.current?.fontMode) {
|
||||
applyFont(originalDataRef.current.fontMode);
|
||||
}
|
||||
if (styleTagRef.current) {
|
||||
document.head.removeChild(styleTagRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const theme = useWatch({ control, name: 'theme' });
|
||||
|
||||
// 检测是否有未保存的更改
|
||||
useEffect(() => {
|
||||
if (originalDataRef.current && dataLoaded) {
|
||||
const colorsChanged = !isThemeColorsEqual(theme, originalDataRef.current);
|
||||
const fontChanged = theme.fontMode !== originalDataRef.current.fontMode;
|
||||
setHasUnsavedChanges(colorsChanged || fontChanged);
|
||||
}
|
||||
}, [theme, dataLoaded]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (data) {
|
||||
setOnebotValue('theme', data);
|
||||
originalDataRef.current = data;
|
||||
// 应用已保存的字体设置
|
||||
if (data.fontMode) {
|
||||
applyFont(data.fontMode);
|
||||
}
|
||||
}
|
||||
setDataLoaded(true);
|
||||
setHasUnsavedChanges(false);
|
||||
// 检查自定义字体是否存在
|
||||
FileManager.checkWebUIFontExists().then(exists => {
|
||||
setCustomFontExists(exists);
|
||||
}).catch(err => console.error('Failed to check custom font:', err));
|
||||
}, [data, setOnebotValue]);
|
||||
|
||||
// 实时应用字体预设(预览)
|
||||
useEffect(() => {
|
||||
if (dataLoaded && theme.fontMode) {
|
||||
applyFont(theme.fontMode);
|
||||
}
|
||||
}, [theme.fontMode, dataLoaded]);
|
||||
|
||||
const onSubmit = handleOnebotSubmit(async (formData) => {
|
||||
try {
|
||||
await WebUIManager.setThemeConfig(formData.theme);
|
||||
// 更新原始数据引用
|
||||
originalDataRef.current = formData.theme;
|
||||
// 更新字体缓存
|
||||
if (formData.theme.fontMode) {
|
||||
updateFontCache(formData.theme.fontMode);
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
toast.success('保存成功');
|
||||
loadTheme();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`保存失败: ${msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
const onRefresh = async () => {
|
||||
try {
|
||||
await refreshAsync();
|
||||
toast.success('刷新成功');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`刷新失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [data, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme && styleTagRef.current) {
|
||||
const css = generateTheme(theme);
|
||||
styleTagRef.current.innerHTML = css;
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// 找到当前选中的主题(预览中的)
|
||||
const selectedThemeName = useMemo(() => {
|
||||
return themes.find(t => isThemeColorsEqual(t.theme, theme))?.name;
|
||||
}, [theme]);
|
||||
|
||||
// 找到已保存的主题名称
|
||||
const savedThemeName = useMemo(() => {
|
||||
const savedData = originalDataRef.current || data;
|
||||
if (!savedData) return null;
|
||||
return themes.find(t => isThemeColorsEqual(t.theme, savedData))?.name || '自定义';
|
||||
}, [data, dataLoaded, hasUnsavedChanges]);
|
||||
|
||||
// 已保存的字体模式显示名称
|
||||
const savedFontModeDisplayName = useMemo(() => {
|
||||
const savedData = originalDataRef.current || data;
|
||||
const mode = savedData?.fontMode || 'aacute';
|
||||
return fontModeNames[mode] || mode;
|
||||
}, [data, dataLoaded, hasUnsavedChanges]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='relative w-full min-h-[220px]'>
|
||||
<PageLoading loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className='py-24 text-danger-500 text-center'>{error.message}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>主题配置 - SnowCat WebUI</title>
|
||||
|
||||
{/* 顶部操作栏 */}
|
||||
<div className='w-full pb-2'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<h1 className='text-base font-semibold text-default-800 dark:text-white tracking-tight'>外观设置</h1>
|
||||
<div className='flex items-center gap-3 text-tiny text-default-500'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<IoIosColorPalette className='text-primary' size={16} />
|
||||
<span className='font-medium text-default-700'>{savedThemeName || '加载中...'}</span>
|
||||
</div>
|
||||
<div className='w-px h-2.5 bg-default-300' />
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<FaFont className='text-secondary' size={12} />
|
||||
<span className='font-medium text-default-700'>{savedFontModeDisplayName}</span>
|
||||
</div>
|
||||
{hasUnsavedChanges && (
|
||||
<>
|
||||
<div className='w-px h-2.5 bg-default-300' />
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='w-1.5 h-1.5 rounded-full bg-warning animate-pulse' />
|
||||
<span className='text-warning font-semibold'>待保存</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='flat'
|
||||
color='default'
|
||||
className='font-medium bg-white/60 dark:bg-white/[0.06] border border-white/50 dark:border-white/10 h-9'
|
||||
onPress={() => {
|
||||
reset();
|
||||
toast.success('已重置');
|
||||
}}
|
||||
isDisabled={!hasUnsavedChanges}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
className='font-semibold bg-gradient-to-r from-primary-400 to-primary-600 shadow-md shadow-primary-500/25 px-6 h-9'
|
||||
isLoading={isSubmitting}
|
||||
onPress={() => onSubmit()}
|
||||
isDisabled={!hasUnsavedChanges}
|
||||
>
|
||||
保存应用
|
||||
</Button>
|
||||
<div className='w-px h-6 bg-divider mx-1 hidden sm:block' />
|
||||
<Button
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='light'
|
||||
className='text-default-500 hover:text-default-900 hidden sm:flex'
|
||||
onPress={onRefresh}
|
||||
>
|
||||
<IoMdRefresh size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full pb-4'>
|
||||
<Tabs
|
||||
aria-label='Theme Config Options'
|
||||
color='primary'
|
||||
variant='underlined'
|
||||
disableAnimation
|
||||
classNames={{
|
||||
tabList: 'gap-8 w-full relative rounded-none p-0 border-b border-divider overflow-x-auto no-scrollbar',
|
||||
cursor: 'w-full bg-primary h-[3px] -bottom-[1.5px]',
|
||||
tab: 'max-w-fit px-0 h-12 hover:opacity-100 opacity-70 data-[selected=true]:opacity-100',
|
||||
tabContent: 'font-semibold py-2',
|
||||
panel: 'py-4',
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
key='font'
|
||||
title={
|
||||
<div className='flex items-center space-x-2'>
|
||||
<FaFont />
|
||||
<span>字体设置</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Card className='shadow-sm border border-white/60 dark:border-white/10 bg-white/50 dark:bg-white/[0.03] backdrop-blur-xl w-full rounded-2xl'>
|
||||
<CardBody className='p-5 md:p-6'>
|
||||
<div className='flex flex-col gap-6 w-full'>
|
||||
<div>
|
||||
<h3 className='text-base font-semibold mb-1'>WebUI 字体</h3>
|
||||
<p className='text-xs text-default-500 mb-4'>自定义界面显示的字体风格</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name='theme.fontMode'
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label='选择字体'
|
||||
variant='bordered'
|
||||
selectedKeys={field.value ? [field.value] : ['aacute']}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
className='max-w-xs'
|
||||
disallowEmptySelection
|
||||
>
|
||||
<SelectItem key='aacute'>Aa 偷吃可爱长大的</SelectItem>
|
||||
<SelectItem key='system'>系统默认</SelectItem>
|
||||
<SelectItem key='custom'>自定义字体</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{theme.fontMode === 'custom' && (
|
||||
<div className='p-4 rounded-xl bg-default-50 border border-default-100'>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<div className='text-sm font-medium'>自定义字体文件</div>
|
||||
{customFontExists && (
|
||||
<Chip size='sm' color='success' variant='flat' startContent={<FaCheck size={10} />}>
|
||||
已上传
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FileInput
|
||||
label='上传字体文件'
|
||||
placeholder='拖拽或点击上传 (.woff/.woff2/.ttf/.otf)'
|
||||
accept='.ttf,.otf,.woff,.woff2'
|
||||
onChange={async (file) => {
|
||||
try {
|
||||
if (customFontExists) {
|
||||
try {
|
||||
await FileManager.deleteWebUIFont();
|
||||
} catch (e) {
|
||||
console.warn('Failed to delete existing font before upload:', e);
|
||||
}
|
||||
}
|
||||
await FileManager.uploadWebUIFont(file);
|
||||
|
||||
// 先自动把选择的字体模式设置为 custom,并保存配置,避免刷新后又变成了旧的/没保存的模式
|
||||
const updatedTheme = { ...theme, fontMode: 'custom' };
|
||||
await WebUIManager.setThemeConfig(updatedTheme);
|
||||
originalDataRef.current = updatedTheme;
|
||||
updateFontCache('custom');
|
||||
setHasUnsavedChanges(false);
|
||||
loadTheme();
|
||||
|
||||
toast.success('上传并保存成功,即将刷新页面');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} catch (error) {
|
||||
toast.error('上传或保存失败: ' + (error as Error).message);
|
||||
}
|
||||
}}
|
||||
onDelete={async () => {
|
||||
try {
|
||||
await FileManager.deleteWebUIFont();
|
||||
// 删除字体后,若当前选中的是自定义字体,则将其重置为系统默认(或其它)并保存,以防页面报错
|
||||
if (theme.fontMode === 'custom') {
|
||||
const updatedTheme = { ...theme, fontMode: 'system' };
|
||||
await WebUIManager.setThemeConfig(updatedTheme);
|
||||
originalDataRef.current = updatedTheme;
|
||||
updateFontCache('system');
|
||||
setHasUnsavedChanges(false);
|
||||
loadTheme();
|
||||
}
|
||||
toast.success('删除成功,即将刷新页面');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} catch (error) {
|
||||
toast.error('删除失败: ' + (error as Error).message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className='text-xs text-default-400 mt-2'>
|
||||
注意:上传新字体会覆盖旧字体文件,更改后需要刷新页面生效。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
key='theme'
|
||||
title={
|
||||
<div className='flex items-center space-x-2'>
|
||||
<IoIosColorPalette size={18} />
|
||||
<span>选择主题</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
|
||||
{themes.map((t) => (
|
||||
<PreviewThemeCard
|
||||
key={t.name}
|
||||
theme={t}
|
||||
isSelected={selectedThemeName === t.name}
|
||||
onPreview={() => {
|
||||
setOnebotValue('theme', { ...t.theme, fontMode: theme.fontMode });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
key='custom-color'
|
||||
title={
|
||||
<div className='flex items-center space-x-2'>
|
||||
<FaPaintbrush />
|
||||
<span>自定义配色</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-6'>
|
||||
{(['light', 'dark'] as const).map((mode) => (
|
||||
<Card key={mode} className={clsx('border shadow-sm', mode === 'dark' ? 'bg-[#18181b] border-zinc-800' : 'bg-white border-zinc-200')}>
|
||||
<CardHeader className='pb-0 pt-4 px-4 flex-col items-start'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
{mode === 'dark' ? <MdDarkMode className='text-zinc-400' size={20} /> : <MdLightMode className='text-orange-400' size={20} />}
|
||||
<h4 className={clsx('font-bold text-large', mode === 'dark' ? 'text-white' : 'text-black')}>
|
||||
{mode === 'dark' ? '深色模式' : '浅色模式'}
|
||||
</h4>
|
||||
</div>
|
||||
<p className={clsx('text-tiny', mode === 'dark' ? 'text-zinc-400' : 'text-zinc-500')}>
|
||||
调整{mode === 'dark' ? '深色' : '浅色'}主题下的颜色变量
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardBody className='p-4'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
|
||||
{colorKeys.map((colorKey) => (
|
||||
<div
|
||||
key={colorKey}
|
||||
className={clsx(
|
||||
'flex items-center gap-3 p-2 rounded-lg border transition-colors',
|
||||
mode === 'dark' ? 'bg-zinc-900/50 border-zinc-800 hover:bg-zinc-900' : 'bg-zinc-50 border-zinc-100 hover:bg-zinc-100'
|
||||
)}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`theme.${mode}.${colorKey}`}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const hslArray = value?.split(' ') ?? [0, 0, 0];
|
||||
const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`;
|
||||
return (
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={(hslString) => {
|
||||
const match = hslString.match(/hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%,\s*(\d+(?:\.\d+)?)%\)/);
|
||||
if (match) {
|
||||
onChange(`${match[1]} ${match[2]}% ${match[3]}%`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col overflow-hidden'>
|
||||
<span className={clsx('text-xs font-medium truncate', mode === 'dark' ? 'text-zinc-300' : 'text-zinc-700')}>
|
||||
{colorKey.replace('--heroui-', '')}
|
||||
</span>
|
||||
<span className={clsx('text-[10px] truncate', mode === 'dark' ? 'text-zinc-500' : 'text-zinc-400')}>
|
||||
Variable
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeConfigCard;
|
||||
Reference in new issue
Block a user