From bd71d326fbb1885f7a15f0293220f433181bdb50 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 18:58:07 +0200 Subject: [PATCH 01/39] style(toolbar): solid bottom bar (classic Habbo look, less glassy) --- src/components/toolbar/ToolbarView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index e8ad69a..cdb385d 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -222,7 +222,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ shellVariants } transition={ SHELL_TRANSITION } - className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(10,10,12,0.58)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> + className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(18,19,24,0.97)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> = props => animate={ visibilityVariant } variants={ mobileNavVariants } transition={ NAV_TRANSITION } - className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(10,10,12,0.58)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> + className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(18,19,24,0.97)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> @@ -448,7 +448,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ mobileNavVariants } transition={ NAV_TRANSITION } style={ staffStackBottom != null ? { top: 'auto', bottom: `${ staffStackBottom }px` } : undefined } - className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(10,10,12,0.58)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> + className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(18,19,24,0.97)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> From c311f3597d624b1e80f48a4f85e795c409a50f9d Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 20:26:47 +0200 Subject: [PATCH 02/39] fix(friendbar): guarantee a single find-friends chip (filter null friends) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy bar rendered MAX_DISPLAY_COUNT FriendBarItemViews and padded empty slots with null, so an empty online-friends list produced three identical 'Trova Amici' buttons. The current bar already renders a single explicit search chip, but harden it: filter null/undefined out of the online-friends array before slicing/mapping so the search chip is the only possible source of that affordance — exactly one, always. --- .../friends/views/friends-bar/FriendsBarView.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/friends/views/friends-bar/FriendsBarView.tsx b/src/components/friends/views/friends-bar/FriendsBarView.tsx index 9234a07..aa66c09 100644 --- a/src/components/friends/views/friends-bar/FriendsBarView.tsx +++ b/src/components/friends/views/friends-bar/FriendsBarView.tsx @@ -79,11 +79,17 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount // below uses it, so a stale `indexOffset` (after the list shrinks or the fit // grows) renders correctly and self-corrects on the next arrow click — no // write-back effect needed. - const maxOffset = Math.max(0, (onlineFriends.length - maxVisible)); + // Defensive: never let a null/undefined slip into the friend map. The + // legacy bar padded empty slots with `null` and rendered each as a + // FriendBarItemView (which falls back to the "find friends" chip), so an + // empty list produced THREE "Trova Amici" buttons. Filtering here makes the + // search chip below the ONLY source of that affordance — exactly one, always. + const validFriends = onlineFriends.filter(Boolean); + const maxOffset = Math.max(0, (validFriends.length - maxVisible)); const safeOffset = Math.min(indexOffset, maxOffset); const canScrollLeft = (safeOffset > 0); const canScrollRight = (safeOffset < maxOffset); - const visibleFriends = onlineFriends.slice(safeOffset, (safeOffset + maxVisible)); + const visibleFriends = validFriends.slice(safeOffset, (safeOffset + maxVisible)); return ( - { (!onlineFriends.length && (requestsCount <= 0)) && + { (!validFriends.length && (requestsCount <= 0)) && Date: Sun, 14 Jun 2026 20:53:39 +0200 Subject: [PATCH 03/39] feat(toolbar): remove show/hide toggle button and its animation Drop the chevron toggle (tb-toggle) and the collapse/expand behavior: the toolbar is now always visible (no isToolbarOpen state, no handleToggleClick, no lock timers). The nav blocks render statically (initial=visible) so there's no show/hide slide-in effect, and the chat-input frame sits in the bar at all times. Removes the now-dead tb-toggle CSS and the unused useRef/useCallback imports. --- src/components/toolbar/ToolbarView.tsx | 71 +++----------------------- 1 file changed, 8 insertions(+), 63 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index cdb385d..7a8b1ec 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -1,6 +1,6 @@ import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer'; import { AnimatePresence, motion, Variants } from 'framer-motion'; -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api'; import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common'; import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMentionsSnapshot, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useSoundboard, useWiredTools } from '../../hooks'; @@ -26,13 +26,11 @@ const shellVariants: Variants = { const SHELL_TRANSITION = { type: 'spring' as const, stiffness: 260, damping: 26 }; const NAV_TRANSITION = { type: 'spring' as const, stiffness: 300, damping: 28 }; const ME_POPOVER_TRANSITION = { type: 'spring' as const, stiffness: 420, damping: 28 }; -const TOGGLE_LOCK_MS = 220; export const ToolbarView: FC<{ isInRoom: boolean }> = props => { const { isInRoom } = props; const [ isMeExpanded, setMeExpanded ] = useState(false); - const [ isToolbarOpen, setIsToolbarOpen ] = useState(false); const [ isTouchLayout, setIsTouchLayout ] = useState(false); const [ staffStackBottom, setStaffStackBottom ] = useState(null); const [ useGuideTool, setUseGuideTool ] = useState(false); @@ -54,26 +52,9 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => () => isMod ? tickets.filter(ticket => ticket && (ticket.state === 1)).length : 0, [ isMod, tickets ] ); - const isVisible = (isToolbarOpen || !isInRoom); - const visibilityVariant = isVisible ? 'visible' : 'hidden'; - const toggleLockRef = useRef(false); - const toggleTimeoutRef = useRef | null>(null); + const visibilityVariant = 'visible'; - useEffect(() => () => - { - if(toggleTimeoutRef.current) clearTimeout(toggleTimeoutRef.current); - }, []); - - const handleToggleClick = useCallback(() => - { - if(toggleLockRef.current) return; - toggleLockRef.current = true; - setIsToolbarOpen(value => !value); - if(toggleTimeoutRef.current) clearTimeout(toggleTimeoutRef.current); - toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS); - }, []); - - const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1700px]:bottom-0' : 'bottom-0'; + const compactFramePosition = 'bottom-[90px] min-[1700px]:bottom-0'; const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1700px]:hidden'; const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:block'; const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:flex'; @@ -196,20 +177,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { isInRoom &&
- - - - - = props =>
} = props => = props =>
= props => room. Always present (Builders Club), plus camera in-room and the staff-only tools when permitted. */ } Date: Sun, 14 Jun 2026 21:01:46 +0200 Subject: [PATCH 04/39] feat(chat-input): move chat-style selector to the left of the input Match the reference layout: the chat-style picker now sits before the text field (left side) instead of after it. Adds left padding + a small gap so the trigger, input and emoji selector are evenly spaced. --- src/components/room/widgets/chat-input/ChatInputView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 1edcfe5..5ab1334 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -313,7 +313,7 @@ export const ChatInputView: FC<{}> = props => return ( createPortal( -
+
{ commandSelectorVisible && = props => onSelect={ mention.apply } onHover={ mention.setSelectedIndex } /> } +
{ !floodBlocked && updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> } @@ -338,7 +339,6 @@ export const ChatInputView: FC<{}> = props => { LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } }
-
, document.getElementById('toolbar-chat-input-container')) ); }; From 450db9f81739c950d10fd2c7a0bfa63e1366fa82 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:05:47 +0200 Subject: [PATCH 05/39] feat(chat-input): style selector trigger shows selected bubble preview + caret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the generic grayscale styles-icon trigger with a ▼ caret plus a small clipped preview of the currently-selected chat bubble (chat-bubble bubble-N), matching the reference layout. --- .../widgets/chat-input/ChatInputStyleSelectorView.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx index 15f2863..a35f391 100644 --- a/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx @@ -11,7 +11,7 @@ interface ChatInputStyleSelectorViewProps export const ChatInputStyleSelectorView: FC = props => { - const { chatStyleIds = null, selectChatStyleId = null } = props; + const { chatStyleId = 0, chatStyleIds = null, selectChatStyleId = null } = props; const [ selectorVisible, setSelectorVisible ] = useState(false); const selectStyle = (styleId: number) => @@ -23,8 +23,13 @@ export const ChatInputStyleSelectorView: FC = p return ( -
-
+
+ + + +
+
+
From ba4c2f102795ecb0f9775c2bc0618e4aa1fb2ab8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:11:57 +0200 Subject: [PATCH 06/39] style(toolbar): make the bar a semi-transparent gray Change the toolbar surface from near-opaque dark (rgba(18,19,24,0.97)) to a semi-transparent gray (rgba(62,64,72,0.55)) so the room shows through, per the reference look. --- src/components/toolbar/ToolbarView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7a8b1ec..53b657e 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -189,7 +189,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ shellVariants } transition={ SHELL_TRANSITION } - className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(18,19,24,0.97)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> + className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(62,64,72,0.55)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> = props => animate={ visibilityVariant } variants={ mobileNavVariants } transition={ NAV_TRANSITION } - className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(18,19,24,0.97)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> + className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> @@ -415,7 +415,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ mobileNavVariants } transition={ NAV_TRANSITION } style={ staffStackBottom != null ? { top: 'auto', bottom: `${ staffStackBottom }px` } : undefined } - className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(18,19,24,0.97)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> + className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> From be471ca39bede333a968a7ca8402b8f87536700b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:16:25 +0200 Subject: [PATCH 07/39] feat(toolbar): add two edge collapse buttons for the icon clusters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a tab button at the left and right outer edges of the desktop toolbar. The left one hides/shows the left action icons, the right one hides/shows the friends/right cluster — each independent, toggled with a chevron that flips direction. Styled as a semi-transparent gray edge tab matching the bar. --- src/components/toolbar/ToolbarView.tsx | 46 ++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 53b657e..d97d880 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -32,6 +32,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => const { isInRoom } = props; const [ isMeExpanded, setMeExpanded ] = useState(false); const [ isTouchLayout, setIsTouchLayout ] = useState(false); + const [ leftCollapsed, setLeftCollapsed ] = useState(false); + const [ rightCollapsed, setRightCollapsed ] = useState(false); const [ staffStackBottom, setStaffStackBottom ] = useState(null); const [ useGuideTool, setUseGuideTool ] = useState(false); const [ youtubeEnabled, setYoutubeEnabled ] = useState(false); @@ -197,6 +199,16 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ leftNavVariants } transition={ NAV_TRANSITION } className={ `tb-nav-clip fixed bottom-0 left-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center pl-3 ${ desktopFlexClasses }` }> + + { !leftCollapsed && @@ -285,7 +297,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> } - + } = props => variants={ rightNavVariants } transition={ NAV_TRANSITION } className={ `tb-nav-clip fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center pr-3 ${ desktopFlexClasses } ${ isInRoom ? 'right-0' : 'right-3' }` }> + { !rightCollapsed && @@ -313,7 +326,16 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => }
- + } + Date: Sun, 14 Jun 2026 21:26:24 +0200 Subject: [PATCH 08/39] style(toolbar): grayer, more opaque bar surface (less brown bleed-through) --- src/components/toolbar/ToolbarView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index d97d880..48e0bdc 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -191,7 +191,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ shellVariants } transition={ SHELL_TRANSITION } - className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(62,64,72,0.55)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> + className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(112,114,122,0.80)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> = props => animate={ visibilityVariant } variants={ mobileNavVariants } transition={ NAV_TRANSITION } - className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> + className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(112,114,122,0.80)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> @@ -437,7 +437,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ mobileNavVariants } transition={ NAV_TRANSITION } style={ staffStackBottom != null ? { top: 'auto', bottom: `${ staffStackBottom }px` } : undefined } - className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> + className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(112,114,122,0.80)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> From 0714bc8e8bc301000c26ddb478f632571c5f78a6 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:28:58 +0200 Subject: [PATCH 09/39] fix(toolbar): revert bar to previous gray + invert collapse arrows Restore the bar surface to rgba(62,64,72,0.55) (the previous look was preferred) and flip both edge-collapse chevrons so they point the way shown in the reference screenshots. --- src/components/toolbar/ToolbarView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 48e0bdc..5efdd59 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -191,7 +191,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ shellVariants } transition={ SHELL_TRANSITION } - className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(112,114,122,0.80)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> + className={ `pointer-events-none fixed bottom-0 left-0 right-0 z-[39] h-[52px] rounded-t-[12px] border border-b-0 border-white/8 bg-[rgba(62,64,72,0.55)] shadow-[0_-6px_18px_rgba(0,0,0,0.18)] ${ desktopBlockClasses }` } /> = props => aria-label="Mostra/Nascondi icone" className="tb-collapse pointer-events-auto mt-[6px] mr-[3px]"> - + { !leftCollapsed && @@ -333,7 +333,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => aria-label="Mostra/Nascondi icone" className="tb-collapse pointer-events-auto mt-[6px] ml-[3px]"> - + @@ -342,7 +342,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ mobileNavVariants } transition={ NAV_TRANSITION } - className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(112,114,122,0.80)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> + className={ `fixed left-1/2 bottom-0 z-40 flex w-[95vw] -translate-x-1/2 items-center overflow-visible ${ mobileOnlyClasses } ${ isInRoom ? 'rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[6px] py-[4px] mb-[3px] shadow-[0_-6px_18px_rgba(0,0,0,0.18)]' : '' }` }> @@ -437,7 +437,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ mobileNavVariants } transition={ NAV_TRANSITION } style={ staffStackBottom != null ? { top: 'auto', bottom: `${ staffStackBottom }px` } : undefined } - className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(112,114,122,0.80)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> + className={ `fixed left-1 z-40 flex flex-col items-center gap-2 rounded-[12px] border border-white/8 bg-[rgba(62,64,72,0.55)] px-[4px] py-[6px] shadow-[0_6px_18px_rgba(0,0,0,0.18)] ${ staffStackBottom == null ? 'top-1/2 -translate-y-1/2' : '' } ${ mobileOnlyClasses }` }> CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> From 45866c17d3d90171bcaab512103ee10908fd4c6a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:37:00 +0200 Subject: [PATCH 10/39] feat(toolbar): keep a core icon set when the left side is collapsed When the left collapse button is active, keep the core icons visible (catalog, avatar/me, builders club, inventory, camera) and hide only the secondary ones (habbo/home, rooms, game, rare-values, fortune-wheel, wired, youtube, soundboard, modtools, housekeeping, furni-editor). --- src/components/toolbar/ToolbarView.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 5efdd59..9a26a5f 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -208,10 +208,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => - { !leftCollapsed && + { !leftCollapsed && (<> { isInRoom ? VisitDesktop() } className="tb-icon" /> @@ -224,6 +224,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('games/toggle') } className="tb-icon" /> } + ) } CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> @@ -261,6 +262,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { (getFullCount > 0) && } + { !leftCollapsed && (<> CreateLinkEvent('rare-values/toggle') } className="tb-icon" /> @@ -271,10 +273,12 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } + ) } { isInRoom && CreateLinkEvent('camera/toggle') } className="tb-icon" /> } + { !leftCollapsed && (<> { (isInRoom && youtubeEnabled) && @@ -297,7 +301,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> } - } + ) } + Date: Sun, 14 Jun 2026 21:40:28 +0200 Subject: [PATCH 11/39] feat(toolbar): keep friends + find-friends when the friend bar is collapsed When the right collapse button is active, keep the friends-list icon and show a compact find-friends (magnifier) button, hiding mentions, the messenger icon and the full friend bar. --- src/components/toolbar/ToolbarView.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 9a26a5f..29dc624 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -1,7 +1,7 @@ -import { CreateLinkEvent, Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, Dispose, DropBounce, EaseOut, FindNewFriendsMessageComposer, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait, YouTubeRoomSettingsEvent } from '@nitrots/nitro-renderer'; import { AnimatePresence, motion, Variants } from 'framer-motion'; import { FC, useEffect, useMemo, useState } from 'react'; -import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api'; +import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, SendMessageComposer, setYoutubeRoomEnabled, VisitDesktop } from '../../api'; import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common'; import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMentionsSnapshot, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useSoundboard, useWiredTools } from '../../hooks'; import { ToolbarItemView } from './ToolbarItemView'; @@ -310,7 +310,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => variants={ rightNavVariants } transition={ NAV_TRANSITION } className={ `tb-nav-clip fixed bottom-0 z-40 h-[52px] max-w-[calc(50vw-242px)] items-center pr-3 ${ desktopFlexClasses } ${ isInRoom ? 'right-0' : 'right-3' }` }> - { !rightCollapsed && @@ -319,6 +318,11 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { (requests.length > 0) && } + { rightCollapsed && + + SendMessageComposer(new FindNewFriendsMessageComposer()) } className="tb-icon" /> + } + { !rightCollapsed && (<> { mentionsEnabled && CreateLinkEvent('mentions/toggle') } className="tb-icon" /> @@ -331,7 +335,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => }
- } + ) } + - - - -
-
-
-
-
-
- { seasonalCurrencies.length > 0 && -
- { seasonalCurrencies.map(type => ) } -
} - - ); -}; diff --git a/src/components/purse/PurseModernView.tsx b/src/components/purse/PurseModernView.tsx deleted file mode 100644 index e3579fd..0000000 --- a/src/components/purse/PurseModernView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { CreateLinkEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo } from 'react'; -import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; -import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; -import { Column, LayoutCurrencyIcon } from '../../common'; -import { usePurse } from '../../hooks'; -import { CurrencyView } from './views/CurrencyView'; -import { SeasonalView } from './views/SeasonalView'; - -const localizeWithFallback = (key: string, fallback: string) => -{ - const text = LocalizeText(key); - return (text && text !== key) ? text : fallback; -}; - -export const PurseModernView: FC<{}> = props => -{ - const { purse = null, hcDisabled = false } = usePurse(); - - const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); - const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); - - const currencyTypes = useMemo(() => - { - if (!purse || !purse.activityPoints || !purse.activityPoints.size) return []; - - const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0)); - types.sort((a, b) => - { - if (a === 0) return -1; - if (b === 0) return 1; - if (a === 5) return -1; - if (b === 5) return 1; - return a - b; - }); - - return types; - }, [ displayedCurrencies, purse ]); - - const hasDiamonds = currencyTypes.indexOf(5) >= 0; - const hasDuckets = currencyTypes.indexOf(0) >= 0; - const otherCurrencies = currencyTypes.filter(type => (type !== 0 && type !== 5)); - - const joinLabel = useMemo(() => localizeWithFallback('purse.join', 'Join'), []); - const earningsLabel = useMemo(() => localizeWithFallback('earnings.title', 'Earnings'), []); - const helpLabel = useMemo(() => localizeWithFallback('help.button.name', 'Help'), []); - - const openClub = useCallback((event: React.MouseEvent) => - { - event.stopPropagation(); - - const page = GetConfigurationValue('hc.buy_hc', 'habbo_club'); - CreateLinkEvent('catalog/open/' + page); - }, []); - - const openEarnings = useCallback((event: React.MouseEvent) => - { - event.stopPropagation(); - CreateLinkEvent('habboUI/open/vault'); - }, []); - - const handleLogout = useCallback(async (event: React.MouseEvent) => - { - event.stopPropagation(); - - const logoutUrl = GetConfigurationValue('login.logout.endpoint', '/api/auth/logout'); - const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? ''; - const rememberToken = GetRememberLogin()?.token || ''; - - try - { - await fetch(logoutUrl, { - method: 'POST', - credentials: 'include', - keepalive: true, - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Requested-With': 'NitroPurseLogout' - }, - body: JSON.stringify({ ssoTicket, rememberToken }) - }); - } - catch - { /* best-effort — proceed with local logout regardless */ } - - ClearRememberLogin(); - if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; - window.location.reload(); - }, []); - - if (!purse) return null; - - return ( - -
-
-
- { hasDiamonds && } - - { hasDuckets && } -
-
- { !hcDisabled && - } - -
-
- - - -
-
-
- { otherCurrencies.length > 0 && -
- { otherCurrencies.map(type => ) } -
} -
- ); -}; diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index b039ebd..32d2fe8 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -1,7 +1,139 @@ -import { FC } from 'react'; -import { PurseModernView } from './PurseModernView'; +import { CreateLinkEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo } from 'react'; +import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; +import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; +import { Column, LayoutCurrencyIcon } from '../../common'; +import { usePurse } from '../../hooks'; +import { CurrencyView } from './views/CurrencyView'; +import { SeasonalView } from './views/SeasonalView'; + +const localizeWithFallback = (key: string, fallback: string) => +{ + const text = LocalizeText(key); + return (text && text !== key) ? text : fallback; +}; export const PurseView: FC<{}> = props => { - return ; + const { purse = null, hcDisabled = false } = usePurse(); + + const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); + const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); + + const currencyTypes = useMemo(() => + { + if (!purse || !purse.activityPoints || !purse.activityPoints.size) return []; + + const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0)); + types.sort((a, b) => + { + if (a === 0) return -1; + if (b === 0) return 1; + if (a === 5) return -1; + if (b === 5) return 1; + return a - b; + }); + + return types; + }, [ displayedCurrencies, purse ]); + + const hasDiamonds = currencyTypes.indexOf(5) >= 0; + const hasDuckets = currencyTypes.indexOf(0) >= 0; + const otherCurrencies = currencyTypes.filter(type => (type !== 0 && type !== 5)); + + const joinLabel = useMemo(() => localizeWithFallback('purse.join', 'Join'), []); + const earningsLabel = useMemo(() => localizeWithFallback('earnings.title', 'Earnings'), []); + const helpLabel = useMemo(() => localizeWithFallback('help.button.name', 'Help'), []); + + const openClub = useCallback((event: React.MouseEvent) => + { + event.stopPropagation(); + + const page = GetConfigurationValue('hc.buy_hc', 'habbo_club'); + CreateLinkEvent('catalog/open/' + page); + }, []); + + const openEarnings = useCallback((event: React.MouseEvent) => + { + event.stopPropagation(); + CreateLinkEvent('habboUI/open/vault'); + }, []); + + const handleLogout = useCallback(async (event: React.MouseEvent) => + { + event.stopPropagation(); + + const logoutUrl = GetConfigurationValue('login.logout.endpoint', '/api/auth/logout'); + const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? ''; + const rememberToken = GetRememberLogin()?.token || ''; + + try + { + await fetch(logoutUrl, { + method: 'POST', + credentials: 'include', + keepalive: true, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'NitroPurseLogout' + }, + body: JSON.stringify({ ssoTicket, rememberToken }) + }); + } + catch + { /* best-effort — proceed with local logout regardless */ } + + ClearRememberLogin(); + if(window.NitroConfig) window.NitroConfig['sso.ticket'] = ''; + window.location.reload(); + }, []); + + if (!purse) return null; + + return ( + +
+
+
+ { hasDiamonds && } + + { hasDuckets && } +
+
+ { !hcDisabled && + } + +
+
+ + + +
+
+
+ { otherCurrencies.length > 0 && +
+ { otherCurrencies.map(type => ) } +
} +
+ ); }; diff --git a/src/css/purse/PurseClassicView.css b/src/css/purse/PurseClassicView.css deleted file mode 100644 index c3456af..0000000 --- a/src/css/purse/PurseClassicView.css +++ /dev/null @@ -1,420 +0,0 @@ -/* Classic (original) purse style. All selectors are scoped under - .nitro-purse-classic so they never collide with the modern PurseView.css - rules that share class names like .nitro-purse. */ - -.nitro-purse-classic { - width: 100%; -} - -/* Extra (seasonal) currency in classic mode reuses the modern boxed - .nitro-purse__other styling, just constrained to the classic purse width. */ -.nitro-purse__other--classic { - max-width: 125px; -} - -/* The #41403c border on the extra-currency box is new-style-only; - classic mode has no border. */ -.nitro-purse__other--classic .nitro-purse-seasonal-currency { - border: 0; -} - -.nitro-purse-classic .nitro-purse-shell { - width: 100%; - max-width: 188px; - margin-top: 6px; - margin-left: auto; -} - -.nitro-purse-classic .nitro-purse-shell.is-closed { - width: 52px; - max-width: 52px; -} - -.nitro-purse-classic .nitro-purse { - width: 100%; - max-width: none; - margin: 0; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 10px; - background: rgba(10, 10, 12, 0.58); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.05), - 0 8px 18px rgba(0, 0, 0, 0.14); - transition: width 0.24s cubic-bezier(0.22, 1, 0.36, 1), max-width 0.24s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.2s ease; -} - -.nitro-purse-classic .nitro-purse.is-closed { - width: 52px; -} - -.nitro-purse-classic .nitro-purse__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - padding: 5px 7px; - cursor: pointer; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.03)); - transition: background-color 0.2s ease, border-color 0.2s ease; -} - -.nitro-purse-classic .nitro-purse__header.is-closed { - justify-content: flex-end; - gap: 5px; - padding: 5px 6px; -} - -.nitro-purse-classic .nitro-purse__header-main { - display: inline-flex; - align-items: center; -} - -.nitro-purse-classic .nitro-purse__header-main.is-closed { - margin-right: 0; -} - -.nitro-purse-classic .nitro-purse__header-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - opacity: 0.9; -} - -.nitro-purse-classic .nitro-purse__header-image { - display: block; - width: auto; - height: 14px; - object-fit: contain; -} - -.nitro-purse-classic .nitro-purse__header-title { - font-size: 0.82rem; - font-weight: 700; - line-height: 1; - color: rgba(255, 255, 255, 0.92) !important; - letter-spacing: 0.01em; -} - -.nitro-purse-classic .nitro-purse__header-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 6px; - color: rgba(255, 255, 255, 0.82); - background: rgba(255, 255, 255, 0.06); - transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.2s ease; -} - -.nitro-purse-classic .nitro-purse__header-toggle.is-open { - transform: rotate(180deg); -} - -.nitro-purse-classic .nitro-purse__content { - display: flex; - flex-direction: column; - gap: 5px; - padding: 6px; - overflow: hidden; - transform-origin: top; - transition: - max-height 0.58s cubic-bezier(0.16, 1, 0.3, 1), - opacity 0.38s ease-out, - transform 0.58s cubic-bezier(0.16, 1, 0.3, 1), - padding 0.4s ease-out; - max-height: 280px; - opacity: 1; - transform: translateY(0); - background: transparent; -} - -.nitro-purse-classic .nitro-purse__content.is-closed { - max-height: 0; - opacity: 0; - transform: translateY(-8px) scaleY(0.95); - padding-top: 0; - padding-bottom: 0; - pointer-events: none; -} - -.nitro-purse-classic .nitro-purse__summary, -.nitro-purse-classic .nitro-purse__seasonal { - transition: - opacity 0.32s ease-out, - transform 0.48s cubic-bezier(0.16, 1, 0.3, 1); -} - -.nitro-purse-classic .nitro-purse__summary { - transition-delay: 0.08s; -} - -.nitro-purse-classic .nitro-purse__seasonal { - transition-delay: 0.16s; -} - -.nitro-purse-classic .nitro-purse__content.is-closed .nitro-purse__summary, -.nitro-purse-classic .nitro-purse__content.is-closed .nitro-purse__seasonal { - opacity: 0; - transform: translateY(-6px); - transition-delay: 0s; -} - -.nitro-purse-classic .nitro-purse__summary { - display: grid; - grid-template-columns: minmax(0, 1fr) 30px 26px; - gap: 5px; - align-items: stretch; -} - -.nitro-purse-classic .nitro-purse__summary.is-no-hc { - grid-template-columns: minmax(0, 1fr) 26px; -} - -.nitro-purse-classic .nitro-purse__primary, -.nitro-purse-classic .nitro-purse__seasonal { - display: flex; - flex-direction: column; - gap: 3px; -} - -.nitro-purse-classic .nitro-purse .nitro-purse-button, -.nitro-purse-classic .nitro-purse-seasonal-currency { - min-height: 22px; - padding: 2px 0; - border: 0 !important; - border-radius: 0 !important; - background: transparent !important; - box-shadow: none !important; -} - -.nitro-purse-classic .nitro-purse .allcurrencypurse, -.nitro-purse-classic .nitro-purse-seasonal-currency { - position: relative; -} - -.nitro-purse-classic .nitro-purse .allcurrencypurse::after, -.nitro-purse-classic .nitro-purse-seasonal-currency::after { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: -1px; - height: 1px; - background: rgba(255, 255, 255, 0.08); -} - -.nitro-purse-classic .nitro-purse__primary > :last-child::after, -.nitro-purse-classic .nitro-purse__seasonal > :last-child::after { - display: none; -} - -.nitro-purse-classic .nitro-purse .allcurrencypurse .text-white { - font-size: 0.76rem; - font-weight: 700; - line-height: 1; - letter-spacing: 0.01em; - color: rgba(255, 255, 255, 0.88) !important; -} - -.nitro-purse-classic .nitro-purse .nitro-purse-button.currency--1 .text-white { - color: #7fdcff !important; -} - -.nitro-purse-classic .nitro-purse .nitro-purse-button.currency-0 .text-white { - color: #ffd76d !important; -} - -.nitro-purse-classic .nitro-purse .nitro-purse-button.currency-5 .text-white { - color: #df95ff !important; -} - -.nitro-purse-classic .nitro-purse-subscription { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - min-height: 62px; - cursor: pointer; - border-left: 1px solid rgba(255, 255, 255, 0.08); - border-right: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.02); -} - -.nitro-purse-classic .nitro-purse-subscription__icon { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - opacity: 0.95; -} - -.nitro-purse-classic .nitro-purse-subscription__copy { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: 1px; -} - -.nitro-purse-classic .nitro-purse-subscription__label { - font-size: 0.5rem; - font-weight: 700; - color: rgba(255, 255, 255, 0.62) !important; - letter-spacing: 0.08em; -} - -.nitro-purse-classic .nitro-purse-subscription__value { - font-size: 0.54rem; - font-weight: 700; - line-height: 1.05; - color: #ffffff !important; -} - -.nitro-purse-classic .nitro-purse__actions { - display: grid; - grid-template-columns: 1fr; - gap: 3px; -} - -.nitro-purse-classic .nitro-purse__action-button { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 20px; - padding: 0; - border: 1px solid rgba(7, 23, 31, 0.82); - border-radius: 7px; - color: rgba(255, 255, 255, 0.88); - background: rgba(255, 255, 255, 0.05); - box-shadow: none; - transition: background-color 0.18s ease, transform 0.18s ease; -} - -.nitro-purse-classic .nitro-purse__action-button:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateY(-1px); -} - -.nitro-purse-classic .nitro-purse__action-button .nitro-icon { - transform: scale(0.82); -} - -.nitro-purse-classic .nitro-purse-seasonal-currency > div { - align-items: center; - gap: 6px; - padding: 0; -} - -.nitro-purse-classic .seasonal-row { - min-width: 0; -} - -.nitro-purse-classic .seasonal-text-padding, -.nitro-purse-classic .seasonal-amount { - display: flex; - align-items: center; - margin-left: 0; -} - -.nitro-purse-classic .seasonal-text { - min-width: 0; - font-size: 0.76rem; - font-weight: 700; - color: rgba(255, 255, 255, 0.76) !important; - line-height: 1; - letter-spacing: 0.01em; -} - -.nitro-purse-classic .seasonal-amount { - margin-left: auto; - white-space: nowrap; - flex: 0 0 auto; - font-size: 0.76rem; - font-weight: 700; - line-height: 1; - color: rgba(255, 255, 255, 0.96) !important; -} - -.nitro-purse-classic .seasonal-image-padding { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border-radius: 0; - background: transparent; - flex: 0 0 auto; -} - -.nitro-purse-classic .seasonal-image { - display: block; - width: auto; - height: 14px; - object-fit: contain; -} - -@media (max-width: 640px) { - .nitro-purse-classic .nitro-purse-shell { - max-width: 100%; - } - - .nitro-purse-classic .nitro-purse-shell.is-closed { - max-width: 52px; - } - - .nitro-purse-classic .nitro-purse { - border-radius: 9px; - } - - .nitro-purse-classic .nitro-purse__content { - padding: 6px; - } - - .nitro-purse-classic .nitro-purse__summary { - grid-template-columns: minmax(0, 1fr) 28px 24px; - gap: 4px; - } - - .nitro-purse-classic .nitro-purse__summary.is-no-hc { - grid-template-columns: minmax(0, 1fr) 24px; - } - - .nitro-purse-classic .nitro-purse-subscription { - min-height: 58px; - } - - .nitro-purse-classic .nitro-purse .allcurrencypurse .text-white, - .nitro-purse-classic .seasonal-text, - .nitro-purse-classic .seasonal-amount { - font-size: 0.72rem; - } - - .nitro-purse-classic .nitro-purse__header-title { - font-size: 0.78rem; - } -} - -@media (max-width: 420px) { - .nitro-purse-classic .nitro-purse__summary { - grid-template-columns: minmax(0, 1fr) 26px 22px; - gap: 4px; - } - - .nitro-purse-classic .nitro-purse__summary.is-no-hc { - grid-template-columns: minmax(0, 1fr) 22px; - } - - .nitro-purse-classic .nitro-purse-subscription { - min-height: 54px; - } - - .nitro-purse-classic .nitro-purse-subscription__value { - font-size: 0.55rem; - } -} From 28df1da69b2370e459f01f50f51596230a4df44a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 22:17:30 +0200 Subject: [PATCH 19/39] style(help): green action buttons + drop the disabled tips button Make the two help actions (report bully / player support) green and remove the always-disabled 'tips' button, matching the reference help window. --- src/components/help/views/HelpIndexView.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/help/views/HelpIndexView.tsx b/src/components/help/views/HelpIndexView.tsx index 339993a..ceaa1ad 100644 --- a/src/components/help/views/HelpIndexView.tsx +++ b/src/components/help/views/HelpIndexView.tsx @@ -27,9 +27,8 @@ export const HelpIndexView: FC<{}> = props => { LocalizeText('help.main.self.description') }
- - - + +
From c2be4dbed3884c46fa8d4653dc3b80abdfc59dfc Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 22:25:55 +0200 Subject: [PATCH 20/39] feat(settings): gear dropdown opening focused settings sections The purse gear now opens a dropdown (Audio / Discord / Chat / Altre / Filtro Parole). Audio/Chat/Altre open UserSettingsView focused on that section (reusing the existing volume + preference controls) with a Back button; Discord and Filtro Parole are placeholders for now. --- src/components/purse/PurseView.tsx | 19 +- .../user-settings/UserSettingsView.tsx | 162 +++++++++++------- src/css/purse/PurseView.css | 39 +++++ 3 files changed, 155 insertions(+), 65 deletions(-) diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index 32d2fe8..f673f1a 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -1,5 +1,5 @@ import { CreateLinkEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo } from 'react'; +import { FC, useCallback, useMemo, useState } from 'react'; import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; import { Column, LayoutCurrencyIcon } from '../../common'; @@ -16,6 +16,13 @@ const localizeWithFallback = (key: string, fallback: string) => export const PurseView: FC<{}> = props => { const { purse = null, hcDisabled = false } = usePurse(); + const [ settingsMenuOpen, setSettingsMenuOpen ] = useState(false); + + const openSettingsSection = useCallback((section: string) => + { + CreateLinkEvent('user-settings/show/' + section); + setSettingsMenuOpen(false); + }, []); const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); @@ -123,13 +130,21 @@ export const PurseView: FC<{}> = props =>
+ { settingsMenuOpen && +
+ + + + + +
} { otherCurrencies.length > 0 &&
{ otherCurrencies.map(type => ) } diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 4ed5dbd..7d4acf5 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -2,13 +2,24 @@ import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, NitroSettingsE import { FC, useEffect, useState } from 'react'; import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; import { classNames } from '../../layout'; +const localizeWithFallback = (key: string, fallback: string) => +{ + const text = LocalizeText(key); + return (text && text !== key) ? text : fallback; +}; + +// null = full window (legacy). 'audio' | 'chat' | 'other' = focused section +// opened from the purse gear dropdown. +type SettingsSection = null | 'audio' | 'chat' | 'other'; + export const UserSettingsView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); + const [ section, setSection ] = useState(null); const [ userSettings, setUserSettings ] = useState(null); const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); @@ -100,12 +111,14 @@ export const UserSettingsView: FC<{}> = props => switch(parts[1]) { case 'show': + setSection((parts[2] as SettingsSection) || null); setIsVisible(true); return; case 'hide': setIsVisible(false); return; case 'toggle': + setSection((parts[2] as SettingsSection) || null); setIsVisible(prevValue => !prevValue); return; } @@ -127,81 +140,104 @@ export const UserSettingsView: FC<{}> = props => if(!isVisible || !userSettings) return null; + const showChat = (section === null || section === 'chat'); + const showOther = (section === null || section === 'other'); + const showAudio = (section === null || section === 'audio'); + const showAccountLink = (section === null); + + const headerText = (section === 'audio') + ? localizeWithFallback('widget.memenu.settings.volume', 'Audio settings') + : (section === 'chat') + ? localizeWithFallback('room.chat.settings.title', 'Chat settings') + : (section === 'other') + ? localizeWithFallback('memenu.settings.other', 'Other settings') + : LocalizeText('widget.memenu.settings.title'); + return ( - processAction('close_view') } /> + processAction('close_view') } /> -
-
- processAction('oldchat', event.target.checked) } /> - { LocalizeText('memenu.settings.chat.prefer.old.chat') } -
-
- processAction('room_invites', event.target.checked) } /> - { LocalizeText('memenu.settings.other.ignore.room.invites') } -
-
- processAction('camera_follow', event.target.checked) } /> - { LocalizeText('memenu.settings.other.disable.room.camera.follow') } -
-
- setCatalogPlaceMultipleObjects(event.target.checked) } /> - { LocalizeText('memenu.settings.other.place.multiple.objects') } -
-
- setCatalogSkipPurchaseConfirmation(event.target.checked) } /> - { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } -
-
- setChatWindowEnabled(event.target.checked) } /> - { LocalizeText('memenu.settings.other.enable.chat.window') } -
-
-
- { LocalizeText('widget.memenu.settings.volume') } + { showChat &&
- { LocalizeText('widget.memenu.settings.volume.ui') }
- { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('oldchat', event.target.checked) } /> + { LocalizeText('memenu.settings.chat.prefer.old.chat') }
-
+
+ setChatWindowEnabled(event.target.checked) } /> + { LocalizeText('memenu.settings.other.enable.chat.window') } +
+
} + { showOther &&
- { LocalizeText('widget.memenu.settings.volume.furni') }
- { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('room_invites', event.target.checked) } /> + { LocalizeText('memenu.settings.other.ignore.room.invites') }
-
-
- { LocalizeText('widget.memenu.settings.volume.trax') }
- { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - + processAction('camera_follow', event.target.checked) } /> + { LocalizeText('memenu.settings.other.disable.room.camera.follow') }
-
-
-
- -
+ } + { showAudio && +
+ { LocalizeText('widget.memenu.settings.volume') } +
+ { LocalizeText('widget.memenu.settings.volume.ui') } +
+ { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.furni') } +
+ { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.trax') } +
+ { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
} + { showAccountLink && +
+ +
} + { (section !== null) && +
+ +
} ); diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index b60fe8b..f6a1294 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -230,6 +230,45 @@ object-fit: contain; } +/* ---- Settings dropdown (gear menu) ---- */ +.nitro-purse-menu { + width: 100%; + max-width: 200px; + margin-top: 4px; + margin-left: auto; + display: flex; + flex-direction: column; + overflow: hidden; + border: 2px solid #41403c; + border-radius: 8px; + background: rgba(10, 10, 12, 0.92); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3); + pointer-events: all; +} + +.nitro-purse-menu__item { + padding: 6px 10px; + text-align: left; + font-size: 0.78rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + background: transparent; + border: 0; + cursor: pointer; + transition: background 0.12s ease; +} + +.nitro-purse-menu__item:hover { + background: rgba(255, 255, 255, 0.08); +} + +.nitro-purse-menu__item--disabled, +.nitro-purse-menu__item--disabled:hover { + color: rgba(255, 255, 255, 0.35); + background: transparent; + cursor: default; +} + @media (max-width: 640px) { .nitro-purse { max-width: 100%; From 63b92a4e6500c5f5017c56a679d8c6f96ee50c99 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:01:17 +0200 Subject: [PATCH 21/39] feat(settings): gear opens a multi-tab settings window with account management Replace the gear dropdown with a single tabbed window: Audio / Chat / Altre / Account. Audio/Chat/Altre reuse the existing volume + preference controls; Account recovers UserAccountSettingsView (now embeddable via an 'embedded' prop that renders its body without its own card). Removes the dropdown menu + dead css. --- src/components/purse/PurseView.tsx | 19 +- .../user-settings/UserAccountSettingsView.tsx | 19 +- .../user-settings/UserSettingsView.tsx | 200 ++++++++---------- src/css/purse/PurseView.css | 39 ---- 4 files changed, 108 insertions(+), 169 deletions(-) diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index f673f1a..32d2fe8 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -1,5 +1,5 @@ import { CreateLinkEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo, useState } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; import { Column, LayoutCurrencyIcon } from '../../common'; @@ -16,13 +16,6 @@ const localizeWithFallback = (key: string, fallback: string) => export const PurseView: FC<{}> = props => { const { purse = null, hcDisabled = false } = usePurse(); - const [ settingsMenuOpen, setSettingsMenuOpen ] = useState(false); - - const openSettingsSection = useCallback((section: string) => - { - CreateLinkEvent('user-settings/show/' + section); - setSettingsMenuOpen(false); - }, []); const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); @@ -130,21 +123,13 @@ export const PurseView: FC<{}> = props => - { settingsMenuOpen && -
- - - - - -
} { otherCurrencies.length > 0 &&
{ otherCurrencies.map(type => ) } diff --git a/src/components/user-settings/UserAccountSettingsView.tsx b/src/components/user-settings/UserAccountSettingsView.tsx index 68678ab..bcf3adb 100644 --- a/src/components/user-settings/UserAccountSettingsView.tsx +++ b/src/components/user-settings/UserAccountSettingsView.tsx @@ -34,7 +34,7 @@ const passwordStrength = (value: string): { score: number; labelKey: string; col return { score: 4, labelKey: 'usersettings.strength.strong', color: 'bg-[#00800b]' }; }; -export const UserAccountSettingsView: FC<{}> = () => +export const UserAccountSettingsView: FC<{ embedded?: boolean }> = ({ embedded = false }) => { const [ isVisible, setIsVisible ] = useState(false); const [ section, setSection ] = useState
('menu'); @@ -390,12 +390,10 @@ export const UserAccountSettingsView: FC<{}> = () => } }; - if(!isVisible) return null; - - return ( - - + if(!embedded && !isVisible) return null; + const accountBody = ( + <>
{ session.figure && ( @@ -753,6 +751,15 @@ export const UserAccountSettingsView: FC<{}> = () =>
) } + + ); + + if(embedded) return accountBody; + + return ( + + + { accountBody } ); }; diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 7d4acf5..b8a55af 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -1,10 +1,11 @@ -import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, NitroSettingsEvent, RemoveLinkEventTracker, UserSettingsCameraFollowComposer, UserSettingsEvent, UserSettingsOldChatComposer, UserSettingsRoomInvitesComposer, UserSettingsSoundComposer } from '@nitrots/nitro-renderer'; +import { AddLinkEventTracker, ILinkEventTracker, NitroSettingsEvent, RemoveLinkEventTracker, UserSettingsCameraFollowComposer, UserSettingsEvent, UserSettingsOldChatComposer, UserSettingsRoomInvitesComposer, UserSettingsSoundComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; +import { FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api'; -import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common'; import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; import { classNames } from '../../layout'; +import { UserAccountSettingsView } from './UserAccountSettingsView'; const localizeWithFallback = (key: string, fallback: string) => { @@ -12,14 +13,12 @@ const localizeWithFallback = (key: string, fallback: string) => return (text && text !== key) ? text : fallback; }; -// null = full window (legacy). 'audio' | 'chat' | 'other' = focused section -// opened from the purse gear dropdown. -type SettingsSection = null | 'audio' | 'chat' | 'other'; +type SettingsTab = 'audio' | 'chat' | 'other' | 'account'; export const UserSettingsView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); - const [ section, setSection ] = useState(null); + const [ activeTab, setActiveTab ] = useState('audio'); const [ userSettings, setUserSettings ] = useState(null); const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); @@ -108,17 +107,19 @@ export const UserSettingsView: FC<{}> = props => if(parts.length < 2) return; + const tab = parts[2] as SettingsTab; + switch(parts[1]) { case 'show': - setSection((parts[2] as SettingsSection) || null); + if(tab) setActiveTab(tab); setIsVisible(true); return; case 'hide': setIsVisible(false); return; case 'toggle': - setSection((parts[2] as SettingsSection) || null); + if(tab) setActiveTab(tab); setIsVisible(prevValue => !prevValue); return; } @@ -140,105 +141,90 @@ export const UserSettingsView: FC<{}> = props => if(!isVisible || !userSettings) return null; - const showChat = (section === null || section === 'chat'); - const showOther = (section === null || section === 'other'); - const showAudio = (section === null || section === 'audio'); - const showAccountLink = (section === null); - - const headerText = (section === 'audio') - ? localizeWithFallback('widget.memenu.settings.volume', 'Audio settings') - : (section === 'chat') - ? localizeWithFallback('room.chat.settings.title', 'Chat settings') - : (section === 'other') - ? localizeWithFallback('memenu.settings.other', 'Other settings') - : LocalizeText('widget.memenu.settings.title'); - return ( - - processAction('close_view') } /> - - { showChat && -
-
- processAction('oldchat', event.target.checked) } /> - { LocalizeText('memenu.settings.chat.prefer.old.chat') } -
-
- setChatWindowEnabled(event.target.checked) } /> - { LocalizeText('memenu.settings.other.enable.chat.window') } -
-
} - { showOther && -
-
- processAction('room_invites', event.target.checked) } /> - { LocalizeText('memenu.settings.other.ignore.room.invites') } -
-
- processAction('camera_follow', event.target.checked) } /> - { LocalizeText('memenu.settings.other.disable.room.camera.follow') } -
-
- setCatalogPlaceMultipleObjects(event.target.checked) } /> - { LocalizeText('memenu.settings.other.place.multiple.objects') } -
-
- setCatalogSkipPurchaseConfirmation(event.target.checked) } /> - { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } -
-
} - { showAudio && -
- { LocalizeText('widget.memenu.settings.volume') } -
- { LocalizeText('widget.memenu.settings.volume.ui') } -
- { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
-
- { LocalizeText('widget.memenu.settings.volume.furni') } -
- { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
-
- { LocalizeText('widget.memenu.settings.volume.trax') } -
- { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
-
} - { showAccountLink && -
- -
} - { (section !== null) && -
- -
} -
+ + processAction('close_view') } /> + + setActiveTab('audio') }> + { localizeWithFallback('widget.memenu.settings.volume', 'Audio') } + + setActiveTab('chat') }> + { localizeWithFallback('room.chat.settings.title', 'Chat') } + + setActiveTab('other') }> + { localizeWithFallback('memenu.settings.other', 'Altre') } + + setActiveTab('account') }> + { localizeWithFallback('usersettings.account.label', 'Account') } + + + { (activeTab === 'account') + ? + : ( + + { (activeTab === 'chat') && +
+
+ processAction('oldchat', event.target.checked) } /> + { LocalizeText('memenu.settings.chat.prefer.old.chat') } +
+
+ setChatWindowEnabled(event.target.checked) } /> + { LocalizeText('memenu.settings.other.enable.chat.window') } +
+
} + { (activeTab === 'other') && +
+
+ processAction('room_invites', event.target.checked) } /> + { LocalizeText('memenu.settings.other.ignore.room.invites') } +
+
+ processAction('camera_follow', event.target.checked) } /> + { LocalizeText('memenu.settings.other.disable.room.camera.follow') } +
+
+ setCatalogPlaceMultipleObjects(event.target.checked) } /> + { LocalizeText('memenu.settings.other.place.multiple.objects') } +
+
+ setCatalogSkipPurchaseConfirmation(event.target.checked) } /> + { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } +
+
} + { (activeTab === 'audio') && +
+ { LocalizeText('widget.memenu.settings.volume') } +
+ { LocalizeText('widget.memenu.settings.volume.ui') } +
+ { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.furni') } +
+ { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.trax') } +
+ { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
} +
+ ) }
); }; diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index f6a1294..b60fe8b 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -230,45 +230,6 @@ object-fit: contain; } -/* ---- Settings dropdown (gear menu) ---- */ -.nitro-purse-menu { - width: 100%; - max-width: 200px; - margin-top: 4px; - margin-left: auto; - display: flex; - flex-direction: column; - overflow: hidden; - border: 2px solid #41403c; - border-radius: 8px; - background: rgba(10, 10, 12, 0.92); - box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3); - pointer-events: all; -} - -.nitro-purse-menu__item { - padding: 6px 10px; - text-align: left; - font-size: 0.78rem; - font-weight: 500; - color: rgba(255, 255, 255, 0.9); - background: transparent; - border: 0; - cursor: pointer; - transition: background 0.12s ease; -} - -.nitro-purse-menu__item:hover { - background: rgba(255, 255, 255, 0.08); -} - -.nitro-purse-menu__item--disabled, -.nitro-purse-menu__item--disabled:hover { - color: rgba(255, 255, 255, 0.35); - background: transparent; - cursor: default; -} - @media (max-width: 640px) { .nitro-purse { max-width: 100%; From 992e65cb3da74d988a48f76987192550c1382406 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:03:52 +0200 Subject: [PATCH 22/39] style(settings): card-style rows + section labels + 340px width Match the account window's look across all settings tabs: each control sits in a rounded white card row with a section label, and the window width is 340px. --- .../user-settings/UserSettingsView.tsx | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index b8a55af..2e8ac23 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -142,7 +142,7 @@ export const UserSettingsView: FC<{}> = props => if(!isVisible || !userSettings) return null; return ( - + processAction('close_view') } /> setActiveTab('audio') }> @@ -161,42 +161,44 @@ export const UserSettingsView: FC<{}> = props => { (activeTab === 'account') ? : ( - + { (activeTab === 'chat') && -
-
+ <> + { localizeWithFallback('room.chat.settings.title', 'Chat') } +
-
+ +
-
} + + } { (activeTab === 'other') && -
-
+ <> + { localizeWithFallback('memenu.settings.other', 'Altre') } +
-
+ +
-
+ +
-
+ +
-
} + + } { (activeTab === 'audio') && -
- { LocalizeText('widget.memenu.settings.volume') } -
- { LocalizeText('widget.memenu.settings.volume.ui') } + <> + { localizeWithFallback('widget.memenu.settings.volume', 'Audio') } +
+ { LocalizeText('widget.memenu.settings.volume.ui') }
{ (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } @@ -204,8 +206,8 @@ export const UserSettingsView: FC<{}> = props =>
-
- { LocalizeText('widget.memenu.settings.volume.furni') } +
+ { LocalizeText('widget.memenu.settings.volume.furni') }
{ (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } @@ -213,8 +215,8 @@ export const UserSettingsView: FC<{}> = props =>
-
- { LocalizeText('widget.memenu.settings.volume.trax') } +
+ { LocalizeText('widget.memenu.settings.volume.trax') }
{ (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } @@ -222,7 +224,7 @@ export const UserSettingsView: FC<{}> = props =>
-
} + } ) } From 340ca3e0e8df82f3796e1b19ea5799fe31084376 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:10:32 +0200 Subject: [PATCH 23/39] feat(settings): revert to gear dropdown menu + add Gestione Account entry Bring back the purse gear dropdown (Audio/Discord/Chat/Altre/Filtro) instead of the tabbed window, and add a 'Gestione Account' item that opens the account settings window. --- src/components/purse/PurseView.tsx | 20 +- .../user-settings/UserAccountSettingsView.tsx | 19 +- .../user-settings/UserSettingsView.tsx | 202 ++++++++++-------- src/css/purse/PurseView.css | 39 ++++ 4 files changed, 170 insertions(+), 110 deletions(-) diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index 32d2fe8..1ac10ef 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -1,5 +1,5 @@ import { CreateLinkEvent } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useMemo } from 'react'; +import { FC, useCallback, useMemo, useState } from 'react'; import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa'; import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api'; import { Column, LayoutCurrencyIcon } from '../../common'; @@ -16,6 +16,13 @@ const localizeWithFallback = (key: string, fallback: string) => export const PurseView: FC<{}> = props => { const { purse = null, hcDisabled = false } = usePurse(); + const [ settingsMenuOpen, setSettingsMenuOpen ] = useState(false); + + const openSettingsSection = useCallback((section: string) => + { + CreateLinkEvent('user-settings/show/' + section); + setSettingsMenuOpen(false); + }, []); const displayedCurrencies = useMemo(() => GetConfigurationValue('system.currency.types', []), []); const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue('currency.display.number.short', false), []); @@ -123,13 +130,22 @@ export const PurseView: FC<{}> = props =>
+ { settingsMenuOpen && +
+ + + + + + +
} { otherCurrencies.length > 0 &&
{ otherCurrencies.map(type => ) } diff --git a/src/components/user-settings/UserAccountSettingsView.tsx b/src/components/user-settings/UserAccountSettingsView.tsx index bcf3adb..68678ab 100644 --- a/src/components/user-settings/UserAccountSettingsView.tsx +++ b/src/components/user-settings/UserAccountSettingsView.tsx @@ -34,7 +34,7 @@ const passwordStrength = (value: string): { score: number; labelKey: string; col return { score: 4, labelKey: 'usersettings.strength.strong', color: 'bg-[#00800b]' }; }; -export const UserAccountSettingsView: FC<{ embedded?: boolean }> = ({ embedded = false }) => +export const UserAccountSettingsView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); const [ section, setSection ] = useState
('menu'); @@ -390,10 +390,12 @@ export const UserAccountSettingsView: FC<{ embedded?: boolean }> = ({ embedded = } }; - if(!embedded && !isVisible) return null; + if(!isVisible) return null; + + return ( + + - const accountBody = ( - <>
{ session.figure && ( @@ -751,15 +753,6 @@ export const UserAccountSettingsView: FC<{ embedded?: boolean }> = ({ embedded =
) } - - ); - - if(embedded) return accountBody; - - return ( - - - { accountBody } ); }; diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 2e8ac23..7d4acf5 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -1,11 +1,10 @@ -import { AddLinkEventTracker, ILinkEventTracker, NitroSettingsEvent, RemoveLinkEventTracker, UserSettingsCameraFollowComposer, UserSettingsEvent, UserSettingsOldChatComposer, UserSettingsRoomInvitesComposer, UserSettingsSoundComposer } from '@nitrots/nitro-renderer'; +import { AddLinkEventTracker, CreateLinkEvent, ILinkEventTracker, NitroSettingsEvent, RemoveLinkEventTracker, UserSettingsCameraFollowComposer, UserSettingsEvent, UserSettingsOldChatComposer, UserSettingsRoomInvitesComposer, UserSettingsSoundComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; +import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common'; +import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; import { classNames } from '../../layout'; -import { UserAccountSettingsView } from './UserAccountSettingsView'; const localizeWithFallback = (key: string, fallback: string) => { @@ -13,12 +12,14 @@ const localizeWithFallback = (key: string, fallback: string) => return (text && text !== key) ? text : fallback; }; -type SettingsTab = 'audio' | 'chat' | 'other' | 'account'; +// null = full window (legacy). 'audio' | 'chat' | 'other' = focused section +// opened from the purse gear dropdown. +type SettingsSection = null | 'audio' | 'chat' | 'other'; export const UserSettingsView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); - const [ activeTab, setActiveTab ] = useState('audio'); + const [ section, setSection ] = useState(null); const [ userSettings, setUserSettings ] = useState(null); const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); @@ -107,19 +108,17 @@ export const UserSettingsView: FC<{}> = props => if(parts.length < 2) return; - const tab = parts[2] as SettingsTab; - switch(parts[1]) { case 'show': - if(tab) setActiveTab(tab); + setSection((parts[2] as SettingsSection) || null); setIsVisible(true); return; case 'hide': setIsVisible(false); return; case 'toggle': - if(tab) setActiveTab(tab); + setSection((parts[2] as SettingsSection) || null); setIsVisible(prevValue => !prevValue); return; } @@ -141,92 +140,105 @@ export const UserSettingsView: FC<{}> = props => if(!isVisible || !userSettings) return null; + const showChat = (section === null || section === 'chat'); + const showOther = (section === null || section === 'other'); + const showAudio = (section === null || section === 'audio'); + const showAccountLink = (section === null); + + const headerText = (section === 'audio') + ? localizeWithFallback('widget.memenu.settings.volume', 'Audio settings') + : (section === 'chat') + ? localizeWithFallback('room.chat.settings.title', 'Chat settings') + : (section === 'other') + ? localizeWithFallback('memenu.settings.other', 'Other settings') + : LocalizeText('widget.memenu.settings.title'); + return ( - - processAction('close_view') } /> - - setActiveTab('audio') }> - { localizeWithFallback('widget.memenu.settings.volume', 'Audio') } - - setActiveTab('chat') }> - { localizeWithFallback('room.chat.settings.title', 'Chat') } - - setActiveTab('other') }> - { localizeWithFallback('memenu.settings.other', 'Altre') } - - setActiveTab('account') }> - { localizeWithFallback('usersettings.account.label', 'Account') } - - - { (activeTab === 'account') - ? - : ( - - { (activeTab === 'chat') && - <> - { localizeWithFallback('room.chat.settings.title', 'Chat') } - - - } - { (activeTab === 'other') && - <> - { localizeWithFallback('memenu.settings.other', 'Altre') } - - - - - } - { (activeTab === 'audio') && - <> - { localizeWithFallback('widget.memenu.settings.volume', 'Audio') } -
- { LocalizeText('widget.memenu.settings.volume.ui') } -
- { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
-
- { LocalizeText('widget.memenu.settings.volume.furni') } -
- { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
-
- { LocalizeText('widget.memenu.settings.volume.trax') } -
- { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } - { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } - processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> - -
-
- } -
- ) } + + processAction('close_view') } /> + + { showChat && +
+
+ processAction('oldchat', event.target.checked) } /> + { LocalizeText('memenu.settings.chat.prefer.old.chat') } +
+
+ setChatWindowEnabled(event.target.checked) } /> + { LocalizeText('memenu.settings.other.enable.chat.window') } +
+
} + { showOther && +
+
+ processAction('room_invites', event.target.checked) } /> + { LocalizeText('memenu.settings.other.ignore.room.invites') } +
+
+ processAction('camera_follow', event.target.checked) } /> + { LocalizeText('memenu.settings.other.disable.room.camera.follow') } +
+
+ setCatalogPlaceMultipleObjects(event.target.checked) } /> + { LocalizeText('memenu.settings.other.place.multiple.objects') } +
+
+ setCatalogSkipPurchaseConfirmation(event.target.checked) } /> + { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } +
+
} + { showAudio && +
+ { LocalizeText('widget.memenu.settings.volume') } +
+ { LocalizeText('widget.memenu.settings.volume.ui') } +
+ { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.furni') } +
+ { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
+ { LocalizeText('widget.memenu.settings.volume.trax') } +
+ { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') } /> + +
+
+
} + { showAccountLink && +
+ +
} + { (section !== null) && +
+ +
} +
); }; diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index b60fe8b..f6a1294 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -230,6 +230,45 @@ object-fit: contain; } +/* ---- Settings dropdown (gear menu) ---- */ +.nitro-purse-menu { + width: 100%; + max-width: 200px; + margin-top: 4px; + margin-left: auto; + display: flex; + flex-direction: column; + overflow: hidden; + border: 2px solid #41403c; + border-radius: 8px; + background: rgba(10, 10, 12, 0.92); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3); + pointer-events: all; +} + +.nitro-purse-menu__item { + padding: 6px 10px; + text-align: left; + font-size: 0.78rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + background: transparent; + border: 0; + cursor: pointer; + transition: background 0.12s ease; +} + +.nitro-purse-menu__item:hover { + background: rgba(255, 255, 255, 0.08); +} + +.nitro-purse-menu__item--disabled, +.nitro-purse-menu__item--disabled:hover { + color: rgba(255, 255, 255, 0.35); + background: transparent; + cursor: default; +} + @media (max-width: 640px) { .nitro-purse { max-width: 100%; From 3a5588d83a9ee469acf9241122802deba035bdc2 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:18:35 +0200 Subject: [PATCH 24/39] feat(vault): add the Guadagni/earnings window (client shell) Wire the dead 'Guadagni' purse button (habboUI/open/vault) to a new VaultView: the 10 earning categories from the reference, each with currency icons, a 0 placeholder amount and a disabled Riscatta button, plus a disabled Richiedili Tutti. Amounts/claims are placeholders until the emulator exposes the data. --- src/components/MainView.tsx | 2 + src/components/vault/VaultView.tsx | 106 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/components/vault/VaultView.tsx diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 8c883c5..1d56034 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -45,6 +45,7 @@ import { TranslationSettingsView } from './translation/TranslationSettingsView'; import { UserProfileView } from './user-profile/UserProfileView'; import { UserAccountSettingsView } from './user-settings/UserAccountSettingsView'; import { UserSettingsView } from './user-settings/UserSettingsView'; +import { VaultView } from './vault/VaultView'; import { WiredView } from './wired/WiredView'; import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; import { MentionsView } from './mentions'; @@ -221,6 +222,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx new file mode 100644 index 0000000..4579731 --- /dev/null +++ b/src/components/vault/VaultView.tsx @@ -0,0 +1,106 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { IconType } from 'react-icons'; +import { FaArrowUp, FaBoxOpen, FaBriefcase, FaCrown, FaGamepad, FaGift, FaHandHoldingHeart, FaShoppingBag, FaStore, FaTrophy } from 'react-icons/fa'; +import { LocalizeText } from '../../api'; +import { Button, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; + +const localizeWithFallback = (key: string, fallback: string) => +{ + const text = LocalizeText(key); + return (text && text !== key) ? text : fallback; +}; + +interface EarningRow +{ + key: string; + label: string; + Icon: IconType; + currencies: number[]; +} + +// Currency ids: 5 = diamonds, 0 = duckets. Amounts are placeholders (0) until +// the emulator exposes the earnings data + claim packets. +const EARNINGS: EarningRow[] = [ + { key: 'daily', label: 'Regalo giornaliero', Icon: FaGift, currencies: [ 5 ] }, + { key: 'games', label: 'Giochi', Icon: FaGamepad, currencies: [ 0 ] }, + { key: 'achievements', label: 'Traguardi', Icon: FaTrophy, currencies: [ 5, 0 ] }, + { key: 'marketplace', label: 'Mercatino', Icon: FaStore, currencies: [ 0 ] }, + { key: 'hcpayday', label: 'Bonus giorno di paga HC', Icon: FaCrown, currencies: [ 0 ] }, + { key: 'level', label: 'Progressione Livello', Icon: FaArrowUp, currencies: [ 5, 0 ] }, + { key: 'donations', label: 'Donazioni', Icon: FaHandHoldingHeart, currencies: [ 0 ] }, + { key: 'bonusbag', label: 'Sacco Bonus', Icon: FaShoppingBag, currencies: [ 0 ] }, + { key: 'surprise', label: 'Scatole Sorprese', Icon: FaBoxOpen, currencies: [ 5, 0 ] }, + { key: 'clubwork', label: 'Club e Lavoro', Icon: FaBriefcase, currencies: [ 0 ] } +]; + +export const VaultView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 3) return; + if(parts[2] !== 'vault') return; + + switch(parts[1]) + { + case 'open': + setIsVisible(true); + return; + case 'close': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + } + }, + eventUrlPrefix: 'habboUI/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + { EARNINGS.map(row => + { + const RowIcon = row.Icon; + + return ( +
+ + + + { localizeWithFallback('earnings.' + row.key, row.label) } +
+ { row.currencies.map((currency, index) => ( + + + 0 + + )) } +
+ +
+ ); + }) } +
+ +
+
+
+ ); +}; From 88f5ecffd3bea7fe893af1a5c104962780ec2b5a Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:25:11 +0200 Subject: [PATCH 25/39] style(purse): widen the gear dropdown to match the purse width (234px) --- src/css/purse/PurseView.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index f6a1294..167cc7e 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -233,7 +233,7 @@ /* ---- Settings dropdown (gear menu) ---- */ .nitro-purse-menu { width: 100%; - max-width: 200px; + max-width: 234px; margin-top: 4px; margin-left: auto; display: flex; From bf59ce231183be0e43d2535184ba0958f04a7aba Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:26:37 +0200 Subject: [PATCH 26/39] style(purse): tighten gear dropdown rows (compact spacing like reference) --- src/css/purse/PurseView.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index 167cc7e..6f67a7b 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -247,10 +247,11 @@ } .nitro-purse-menu__item { - padding: 6px 10px; + padding: 2px 10px; text-align: left; font-size: 0.78rem; font-weight: 500; + line-height: 1.3; color: rgba(255, 255, 255, 0.9); background: transparent; border: 0; From 5ac3b34916f05acb907be2f9deed7880a580862f Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:28:39 +0200 Subject: [PATCH 27/39] fix(purse): Join/Entra opens the HC Center (habboUI/open/hccenter) The Entra button pointed at catalog/open/ (a catalog page that often doesn't exist); open the real HC Center view instead. --- src/components/purse/PurseView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx index 1ac10ef..60786fe 100644 --- a/src/components/purse/PurseView.tsx +++ b/src/components/purse/PurseView.tsx @@ -55,9 +55,7 @@ export const PurseView: FC<{}> = props => const openClub = useCallback((event: React.MouseEvent) => { event.stopPropagation(); - - const page = GetConfigurationValue('hc.buy_hc', 'habbo_club'); - CreateLinkEvent('catalog/open/' + page); + CreateLinkEvent('habboUI/open/hccenter'); }, []); const openEarnings = useCallback((event: React.MouseEvent) => From 8c9566f92867906f96bc8f3ed9542e22aa94f4a5 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:31:25 +0200 Subject: [PATCH 28/39] style(vault): match the reference Guadagni layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each row is now a white bordered card (icon + name) with the currencies and a gray disabled Riscatta button outside it, plus a gray disabled Richiedili Tutti at the bottom — matching the reference screenshot. --- src/components/vault/VaultView.tsx | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 4579731..4f84959 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useState } from 'react'; import { IconType } from 'react-icons'; import { FaArrowUp, FaBoxOpen, FaBriefcase, FaCrown, FaGamepad, FaGift, FaHandHoldingHeart, FaShoppingBag, FaStore, FaTrophy } from 'react-icons/fa'; import { LocalizeText } from '../../api'; -import { Button, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; const localizeWithFallback = (key: string, fallback: string) => { @@ -20,7 +20,8 @@ interface EarningRow } // Currency ids: 5 = diamonds, 0 = duckets. Amounts are placeholders (0) until -// the emulator exposes the earnings data + claim packets. +// the emulator exposes the earnings data + claim packets. The category icons +// are FontAwesome placeholders — swap in the exact pixel icons once available. const EARNINGS: EarningRow[] = [ { key: 'daily', label: 'Regalo giornaliero', Icon: FaGift, currencies: [ 5 ] }, { key: 'games', label: 'Giochi', Icon: FaGamepad, currencies: [ 0 ] }, @@ -74,31 +75,37 @@ export const VaultView: FC<{}> = props => return ( setIsVisible(false) } /> - + { EARNINGS.map(row => { const RowIcon = row.Icon; return ( -
- - - - { localizeWithFallback('earnings.' + row.key, row.label) } -
+
+
+ + + + { localizeWithFallback('earnings.' + row.key, row.label) } +
+
{ row.currencies.map((currency, index) => ( - 0 + 0 )) }
- +
); }) }
- +
From 6b34bd9103967cd4b1535e1549e1a4d4764b94e5 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:40:32 +0200 Subject: [PATCH 29/39] feat(vault): use the hotel's real earnings_icon assets Replace the FontAwesome placeholders with the hotel's actual earnings_icon_* PNGs (daily gift, games, achievements, marketplace, HC payday, level progression, donations, bonus bag, surprise). Club e Lavoro uses the generic earnings icon (no dedicated asset). --- src/assets/images/vault/achievements.png | Bin 0 -> 724 bytes src/assets/images/vault/bonusbag.png | Bin 0 -> 815 bytes src/assets/images/vault/dailygift.png | Bin 0 -> 524 bytes src/assets/images/vault/donations.png | Bin 0 -> 454 bytes src/assets/images/vault/games.png | Bin 0 -> 749 bytes src/assets/images/vault/generic.png | Bin 0 -> 162 bytes src/assets/images/vault/hcpayday.png | Bin 0 -> 776 bytes src/assets/images/vault/levelprogression.png | Bin 0 -> 525 bytes src/assets/images/vault/marketplace.png | Bin 0 -> 617 bytes src/assets/images/vault/surprise.png | Bin 0 -> 780 bytes src/components/vault/VaultView.tsx | 85 ++++++++++--------- 11 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 src/assets/images/vault/achievements.png create mode 100644 src/assets/images/vault/bonusbag.png create mode 100644 src/assets/images/vault/dailygift.png create mode 100644 src/assets/images/vault/donations.png create mode 100644 src/assets/images/vault/games.png create mode 100644 src/assets/images/vault/generic.png create mode 100644 src/assets/images/vault/hcpayday.png create mode 100644 src/assets/images/vault/levelprogression.png create mode 100644 src/assets/images/vault/marketplace.png create mode 100644 src/assets/images/vault/surprise.png diff --git a/src/assets/images/vault/achievements.png b/src/assets/images/vault/achievements.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3d98a6f8182761c4b4dcc1b7dcf18cf9d4f511 GIT binary patch literal 724 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 fmg?j@f&=^qi|-8;d%F!$qGz=jyG~<|GE45 z`Tyr{&;PHt`Yhqn{L}n$eit&I@m#Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0-Z@jK~zXfos_Xp z6hRcmfA11BG$bl_U;>FIG~9Wj{{RUj(tt4*lAwhYQcA5fK%>1a7T6pK6iS3bNI4hm@HFsJjQ zty($g)ZO|Z6pBbd z6&I-zz=+tYlZi{QC4va77iJlldS_y%2vCFA+YE?R)m09#UYJ7!x`qZ3iMlw+GDsrc zD{dhoXeiTGtO|D?50c4e0oW^+XzxuBNFw%%B^0H-cL0Fx)kRd5jhVSB;uNq`ERo4) zBM0sT7Dk30u{}In$&ZxLTe|S-fJ2wh*z1xUIPR zWDwQx?gt1H1N6O~;8ODqFaphh7Z5I*CBgEInK>X@v4AU}9tdz9_=#}=!QtVEgBv3P zs?grs&+dngpb;g-pU*xb@O9-c58muV*&&?*ei;$kdiq%}%+fXV$W?6{D3j08a{J@~ zJOgR-bA3?dBDm`Tv54^LMerote;YK?{AjCL8X#7E+WhZ_8?Dv2aR&~t^l}2s!C_v_ zI=0C1h6S7p-zz5)TU=Ctu#1FuK7up0sHT8(k$|+l&gXHEvI7Hmqe5^u;v$GUz%*VG z_TvxZDX8fHLlpnEv3i_Da*`aJhFb38a7CV1fI37XJMbU-dPQwvCR?Kb0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0isDnK~zXftyD2i zLop0}2@4wwogz-aSXA{SY>bGJvoIn?I`t%|DkCRAVd}z0hb9cM^BpJo{|oV?v~}#f z7dv(h<|8WGD7%SSK@D=o31xmhC3 z%_(xm9Fc0HM}CqK0M6)pN<<4(A!(Bwp%>Hf=@5h0!Te|Oi=HT>=4U7Z}C_Fhcf10ao`Ia)B=h-myarK zRicVVWmuwi>Qc30N{srqnM&2FZ P00000NkvXXu0mjfqh{N~ literal 0 HcmV?d00001 diff --git a/src/assets/images/vault/marketplace.png b/src/assets/images/vault/marketplace.png new file mode 100644 index 0000000000000000000000000000000000000000..44f3b4b4e80739518d3562ca71aded82ed107da5 GIT binary patch literal 617 zcmV-v0+#)WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0scuuK~zXfwUoV5 zQ$ZAfzZ293OQBK`geRLcD>XAu@`K z=QFU`lQSqTs;Uj=Y&2;x+J89{pSyH+_S0j-KGVFN(QGc$+1a=M#Iwf_IUHP~xV}3MKqsCh{l^BUrOi$x zRu^LJ(7MO_YNGBI1V4Q0lD9Js2ba8>l71O^JL7(H+4iLw%tGGIc(=8Y9vibbSV%@K z(%MWTHEO+R8nZY^YgtQAfII171%qT?OK+AM{1blxW$&+;ad2Wz00000NkvXXu0mjf DHvkhR literal 0 HcmV?d00001 diff --git a/src/assets/images/vault/surprise.png b/src/assets/images/vault/surprise.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b1ce944fa3683796e82f405d1331e95baef4de GIT binary patch literal 780 zcmV+n1M~ceP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0-;GnK~zXfjgwDE z6j2<M~1e|@L_>M#I< zy=ROTv}LCIv5X?xbYae~$TWSI9SO2xTZ4k+kxsDMH1R^FzFv#`YmgfDjqf(KSH+VGjU(w?;yB2h)+*XXEaq_~-%v zT{mAM<5c@arsv+XxU|6SySgPG+&K+k##X>GPJj?P6A40jq}nc-0KqaALY`3pk!MnC z2Nt1~3vmHLC@6x^JKsrwuw78Bu@Dy^xIr$F#==el1UD$wSojZ!eIf7mRKN590000< KMNUMnLSTXycWmAO literal 0 HcmV?d00001 diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 4f84959..bb402d1 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -1,8 +1,16 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { IconType } from 'react-icons'; -import { FaArrowUp, FaBoxOpen, FaBriefcase, FaCrown, FaGamepad, FaGift, FaHandHoldingHeart, FaShoppingBag, FaStore, FaTrophy } from 'react-icons/fa'; import { LocalizeText } from '../../api'; +import imgAchievements from '../../assets/images/vault/achievements.png'; +import imgBonusbag from '../../assets/images/vault/bonusbag.png'; +import imgDailygift from '../../assets/images/vault/dailygift.png'; +import imgDonations from '../../assets/images/vault/donations.png'; +import imgGames from '../../assets/images/vault/games.png'; +import imgGeneric from '../../assets/images/vault/generic.png'; +import imgHcpayday from '../../assets/images/vault/hcpayday.png'; +import imgLevel from '../../assets/images/vault/levelprogression.png'; +import imgMarketplace from '../../assets/images/vault/marketplace.png'; +import imgSurprise from '../../assets/images/vault/surprise.png'; import { LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; const localizeWithFallback = (key: string, fallback: string) => @@ -15,24 +23,24 @@ interface EarningRow { key: string; label: string; - Icon: IconType; + img: string; currencies: number[]; } -// Currency ids: 5 = diamonds, 0 = duckets. Amounts are placeholders (0) until -// the emulator exposes the earnings data + claim packets. The category icons -// are FontAwesome placeholders — swap in the exact pixel icons once available. +// Icons are the hotel's real earnings_icon_* assets. Amounts are placeholders +// (0) and claims are disabled until the emulator exposes the data + packets. +// 'clubwork' has no dedicated earnings icon — uses the generic one for now. const EARNINGS: EarningRow[] = [ - { key: 'daily', label: 'Regalo giornaliero', Icon: FaGift, currencies: [ 5 ] }, - { key: 'games', label: 'Giochi', Icon: FaGamepad, currencies: [ 0 ] }, - { key: 'achievements', label: 'Traguardi', Icon: FaTrophy, currencies: [ 5, 0 ] }, - { key: 'marketplace', label: 'Mercatino', Icon: FaStore, currencies: [ 0 ] }, - { key: 'hcpayday', label: 'Bonus giorno di paga HC', Icon: FaCrown, currencies: [ 0 ] }, - { key: 'level', label: 'Progressione Livello', Icon: FaArrowUp, currencies: [ 5, 0 ] }, - { key: 'donations', label: 'Donazioni', Icon: FaHandHoldingHeart, currencies: [ 0 ] }, - { key: 'bonusbag', label: 'Sacco Bonus', Icon: FaShoppingBag, currencies: [ 0 ] }, - { key: 'surprise', label: 'Scatole Sorprese', Icon: FaBoxOpen, currencies: [ 5, 0 ] }, - { key: 'clubwork', label: 'Club e Lavoro', Icon: FaBriefcase, currencies: [ 0 ] } + { key: 'daily', label: 'Regalo giornaliero', img: imgDailygift, currencies: [ 5 ] }, + { key: 'games', label: 'Giochi', img: imgGames, currencies: [ 0 ] }, + { key: 'achievements', label: 'Traguardi', img: imgAchievements, currencies: [ 5, 0 ] }, + { key: 'marketplace', label: 'Mercatino', img: imgMarketplace, currencies: [ 0 ] }, + { key: 'hcpayday', label: 'Bonus giorno di paga HC', img: imgHcpayday, currencies: [ 0 ] }, + { key: 'level', label: 'Progressione Livello', img: imgLevel, currencies: [ 5, 0 ] }, + { key: 'donations', label: 'Donazioni', img: imgDonations, currencies: [ 0 ] }, + { key: 'bonusbag', label: 'Sacco Bonus', img: imgBonusbag, currencies: [ 0 ] }, + { key: 'surprise', label: 'Scatole Sorprese', img: imgSurprise, currencies: [ 5, 0 ] }, + { key: 'clubwork', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } ]; export const VaultView: FC<{}> = props => @@ -76,32 +84,27 @@ export const VaultView: FC<{}> = props => setIsVisible(false) } /> - { EARNINGS.map(row => - { - const RowIcon = row.Icon; - - return ( -
-
- - - - { localizeWithFallback('earnings.' + row.key, row.label) } -
-
- { row.currencies.map((currency, index) => ( - - - 0 - - )) } -
- + { EARNINGS.map(row => ( +
+
+ + + + { localizeWithFallback('earnings.' + row.key, row.label) }
- ); - }) } +
+ { row.currencies.map((currency, index) => ( + + + 0 + + )) } +
+ +
+ )) }
)) }
-
From 38ffc80d0a840b2119b4f342b6bca44baf6beece Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:47:04 +0200 Subject: [PATCH 31/39] style(vault): bold black currency amounts + align Riscatta buttons --- src/components/vault/VaultView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index ab6d749..77c500c 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -92,11 +92,11 @@ export const VaultView: FC<{}> = props => { localizeWithFallback('earnings.' + row.key, row.label) }
-
+
{ row.currencies.map((currency, index) => ( - 0 + 0 )) }
From 7a2b01670497e003b8f8d79aea5c7cc04a0cfcc1 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:54:33 +0200 Subject: [PATCH 32/39] fix(vault): header reads 'Guadagni' (key resolved to English) --- src/components/vault/VaultView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 77c500c..69f4479 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -82,7 +82,7 @@ export const VaultView: FC<{}> = props => return ( - setIsVisible(false) } /> + setIsVisible(false) } /> { EARNINGS.map(row => (
From eaf9a1b04da41f66f81c57bd35b6fcaced685fc1 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:04:59 +0200 Subject: [PATCH 33/39] fix(vault): use the standard earnings.* localization keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the real gamedata keys (earnings.title, earnings.dailygift.label, earnings.achievements.label, earnings.claim.button, ...) instead of invented ones, so the window is properly localized. 'games'/'clubwork' have no standard key — custom key + Italian fallback. --- src/components/vault/VaultView.tsx | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 69f4479..ae2ea62 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -22,6 +22,9 @@ const localizeWithFallback = (key: string, fallback: string) => interface EarningRow { key: string; + // Standard gamedata localization key (ExternalTexts). 'label' is only the + // fallback shown when the key is missing in the active texts. + textKey: string; label: string; img: string; currencies: number[]; @@ -29,18 +32,19 @@ interface EarningRow // Icons are the hotel's real earnings_icon_* assets. Amounts are placeholders // (0) and claims are disabled until the emulator exposes the data + packets. -// 'clubwork' has no dedicated earnings icon — uses the generic one for now. +// 'games' and 'clubwork' have no standard earnings.*.label key — they use a +// custom key (add it to your texts) and fall back to the Italian label. const EARNINGS: EarningRow[] = [ - { key: 'daily', label: 'Regalo giornaliero', img: imgDailygift, currencies: [ 5 ] }, - { key: 'games', label: 'Giochi', img: imgGames, currencies: [ 0 ] }, - { key: 'achievements', label: 'Traguardi', img: imgAchievements, currencies: [ 5, 0 ] }, - { key: 'marketplace', label: 'Mercatino', img: imgMarketplace, currencies: [ 0 ] }, - { key: 'hcpayday', label: 'Bonus giorno di paga HC', img: imgHcpayday, currencies: [ 0 ] }, - { key: 'level', label: 'Progressione Livello', img: imgLevel, currencies: [ 5, 0 ] }, - { key: 'donations', label: 'Donazioni', img: imgDonations, currencies: [ 0 ] }, - { key: 'bonusbag', label: 'Sacco Bonus', img: imgBonusbag, currencies: [ 0 ] }, - { key: 'surprise', label: 'Scatole Sorprese', img: imgSurprise, currencies: [ 5, 0 ] }, - { key: 'clubwork', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } + { key: 'daily', textKey: 'earnings.dailygift.label', label: 'Regalo giornaliero', img: imgDailygift, currencies: [ 5 ] }, + { key: 'games', textKey: 'earnings.games.label', label: 'Giochi', img: imgGames, currencies: [ 0 ] }, + { key: 'achievements', textKey: 'earnings.achievements.label', label: 'Traguardi', img: imgAchievements, currencies: [ 5, 0 ] }, + { key: 'marketplace', textKey: 'earnings.marketplace.label', label: 'Mercatino', img: imgMarketplace, currencies: [ 0 ] }, + { key: 'hcpayday', textKey: 'earnings.hc.label', label: 'Bonus giorno di paga HC', img: imgHcpayday, currencies: [ 0 ] }, + { key: 'level', textKey: 'earnings.levelprogression.label', label: 'Progressione Livello', img: imgLevel, currencies: [ 5, 0 ] }, + { key: 'donations', textKey: 'earnings.donations.label', label: 'Donazioni', img: imgDonations, currencies: [ 0 ] }, + { key: 'bonusbag', textKey: 'earnings.bonusbag.label', label: 'Sacco Bonus', img: imgBonusbag, currencies: [ 0 ] }, + { key: 'surprise', textKey: 'earnings.surpriseboxes.label', label: 'Scatole Sorprese', img: imgSurprise, currencies: [ 5, 0 ] }, + { key: 'clubwork', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } ]; export const VaultView: FC<{}> = props => @@ -82,7 +86,7 @@ export const VaultView: FC<{}> = props => return ( - setIsVisible(false) } /> + setIsVisible(false) } /> { EARNINGS.map(row => (
@@ -90,7 +94,7 @@ export const VaultView: FC<{}> = props => - { localizeWithFallback('earnings.' + row.key, row.label) } + { localizeWithFallback(row.textKey, row.label) }
{ row.currencies.map((currency, index) => ( @@ -101,7 +105,7 @@ export const VaultView: FC<{}> = props => )) }
)) } From 983c659e9a5f819c8e88cf0269d8360cb442f8be Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:25:12 +0200 Subject: [PATCH 34/39] style(vault): classic blue header + cool grey body (scoped to the window) Override the shared teal/cream primary-slim theme just for the Guadagni window: blue header gradient + grey-blue body, matching the reference. --- src/components/vault/VaultView.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index ae2ea62..14dc7e9 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -47,6 +47,20 @@ const EARNINGS: EarningRow[] = [ { key: 'clubwork', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } ]; +// Scoped colour override for the Guadagni window only: classic blue header + +// cool grey body (the shared 'primary-slim' theme is teal + cream). Higher +// specificity (.nitro-card.nitro-vault ...) than the theme so it wins. +const VAULT_STYLES = ` + .nitro-card.nitro-vault .nitro-card-header { + background: linear-gradient(180deg, #5a80b8 0%, #3f63a0 100%); + border-color: #34548a; + } + .nitro-card.nitro-vault, + .nitro-card.nitro-vault .content-area { + background: #dde1e6; + } +`; + export const VaultView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); @@ -88,6 +102,7 @@ export const VaultView: FC<{}> = props => setIsVisible(false) } /> + { EARNINGS.map(row => (
From e94463df6a6a7b5a2ffdfe14268c4ba5acf2a369 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:32:40 +0200 Subject: [PATCH 35/39] style(vault): fix body bg selector (content-shell) + compact rows Target .nitro-card-content-shell for the grey body (the previous .content-area selector didn't match), and tighten row height (smaller icon box + less gap) to match the more compact Habbo reference. --- src/components/vault/VaultView.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 14dc7e9..7d62637 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -56,8 +56,9 @@ const VAULT_STYLES = ` border-color: #34548a; } .nitro-card.nitro-vault, - .nitro-card.nitro-vault .content-area { - background: #dde1e6; + .nitro-card.nitro-vault .content-area, + .nitro-card.nitro-vault .nitro-card-content-shell { + background: #dde1e6 !important; } `; @@ -101,13 +102,13 @@ export const VaultView: FC<{}> = props => return ( setIsVisible(false) } /> - + { EARNINGS.map(row => (
- - + + { localizeWithFallback(row.textKey, row.label) }
From d15457b43c4a47d60f3cc650a62f56fc6e750f1a Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:00:08 +0200 Subject: [PATCH 36/39] feat(vault): wire Guadagni window to earnings packets Request earnings on open (RequestEarningsCenterComposer), render real amounts/claimable per category from EarningsCenterEvent, per-row Riscatta + Richiedili Tutti send the claim composers, refresh on EarningsClaimResultEvent. Category keys aligned to the emulator contract; reward currencies derived from reward type; rows fall back to the static skeleton before data lands. --- src/components/vault/VaultView.tsx | 194 +++++++++++++++++++++++------ 1 file changed, 153 insertions(+), 41 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 7d62637..cae1964 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -1,6 +1,6 @@ -import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { LocalizeText } from '../../api'; +import { AddLinkEventTracker, ClaimAllEarningsRewardsComposer, ClaimEarningsRewardComposer, EarningsCenterEvent, EarningsClaimResultEvent, IEarningsEntry, IEarningsReward, ILinkEventTracker, RemoveLinkEventTracker, RequestEarningsCenterComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../api'; import imgAchievements from '../../assets/images/vault/achievements.png'; import imgBonusbag from '../../assets/images/vault/bonusbag.png'; import imgDailygift from '../../assets/images/vault/dailygift.png'; @@ -12,6 +12,7 @@ import imgLevel from '../../assets/images/vault/levelprogression.png'; import imgMarketplace from '../../assets/images/vault/marketplace.png'; import imgSurprise from '../../assets/images/vault/surprise.png'; import { LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useMessageEvent } from '../../hooks'; const localizeWithFallback = (key: string, fallback: string) => { @@ -19,37 +20,56 @@ const localizeWithFallback = (key: string, fallback: string) => return (text && text !== key) ? text : fallback; }; -interface EarningRow +interface EarningCategory { + // Wire categoryKey — MUST match the emulator contract + // (emulatore/docs/earnings-packet-contract.md). key: string; // Standard gamedata localization key (ExternalTexts). 'label' is only the // fallback shown when the key is missing in the active texts. textKey: string; label: string; img: string; - currencies: number[]; + // Placeholder currency icons used only before the server entry arrives. + fallbackCurrencies: number[]; } -// Icons are the hotel's real earnings_icon_* assets. Amounts are placeholders -// (0) and claims are disabled until the emulator exposes the data + packets. -// 'games' and 'clubwork' have no standard earnings.*.label key — they use a -// custom key (add it to your texts) and fall back to the Italian label. -const EARNINGS: EarningRow[] = [ - { key: 'daily', textKey: 'earnings.dailygift.label', label: 'Regalo giornaliero', img: imgDailygift, currencies: [ 5 ] }, - { key: 'games', textKey: 'earnings.games.label', label: 'Giochi', img: imgGames, currencies: [ 0 ] }, - { key: 'achievements', textKey: 'earnings.achievements.label', label: 'Traguardi', img: imgAchievements, currencies: [ 5, 0 ] }, - { key: 'marketplace', textKey: 'earnings.marketplace.label', label: 'Mercatino', img: imgMarketplace, currencies: [ 0 ] }, - { key: 'hcpayday', textKey: 'earnings.hc.label', label: 'Bonus giorno di paga HC', img: imgHcpayday, currencies: [ 0 ] }, - { key: 'level', textKey: 'earnings.levelprogression.label', label: 'Progressione Livello', img: imgLevel, currencies: [ 5, 0 ] }, - { key: 'donations', textKey: 'earnings.donations.label', label: 'Donazioni', img: imgDonations, currencies: [ 0 ] }, - { key: 'bonusbag', textKey: 'earnings.bonusbag.label', label: 'Sacco Bonus', img: imgBonusbag, currencies: [ 0 ] }, - { key: 'surprise', textKey: 'earnings.surpriseboxes.label', label: 'Scatole Sorprese', img: imgSurprise, currencies: [ 5, 0 ] }, - { key: 'clubwork', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } +// Fixed display order + icons/labels. Amounts, claimable state and the actual +// reward currencies come from the server (EarningsCenterEvent); these rows are +// the always-visible skeleton so the window matches the Habbo reference even +// before data lands. 'games' and 'club_job' have no standard earnings.*.label +// key — they use a custom key (add it to your texts) and fall back to Italian. +const CATEGORIES: EarningCategory[] = [ + { key: 'daily_gift', textKey: 'earnings.dailygift.label', label: 'Regalo giornaliero', img: imgDailygift, fallbackCurrencies: [ 5 ] }, + { key: 'games', textKey: 'earnings.games.label', label: 'Giochi', img: imgGames, fallbackCurrencies: [ 0 ] }, + { key: 'achievements', textKey: 'earnings.achievements.label', label: 'Traguardi', img: imgAchievements, fallbackCurrencies: [ 5, 0 ] }, + { key: 'marketplace', textKey: 'earnings.marketplace.label', label: 'Mercatino', img: imgMarketplace, fallbackCurrencies: [ 0 ] }, + { key: 'hc_payday', textKey: 'earnings.hc.label', label: 'Bonus giorno di paga HC', img: imgHcpayday, fallbackCurrencies: [ 0 ] }, + { key: 'level_progress', textKey: 'earnings.levelprogression.label', label: 'Progressione Livello', img: imgLevel, fallbackCurrencies: [ 5, 0 ] }, + { key: 'donations', textKey: 'earnings.donations.label', label: 'Donazioni', img: imgDonations, fallbackCurrencies: [ 0 ] }, + { key: 'bonus_bag', textKey: 'earnings.bonusbag.label', label: 'Sacco Bonus', img: imgBonusbag, fallbackCurrencies: [ 0 ] }, + { key: 'mystery_boxes', textKey: 'earnings.surpriseboxes.label', label: 'Scatole Sorprese', img: imgSurprise, fallbackCurrencies: [ 5, 0 ] }, + { key: 'club_job', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, fallbackCurrencies: [ 0 ] } ]; +// Map a server reward type to a LayoutCurrencyIcon `type`. Returns null for +// rewards that aren't a currency (badge / item) — those show just the amount. +const rewardCurrencyType = (reward: IEarningsReward): number | string | null => +{ + switch(reward.type) + { + case 'credits': return -1; + case 'pixels': return 0; + case 'points': return reward.pointsType; + case 'hc_days': return 'hc'; + default: return null; + } +}; + // Scoped colour override for the Guadagni window only: classic blue header + // cool grey body (the shared 'primary-slim' theme is teal + cream). Higher -// specificity (.nitro-card.nitro-vault ...) than the theme so it wins. +// specificity (.nitro-card.nitro-vault ...) than the theme so it wins. The body +// element renders `.nitro-card-content-shell`, NOT `.content-area`. const VAULT_STYLES = ` .nitro-card.nitro-vault .nitro-card-header { background: linear-gradient(180deg, #5a80b8 0%, #3f63a0 100%); @@ -65,6 +85,51 @@ const VAULT_STYLES = ` export const VaultView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); + const [ entries, setEntries ] = useState([]); + + const entriesByKey = useMemo(() => + { + const map = new Map(); + for(const entry of entries) map.set(entry.categoryKey, entry); + return map; + }, [ entries ]); + + const anyClaimable = useMemo(() => entries.some(entry => entry.enabled && entry.claimable), [ entries ]); + + useMessageEvent(EarningsCenterEvent, useCallback((event: EarningsCenterEvent) => + { + const parser = event.getParser(); + if(!parser) return; + setEntries(parser.entries ?? []); + }, [])); + + useMessageEvent(EarningsClaimResultEvent, useCallback((event: EarningsClaimResultEvent) => + { + const parser = event.getParser(); + if(!parser) return; + + setEntries(prev => + { + const next = prev.slice(); + + for(const result of parser.results) + { + if(result.hasEntry && result.entry) + { + const idx = next.findIndex(e => e.categoryKey === result.entry.categoryKey); + if(idx >= 0) next[idx] = result.entry; else next.push(result.entry); + } + else if(result.success) + { + // No refreshed entry but the claim worked — mark it spent. + const idx = next.findIndex(e => e.categoryKey === result.categoryKey); + if(idx >= 0) next[idx] = { ...next[idx], claimable: false }; + } + } + + return next; + }); + }, [])); useEffect(() => { @@ -97,6 +162,23 @@ export const VaultView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, []); + // Ask the server for fresh earnings every time the window opens. + useEffect(() => + { + if(!isVisible) return; + SendMessageComposer(new RequestEarningsCenterComposer()); + }, [ isVisible ]); + + const claimOne = useCallback((categoryKey: string) => + { + SendMessageComposer(new ClaimEarningsRewardComposer(categoryKey)); + }, []); + + const claimAll = useCallback(() => + { + SendMessageComposer(new ClaimAllEarningsRewardsComposer()); + }, []); + if(!isVisible) return null; return ( @@ -104,29 +186,59 @@ export const VaultView: FC<{}> = props => setIsVisible(false) } /> - { EARNINGS.map(row => ( -
-
- - - - { localizeWithFallback(row.textKey, row.label) } -
-
- { row.currencies.map((currency, index) => ( - - - 0 + { CATEGORIES.map(category => + { + const entry = entriesByKey.get(category.key) ?? null; + const canClaim = !!entry && entry.enabled && entry.claimable; + const rewards = entry?.rewards ?? []; + + return ( +
+
+ + - )) } + { localizeWithFallback(category.textKey, category.label) } +
+
+ { rewards.length > 0 + ? rewards.map((reward, index) => + { + const currencyType = rewardCurrencyType(reward); + return ( + + { currencyType !== null && } + { reward.amount } + + ); + }) + : category.fallbackCurrencies.map((currency, index) => ( + + + 0 + + )) } +
+
- -
- )) } + ); + }) }
-
From 20ad8b705adacd21336f870cd97fb93b9f836121 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:16:39 +0200 Subject: [PATCH 37/39] feat(help): restyle Aiuto window to match Habbo reference Centered single-column index (blue header + light grey body), the real help_duck asset, two green buttons (report + player support), and three green-arrow links: read more about safety, my sanctions, my reports. The report-flow steps keep the original 2-column grid. --- src/assets/images/help/help-duck.png | Bin 0 -> 4808 bytes src/components/help/HelpView.tsx | 37 +++++++++++++++----- src/components/help/views/HelpIndexView.tsx | 32 ++++++++++++----- 3 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/assets/images/help/help-duck.png diff --git a/src/assets/images/help/help-duck.png b/src/assets/images/help/help-duck.png new file mode 100644 index 0000000000000000000000000000000000000000..855a48e413dd031fc362b712c099784285022825 GIT binary patch literal 4808 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 = props => { const [ isVisible, setIsVisible ] = useState(false); @@ -96,17 +112,20 @@ export const HelpView: FC<{}> = props => return ( <> { isVisible && - + - - -
- - - - - + + { activeReport + ? + +
+ + + + + + : } } diff --git a/src/components/help/views/HelpIndexView.tsx b/src/components/help/views/HelpIndexView.tsx index ceaa1ad..36bfe85 100644 --- a/src/components/help/views/HelpIndexView.tsx +++ b/src/components/help/views/HelpIndexView.tsx @@ -1,9 +1,11 @@ import { GetCfhStatusMessageComposer } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { DispatchUiEvent, GetConfigurationValue, LocalizeText, ReportState, ReportType, SendMessageComposer } from '../../../api'; +import { FaArrowCircleRight } from 'react-icons/fa'; +import { CreateLinkEvent, DispatchUiEvent, GetConfigurationValue, LocalizeText, ReportState, ReportType, SendMessageComposer } from '../../../api'; import { Button, Text } from '../../../common'; import { GuideToolEvent } from '../../../events'; import { useHelp } from '../../../hooks'; +import helpDuck from '../../../assets/images/help/help-duck.png'; export const HelpIndexView: FC<{}> = props => { @@ -21,16 +23,30 @@ export const HelpIndexView: FC<{}> = props => }; return ( - <> -
- { LocalizeText('help.main.frame.title') } - { LocalizeText('help.main.self.description') } +
+ { LocalizeText('help.main.frame.title') } + { LocalizeText('help.main.frame.description') } +
+
-
+
- - +
+ + + +
+
); }; From 4fa93cfaf368646db046f78f3ec6a392435af33b Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:23:16 +0200 Subject: [PATCH 38/39] fix(help): Habbo-green buttons + restyle sanctions box Move the .nitro-help blue-header / grey-body override to global CSS so it also covers the separate SanctionStatusView card (was an inline { activeReport ? diff --git a/src/components/help/views/HelpIndexView.tsx b/src/components/help/views/HelpIndexView.tsx index 36bfe85..eea952b 100644 --- a/src/components/help/views/HelpIndexView.tsx +++ b/src/components/help/views/HelpIndexView.tsx @@ -2,7 +2,7 @@ import { GetCfhStatusMessageComposer } from '@nitrots/nitro-renderer'; import { FC } from 'react'; import { FaArrowCircleRight } from 'react-icons/fa'; import { CreateLinkEvent, DispatchUiEvent, GetConfigurationValue, LocalizeText, ReportState, ReportType, SendMessageComposer } from '../../../api'; -import { Button, Text } from '../../../common'; +import { Text } from '../../../common'; import { GuideToolEvent } from '../../../events'; import { useHelp } from '../../../hooks'; import helpDuck from '../../../assets/images/help/help-duck.png'; @@ -30,20 +30,20 @@ export const HelpIndexView: FC<{}> = props =>
- - + +
- - -
diff --git a/src/components/help/views/SanctionStatusView.tsx b/src/components/help/views/SanctionStatusView.tsx index 88beb43..a0106a5 100644 --- a/src/components/help/views/SanctionStatusView.tsx +++ b/src/components/help/views/SanctionStatusView.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; -import { LocalizeText } from '../../../api'; -import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common'; +import { FaArrowCircleRight } from 'react-icons/fa'; +import { CreateLinkEvent, LocalizeText } from '../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common'; import { useHelp } from '../../../hooks'; export const SanctionSatusView: FC<{}> = props => @@ -37,38 +38,45 @@ export const SanctionSatusView: FC<{}> = props => if(!sanctionInfo) return null; return ( - + setSanctionInfo(null) } /> - - -
- - +
+
{ (sanctionInfo.sanctionReason === 'cfh.reason.EMPTY') - ?
{ LocalizeText('help.sanction.current.none') }
+ ?
{ LocalizeText('help.sanction.current.none') }
: <> { ((sanctionInfo.probationHoursLeft > 0) || (sanctionInfo.isSanctionActive)) && -
{ LocalizeText('help.sanction.probation.reminder') }
+
{ LocalizeText('help.sanction.probation.reminder') }
} -
+
{ LocalizeText('help.sanction.last.sanction') } { sanctionLocalization('current', sanctionInfo.sanctionName, sanctionInfo.sanctionLengthHours) }
-
{ LocalizeText('generic.start.time') } { sanctionInfo.sanctionCreationTime }
-
{ LocalizeText('generic.reason') } { sanctionInfo.sanctionReason }
-
{ LocalizeText('help.sanction.probation.days.left') } { Math.trunc((sanctionInfo.probationHoursLeft / 24)) + 1 }
+
{ LocalizeText('generic.start.time') } { sanctionInfo.sanctionCreationTime }
+
{ LocalizeText('generic.reason') } { sanctionInfo.sanctionReason }
+
{ LocalizeText('help.sanction.probation.days.left') } { Math.trunc((sanctionInfo.probationHoursLeft / 24)) + 1 }
} { ((sanctionInfo.hasCustomMute) && (!(sanctionInfo.isSanctionActive))) && -
{ LocalizeText('help.sanction.custom.mute') }
+
{ LocalizeText('help.sanction.custom.mute') }
} { (sanctionInfo.tradeLockExpiryTime && sanctionInfo.tradeLockExpiryTime.length > 0) && -
{ LocalizeText('trade.locked.until') } { sanctionInfo.tradeLockExpiryTime }
+
{ LocalizeText('trade.locked.until') } { sanctionInfo.tradeLockExpiryTime }
} -
{ sanctionLocalization('next', sanctionInfo.nextSanctionName, sanctionInfo.nextSanctionLengthHours) }
- - - + { (sanctionInfo.sanctionReason !== 'cfh.reason.EMPTY') && +
{ sanctionLocalization('next', sanctionInfo.nextSanctionName, sanctionInfo.nextSanctionLengthHours) }
+ } +
+
+ + +
+
); diff --git a/src/css/help/HelpView.css b/src/css/help/HelpView.css new file mode 100644 index 0000000..e05897e --- /dev/null +++ b/src/css/help/HelpView.css @@ -0,0 +1,83 @@ +/* Aiuto (Help) + Sanctions windows — custom hotel restyle. + Global (not an inline