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
@@ -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;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@@ -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,244 +12,299 @@ 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);
|
const [backgroundId, setBackgroundId] = useState<number>(null);
|
||||||
const [backgroundId, setBackgroundId] = useState<number>(null);
|
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();
|
||||||
|
|
||||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||||
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);
|
||||||
setIsEditingMotto(false);
|
setIsEditingMotto(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
const parser = event.getParser();
|
|
||||||
|
|
||||||
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
// Deduplicate badges from server
|
||||||
|
const seen = new Set<string>();
|
||||||
setRelationships(parser);
|
const dedupedBadges = event.badges.map(code => {
|
||||||
|
if (!code || seen.has(code)) return '';
|
||||||
|
seen.add(code);
|
||||||
|
return code;
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() =>
|
const oldBadges = avatarInfo.badges.join('');
|
||||||
{
|
|
||||||
setIsEditingMotto(false);
|
|
||||||
setMotto(avatarInfo.motto);
|
|
||||||
setBackgroundId(avatarInfo.backgroundId);
|
|
||||||
setStandId(avatarInfo.standId);
|
|
||||||
setOverlayId(avatarInfo.overlayId);
|
|
||||||
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
|
||||||
|
|
||||||
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
if (oldBadges === dedupedBadges.join('')) return;
|
||||||
|
|
||||||
return () =>
|
setAvatarInfo(prevValue => {
|
||||||
{
|
if (!prevValue) return prevValue;
|
||||||
setRelationships(null);
|
|
||||||
};
|
|
||||||
}, [avatarInfo]);
|
|
||||||
|
|
||||||
if (!avatarInfo) return null;
|
const newValue = CloneObject(prevValue);
|
||||||
|
newValue.badges = dedupedBadges;
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
|
||||||
<>
|
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) 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}`}>
|
|
||||||
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<UserProfileIconView userId={avatarInfo.webID} />
|
|
||||||
<UserIdentityView
|
|
||||||
className="text-[12px]"
|
|
||||||
displayOrder={ avatarInfo.displayOrder }
|
|
||||||
nameClassName="text-white"
|
|
||||||
nickIcon={ avatarInfo.nickIcon }
|
|
||||||
prefixColor={ avatarInfo.prefixColor }
|
|
||||||
prefixEffect={ avatarInfo.prefixEffect }
|
|
||||||
prefixFont={ avatarInfo.prefixFont }
|
|
||||||
prefixIcon={ avatarInfo.prefixIcon }
|
|
||||||
prefixText={ avatarInfo.prefixText }
|
|
||||||
username={ avatarInfo.name } />
|
|
||||||
</div>
|
|
||||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Column
|
|
||||||
fullWidth
|
|
||||||
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
|
||||||
onClick={handleProfileClick}
|
|
||||||
>
|
|
||||||
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
|
||||||
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
|
||||||
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
|
||||||
</Column>
|
|
||||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
|
||||||
<Base
|
|
||||||
className="background-edit-icon background-edit-position"
|
|
||||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
|
||||||
onClick={handleEditClick}
|
|
||||||
aria-label="Edit profile background"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Column grow alignItems="center" gap={0}>
|
|
||||||
{ (() =>
|
|
||||||
{
|
|
||||||
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
|
||||||
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
|
||||||
const showGroup = maxSlots <= 5;
|
|
||||||
|
|
||||||
const items: React.ReactNode[] = [];
|
setAvatarInfo(prevValue => {
|
||||||
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
if (!prevValue) return prevValue;
|
||||||
|
|
||||||
if(showGroup)
|
const newValue = CloneObject(prevValue);
|
||||||
{
|
newValue.figure = event.figure;
|
||||||
items.push(
|
newValue.motto = event.customInfo;
|
||||||
<Flex key="group" center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
newValue.achievementScore = event.activityPoints;
|
||||||
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
newValue.nickIcon = event.nickIcon;
|
||||||
</Flex>
|
newValue.prefixText = event.prefixText;
|
||||||
);
|
newValue.prefixColor = event.prefixColor;
|
||||||
}
|
newValue.prefixIcon = event.prefixIcon;
|
||||||
else
|
newValue.prefixEffect = event.prefixEffect;
|
||||||
{
|
newValue.displayOrder = event.displayOrder;
|
||||||
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
newValue.backgroundId = event.backgroundId;
|
||||||
}
|
newValue.standId = event.standId;
|
||||||
|
newValue.overlayId = event.overlayId;
|
||||||
|
newValue.cardBackgroundId = event.cardBackgroundId ?? 0;
|
||||||
|
newValue.borderId = event.borderId ?? 0;
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const startIdx = showGroup ? 1 : 2;
|
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
|
||||||
for(let i = startIdx; i < maxSlots; i++)
|
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||||
{
|
|
||||||
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows: React.ReactNode[][] = [];
|
setAvatarInfo(prevValue => {
|
||||||
for(let i = 0; i < items.length; i += 2)
|
if (!prevValue) return prevValue;
|
||||||
{
|
|
||||||
rows.push(items.slice(i, i + 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.map((row, idx) => (
|
const newValue = CloneObject(prevValue);
|
||||||
<Flex key={idx} center gap={1}>{row}</Flex>
|
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||||
));
|
|
||||||
})() }
|
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||||
</Column>
|
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
||||||
</div>
|
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
return newValue;
|
||||||
</div>
|
});
|
||||||
<div className="flex flex-col gap-1">
|
});
|
||||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
|
||||||
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
|
||||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
const parser = event.getParser();
|
||||||
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
|
||||||
</Flex>
|
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
||||||
)}
|
|
||||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
setRelationships(parser);
|
||||||
<Flex grow alignItems="center" gap={2}>
|
});
|
||||||
<FaPencilAlt className="small fa-icon" />
|
|
||||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
useEffect(() => {
|
||||||
{!isEditingMotto && (
|
setIsEditingMotto(false);
|
||||||
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
setMotto(avatarInfo.motto);
|
||||||
{motto}
|
setBackgroundId(avatarInfo.backgroundId);
|
||||||
</Text>
|
setStandId(avatarInfo.standId);
|
||||||
)}
|
setOverlayId(avatarInfo.overlayId);
|
||||||
{isEditingMotto && (
|
setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0);
|
||||||
<input
|
setBorderId(avatarInfo.borderId ?? 0);
|
||||||
autoFocus={true}
|
|
||||||
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
||||||
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
|
||||||
type="text"
|
return () => {
|
||||||
value={motto}
|
setRelationships(null);
|
||||||
onBlur={onMottoBlur}
|
};
|
||||||
onChange={event => setMotto(event.target.value)}
|
}, [avatarInfo]);
|
||||||
onKeyDown={onMottoKeyDown}
|
|
||||||
/>
|
if (!avatarInfo) return null;
|
||||||
)}
|
|
||||||
</Flex>
|
return (
|
||||||
</Flex>
|
<>
|
||||||
)}
|
<div className="relative min-w-[190px] max-w-[190px] pointer-events-auto z-30">
|
||||||
</Flex>
|
{borderId ? <Base className={`infostand-border ${infostandBorderClass}`} /> : null}
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
<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}`}>
|
||||||
</div>
|
<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">
|
||||||
<Text small wrap variant="white">
|
<div className="flex items-center justify-between">
|
||||||
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
<div className="flex items-center gap-1">
|
||||||
</Text>
|
<UserProfileIconView userId={avatarInfo.webID} />
|
||||||
{avatarInfo.carryItem > 0 && (
|
<UserIdentityView
|
||||||
<>
|
className="text-[12px]"
|
||||||
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
displayOrder={ avatarInfo.displayOrder }
|
||||||
<Text small wrap variant="white">
|
nameClassName="text-white"
|
||||||
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
nickIcon={ avatarInfo.nickIcon }
|
||||||
</Text>
|
prefixColor={ avatarInfo.prefixColor }
|
||||||
</>
|
prefixEffect={ avatarInfo.prefixEffect }
|
||||||
)}
|
prefixFont={ avatarInfo.prefixFont }
|
||||||
</div>
|
prefixIcon={ avatarInfo.prefixIcon }
|
||||||
<div className="flex flex-col gap-1">
|
prefixText={ avatarInfo.prefixText }
|
||||||
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
username={ avatarInfo.name } />
|
||||||
</div>
|
</div>
|
||||||
{GetConfigurationValue('user.tags.enabled') && (
|
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||||
<Column className="mt-1" gap={1}>
|
</div>
|
||||||
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
</Column>
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Column
|
||||||
|
fullWidth
|
||||||
|
className={`flex items-center w-full max-w-[68px] rounded-sm relative overflow-hidden profile-background ${infostandBackgroundClass}`}
|
||||||
|
onClick={handleProfileClick}
|
||||||
|
>
|
||||||
|
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
||||||
|
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
||||||
|
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
||||||
|
</Column>
|
||||||
|
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
|
<Base
|
||||||
|
className="background-edit-icon background-edit-position"
|
||||||
|
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||||
|
onClick={handleEditClick}
|
||||||
|
aria-label="Edit profile background"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Column grow alignItems="center" gap={0}>
|
||||||
|
{ (() => {
|
||||||
|
const maxSlots = GetConfigurationValue<number>('user.badges.max.slots', 5);
|
||||||
|
const isOwnUser = avatarInfo.type === AvatarInfoUser.OWN_USER;
|
||||||
|
const showGroup = maxSlots <= 5;
|
||||||
|
|
||||||
|
const items: React.ReactNode[] = [];
|
||||||
|
items.push(<InfoStandBadgeSlotView key={0} slotIndex={0} badgeCode={avatarInfo.badges[0]} isOwnUser={isOwnUser} />);
|
||||||
|
|
||||||
|
if(showGroup) {
|
||||||
|
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)}>
|
||||||
|
{avatarInfo.groupId > 0 && <LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items.push(<InfoStandBadgeSlotView key="slot1" slotIndex={1} badgeCode={avatarInfo.badges[1]} isOwnUser={isOwnUser} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIdx = showGroup ? 1 : 2;
|
||||||
|
for(let i = startIdx; i < maxSlots; i++) {
|
||||||
|
items.push(<InfoStandBadgeSlotView key={i} slotIndex={i} badgeCode={avatarInfo.badges[i]} isOwnUser={isOwnUser} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: React.ReactNode[][] = [];
|
||||||
|
for(let i = 0; i < items.length; i += 2) {
|
||||||
|
rows.push(items.slice(i, i + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map((row, idx) => (
|
||||||
|
<Flex key={idx} center gap={1}>{row}</Flex>
|
||||||
|
));
|
||||||
|
})() }
|
||||||
|
</Column>
|
||||||
|
</div>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||||
|
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
||||||
|
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||||
|
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
|
<Flex grow alignItems="center" gap={2}>
|
||||||
|
<FaPencilAlt className="small fa-icon" />
|
||||||
|
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||||
|
{!isEditingMotto && (
|
||||||
|
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
||||||
|
{motto}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Column>
|
{isEditingMotto && (
|
||||||
</Column>
|
<input
|
||||||
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
autoFocus={true}
|
||||||
<div className="backgrounds-view-container">
|
className="w-full h-full text-[12px] p-0 outline-0 border-0 text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
||||||
<BackgroundsView
|
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
||||||
setIsVisible={setIsVisible}
|
type="text"
|
||||||
selectedBackground={backgroundId}
|
value={motto}
|
||||||
setSelectedBackground={setBackgroundId}
|
onBlur={onMottoBlur}
|
||||||
selectedStand={standId}
|
onChange={event => setMotto(event.target.value)}
|
||||||
setSelectedStand={setStandId}
|
onKeyDown={onMottoKeyDown}
|
||||||
selectedOverlay={overlayId}
|
/>
|
||||||
setSelectedOverlay={setOverlayId}
|
)}
|
||||||
selectedCardBackground={cardBackgroundId}
|
</Flex>
|
||||||
setSelectedCardBackground={setCardBackgroundId}
|
</Flex>
|
||||||
/>
|
)}
|
||||||
</div>
|
</Flex>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Text small wrap variant="white">
|
||||||
|
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
||||||
|
</Text>
|
||||||
|
{avatarInfo.carryItem > 0 && (
|
||||||
|
<>
|
||||||
|
<hr className="m-0 bg-[#0003] border-0 opacity-[0.5] h-px" />
|
||||||
|
<Text small wrap variant="white">
|
||||||
|
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
<div className="flex flex-col gap-1">
|
||||||
|
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
||||||
|
</div>
|
||||||
|
{GetConfigurationValue('user.tags.enabled') && (
|
||||||
|
<Column className="mt-1" gap={1}>
|
||||||
|
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
|
</div>
|
||||||
|
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||||
|
<div className="backgrounds-view-container">
|
||||||
|
<BackgroundsView
|
||||||
|
setIsVisible={setIsVisible}
|
||||||
|
selectedBackground={backgroundId}
|
||||||
|
setSelectedBackground={setBackgroundId}
|
||||||
|
selectedStand={standId}
|
||||||
|
setSelectedStand={setStandId}
|
||||||
|
selectedOverlay={overlayId}
|
||||||
|
setSelectedOverlay={setOverlayId}
|
||||||
|
selectedCardBackground={cardBackgroundId}
|
||||||
|
setSelectedCardBackground={setCardBackgroundId}
|
||||||
|
selectedBorder={borderId}
|
||||||
|
setSelectedBorder={setBorderId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'); }
|
||||||