diff --git a/src/api/room/widgets/AvatarInfoUser.ts b/src/api/room/widgets/AvatarInfoUser.ts index 2d3157e..6835cc1 100644 --- a/src/api/room/widgets/AvatarInfoUser.ts +++ b/src/api/room/widgets/AvatarInfoUser.ts @@ -24,6 +24,7 @@ export class AvatarInfoUser implements IAvatarInfo public standId: number = 0; public overlayId: number = 0; public cardBackgroundId: number = 0; + public borderId: number = 0; public webID: number = 0; public xp: number = 0; public userType: number = -1; diff --git a/src/api/room/widgets/AvatarInfoUtilities.ts b/src/api/room/widgets/AvatarInfoUtilities.ts index ffd8b8a..869fcd2 100644 --- a/src/api/room/widgets/AvatarInfoUtilities.ts +++ b/src/api/room/widgets/AvatarInfoUtilities.ts @@ -194,6 +194,7 @@ export class AvatarInfoUtilities userInfo.standId = userData.stand; userInfo.overlayId = userData.overlay; userInfo.cardBackgroundId = userData.cardBackground ?? 0; + userInfo.borderId = (userData as any).borderId ?? 0; userInfo.achievementScore = userData.activityPoints; userInfo.webID = userData.webID; userInfo.roomIndex = userData.roomIndex; diff --git a/src/assets/images/backgrounds/borders/border_17.webp b/src/assets/images/backgrounds/borders/border_17.webp new file mode 100644 index 0000000..78f5a10 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_17.webp differ diff --git a/src/assets/images/backgrounds/borders/border_18.webp b/src/assets/images/backgrounds/borders/border_18.webp new file mode 100644 index 0000000..caec392 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_18.webp differ diff --git a/src/assets/images/backgrounds/borders/border_19.webp b/src/assets/images/backgrounds/borders/border_19.webp new file mode 100644 index 0000000..4092794 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_19.webp differ diff --git a/src/assets/images/backgrounds/borders/border_20.webp b/src/assets/images/backgrounds/borders/border_20.webp new file mode 100644 index 0000000..8227088 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_20.webp differ diff --git a/src/assets/images/backgrounds/borders/border_21.webp b/src/assets/images/backgrounds/borders/border_21.webp new file mode 100644 index 0000000..3b2fe8c Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_21.webp differ diff --git a/src/assets/images/backgrounds/borders/border_22.webp b/src/assets/images/backgrounds/borders/border_22.webp new file mode 100644 index 0000000..d09ce2b Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_22.webp differ diff --git a/src/assets/images/backgrounds/borders/border_23.webp b/src/assets/images/backgrounds/borders/border_23.webp new file mode 100644 index 0000000..40ea0a5 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_23.webp differ diff --git a/src/assets/images/backgrounds/borders/border_24.webp b/src/assets/images/backgrounds/borders/border_24.webp new file mode 100644 index 0000000..6da2b04 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_24.webp differ diff --git a/src/assets/images/backgrounds/borders/border_25.webp b/src/assets/images/backgrounds/borders/border_25.webp new file mode 100644 index 0000000..0826e68 Binary files /dev/null and b/src/assets/images/backgrounds/borders/border_25.webp differ diff --git a/src/components/backgrounds/BackgroundsView.tsx b/src/components/backgrounds/BackgroundsView.tsx index 9216447..0f7b356 100644 --- a/src/components/backgrounds/BackgroundsView.tsx +++ b/src/components/backgrounds/BackgroundsView.tsx @@ -18,12 +18,14 @@ interface BackgroundsViewProps { setSelectedOverlay: Dispatch>; selectedCardBackground: number; setSelectedCardBackground: Dispatch>; + selectedBorder: number; + setSelectedBorder: Dispatch>; } -const TABS = ['backgrounds', 'stands', 'overlays', 'cards'] as const; +const TABS = ['backgrounds', 'stands', 'overlays', 'cards', 'borders'] as const; type TabType = typeof TABS[number]; -type RemoteData = Partial>; +type RemoteData = Partial>; export const BackgroundsView: FC = ({ setIsVisible, @@ -34,7 +36,9 @@ export const BackgroundsView: FC = ({ selectedOverlay, setSelectedOverlay, selectedCardBackground, - setSelectedCardBackground + setSelectedCardBackground, + selectedBorder, + setSelectedBorder }) => { const [activeTab, setActiveTab] = useState('backgrounds'); const [remoteData, setRemoteData] = useState(null); @@ -55,7 +59,7 @@ export const BackgroundsView: FC = ({ 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]; if(Array.isArray(fromRemote)) return fromRemote; return GetOptionalConfigurationValue(key, []) || []; @@ -65,20 +69,27 @@ export const BackgroundsView: FC = ({ backgrounds: processData(readData('backgrounds.data'), 'backgroundId'), stands: processData(readData('stands.data'), 'standId'), 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]); const handleSelection = useCallback((id: number) => { 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); const newValues = { ...currentValues, [activeTab]: id }; - roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards ); - }, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, selectedCardBackground, setSelectedBackground, setSelectedStand, setSelectedOverlay, setSelectedCardBackground]); + roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays, newValues.cards, newValues.borders ); + }, [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) => ( = ({ > ), [handleSelection]); @@ -111,7 +126,7 @@ export const BackgroundsView: FC = ({ Select an Option - {allData[activeTab].map(item => renderItem(item, activeTab === 'cards' ? 'card-background' : activeTab.slice(0, -1)))} + {allData[activeTab].map(item => renderItem(item, itemTypeFor(activeTab)))} diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 6ecb092..8a84dc0 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -25,6 +25,7 @@ export const InfoStandWidgetUserView: FC = props = const [standId, setStandId] = useState(null); const [overlayId, setOverlayId] = useState(null); const [cardBackgroundId, setCardBackgroundId] = useState(null); + const [borderId, setBorderId] = useState(null); const [isVisible, setIsVisible] = useState(false); const { roomSession = null } = useRoom(); @@ -32,6 +33,7 @@ export const InfoStandWidgetUserView: FC = props = const infostandStandClass = `stand-${standId ?? 'default'}`; const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`; const infostandCardBackgroundClass = cardBackgroundId ? `card-background-${cardBackgroundId}` : ''; + const infostandBorderClass = borderId ? `border-${borderId}` : ''; const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []); @@ -99,6 +101,7 @@ export const InfoStandWidgetUserView: FC = props = newValue.standId = event.standId; newValue.overlayId = event.overlayId; newValue.cardBackgroundId = event.cardBackgroundId ?? 0; + newValue.borderId = event.borderId ?? 0; return newValue; }); }); @@ -134,6 +137,7 @@ export const InfoStandWidgetUserView: FC = props = setStandId(avatarInfo.standId); setOverlayId(avatarInfo.overlayId); setCardBackgroundId(avatarInfo.cardBackgroundId ?? 0); + setBorderId(avatarInfo.borderId ?? 0); SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID)); @@ -146,7 +150,9 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
+ {borderId ? : null} +
@@ -281,6 +287,7 @@ export const InfoStandWidgetUserView: FC = props = )} +
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
= props = setSelectedOverlay={setOverlayId} selectedCardBackground={cardBackgroundId} setSelectedCardBackground={setCardBackgroundId} + selectedBorder={borderId} + setSelectedBorder={setBorderId} />
)} diff --git a/src/css/backgrounds/BackgroundsView.css b/src/css/backgrounds/BackgroundsView.css index 1aa6114..a5e1fa8 100644 --- a/src/css/backgrounds/BackgroundsView.css +++ b/src/css/backgrounds/BackgroundsView.css @@ -1594,4 +1594,129 @@ &.overlay-8 { background-image: url('@/assets/images/backgrounds/overlay/overlay_8.png'); } -} \ No newline at end of file +} + +/* 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'); } \ No newline at end of file