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
+24 -14
View File
@@ -2,9 +2,11 @@ import { GetSessionDataManager, HabboClubLevelEnum} from '@nitrots/nitro-rendere
import { Dispatch, FC, SetStateAction, useCallback, useMemo, useState } from 'react';
import { Base, Grid, Flex, NitroCardView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardContentView, Text, LayoutCurrencyIcon } from '../../common';
import { useRoom } from '../../hooks';
import { GetClubMemberLevel, GetConfigurationValue } from '../../api';
import { GetClubMemberLevel, GetConfigurationValue, LocalizeText } from '../../api';
import { InterfaceColorTabView } from '../interface-settings/InterfaceColorTabView';
import { InterfaceImageTabView } from '../interface-settings/InterfaceImageTabView';
interface ItemData {
interface ItemData {
id: number;
isHcOnly: boolean;
minRank: number;
@@ -22,7 +24,7 @@ interface BackgroundsViewProps {
setSelectedOverlay: Dispatch<SetStateAction<number>>;
}
const TABS = ['backgrounds', 'stands', 'overlays'] as const;
const TABS = ['backgrounds', 'stands', 'overlays', 'color', 'image'] as const;
type TabType = typeof TABS[number];
export const BackgroundsView: FC<BackgroundsViewProps> = ({
@@ -36,7 +38,7 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
}) => {
const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
const { roomSession } = useRoom();
const userData = useMemo(() => ({
isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB,
securityLevel: GetSessionDataManager().canChangeName,
@@ -45,7 +47,7 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
const processData = useCallback((configData: any[], dataType: string): ItemData[] => {
if (!configData?.length) return [];
return configData
.filter(item => {
const meetsRank = userData.securityLevel >= item.minRank;
@@ -65,10 +67,10 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
if (!roomSession) return;
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay };
const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay };
setters[activeTab](id);
setters[activeTab as 'backgrounds' | 'stands' | 'overlays'](id);
const newValues = { ...currentValues, [activeTab]: id };
roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays );
}, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, setSelectedBackground, setSelectedStand, setSelectedOverlay]);
@@ -86,9 +88,11 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
</Flex>
), [handleSelection]);
const isProfileTab = activeTab === 'backgrounds' || activeTab === 'stands' || activeTab === 'overlays';
return (
<NitroCardView uniqueKey="backgrounds" className="absolute min-w-[535px] max-w-[535px] min-h-[389px] max-h-[389px]">
<NitroCardHeaderView headerText="Profile Background" onCloseClick={() => setIsVisible(false)} />
<NitroCardHeaderView headerText={ LocalizeText('interface.settings.title') } onCloseClick={() => setIsVisible(false)} />
<NitroCardTabsView>
{TABS.map(tab => (
<NitroCardTabsItemView
@@ -96,16 +100,22 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
isActive={activeTab === tab}
onClick={() => setActiveTab(tab)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
{ LocalizeText(`interface.settings.tab.${ tab }`) }
</NitroCardTabsItemView>
))}
</NitroCardTabsView>
<NitroCardContentView gap={1}>
<Text bold center>Select an Option</Text>
<Grid gap={1} columnCount={7} overflow="auto">
{allData[activeTab].map(item => renderItem(item, activeTab.slice(0, -1)))}
</Grid>
{ isProfileTab && (
<>
<Text bold center>{ LocalizeText('interface.settings.select.option') }</Text>
<Grid gap={1} columnCount={7} overflow="auto">
{allData[activeTab as 'backgrounds' | 'stands' | 'overlays'].map(item => renderItem(item, activeTab.slice(0, -1)))}
</Grid>
</>
) }
{ activeTab === 'color' && <InterfaceColorTabView /> }
{ activeTab === 'image' && <InterfaceImageTabView /> }
</NitroCardContentView>
</NitroCardView>
);
};
};