feat: UI color theming system with live preview, presets and server sync

- RGBA color picker with live preview (debounce 50ms)
- 30 preset colors + 12 theme presets (Ocean, Forest, Sunset, Royal, etc.)
- Header image selection from configurable image library
- Export/Import theme as JSON via clipboard
- CSS variable theming across all UI elements: NitroCard headers/tabs,
  context menus, buttons (primary/dark/gray), InfoStand, toolbar,
  room tools, purse, progress bars, sliders
- All elements use var(--name, fallback) for zero visual change when default
- Smooth 0.3s CSS transitions on theme change
- Server-side persistence via WebSocket (packets 10047/10048)
- Integrated Color/Image tabs into BackgroundsView panel
- All strings use LocalizeText() for i18n support
- Settings persisted in localStorage + server sync with 1s debounce
- Added react-colorful dependency
This commit is contained in:
Life
2026-03-22 21:39:44 +01:00
parent b73c0841f2
commit 9c2dccaad6
28 changed files with 774 additions and 70 deletions
@@ -0,0 +1,52 @@
import { FC, useCallback, useMemo } from 'react';
import { GetConfigurationValue, useUiSettings } from '../../api';
export const InterfaceImageTabView: FC<{}> = () =>
{
const { settings, updateSettings } = useUiSettings();
const imageCount = useMemo(() =>
{
return GetConfigurationValue<number>('ui.header.images.count', 30);
}, []);
const baseUrl = useMemo(() =>
{
return GetConfigurationValue<string>('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif');
}, []);
const images = useMemo(() =>
{
const result: string[] = [];
for(let i = 1; i <= imageCount; i++)
{
result.push(baseUrl.replace('{id}', String(i)));
}
return result;
}, [ imageCount, baseUrl ]);
const onImageSelect = useCallback((url: string) =>
{
updateSettings({
colorMode: 'image',
headerImageUrl: url
});
}, [ updateSettings ]);
return (
<div className="grid grid-cols-8 gap-1 p-2 overflow-auto max-h-[400px]">
{ images.map((url, i) => (
<div
key={ i }
className={ `w-[75px] h-[75px] rounded cursor-pointer border-2 transition-all hover:scale-105 ${ (settings.colorMode === 'image' && settings.headerImageUrl === url) ? 'border-white shadow-lg' : 'border-transparent' }` }
style={ {
backgroundImage: `url(${ url })`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} }
onClick={ () => onImageSelect(url) }
/>
)) }
</div>
);
};