Merge remote-tracking branch 'origin/Dev' into feat/react19-modernization

# Conflicts:
#	src/components/backgrounds/BackgroundsView.tsx
#	src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
This commit is contained in:
simoleo89
2026-05-19 20:40:07 +02:00
15 changed files with 429 additions and 232 deletions
+1
View File
@@ -24,6 +24,7 @@ export class AvatarInfoUser implements IAvatarInfo
public standId: number = 0; public standId: number = 0;
public overlayId: number = 0; public overlayId: number = 0;
public cardBackgroundId: number = 0; public cardBackgroundId: number = 0;
public borderId: number = 0;
public webID: number = 0; public webID: number = 0;
public xp: number = 0; public xp: number = 0;
public userType: number = -1; public userType: number = -1;
@@ -194,6 +194,7 @@ export class AvatarInfoUtilities
userInfo.standId = userData.stand; userInfo.standId = userData.stand;
userInfo.overlayId = userData.overlay; userInfo.overlayId = userData.overlay;
userInfo.cardBackgroundId = userData.cardBackground ?? 0; userInfo.cardBackgroundId = userData.cardBackground ?? 0;
userInfo.borderId = (userData as any).borderId ?? 0;
userInfo.achievementScore = userData.activityPoints; userInfo.achievementScore = userData.activityPoints;
userInfo.webID = userData.webID; userInfo.webID = userData.webID;
userInfo.roomIndex = userData.roomIndex; userInfo.roomIndex = userData.roomIndex;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+26 -11
View File
@@ -18,12 +18,14 @@ interface BackgroundsViewProps {
setSelectedOverlay: Dispatch<SetStateAction<number>>; setSelectedOverlay: Dispatch<SetStateAction<number>>;
selectedCardBackground: number; selectedCardBackground: number;
setSelectedCardBackground: Dispatch<SetStateAction<number>>; setSelectedCardBackground: Dispatch<SetStateAction<number>>;
selectedBorder: number;
setSelectedBorder: Dispatch<SetStateAction<number>>;
} }
const TABS = ['backgrounds', 'stands', 'overlays', 'cards'] as const; const TABS = ['backgrounds', 'stands', 'overlays', 'cards', 'borders'] as const;
type TabType = typeof TABS[number]; type TabType = typeof TABS[number];
type RemoteData = Partial<Record<'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data', any[]>>; type RemoteData = Partial<Record<'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data' | 'borders.data', any[]>>;
let backgroundsDataPromise: Promise<RemoteData | null> | null = null; let backgroundsDataPromise: Promise<RemoteData | null> | null = null;
@@ -48,7 +50,9 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
selectedOverlay, selectedOverlay,
setSelectedOverlay, setSelectedOverlay,
selectedCardBackground, selectedCardBackground,
setSelectedCardBackground setSelectedCardBackground,
selectedBorder,
setSelectedBorder
}) => }) =>
{ {
const [activeTab, setActiveTab] = useState<TabType>('backgrounds'); const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
@@ -62,7 +66,7 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] })); return configData.map(item => ({ id: typeof item === 'number' ? item : item[idField] }));
}, []); }, []);
const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data'): any[] => const readData = useCallback((key: 'backgrounds.data' | 'stands.data' | 'overlays.data' | 'cards.data' | 'borders.data'): any[] =>
{ {
const fromRemote = remoteData?.[key]; const fromRemote = remoteData?.[key];
if(Array.isArray(fromRemote)) return fromRemote; if(Array.isArray(fromRemote)) return fromRemote;
@@ -73,21 +77,28 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
backgrounds: processData(readData('backgrounds.data'), 'backgroundId'), backgrounds: processData(readData('backgrounds.data'), 'backgroundId'),
stands: processData(readData('stands.data'), 'standId'), stands: processData(readData('stands.data'), 'standId'),
overlays: processData(readData('overlays.data'), 'overlayId'), overlays: processData(readData('overlays.data'), 'overlayId'),
cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId') cards: processData(readData('cards.data').length ? readData('cards.data') : readData('backgrounds.data'), 'backgroundId'),
borders: processData(readData('borders.data'), 'borderId')
}), [processData, readData]); }), [processData, readData]);
const handleSelection = useCallback((id: number) => const handleSelection = useCallback((id: number) =>
{ {
if (!roomSession) return; if (!roomSession) return;
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground }; const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay, cards: setSelectedCardBackground, borders: setSelectedBorder };
const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay, cards: selectedCardBackground }; const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay, cards: selectedCardBackground, borders: selectedBorder };
setters[activeTab](id); setters[activeTab](id);
const newValues = { ...currentValues, [activeTab]: id }; const newValues = { ...currentValues, [activeTab]: id };
roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards ); roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards, newValues.borders );
}, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, selectedCardBackground, setSelectedBackground, setSelectedStand, setSelectedOverlay, setSelectedCardBackground]); }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, selectedCardBackground, selectedBorder, setSelectedBackground, setSelectedStand, setSelectedOverlay, setSelectedCardBackground, setSelectedBorder]);
const itemTypeFor = (tab: TabType): string => {
if(tab === 'cards') return 'card-background';
if(tab === 'borders') return 'border';
return tab.slice(0, -1);
};
const renderItem = useCallback((item: ItemData, type: string) => ( const renderItem = useCallback((item: ItemData, type: string) => (
<Flex <Flex
@@ -98,7 +109,11 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
> >
<Base <Base
className={`profile-${type} ${type}-${item.id}`} className={`profile-${type} ${type}-${item.id}`}
style={type === 'card-background' ? { width: 60, height: 80, borderRadius: 4 } : undefined} style={
type === 'card-background' ? { width: 60, height: 80, borderRadius: 4 }
: type === 'border' ? { width: 60, height: 76, backgroundSize: 'contain', backgroundRepeat: 'no-repeat', backgroundPosition: 'center' }
: undefined
}
/> />
</Flex> </Flex>
), [handleSelection]); ), [handleSelection]);
@@ -120,7 +135,7 @@ export const BackgroundsView: FC<BackgroundsViewProps> = ({
<NitroCardContentView gap={1}> <NitroCardContentView gap={1}>
<Text bold center>Select an Option</Text> <Text bold center>Select an Option</Text>
<Grid gap={1} columnCount={7} overflow="auto"> <Grid gap={1} columnCount={7} overflow="auto">
{allData[activeTab].map(item => renderItem(item, activeTab === 'cards' ? 'card-background' : activeTab.slice(0, -1)))} {allData[activeTab].map(item => renderItem(item, itemTypeFor(activeTab)))}
</Grid> </Grid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
@@ -229,7 +229,7 @@ export const AvatarInfoWidgetView: FC<{}> = props =>
} }
} }
return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />; return <InfoStandWidgetUserView avatarInfo={ (avatarInfo as AvatarInfoUser) } setAvatarInfo={ setAvatarInfo } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoUser.BOT: case AvatarInfoUser.BOT:
return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />; return <InfoStandWidgetBotView avatarInfo={ (avatarInfo as AvatarInfoUser) } onClose={ () => setAvatarInfo(null) } />;
case AvatarInfoRentableBot.RENTABLE_BOT: case AvatarInfoRentableBot.RENTABLE_BOT:
@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { FC, FocusEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react'; import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common'; import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView, UserProfileIconView } from '../../../../../common';
import { useMessageEvent, useRoom } from '../../../../../hooks'; import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView'; import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView'; import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView'; import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
@@ -12,12 +12,12 @@ import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
interface InfoStandWidgetUserViewProps { interface InfoStandWidgetUserViewProps {
avatarInfo: AvatarInfoUser; avatarInfo: AvatarInfoUser;
setAvatarInfo: Dispatch<SetStateAction<AvatarInfoUser>>;
onClose: () => void; onClose: () => void;
} }
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => {
{ const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
const { avatarInfo = null, onClose = null } = props;
const [motto, setMotto] = useState<string>(null); const [motto, setMotto] = useState<string>(null);
const [isEditingMotto, setIsEditingMotto] = useState(false); const [isEditingMotto, setIsEditingMotto] = useState(false);
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null); const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
@@ -25,6 +25,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const [standId, setStandId] = useState<number>(null); const [standId, setStandId] = useState<number>(null);
const [overlayId, setOverlayId] = useState<number>(null); const [overlayId, setOverlayId] = useState<number>(null);
const [cardBackgroundId, setCardBackgroundId] = useState<number>(null); const [cardBackgroundId, setCardBackgroundId] = useState<number>(null);
const [borderId, setBorderId] = useState<number>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const { roomSession = null } = useRoom(); const { roomSession = null } = useRoom();
@@ -32,18 +33,12 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const infostandStandClass = `stand-${standId ?? 'default'}`; const infostandStandClass = `stand-${standId ?? 'default'}`;
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`; const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : ''; const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : '';
const handleProfileClick = useCallback(() => const infostandBorderClass = borderId ? `border-${borderId}` : '';
{ const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
GetUserProfile(avatarInfo.webID);
}, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
{
event.stopPropagation(); setIsVisible(prev => !prev);
}, []);
const saveMotto = (motto: string) => const saveMotto = (motto: string) => {
{
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return; if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
roomSession.sendMottoMessage(motto); roomSession.sendMottoMessage(motto);
@@ -52,20 +47,82 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value); const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
{
event.stopPropagation(); event.stopPropagation();
switch (event.key) switch (event.key) {
{
case 'Enter': case 'Enter':
saveMotto((event.target as HTMLInputElement).value); saveMotto((event.target as HTMLInputElement).value);
return; return;
} }
}; };
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => {
{ if (!avatarInfo || avatarInfo.webID !== event.userId) return;
// Deduplicate badges from server
const seen = new Set<string>();
const dedupedBadges = event.badges.map(code => {
if (!code || seen.has(code)) return '';
seen.add(code);
return code;
});
const oldBadges = avatarInfo.badges.join('');
if (oldBadges === dedupedBadges.join('')) return;
setAvatarInfo(prevValue => {
if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue);
newValue.badges = dedupedBadges;
return newValue;
});
});
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => {
if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue);
newValue.figure = event.figure;
newValue.motto = event.customInfo;
newValue.achievementScore = event.activityPoints;
newValue.nickIcon = event.nickIcon;
newValue.prefixText = event.prefixText;
newValue.prefixColor = event.prefixColor;
newValue.prefixIcon = event.prefixIcon;
newValue.prefixEffect = event.prefixEffect;
newValue.displayOrder = event.displayOrder;
newValue.backgroundId = event.backgroundId;
newValue.standId = event.standId;
newValue.overlayId = event.overlayId;
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
newValue.borderId = event.borderId ?? 0;
return newValue;
});
});
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
setAvatarInfo(prevValue => {
if (!prevValue) return prevValue;
const newValue = CloneObject(prevValue);
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
newValue.groupName = clearGroup ? null : event.habboGroupName;
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
return newValue;
});
});
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
const parser = event.getParser(); const parser = event.getParser();
if (!avatarInfo || avatarInfo.webID !== parser.userId) return; if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
@@ -73,19 +130,18 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
setRelationships(parser); setRelationships(parser);
}); });
useEffect(() => useEffect(() => {
{
setIsEditingMotto(false); setIsEditingMotto(false);
setMotto(avatarInfo.motto); setMotto(avatarInfo.motto);
setBackgroundId(avatarInfo.backgroundId); setBackgroundId(avatarInfo.backgroundId);
setStandId(avatarInfo.standId); setStandId(avatarInfo.standId);
setOverlayId(avatarInfo.overlayId); setOverlayId(avatarInfo.overlayId);
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0); setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
setBorderId(avatarInfo.borderId ?? 0);
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
return () => return () => {
{
setRelationships(null); setRelationships(null);
}; };
}, [avatarInfo]); }, [avatarInfo]);
@@ -94,7 +150,9 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
return ( return (
<> <>
<Column className={`relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}> <div className="relative min-w-[190px] max-w-[190px] pointer-events-auto z-30">
{borderId ? <Base className={`infostand-border ${infostandBorderClass}`} /> : null}
<Column className={`relative min-w-[190px] max-w-[190px] ${cardBackgroundId ? '' : 'bg-[rgba(28,28,32,0.95)]'} [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded overflow-hidden profile-card-background ${infostandCardBackgroundClass}`}>
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible"> <Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -136,8 +194,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
/> />
)} )}
<Column grow alignItems="center" gap={0}> <Column grow alignItems="center" gap={0}>
{ (() => { (() => {
{
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5); const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER; const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
const showGroup = maxSlots <= 5; const showGroup = maxSlots <= 5;
@@ -145,28 +202,23 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const items: React.ReactNode[] = []; const items: React.ReactNode[] = [];
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
if(showGroup) if(showGroup) {
{
items.push( items.push(
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}> <Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />} {avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
</Flex> </Flex>
); );
} } else {
else
{
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
} }
const startIdx = showGroup ? 1 : 2; const startIdx = showGroup ? 1 : 2;
for(let i = startIdx; i < maxSlots; i++) for(let i = startIdx; i < maxSlots; i++) {
{
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />); items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
} }
const rows: React.ReactNode[][] = []; const rows: React.ReactNode[][] = [];
for(let i = 0; i < items.length; i += 2) for(let i = 0; i < items.length; i += 2) {
{
rows.push(items.slice(i, i + 2)); rows.push(items.slice(i, i + 2));
} }
@@ -191,7 +243,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
<Flex grow alignItems="center" className="min-h-[18px]"> <Flex grow alignItems="center" className="min-h-[18px]">
{!isEditingMotto && ( {!isEditingMotto && (
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}> <Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
{motto} {motto} 
</Text> </Text>
)} )}
{isEditingMotto && ( {isEditingMotto && (
@@ -235,6 +287,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
)} )}
</Column> </Column>
</Column> </Column>
</div>
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && ( {isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
<div className="backgrounds-view-container"> <div className="backgrounds-view-container">
<BackgroundsView <BackgroundsView
@@ -247,6 +300,8 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
setSelectedOverlay={setOverlayId} setSelectedOverlay={setOverlayId}
selectedCardBackground={cardBackgroundId} selectedCardBackground={cardBackgroundId}
setSelectedCardBackground={setCardBackgroundId} setSelectedCardBackground={setCardBackgroundId}
selectedBorder={borderId}
setSelectedBorder={setBorderId}
/> />
</div> </div>
)} )}
+125
View File
@@ -1595,3 +1595,128 @@
background-image: url('@/assets/images/backgrounds/overlay/overlay_8.png'); background-image: url('@/assets/images/backgrounds/overlay/overlay_8.png');
} }
} }
/* Infostand-only colored border. A thin CSS-drawn rounded ring drawn 5px
outside the card on each side. Pure CSS — no artwork files. */
.infostand-border {
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
pointer-events: none;
z-index: 5;
border-width: 5px;
border-style: solid;
border-radius: 10px;
box-sizing: content-box;
}
.infostand-border.border-1 { border-color: #ef4444; } /* Red */
.infostand-border.border-2 { border-color: #f97316; } /* Orange */
.infostand-border.border-3 { border-color: #eab308; } /* Yellow */
.infostand-border.border-4 { border-color: #84cc16; } /* Lime */
.infostand-border.border-5 { border-color: #22c55e; } /* Green */
.infostand-border.border-6 { border-color: #14b8a6; } /* Teal */
.infostand-border.border-7 { border-color: #06b6d4; } /* Cyan */
.infostand-border.border-8 { border-color: #3b82f6; } /* Blue */
.infostand-border.border-9 { border-color: #6366f1; } /* Indigo */
.infostand-border.border-10 { border-color: #a855f7; } /* Purple */
.infostand-border.border-11 { border-color: #ec4899; } /* Pink */
.infostand-border.border-12 { border-color: #f43f5e; } /* Rose */
.infostand-border.border-13 { border-color: #92400e; } /* Brown */
.infostand-border.border-14 { border-color: #d4a020; } /* Gold */
.infostand-border.border-15 { border-color: #cbd5e1; } /* Silver */
.infostand-border.border-16 { border-color: #1f2937; } /* Black */
/* Image-based borders (17-25). These override the colour-border insets and
strip the CSS border so the artwork sits ~22px outside the card and
stretches to fill the frame area. */
.infostand-border.border-17,
.infostand-border.border-18,
.infostand-border.border-19,
.infostand-border.border-20,
.infostand-border.border-21,
.infostand-border.border-22,
.infostand-border.border-23,
.infostand-border.border-24,
.infostand-border.border-25 {
top: -22px;
bottom: -22px;
left: -22px;
right: -22px;
border: none;
border-radius: 0;
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
}
.infostand-border.border-17 { background-image: url('@/assets/images/backgrounds/borders/border_17.webp'); }
.infostand-border.border-18 { background-image: url('@/assets/images/backgrounds/borders/border_18.webp'); }
.infostand-border.border-19 { background-image: url('@/assets/images/backgrounds/borders/border_19.webp'); }
.infostand-border.border-20 { background-image: url('@/assets/images/backgrounds/borders/border_20.webp'); }
.infostand-border.border-21 { background-image: url('@/assets/images/backgrounds/borders/border_21.webp'); }
.infostand-border.border-22 { background-image: url('@/assets/images/backgrounds/borders/border_22.webp'); }
.infostand-border.border-23 { background-image: url('@/assets/images/backgrounds/borders/border_23.webp'); }
.infostand-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); }
.infostand-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); }
/* Picker thumbnails inside the BackgroundsView "Borders" tab.
Each thumbnail is a small rounded box outlined in its border colour. */
.profile-border {
width: 60px;
height: 76px;
border-width: 4px;
border-style: solid;
border-radius: 8px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.05);
}
/* border-0 = no border (default) — show as a dashed translucent outline */
.profile-border.border-0 { border: 2px dashed rgba(255, 255, 255, 0.25); }
.profile-border.border-1 { border-color: #ef4444; }
.profile-border.border-2 { border-color: #f97316; }
.profile-border.border-3 { border-color: #eab308; }
.profile-border.border-4 { border-color: #84cc16; }
.profile-border.border-5 { border-color: #22c55e; }
.profile-border.border-6 { border-color: #14b8a6; }
.profile-border.border-7 { border-color: #06b6d4; }
.profile-border.border-8 { border-color: #3b82f6; }
.profile-border.border-9 { border-color: #6366f1; }
.profile-border.border-10 { border-color: #a855f7; }
.profile-border.border-11 { border-color: #ec4899; }
.profile-border.border-12 { border-color: #f43f5e; }
.profile-border.border-13 { border-color: #92400e; }
.profile-border.border-14 { border-color: #d4a020; }
.profile-border.border-15 { border-color: #cbd5e1; }
.profile-border.border-16 { border-color: #1f2937; }
/* Image-border picker thumbnails — drop the CSS frame and show the artwork. */
.profile-border.border-17,
.profile-border.border-18,
.profile-border.border-19,
.profile-border.border-20,
.profile-border.border-21,
.profile-border.border-22,
.profile-border.border-23,
.profile-border.border-24,
.profile-border.border-25 {
border: none;
background: transparent;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.profile-border.border-17 { background-image: url('@/assets/images/backgrounds/borders/border_17.webp'); }
.profile-border.border-18 { background-image: url('@/assets/images/backgrounds/borders/border_18.webp'); }
.profile-border.border-19 { background-image: url('@/assets/images/backgrounds/borders/border_19.webp'); }
.profile-border.border-20 { background-image: url('@/assets/images/backgrounds/borders/border_20.webp'); }
.profile-border.border-21 { background-image: url('@/assets/images/backgrounds/borders/border_21.webp'); }
.profile-border.border-22 { background-image: url('@/assets/images/backgrounds/borders/border_22.webp'); }
.profile-border.border-23 { background-image: url('@/assets/images/backgrounds/borders/border_23.webp'); }
.profile-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); }
.profile-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); }