From 49836bbeef3bd7ee960708af6748fb8496fe2e99 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 28 May 2026 15:31:24 +0200 Subject: [PATCH 01/24] feat: branding furni image position editor (move + scale) Adds an "Editor Posizione" button to the furni infostand action bar for branding / MPU furni, opening a dialog to position and zoom the image: - draggable dot moves offsetX/Y (live, local preview only) - slider zooms the image (scale, via the renderer's per-sprite scale) - offsetZ kept as z-index; Save persists + broadcasts via SetObjectData - radio "Live" + all editor labels go through LocalizeText (external texts) Pairs with the renderer branding scale/offset support and Arcturus' `scale` default on InteractionRoomAds. --- src/components/radio/RadioView.tsx | 2 +- .../infostand/ImagePositionEditorView.tsx | 147 ++++++++++++++++++ .../infostand/InfoStandWidgetFurniView.tsx | 56 +++++++ 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/components/room/widgets/avatar-info/infostand/ImagePositionEditorView.tsx diff --git a/src/components/radio/RadioView.tsx b/src/components/radio/RadioView.tsx index 15e7c95..6cd825b 100644 --- a/src/components/radio/RadioView.tsx +++ b/src/components/radio/RadioView.tsx @@ -76,7 +76,7 @@ export const RadioView: FC<{}> = () =>
{ selectedPlaying && - Live + { LocalizeText('radio.live') } } { selected?.genre && { selected.genre } } diff --git a/src/components/room/widgets/avatar-info/infostand/ImagePositionEditorView.tsx b/src/components/room/widgets/avatar-info/infostand/ImagePositionEditorView.tsx new file mode 100644 index 0000000..1362ae0 --- /dev/null +++ b/src/components/room/widgets/avatar-info/infostand/ImagePositionEditorView.tsx @@ -0,0 +1,147 @@ +import { GetRoomEngine, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { FC, PointerEvent as ReactPointerEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../common'; + +const PAD_W = 230; +const PAD_H = 150; +// How many offset units one pixel of drag represents. +const UNITS_PER_PX = 1.5; + +interface Props +{ + roomId: number; + objectId: number; + isWallItem: boolean; + initialX: number; + initialY: number; + initialZ: number; + initialScale: number; + onClose: () => void; + onSave: (x: number, y: number, z: number, scale: number) => void; +} + +export const ImagePositionEditorView: FC = props => +{ + const { roomId, objectId, isWallItem, initialX, initialY, initialZ, initialScale, onClose, onSave } = props; + const [ x, setX ] = useState(initialX); + const [ y, setY ] = useState(initialY); + const [ z, setZ ] = useState(initialZ); + const [ scale, setScale ] = useState(initialScale || 100); + const padRef = useRef(null); + const draggingRef = useRef(false); + + const category = isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR; + + // Local-only live preview: set the branding model values directly. The model + // bumps its update counter so the visualization re-renders next frame. + // Nothing is sent to the server until Save. + const applyLive = useCallback((nx: number, ny: number, nz: number, nScale: number) => + { + const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, category); + if(!roomObject?.model) return; + + roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_X, nx); + roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Y, ny); + roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Z, nz); + roomObject.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_SCALE, nScale); + }, [ roomId, objectId, category ]); + + useEffect(() => { applyLive(x, y, z, scale); }, [ x, y, z, scale, applyLive ]); + + const setFromPointer = useCallback((clientX: number, clientY: number) => + { + const rect = padRef.current?.getBoundingClientRect(); + if(!rect) return; + + const cx = rect.left + (rect.width / 2); + const cy = rect.top + (rect.height / 2); + + setX(Math.round((clientX - cx) * UNITS_PER_PX)); + setY(Math.round((clientY - cy) * UNITS_PER_PX)); + }, []); + + const onPointerDown = (event: ReactPointerEvent) => + { + draggingRef.current = true; + padRef.current?.setPointerCapture(event.pointerId); + setFromPointer(event.clientX, event.clientY); + }; + + const onPointerMove = (event: ReactPointerEvent) => + { + if(draggingRef.current) setFromPointer(event.clientX, event.clientY); + }; + + const onPointerUp = (event: ReactPointerEvent) => + { + draggingRef.current = false; + padRef.current?.releasePointerCapture?.(event.pointerId); + }; + + const cancel = () => + { + applyLive(initialX, initialY, initialZ, initialScale || 100); + onClose(); + }; + + const save = () => + { + onSave(x, y, z, scale); + onClose(); + }; + + const dotLeft = (PAD_W / 2) + (x / UNITS_PER_PX); + const dotTop = (PAD_H / 2) + (y / UNITS_PER_PX); + const clampedLeft = Math.max(0, Math.min(PAD_W, dotLeft)); + const clampedTop = Math.max(0, Math.min(PAD_H, dotTop)); + + return ( + + + +
+ { LocalizeText('image.position.editor.hint') } +
+ { /* center crosshair */ } +
+
+ { /* draggable dot */ } +
+
+ +
+ { LocalizeText('image.position.editor.scale') } + setScale(e.target.valueAsNumber || 100) } className="grow" /> + { (scale / 100).toFixed(2) }x +
+ +
+ + + +
+ +
+ + +
+
+ + + ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d2a861c..40b458f 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -6,6 +6,7 @@ import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer import { Button, Column, Flex, LayoutBadgeImageView, LayoutCurrencyIcon, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common'; import { useHasPermission, useMessageEvent, useNitroEvent, useRareValues, useRoom, useWiredTools } from '../../../../../hooks'; import { NitroInput } from '../../../../../layout'; +import { ImagePositionEditorView } from './ImagePositionEditorView'; interface InfoStandWidgetFurniViewProps { @@ -43,6 +44,7 @@ export const InfoStandWidgetFurniView: FC = props const [ isJukeBox, setIsJukeBox ] = useState(false); const [ isSongDisk, setIsSongDisk ] = useState(false); const [ isBranded, setIsBranded ] = useState(false); + const [ showPositionEditor, setShowPositionEditor ] = useState(false); const [ songId, setSongId ] = useState(-1); const [ songName, setSongName ] = useState(''); const [ songCreator, setSongCreator ] = useState(''); @@ -393,6 +395,45 @@ export const InfoStandWidgetFurniView: FC = props return data; }, [ furniKeys, furniValues ]); + const getBrandingOffset = useCallback((key: string): number => + { + const index = furniKeys.indexOf(key); + if(index < 0) return 0; + const value = parseInt(furniValues[index]); + return isNaN(value) ? 0 : value; + }, [ furniKeys, furniValues ]); + + const hasBrandingOffsets = isBranded && (furniKeys.indexOf('offsetX') >= 0); + + // Persist the position from the editor: rebuild the branding map with the + // new offsets and send it (same path as Save), then reflect it in the fields. + const savePositionEditor = useCallback((x: number, y: number, z: number, scale: number) => + { + const map = new Map(); + const clone = Array.from(furniValues); + let hasScale = false; + + for(let i = 0; i < furniKeys.length; i++) + { + const key = furniKeys[i]; + let value = furniValues[i]; + + if(key === 'offsetX') value = String(x); + else if(key === 'offsetY') value = String(y); + else if(key === 'offsetZ') value = String(z); + else if(key === 'scale') { value = String(scale); hasScale = true; } + + clone[i] = value; + map.set(key, value); + } + + // older branding furni may not carry a scale key yet — always send it + if(!hasScale) map.set('scale', String(scale)); + + GetRoomEngine().modifyRoomObjectDataWithMap(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_SAVE_STUFF_DATA, map); + setFurniValues(clone); + }, [ avatarInfo, furniKeys, furniValues ]); + const processButtonAction = useCallback((action: string) => { if(!action || (action === '')) return; @@ -749,6 +790,10 @@ export const InfoStandWidgetFurniView: FC = props } + { hasBrandingOffsets && + } { ((furniKeys.length > 0 && furniValues.length > 0) && (furniKeys.length === furniValues.length)) && } + { showPositionEditor && + setShowPositionEditor(false) } + onSave={ savePositionEditor } /> } ); }; From 7a0b57e26785e0bff2087e16622e0bf97176a954 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 28 May 2026 15:39:46 +0200 Subject: [PATCH 02/24] fix(navigator): ignore search events while disabled + invalidate on FlatCreated useNavigatorSearch had two gaps its tests cover: - with no active tab the query is disabled, but a NavigatorSearchEvent still updated the data; now such events are ignored until a tab is active - a newly created room (FlatCreatedEvent) now invalidates the ['navigator','search'] query and refetches the current search Fixes the 2 failing useNavigatorSearch tests; full suite 472/472. --- src/hooks/navigator/useNavigatorSearch.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hooks/navigator/useNavigatorSearch.ts b/src/hooks/navigator/useNavigatorSearch.ts index ab3744d..7f55479 100644 --- a/src/hooks/navigator/useNavigatorSearch.ts +++ b/src/hooks/navigator/useNavigatorSearch.ts @@ -1,4 +1,5 @@ -import { NavigatorSearchComposer, NavigatorSearchEvent, NavigatorSearchResultSet } from '@nitrots/nitro-renderer'; +import { FlatCreatedEvent, NavigatorSearchComposer, NavigatorSearchEvent, NavigatorSearchResultSet } from '@nitrots/nitro-renderer'; +import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { SendMessageComposer } from '../../api'; import { useMessageEvent } from '../events'; @@ -9,6 +10,7 @@ export const useNavigatorSearch = () => { const tabCode = useNavigatorUiStore(s => s.currentTabCode); const filter = useNavigatorUiStore(s => s.currentFilter); + const queryClient = useQueryClient(); const [ searchResult, setSearchResult ] = useState(null); const [ isFetching, setIsFetching ] = useState(false); @@ -26,12 +28,25 @@ export const useNavigatorSearch = () => const result = event.getParser()?.result; if(!result) return; - if(tabCode && result.code !== tabCode) return; + // No active tab → the search query is disabled, ignore any event. + // Otherwise only accept the event whose code matches the active tab. + if(!tabCode || (result.code !== tabCode)) return; setSearchResult(result); setIsFetching(false); }); + // A newly created room invalidates the current search so it refetches. + useMessageEvent(FlatCreatedEvent, () => + { + queryClient.invalidateQueries({ queryKey: [ 'navigator', 'search' ] }); + + if(!tabCode) return; + + setIsFetching(true); + SendMessageComposer(new NavigatorSearchComposer(tabCode, filter)); + }); + return { searchResult, isFetching, From fbcda88cd3c26b26c6ec0330ff5bd1331ee76a27 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 08:31:18 +0200 Subject: [PATCH 03/24] =?UTF-8?q?=F0=9F=86=99=20Update=20Rare-Value=20page?= =?UTF-8?q?=20and=20fix=20the=20loading=20of=20the=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/rare-values/RareValuesView.tsx | 94 +++++++++++++++---- src/hooks/radio/useRadio.ts | 4 +- .../rooms/widgets/useChatCommandSelector.ts | 26 +---- src/hooks/soundboard/useSoundboard.ts | 4 +- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/src/components/rare-values/RareValuesView.tsx b/src/components/rare-values/RareValuesView.tsx index c03f787..266c32b 100644 --- a/src/components/rare-values/RareValuesView.tsx +++ b/src/components/rare-values/RareValuesView.tsx @@ -1,10 +1,12 @@ import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, IRareValue, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useMemo, useState } from 'react'; +import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { LocalizeFormattedNumber, LocalizeText } from '../../api'; import { Column, Flex, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; import { useRareValues } from '../../hooks'; import { NitroCard, NitroInput } from '../../layout'; +const PAGE_SIZE = 50; + interface RareValueRow { spriteId: number; @@ -17,7 +19,10 @@ export const RareValuesView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); const [ searchValue, setSearchValue ] = useState(''); + const [ visibleCount, setVisibleCount ] = useState(PAGE_SIZE); const { values = null, loaded = false } = useRareValues(); + const scrollRef = useRef(null); + const sentinelRef = useRef(null); useEffect(() => { @@ -79,35 +84,90 @@ export const RareValuesView: FC<{}> = () => return rows.filter(row => row.name.toLocaleLowerCase().includes(query)); }, [ rows, searchValue ]); + useEffect(() => + { + setVisibleCount(PAGE_SIZE); + if(scrollRef.current) scrollRef.current.scrollTop = 0; + }, [ filtered ]); + + useEffect(() => + { + if(!isVisible) return; + if(visibleCount >= filtered.length) return; + + const sentinel = sentinelRef.current; + const root = scrollRef.current; + if(!sentinel || !root) return; + + const observer = new IntersectionObserver(entries => + { + if(entries.some(entry => entry.isIntersecting)) + { + setVisibleCount(prev => Math.min(prev + PAGE_SIZE, filtered.length)); + } + }, { root, rootMargin: '120px 0px' }); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, [ isVisible, visibleCount, filtered.length ]); + if(!isVisible) return null; + const visibleRows = filtered.slice(0, visibleCount); + const hasMore = visibleCount < filtered.length; + return ( - + setIsVisible(false) } /> - - setSearchValue(event.target.value) } /> - + + + 🔍 + setSearchValue(event.target.value) } + className="grow !border-0 !bg-transparent !p-0 !shadow-none focus:!ring-0" /> + + { loaded && + + { filtered.length } { LocalizeText('rarevalues.title').toLowerCase() } + { hasMore && { visibleRows.length } / { filtered.length } } + } +
{ !loaded && - { LocalizeText('rarevalues.loading') } } +
+ { LocalizeText('rarevalues.loading') } +
} { (loaded && !filtered.length) && - { LocalizeText('rarevalues.empty') } } - { filtered.map(row => ( - - - { row.name } - - { LocalizeFormattedNumber(row.value.points) } +
+ { LocalizeText('rarevalues.empty') } +
} + { visibleRows.map((row, index) => ( + +
+ +
+ { row.name } + + { LocalizeFormattedNumber(row.value.points) }
)) } - + { hasMore && +
+ { LocalizeText('rarevalues.loading.more') } +
} +
diff --git a/src/hooks/radio/useRadio.ts b/src/hooks/radio/useRadio.ts index 88bff81..bafc320 100644 --- a/src/hooks/radio/useRadio.ts +++ b/src/hooks/radio/useRadio.ts @@ -31,7 +31,9 @@ const useRadioState = () => if(loadStartedRef.current) return; loadStartedRef.current = true; - const url = GetConfigurationValue('radio.stations.url') || 'configuration/radio-stations.json5'; + const url = GetConfigurationValue('radio.url') + || GetConfigurationValue('radio.stations.url') + || 'configuration/radio-stations.json5'; (async () => { diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 31dd495..1c23877 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -32,18 +32,6 @@ const CLIENT_COMMANDS: { key: string; descriptionKey: string }[] = [ { key: 'nitro', descriptionKey: 'chatcmd.client.info' }, ]; -/** - * Server-pushed command cache. Lives in a Zustand store (instead of - * module-level `let` variables) so the React Compiler can analyze the - * surrounding hook cleanly, and so a future test can `setState({…})` - * a deterministic fixture without monkey-patching the module. - * - * The `isListenerRegistered` flag prevents the renderer from getting - * two AvailableCommandsEvent listeners — one from the module-level - * pre-mount registration (which captures the server's reply that lands - * during login, BEFORE any React widget mounts) and one from the - * in-hook `useMessageEvent` (which covers later rank-change refreshes). - */ interface ChatCommandStore { serverCommands: CommandDefinition[]; @@ -74,15 +62,9 @@ const ensureGlobalListener = (): void => GetCommunication().registerMessageEvent(event); useChatCommandStore.getState().markListenerRegistered(); } - catch - { - // Communication not ready yet — the in-hook useMessageEvent - // below covers later mounts. - } + catch {} }; -// Try once at module load so the server's response landing before any -// React mount still hits the cache. ensureGlobalListener(); export const useChatCommandSelector = (chatValue: string) => @@ -94,13 +76,9 @@ export const useChatCommandSelector = (chatValue: string) => useEffect(() => { - // Cover the case where the module-level registration failed - // because GetCommunication() wasn't ready at import time. ensureGlobalListener(); }, []); - // Late updates (rank change, etc.) — go through the store so all - // consumers see the same data. useMessageEvent(AvailableCommandsEvent, event => { const parser = event.getParser(); @@ -164,13 +142,11 @@ export const useChatCommandSelector = (chatValue: string) => setDismissed(true); }, []); - // Reset dismissed when chatValue changes to a new command start useEffect(() => { if(chatValue === ':' || chatValue === '') setDismissed(false); }, [ chatValue ]); - // Reset selectedIndex when filtered list changes useEffect(() => { setSelectedIndex(0); diff --git a/src/hooks/soundboard/useSoundboard.ts b/src/hooks/soundboard/useSoundboard.ts index e671e59..25624ce 100644 --- a/src/hooks/soundboard/useSoundboard.ts +++ b/src/hooks/soundboard/useSoundboard.ts @@ -65,7 +65,9 @@ const useSoundboardState = () => if(!enabled || serverSounds.length || fileLoadStartedRef.current) return; fileLoadStartedRef.current = true; - const url = GetConfigurationValue('soundboard.sounds.url') || 'configuration/soundboard-sounds.json5'; + const url = GetConfigurationValue('soundboard.url') + || GetConfigurationValue('soundboard.sounds.url') + || 'configuration/soundboard-sounds.json5'; (async () => { From d0c11f047aa2bc72e80e7528ffe4ec93a5612a23 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 11:30:17 +0200 Subject: [PATCH 04/24] =?UTF-8?q?=F0=9F=86=99=20Complete=20rebuild=20of=20?= =?UTF-8?q?toolbar=20/=20catalog=20/=20inventory=20make=20it=20100%=20mobi?= =?UTF-8?q?le=20friendly=20Take=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/ui-config.example | 23 -- src/components/catalog/CatalogClassicView.tsx | 2 +- src/components/catalog/CatalogModernView.tsx | 331 --------------- src/components/catalog/CatalogView.tsx | 10 - .../page/layout/CatalogLayoutDefaultView.tsx | 13 +- .../page/layout/CatalogLayoutTrophiesView.tsx | 8 +- .../widgets/CatalogPurchaseWidgetView.tsx | 2 +- src/components/inventory/InventoryView.tsx | 21 +- .../views/InventoryCategoryFilterView.tsx | 2 +- .../inventory/views/bot/InventoryBotView.tsx | 6 +- .../furniture/InventoryFurnitureView.tsx | 4 +- .../inventory/views/pet/InventoryPetView.tsx | 4 +- src/components/rare-values/RareValuesView.tsx | 4 + src/components/toolbar/ToolbarView.tsx | 61 ++- src/css/catalog/CatalogClassicView.css | 384 ++++++++++++++---- src/css/inventory/InventoryView.css | 241 +++++++++++ src/hooks/navigator/useNavigatorSearch.ts | 21 +- .../rooms/widgets/useChatCommandSelector.ts | 29 +- src/index.tsx | 2 + 19 files changed, 680 insertions(+), 488 deletions(-) delete mode 100644 src/components/catalog/CatalogModernView.tsx create mode 100644 src/css/inventory/InventoryView.css diff --git a/public/configuration/ui-config.example b/public/configuration/ui-config.example index d531398..e65c501 100644 --- a/public/configuration/ui-config.example +++ b/public/configuration/ui-config.example @@ -27,7 +27,6 @@ "guides.enabled": true, "housekeeping.enabled": true, "toolbar.hide.quests": true, - "catalog.style.new": true, "show.google.ads": false, "loginview": { "images": { @@ -39,28 +38,6 @@ "right": "${asset.url}/c_images/reception/US_right.png", "right.repeat": "${asset.url}/c_images/reception/US_top_right.png" }, - "widgets": { - "slot.1.widget": "promoarticle", - "slot.1.conf": {}, - "slot.2.widget": "widgetcontainer", - "slot.2.conf": { - "image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png", - "texts": "2021NitroPromo", - "btnLink": "" - }, - "slot.3.widget": "", - "slot.3.conf": {}, - "slot.4.widget": "", - "slot.4.conf": {}, - "slot.5.widget": "", - "slot.5.conf": {}, - "slot.6.widget": "", - "slot.6.conf": { - "campaign": "" - }, - "slot.7.widget": "", - "slot.7.conf": {} - } }, "navigator.room.models": [ { diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index a3f2a73..eb16cc2 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -1,7 +1,7 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { FC, useEffect } from 'react'; import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; -import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api'; +import { CatalogType, LocalizeText } from '../../api'; import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks'; import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx deleted file mode 100644 index 1c4af9a..0000000 --- a/src/components/catalog/CatalogModernView.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa'; -import { CatalogType, LocalizeText } from '../../api'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; -import { useCatalogActions, useCatalogData, useCatalogFavorites, useCatalogUiState, useHasPermission } from '../../hooks'; -import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; -import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; -import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; -import { CatalogBuildersClubStatusView } from './views/catalog-header/CatalogBuildersClubStatusView'; -import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; -import { CatalogFavoritesView } from './views/favorites/CatalogFavoritesView'; -import { CatalogGiftView } from './views/gift/CatalogGiftView'; -import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; -import { CatalogSearchView } from './views/page/common/CatalogSearchView'; -import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; -import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; - -const CatalogModernViewInner: FC<{}> = () => -{ - const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData(); - const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState(); - const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions(); - const catalogAdmin = useCatalogAdmin(); - const adminMode = catalogAdmin?.adminMode ?? false; - const setAdminMode = catalogAdmin?.setAdminMode ?? (() => - {}); - const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; - const publishCatalog = catalogAdmin?.publishCatalog ?? (() => - {}); - const loading = catalogAdmin?.loading ?? false; - const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites(); - const [ showFavorites, setShowFavorites ] = useState(false); - - const isMod = useHasPermission('acc_catalogfurni'); - const totalFavs = favoriteOfferIds.length + favoritePageIds.length; - const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) - ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } - : undefined; - - useEffect(() => - { - const getCatalogTypeFromLink = (type?: string) => - { - switch((type || '').toLowerCase()) - { - case 'bc': - case 'builder': - case 'buildersclub': - case 'builders_club': - return CatalogType.BUILDER; - default: - return CatalogType.NORMAL; - } - }; - - const linkTracker: ILinkEventTracker = { - linkReceived: (url: string) => - { - const parts = url.split('/'); - - if(parts.length < 2) return; - - switch(parts[1]) - { - case 'show': - if(parts.length > 2) - { - openCatalogByType(getCatalogTypeFromLink(parts[2])); - - return; - } - - setIsVisible(true); - return; - case 'hide': - setIsVisible(false); - return; - case 'toggle': - if(parts.length > 2) - { - toggleCatalogByType(getCatalogTypeFromLink(parts[2])); - - return; - } - - setIsVisible(prevValue => !prevValue); - return; - case 'open': - if(parts.length > 2) - { - if(parts.length === 4) - { - switch(parts[2]) - { - case 'offerId': - openPageByOfferId(parseInt(parts[3])); - return; - } - } - else - { - openPageByName(parts[2]); - } - } - else - { - setIsVisible(true); - } - - return; - } - }, - eventUrlPrefix: 'catalog/' - }; - - AddLinkEventTracker(linkTracker); - - return () => RemoveLinkEventTracker(linkTracker); - }, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]); - - return ( - <> - { isVisible && - - setIsVisible(false) } style={ buildersClubHeaderStyle } /> - - { /* Admin banner */ } - { adminMode && -
- ⚙ Admin Mode - -
} - - -
- { /* === LEFT SIDEBAR === */ } -
- - { /* Favorites toggle */ } -
setShowFavorites(!showFavorites) } - > -
- 0 ? 'text-danger' : 'text-muted' }` } /> - { totalFavs > 0 && - - { totalFavs } - } -
- { LocalizeText('catalog.favorites') } -
- -
- - { /* Admin: root page actions */ } - { adminMode && rootNode && -
- - -
} - - { /* Category icons */ } - { rootNode && rootNode.children.length > 0 && rootNode.children.map((child, index) => - { - if(!adminMode && !child.isVisible) return null; - - const isHidden = !child.isVisible; - - return ( -
- { - if(searchResult) setSearchResult(null); - if(showFavorites) setShowFavorites(false); - activateNode(child); - } } - > -
- - { isHidden && } -
- - { child.localization } - - { /* Admin actions on each root category */ } - { adminMode && -
-
- { - e.stopPropagation(); - catalogAdmin.setEditingPageNode(child); - catalogAdmin.setEditingRootPage(false); - catalogAdmin.setEditingPageData(true); - } } - > - -
-
- { - e.stopPropagation(); - catalogAdmin.togglePageVisible(child.pageId); - } } - > - { isHidden - ? - : } -
-
- { - e.stopPropagation(); - if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) - { - catalogAdmin.deletePage(child.pageId); - } - } } - > - -
-
} -
- ); - }) } -
- - { /* === MAIN AREA === */ } -
- { /* Toolbar: search + admin */ } -
- { /* Breadcrumb */ } -
- - { activeNodes && activeNodes.length > 0 - ? activeNodes.map((node, i) => ( - - { i > 0 && } - activateNode(node) : undefined }> - { node.localization } - - - )) - : { LocalizeText('catalog.title') } } -
- -
- -
- - { isMod && - } -
- - { /* Content area */ } -
- { showFavorites - ?
- setShowFavorites(false) } /> -
- : <> - { !navigationHidden && activeNodes && activeNodes.length > 0 && -
- -
} -
- { adminMode && } - { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } -
- } -
-
-
- - } - - - - - ); -}; - -export const CatalogModernView: FC<{}> = () => -{ - return ( - - - - ); -}; diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index 2652fd6..fbc700e 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -1,20 +1,10 @@ import { FC } from 'react'; -import { GetConfigurationValue } from '../../api'; import { useCatalogData } from '../../hooks'; import { CatalogClassicView } from './CatalogClassicView'; -import { CatalogModernView } from './CatalogModernView'; export const CatalogView: FC<{}> = () => { const { catalogLocalizationVersion = 0 } = useCatalogData(); - const useNewStyle = GetConfigurationValue('catalog.style.new', false); - - if(useNewStyle) return ( - <> -
- - - ); return ( <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 3490e60..6866dbc 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -43,9 +43,11 @@ export const CatalogLayoutDefaultView: FC = props =>
} - { /* Product detail card */ } + { /* Product detail card. shrink-0 + visible overflow so the Buy + button never gets squeezed off-screen when the grid below + holds a lot of items. */ } { currentOffer && -
+
{ /* Preview area */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) && @@ -81,15 +83,16 @@ export const CatalogLayoutDefaultView: FC = props => { /* Spinner */ } - { /* Actions */ } -
+ { /* Actions - natural flow, no mt-auto so they can't + be pushed past the panel's bottom edge. */ } +
} { !currentOffer && -
+
{ !!page.localization.getImage(1) && } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx index 31299a0..6bc187c 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx @@ -58,9 +58,11 @@ export const CatalogLayoutTrophiesView: FC = props =>
} - { /* Selected trophy card */ } + { /* Selected trophy card. shrink-0 + no overflow-hidden so the + Buy button stays inside the panel even when the grid below + holds many trophies. */ } { currentOffer - ?
+ ?
{ /* Preview */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) @@ -90,7 +92,7 @@ export const CatalogLayoutTrophiesView: FC = props => { !canPurchase && { LocalizeText('catalog.trophies.write.hint') } } -
+
diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index 5fe8ef8..e04cac0 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -240,7 +240,7 @@ export const CatalogPurchaseWidgetView: FC = pro return ; case CatalogPurchaseState.NONE: default: - return ; + return ; } }; diff --git a/src/components/inventory/InventoryView.tsx b/src/components/inventory/InventoryView.tsx index 65c3a11..4c95c35 100644 --- a/src/components/inventory/InventoryView.tsx +++ b/src/components/inventory/InventoryView.tsx @@ -1,5 +1,6 @@ import { AddLinkEventTracker, BadgePointLimitsEvent, GetLocalizationManager, GetRoomEngine, ILinkEventTracker, IRoomSession, RemoveLinkEventTracker, RoomEngineObjectEvent, RoomEngineObjectPlacedEvent, RoomPreviewer, RoomSessionEvent } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, ReactNode, useEffect, useState } from 'react'; +import { FaAward, FaCouch, FaPaw, FaRobot, FaTag } from 'react-icons/fa'; import { GroupItem, LocalizeText, UnseenItemCategory, isObjectMoverRequested, setObjectMoverRequested } from '../../api'; import { NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; import { useInventoryBadges, useInventoryFurni, useInventoryPrefixes, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks'; @@ -19,6 +20,13 @@ const TAB_BADGES: string = 'inventory.badges'; const TAB_PREFIXES: string = 'inventory.prefixes'; const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_PREFIXES, TAB_BOTS ]; const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.PREFIX, UnseenItemCategory.BOT ]; +const TAB_ICONS: Record = { + [TAB_FURNITURE]: , + [TAB_PETS]: , + [TAB_BADGES]: , + [TAB_PREFIXES]: , + [TAB_BOTS]: +}; export const InventoryView: FC<{}> = props => { @@ -129,13 +137,13 @@ export const InventoryView: FC<{}> = props => return ( <> - + { !isTrading && <> - + { TABS.map((name, index) => { return ( @@ -144,12 +152,13 @@ export const InventoryView: FC<{}> = props => count={ getCount(UNSEEN_CATEGORIES[index]) } isActive={ (currentTab === name) } onClick={ event => setCurrentTab(name) }> - { LocalizeText(name) } + { TAB_ICONS[name] } + { LocalizeText(name) } ); }) } -
+
{ showFilter && = props =>
} { isTrading && -
+
} diff --git a/src/components/inventory/views/InventoryCategoryFilterView.tsx b/src/components/inventory/views/InventoryCategoryFilterView.tsx index 4c09e7b..5dc5afe 100644 --- a/src/components/inventory/views/InventoryCategoryFilterView.tsx +++ b/src/components/inventory/views/InventoryCategoryFilterView.tsx @@ -87,7 +87,7 @@ export const InventoryCategoryFilterView: FC = return (
columnCount={ 4 } + estimateSize={ 110 } itemRender={ item => } - items={ botItems } /> + items={ botItems } + rowGap={ 4 } />
@@ -80,7 +82,7 @@ export const InventoryBotView: FC<{
{ selectedBot.botData.name } { !!roomSession && - attemptBotPlacement(selectedBot) }> + attemptBotPlacement(selectedBot) }> { LocalizeText('inventory.furni.placetoroom') } }
} diff --git a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx index 1fe708f..364cfe3 100644 --- a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx +++ b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx @@ -147,11 +147,11 @@ export const InventoryFurnitureView: FC<{ { selectedItem.description } }
{ !!roomSession && - attemptItemPlacement(selectedItem) }> + attemptItemPlacement(selectedItem) }> { LocalizeText('inventory.furni.placetoroom') } } { selectedItem.isSellable && - attemptPlaceMarketplaceOffer(selectedItem) }> + attemptPlaceMarketplaceOffer(selectedItem) }> { LocalizeText('inventory.marketplace.sell') } }
diff --git a/src/components/inventory/views/pet/InventoryPetView.tsx b/src/components/inventory/views/pet/InventoryPetView.tsx index 4af1c78..e78c742 100644 --- a/src/components/inventory/views/pet/InventoryPetView.tsx +++ b/src/components/inventory/views/pet/InventoryPetView.tsx @@ -84,6 +84,8 @@ export const InventoryPetView: FC<{
columnCount={ 6 } + estimateSize={ 46 } + itemMinWidth={ 46 } itemRender={ item => } items={ petItems } />
@@ -101,7 +103,7 @@ export const InventoryPetView: FC<{
{ selectedPet.petData.name } { !!roomSession && - attemptPetPlacement(selectedPet) }> + attemptPetPlacement(selectedPet) }> { LocalizeText('inventory.furni.placetoroom') } }
} diff --git a/src/components/rare-values/RareValuesView.tsx b/src/components/rare-values/RareValuesView.tsx index 266c32b..e6dee22 100644 --- a/src/components/rare-values/RareValuesView.tsx +++ b/src/components/rare-values/RareValuesView.tsx @@ -84,12 +84,16 @@ export const RareValuesView: FC<{}> = () => return rows.filter(row => row.name.toLocaleLowerCase().includes(query)); }, [ rows, searchValue ]); + // Reset paging when the underlying list changes (typed in search, data loaded). useEffect(() => { setVisibleCount(PAGE_SIZE); if(scrollRef.current) scrollRef.current.scrollTop = 0; }, [ filtered ]); + // Infinite scroll: grow visibleCount by PAGE_SIZE whenever the sentinel + // enters the viewport. The root is the scroll container so the trigger + // fires reliably inside an in-app modal (no document scroll). useEffect(() => { if(!isVisible) return; diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index adf3285..7ce61b9 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -362,9 +362,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { (getTotalUnseen > 0) && } - - CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> - CreateLinkEvent('inventory/toggle') } className="tb-icon" /> { (getFullCount > 0) && @@ -384,10 +381,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => } - { isInRoom && - - CreateLinkEvent('camera/toggle') } className="tb-icon" /> - } { (isInRoom && youtubeEnabled) && @@ -396,20 +389,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('soundboard/toggle') } className="tb-icon" /> } - { isMod && - - CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> - { (openTicketsCount > 0) && - } - } - { (isHk && hkEnabled) && - - CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> - } - { isMod && - - CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> - } CreateLinkEvent('friends/toggle') } className="tb-icon" /> { (requests.length > 0) && @@ -417,6 +396,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => + { /* Mobile side tools — moved out of the bottom bar into a + vertical pill stack on the left edge so the bottom bar has + room. Always present (Builders Club), plus camera in-room + and the staff-only tools when permitted. */ } + + + CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> + + { isInRoom && + + CreateLinkEvent('camera/toggle') } className="tb-icon" /> + } + { isMod && + + CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> + { (openTicketsCount > 0) && + } + } + { (isHk && hkEnabled) && + + CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> + } + { isMod && + + CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> + } + ); }; @@ -494,6 +505,14 @@ const TOOLBAR_STYLES = ` flex-wrap: nowrap; } + /* Keep each icon at its natural size so the mobile bar scrolls + horizontally instead of squashing the items into each other. + (Default flex-shrink:1 let the fixed-size icon backgrounds overlap + once enough icons were present to exceed the bar width.) */ + .tb-bar-scroll > * { + flex-shrink: 0; + } + .tb-bar-scroll::-webkit-scrollbar { display: none; } diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index b1dff1b..c40f46c 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -1,10 +1,52 @@ +/* --------------------------------------------------------------------------- + * Catalog "classic" window — Habbo mobile shop redesign. + * + * Palette (from the mobile shop mockup): + * --cat-blue #4b7a94 header / app chrome + * --cat-blue-dark #385d73 header borders + * --cat-ink #233a47 strong outlines / active tab border + * --cat-strip #d9e2e8 tab strip + footer background + * --cat-tab #b7c7d1 inactive tab fill + * --cat-tab-border #7a9cb0 tab outline + * --cat-panel #eef2f5 sidebar / search panels + * --cat-sub #e1e7ec sub-node rows + * --cat-line #b7c7d1 card / divider lines + * --cat-canvas #d4dadf isometric preview canvas + * --cat-canvas-2 #c9cfd4 preview checker tiles + * --cat-select #3a82a7 selected card outline + * --cat-select-bg #f0f5f8 selected card fill + * --cat-gold #f7d673 price badge fill + * --cat-gold-border #d4af37 price badge outline + * --cat-gold-ink #4a3300 price badge text + * --cat-buy #009900 buy button + * ------------------------------------------------------------------------- */ + .nitro-catalog-classic-window { + --cat-blue: #4b7a94; + --cat-blue-dark: #385d73; + --cat-ink: #233a47; + --cat-strip: #d9e2e8; + --cat-tab: #b7c7d1; + --cat-tab-border: #7a9cb0; + --cat-panel: #eef2f5; + --cat-sub: #e1e7ec; + --cat-line: #b7c7d1; + --cat-canvas: #d4dadf; + --cat-canvas-2: #c9cfd4; + --cat-select: #3a82a7; + --cat-select-bg: #f0f5f8; + --cat-gold: #f7d673; + --cat-gold-border: #d4af37; + --cat-gold-ink: #4a3300; + --cat-buy: #009900; + width: 640px !important; height: 600px !important; max-width: 640px !important; min-width: 640px !important; min-height: 600px !important; max-height: 600px !important; + background: #ffffff !important; } .nitro-catalog-classic-window .nitro-card-title { @@ -17,48 +59,62 @@ max-height: 38px; } +/* Blue Habbo-mobile header. */ +.nitro-catalog-classic-window .nitro-card-header { + background: var(--cat-blue); + border-color: var(--cat-blue); + border-bottom-color: var(--cat-ink); +} + .nitro-catalog-classic-admin-banner { border-bottom: 1px solid rgba(0, 0, 0, 0.18); background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); } +/* Tab strip — light blue-grey shelf with tab "folders". */ .nitro-catalog-classic-tabs-shell { flex-wrap: nowrap; - gap: 1px; - min-height: 30px; - max-height: 30px; - padding: 0 6px; + gap: 2px; + min-height: 32px; + max-height: 32px; + padding: 4px 6px 0; overflow-x: auto; overflow-y: hidden; align-items: end; - background: #e7e8df; - border-bottom: 1px solid #b8beb4; + background: var(--cat-strip); + border-bottom: 2px solid var(--cat-ink); } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { min-height: 28px; - padding: 5px 10px 4px; - border: 1px solid #8f8f8b; + padding: 5px 12px 4px; + border: 1px solid var(--cat-tab-border); border-bottom: 0; border-radius: 5px 5px 0 0; - background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + background: var(--cat-tab); + color: var(--cat-ink); + box-shadow: none; white-space: nowrap; + font-weight: 700; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); + background: #c7d4dd; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #f2f2eb; - transform: translateY(0); + background: #ffffff; + color: #000000; position: relative; top: 1px; + border-color: var(--cat-ink); + box-shadow: inset 0 -1px 0 #ffffff; + font-weight: 700; } .nitro-catalog-classic-content-shell { padding: 6px 8px 8px !important; + background: #ffffff !important; } .nitro-catalog-classic-stage { @@ -82,75 +138,83 @@ } .nitro-catalog-classic-search-shell { - padding: 3px; - border: 1px solid #a7aba1; + padding: 4px; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); + background: var(--cat-panel); } .nitro-catalog-classic-search-shell input { - height: 18px; + height: 20px; padding-top: 0 !important; padding-bottom: 0 !important; border-width: 1px !important; - border-color: #8f9588 !important; + border-color: var(--cat-tab-border) !important; border-radius: 3px !important; background: #fff !important; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.06); } .nitro-catalog-classic-search-shell svg { - color: #61645b !important; + color: #5b7080 !important; } +/* Sidebar category tree — flat rows that echo the mockup's landscape sidebar. */ .nitro-catalog-classic-navigation-shell { flex: 1 1 auto; min-height: 0; - padding: 3px 2px 3px 3px; - border: 1px solid #a7aba1; + padding: 4px 0; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); + background: var(--cat-panel); overflow: auto; } .nitro-catalog-classic-navigation-list { display: flex; flex-direction: column; - gap: 2px; + gap: 0; } -.nitro-catalog-classic-navigation-node.is-child { - margin-left: 10px; +.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { + padding-left: 22px; + background: var(--cat-sub); } .nitro-catalog-classic-navigation-item { display: flex; align-items: center; - gap: 4px; - min-height: 21px; - padding: 1px 6px 1px 5px; - border: 1px solid #bdc2ba; - border-radius: 4px; - background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); - color: #2e2e2e; + gap: 6px; + min-height: 28px; + padding: 4px 10px; + border: 0; + border-left: 4px solid transparent; + border-radius: 0; + background: transparent; + color: var(--cat-ink); + font-weight: 700; cursor: pointer; - transition: background-color 0.12s ease, border-color 0.12s ease; + transition: background-color 0.12s ease; +} + +.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { + font-weight: 400; } .nitro-catalog-classic-navigation-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); - border-color: #9ea79b; + background: #dde6ec; } .nitro-catalog-classic-navigation-item.is-active { - background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); - border-color: #8e9ba5; + background: #ffffff; + border-left-color: var(--cat-blue); + color: #000000; font-weight: 700; } .nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(48, 114, 140, 0.35); - outline-offset: 1px; + outline: 2px solid rgba(58, 130, 167, 0.4); + outline-offset: -2px; } .nitro-catalog-classic-navigation-icon { @@ -188,18 +252,19 @@ } .nitro-catalog-classic-navigation-caret { - color: #676d66 !important; + color: #5b7080 !important; } +/* Right-hand content column. */ .nitro-catalog-classic-layout-shell { display: flex; flex-direction: column; min-width: 0; min-height: 0; height: 100%; - border: 1px solid #a7aba1; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); + background: #ffffff; overflow: hidden; } @@ -207,10 +272,10 @@ display: flex; flex-direction: column; gap: 3px; - min-height: 66px; - padding: 5px 7px; - border-bottom: 1px solid #c8cdc3; - background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); + min-height: 0; + padding: 6px 8px; + border-bottom: 1px solid var(--cat-line); + background: #ffffff; } .nitro-catalog-classic-layout-hero { @@ -234,7 +299,7 @@ flex: 1 1 auto; min-height: 0; padding: 6px; - background: #f2f2eb; + background: #ffffff; overflow: hidden; } @@ -242,58 +307,91 @@ gap: 8px; } +/* Offer / detail card — the mockup's preview + info panel. */ .nitro-catalog-classic-offer-panel, .nitro-catalog-classic-welcome { - border: 1px solid #bfc4bc; + border: 1px solid var(--cat-line); border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); + background: #ffffff; } +.nitro-catalog-classic-offer-panel { + min-height: 132px; + overflow: hidden; +} + +/* Isometric checkered preview canvas, straight from the mockup. */ .nitro-catalog-classic-offer-preview { width: 136px; min-width: 136px; padding: 8px; - border-right: 1px solid #c9cec5; - background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); + border-right: 1px solid var(--cat-line); + background-color: var(--cat-canvas); + background-image: + linear-gradient(30deg, var(--cat-canvas-2) 25%, transparent 25%), + linear-gradient(-30deg, var(--cat-canvas-2) 25%, transparent 25%), + linear-gradient(30deg, transparent 75%, var(--cat-canvas-2) 75%), + linear-gradient(-30deg, transparent 75%, var(--cat-canvas-2) 75%); + background-size: 30px 17px; } .nitro-catalog-classic-offer-info { padding: 10px; } +/* Solid-gold price pills (mockup price badge). */ +.nitro-catalog-classic-offer-info .rounded-full { + background: var(--cat-gold) !important; + border-color: var(--cat-gold-border) !important; +} + +.nitro-catalog-classic-offer-info .rounded-full, +.nitro-catalog-classic-offer-info .rounded-full * { + color: var(--cat-gold-ink) !important; +} + .nitro-catalog-classic-welcome { min-height: 128px; padding: 10px; } +/* Product grid. */ .nitro-catalog-classic-grid-shell { min-height: 150px; - padding: 4px; - border: 1px solid #bcc2b8; + padding: 6px; + border: 1px solid var(--cat-line); border-radius: 6px; - background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); + background: #ffffff; height: auto; flex: 1 1 auto; } .nitro-catalog-classic-grid { - gap: 4px !important; + gap: 6px !important; align-content: start; } .nitro-catalog-classic-window .layout-grid-item { height: 54px; - border: 1px solid #b8beb6 !important; - border-radius: 6px !important; - background-color: #d7dde2; + border: 1px solid var(--cat-line) !important; + border-radius: 4px !important; + background-color: #ffffff; background-image: none; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); + box-shadow: none; + transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; +} + +.nitro-catalog-classic-window .layout-grid-item:hover { + background-color: var(--cat-select-bg) !important; + border-color: var(--cat-select) !important; + box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.2); } .nitro-catalog-classic-window .layout-grid-item.is-active { - background-color: #e5ebef !important; - border-color: #8f978b !important; + background-color: var(--cat-select-bg) !important; + border-color: var(--cat-select) !important; + border-width: 2px !important; + box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.35); } .nitro-catalog-classic-grid-offer-icon { @@ -309,9 +407,9 @@ min-height: 56px; margin-bottom: 6px; padding: 4px 6px; - border: 1px solid #bec3ba; + border: 1px solid var(--cat-line); border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); + background: #ffffff; } .nitro-catalog-classic-window .nitro-catalog-header img { @@ -322,13 +420,19 @@ object-fit: contain; } +/* Green "buy" action button (mockup "Acquista"). */ +.nitro-catalog-classic-offer-info .bg-\[\#00800b\] { + background-color: var(--cat-buy) !important; + border-color: #007a00 !important; +} + .nitro-catalog-classic-breadcrumb { display: flex; align-items: center; gap: 5px; min-height: 16px; overflow: hidden; - color: #666a63; + color: #5b7080; font-size: 10px; line-height: 1; white-space: nowrap; @@ -342,9 +446,10 @@ } .nitro-catalog-classic-breadcrumb-separator { - color: #9ea395; + color: #94a7b3; } +/* Scrollbars — blue-grey to match the chrome. */ .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { width: 12px; @@ -352,43 +457,166 @@ .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { - border-left: 1px solid #c2c6be; - background: #dde2d8; + border-left: 1px solid var(--cat-line); + background: var(--cat-panel); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { - border: 1px solid #7d8680; + border: 1px solid var(--cat-tab-border); border-radius: 6px; - background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); + background: linear-gradient(180deg, #a9bcc9 0%, #89a0ae 100%); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { height: 12px; - background: #dde2d8; + background: var(--cat-panel); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { height: 12px; - background: #dde2d8; + background: var(--cat-panel); } -@media (max-width: 991.98px) { +/* --------------------------------------------------------------------------- + * Responsive layout + * + * Three tiers: + * > 1024px desktop, fixed 640x600 (default rules above) + * 641px - 1024px tablet, fluid card, single-column stage + * <= 640px phone, full-screen modal with stacked layout + + * larger touch targets and a horizontally + * scrollable tab strip + * + * A separate short-landscape branch trims chrome when height <= 480px + * (typical phone-in-landscape) so the grid still has room to breathe. + * ------------------------------------------------------------------------- */ + +/* Tablet (portrait + landscape between phone and desktop). */ +@media (max-width: 1024px) and (min-width: 641px) { .nitro-catalog-classic-window { - width: min(calc(100vw - 16px), 570px) !important; + width: min(calc(100vw - 24px), 720px) !important; min-width: 0 !important; - height: min(calc(100vh - 16px), 635px) !important; + max-width: calc(100vw - 24px) !important; + height: min(calc(100vh - 24px), 720px) !important; min-height: 0 !important; - max-width: calc(100vw - 16px) !important; + max-height: calc(100vh - 24px) !important; } + /* Drop the sidebar to a horizontal row above the content so the grid + * has the full card width on the narrower tablet viewports. */ .nitro-catalog-classic-stage { grid-template-columns: minmax(0, 1fr); } .nitro-catalog-classic-sidebar { - max-height: 180px; + max-height: 200px; + } +} + +/* Phone — portrait and landscape. */ +@media (max-width: 640px) { + .nitro-catalog-classic-window { + width: 100vw !important; + min-width: 0 !important; + max-width: 100vw !important; + height: 100vh !important; + min-height: 0 !important; + max-height: 100vh !important; + /* Drop the soft drop-shadow / outer border on full-screen so it + * blends with the viewport edges. */ + border-radius: 0 !important; + } + + /* Tabs become a horizontally scrollable strip with bigger tap targets + * (≥ 44px tall is the WCAG / iOS recommendation). */ + .nitro-catalog-classic-tabs-shell { + min-height: 44px; + max-height: 44px; + padding: 4px 4px 0; + -webkit-overflow-scrolling: touch; + } + + .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { + min-height: 42px; + padding: 6px 14px; + font-size: 12px; + } + + .nitro-catalog-classic-content-shell { + padding: 6px !important; + } + + .nitro-catalog-classic-stage { + grid-template-columns: minmax(0, 1fr); + gap: 6px; + } + + /* Sidebar above content, capped so most of the viewport is the grid. */ + .nitro-catalog-classic-sidebar { + max-height: 33vh; + } + + /* Search input + nav items grow into real touch targets. */ + .nitro-catalog-classic-search-shell input { + height: 28px; + font-size: 13px; + } + + .nitro-catalog-classic-navigation-item { + min-height: 40px; + padding: 6px 12px; + } + + .nitro-catalog-classic-navigation-label { + font-size: 13px; + } + + /* Bigger furni icons in the grid so a fingertip can hit them. */ + .nitro-catalog-classic-window .layout-grid-item { + height: 64px; + } + + /* Modal corner radius cleanups so the full-screen look is consistent. */ + .nitro-catalog-classic-window .nitro-card-header-shell, + .nitro-catalog-classic-window .nitro-card-content-shell { + border-radius: 0 !important; + } +} + +/* Phone in landscape — short viewport. Trim header / hero so the grid keeps + * usable height. Triggers regardless of portrait/landscape on any short + * screen, which is also the right answer for very small laptop windows. */ +@media (max-height: 480px) { + .nitro-catalog-classic-window { + height: 100vh !important; + max-height: 100vh !important; + } + + .nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 32px; + max-height: 32px; + } + + .nitro-catalog-classic-tabs-shell { + min-height: 38px; + max-height: 38px; + } + + .nitro-catalog-classic-layout-header-shell { + min-height: 0; + padding: 3px 6px; + } + + /* Hide the marketing hero image in landscape - it's the first thing to + * sacrifice when the user clearly wants more grid. */ + .nitro-catalog-classic-layout-hero { + display: none; + } + + .nitro-catalog-classic-sidebar { + max-height: 26vh; } } diff --git a/src/css/inventory/InventoryView.css b/src/css/inventory/InventoryView.css new file mode 100644 index 0000000..125f8bc --- /dev/null +++ b/src/css/inventory/InventoryView.css @@ -0,0 +1,241 @@ +/* --------------------------------------------------------------------------- + * Inventory window — Habbo mobile inventory skin. + * + * Matches the mobile inventory mockup and stays consistent with the catalog + * redesign: blue header, beige body, folder tabs, framed preview canvas, + * teal-accented item slots, and chunky green "place" / red "sell" buttons. + * + * Palette (from the mockup): + * --inv-blue #4a7d8c header + * --inv-beige #e2e0d6 window / body / slot fill + * --inv-tab #c7c5ba inactive tab fill + * --inv-line #919088 slot / preview outline + * --inv-accent #4a7d8c selected slot glow + count text + * --inv-place* green place button + * --inv-sell* red sell button + * ------------------------------------------------------------------------- */ + +.nitro-inventory-window { + --inv-blue: #4a7d8c; + --inv-beige: #e2e0d6; + --inv-tab: #c7c5ba; + --inv-line: #919088; + --inv-accent: #4a7d8c; + --inv-place: #5ca843; + --inv-place-dark: #397025; + --inv-place-light: #8ee374; + --inv-sell: #d13e31; + --inv-sell-dark: #881e15; + --inv-sell-light: #f07e74; + --inv-border: #6b6f73; + + background: var(--inv-beige) !important; +} + +/* Blue Habbo header. */ +.nitro-inventory-window .nitro-card-header { + background: var(--inv-blue); + border-color: var(--inv-blue); + border-bottom-color: var(--inv-border); + box-shadow: inset 0 2px 0 #709da9, inset 0 -2px 0 #315863; +} + +/* Tab strip — beige shelf with folder tabs. Light, low-contrast outlines. */ +.nitro-inventory-window .nitro-inventory-tabs-shell { + background: var(--inv-beige); + gap: 4px; + padding: 6px 8px 0; + border-bottom: 1px solid #b9c4ca; +} + +.nitro-inventory-window .nitro-inventory-tabs-shell .nitro-card-tab-item { + background: var(--inv-tab); + border: 1px solid #aeb9bf; + border-bottom: 0; + border-radius: 6px 6px 0 0; + color: #8a9aa2; + font-weight: 600; + box-shadow: inset 1px 1px 0 #fff; +} + +.nitro-inventory-window .nitro-inventory-tabs-shell .nitro-card-tab-item:hover { + background: #d2d0c6; + color: #6b7d86; +} + +.nitro-inventory-window .nitro-inventory-tabs-shell .nitro-card-tab-item-active { + background: #fff; + color: #566a74; + border-color: #93a1a8; + position: relative; + top: 1px; +} + +/* Tabs show the icon first, then the text label (desktop). */ +.nitro-inventory-window .nitro-inventory-tab-icon { + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 5px; + font-size: 14px; + line-height: 1; +} + +/* Body — grey-beige backdrop. */ +.nitro-inventory-window .nitro-inventory-body { + background: var(--inv-beige); +} + +/* Filter bar — white search box + beige select, hard outlines. */ +.nitro-inventory-window .nitro-inventory-filter-bar { + background: #fff; + border: 2px solid var(--inv-border); + border-radius: 4px; +} + +.nitro-inventory-window .nitro-inventory-filter-bar select { + background: var(--inv-beige); + border: 2px solid var(--inv-border) !important; + border-radius: 4px; + font-weight: 700; +} + +/* Item slots — beige tiles, grey outline, teal glow when selected. */ +.nitro-inventory-window .bg-card-grid-item { + background-color: var(--inv-beige) !important; +} + +.nitro-inventory-window .border-card-grid-item-border { + border-color: var(--inv-line) !important; +} + +.nitro-inventory-window .bg-card-grid-item-active { + background-color: #fff !important; +} + +.nitro-inventory-window .border-card-grid-item-active { + border-color: var(--inv-border) !important; + box-shadow: 0 0 0 2px var(--inv-accent); +} + +/* Count badge — white pill with a teal number (mockup). */ +.nitro-inventory-window .bg-red-700 { + background-color: #fff !important; + color: var(--inv-accent) !important; + border-color: #75746e !important; +} + +/* Preview canvas — white framed box around the room previewer. */ +.nitro-inventory-window .shadow-room-previewer { + background-color: #fff; + border: 2px solid var(--inv-line); +} + +/* Action buttons — chunky beveled Habbo buttons. */ +.nitro-inventory-window .nitro-inventory-btn-place, +.nitro-inventory-window .nitro-inventory-btn-sell { + border: 2px solid var(--inv-border) !important; + border-radius: 4px; + color: #fff !important; + font-weight: 700; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); +} + +.nitro-inventory-window .nitro-inventory-btn-place { + background: var(--inv-place) !important; + box-shadow: inset -2px -2px 0 var(--inv-place-dark), inset 2px 2px 0 var(--inv-place-light); +} + +.nitro-inventory-window .nitro-inventory-btn-place:hover { + background: #67b94e !important; +} + +.nitro-inventory-window .nitro-inventory-btn-sell { + background: var(--inv-sell) !important; + box-shadow: inset -2px -2px 0 var(--inv-sell-dark), inset 2px 2px 0 var(--inv-sell-light); +} + +.nitro-inventory-window .nitro-inventory-btn-sell:hover { + background: #db4d40 !important; +} + +/* --------------------------------------------------------------------------- + * Responsive layout + * + * The window ships a fixed 528x420 size (Tailwind utilities on the card). + * Mirror the catalog's three tiers so it stays usable down to phones: + * > 1024px desktop, fixed size (utilities) + * 641px - 1024px tablet, fluid card + * <= 640px phone, full-screen + stacked grid / preview + * ------------------------------------------------------------------------- */ + +/* Tablet. */ +@media (max-width: 1024px) and (min-width: 641px) { + .nitro-inventory-window { + width: min(calc(100vw - 24px), 640px) !important; + min-width: 0 !important; + max-width: calc(100vw - 24px) !important; + height: min(calc(100vh - 24px), 560px) !important; + min-height: 0 !important; + max-height: calc(100vh - 24px) !important; + } +} + +/* Phone — full-screen, single-column stack (grid on top, preview below). */ +@media (max-width: 640px) { + .nitro-inventory-window { + width: 100vw !important; + min-width: 0 !important; + max-width: 100vw !important; + height: 100vh !important; + min-height: 0 !important; + max-height: 100vh !important; + border-radius: 0 !important; + } + + .nitro-inventory-window .nitro-card-header-shell, + .nitro-inventory-window .nitro-card-content-shell { + border-radius: 0 !important; + } + + /* Icon-only tabs spread evenly across the width — text labels don't + * fit on a phone, so swap them for the per-tab glyph. */ + .nitro-inventory-window .nitro-inventory-tabs-shell { + flex-wrap: nowrap; + overflow: hidden; + } + + .nitro-inventory-window .nitro-inventory-tabs-shell .nitro-card-tab-item { + flex: 1 1 0; + min-width: 0; + padding: 8px 0; + justify-content: center; + } + + .nitro-inventory-window .nitro-inventory-tab-label { + display: none; + } + + .nitro-inventory-window .nitro-inventory-tab-icon { + margin-right: 0; + font-size: 17px; + } + + /* Stack the item grid above the preview / action panel. */ + .nitro-inventory-window .grid.grid-cols-12 { + grid-template-columns: minmax(0, 1fr) !important; + grid-template-rows: minmax(0, 1fr) auto !important; + } + + .nitro-inventory-window .grid.grid-cols-12 > .col-span-7, + .nitro-inventory-window .grid.grid-cols-12 > .col-span-5 { + grid-column: 1 / -1 !important; + } + + /* Roomier touch targets for the action buttons. */ + .nitro-inventory-window .nitro-inventory-btn-place, + .nitro-inventory-window .nitro-inventory-btn-sell { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } +} diff --git a/src/hooks/navigator/useNavigatorSearch.ts b/src/hooks/navigator/useNavigatorSearch.ts index ab3744d..6ee7db0 100644 --- a/src/hooks/navigator/useNavigatorSearch.ts +++ b/src/hooks/navigator/useNavigatorSearch.ts @@ -1,10 +1,24 @@ -import { NavigatorSearchComposer, NavigatorSearchEvent, NavigatorSearchResultSet } from '@nitrots/nitro-renderer'; +import { NavigatorSearchComposer, NavigatorSearchEvent, + NavigatorSearchResultSet } from '@nitrots/nitro-renderer'; import { useEffect, useState } from 'react'; import { SendMessageComposer } from '../../api'; import { useMessageEvent } from '../events'; import { useNavigatorUiStore } from './navigatorUiStore'; - +/** + * Navigator search hook. + * + * Fires NavigatorSearchComposer(tabCode, filter) whenever the active tab + * or filter changes (skipped when tabCode is '' — initial state, before + * metadata arrives). Holds the latest NavigatorSearchResultSet that + * matches the active tab. + * + * The TanStack Query variant (see useNitroQuery) was tried earlier but + * its one-shot listener doesn't always reach NavigatorSearchEvent in + * production builds with older renderer SDKs; the persistent + * useMessageEvent listener used here matches the rest of the codebase + * and reliably catches every server push. + */ export const useNavigatorSearch = () => { const tabCode = useNavigatorUiStore(s => s.currentTabCode); @@ -26,6 +40,9 @@ export const useNavigatorSearch = () => const result = event.getParser()?.result; if(!result) return; + // Accept any incoming result for the currently active tab. Server + // can push extra results unprompted (e.g. after a room is + // created); accepting them keeps the panel in sync. if(tabCode && result.code !== tabCode) return; setSearchResult(result); diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 1c23877..c999dec 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -4,6 +4,9 @@ import { CommandDefinition, LocalizeText } from '../../../api'; import { createNitroStore } from '../../../state/createNitroStore'; import { useMessageEvent } from '../../events'; +// Client-only commands are static; safe to keep at module scope. The +// `descriptionKey` is a LocalizeText slot resolved at merge time so +// hotels in different locales see the right language. const CLIENT_COMMANDS: { key: string; descriptionKey: string }[] = [ // Room effects { key: 'shake', descriptionKey: 'chatcmd.client.shake' }, @@ -32,6 +35,18 @@ const CLIENT_COMMANDS: { key: string; descriptionKey: string }[] = [ { key: 'nitro', descriptionKey: 'chatcmd.client.info' }, ]; +/** + * Server-pushed command cache. Lives in a Zustand store (instead of + * module-level `let` variables) so the React Compiler can analyze the + * surrounding hook cleanly, and so a future test can `setState({…})` + * a deterministic fixture without monkey-patching the module. + * + * The `isListenerRegistered` flag prevents the renderer from getting + * two AvailableCommandsEvent listeners — one from the module-level + * pre-mount registration (which captures the server's reply that lands + * during login, BEFORE any React widget mounts) and one from the + * in-hook `useMessageEvent` (which covers later rank-change refreshes). + */ interface ChatCommandStore { serverCommands: CommandDefinition[]; @@ -62,9 +77,15 @@ const ensureGlobalListener = (): void => GetCommunication().registerMessageEvent(event); useChatCommandStore.getState().markListenerRegistered(); } - catch {} + catch + { + // Communication not ready yet — the in-hook useMessageEvent + // below covers later mounts. + } }; +// Try once at module load so the server's response landing before any +// React mount still hits the cache. ensureGlobalListener(); export const useChatCommandSelector = (chatValue: string) => @@ -76,9 +97,13 @@ export const useChatCommandSelector = (chatValue: string) => useEffect(() => { + // Cover the case where the module-level registration failed + // because GetCommunication() wasn't ready at import time. ensureGlobalListener(); }, []); + // Late updates (rank change, etc.) — go through the store so all + // consumers see the same data. useMessageEvent(AvailableCommandsEvent, event => { const parser = event.getParser(); @@ -142,11 +167,13 @@ export const useChatCommandSelector = (chatValue: string) => setDismissed(true); }, []); + // Reset dismissed when chatValue changes to a new command start useEffect(() => { if(chatValue === ':' || chatValue === '') setDismissed(false); }, [ chatValue ]); + // Reset selectedIndex when filtered list changes useEffect(() => { setSelectedIndex(0); diff --git a/src/index.tsx b/src/index.tsx index 3af6732..3825168 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -37,6 +37,8 @@ import './css/login/LoginView.css'; import './css/icons/icons.css'; +import './css/inventory/InventoryView.css'; + import './css/layout/LayoutTrophy.css'; From 888073acb1f967667e40c773fbd5809bf3003de8 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 15:16:14 +0200 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=86=99=20Update=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/catalog/CatalogClassicView.tsx | 53 +++- .../page/layout/CatalogLayoutDefaultView.tsx | 24 +- .../widgets/CatalogViewProductWidgetView.tsx | 4 + .../furniture/InventoryFurnitureView.tsx | 58 ++-- src/css/catalog/CatalogClassicView.css | 257 +++++++++++++----- src/css/inventory/InventoryView.css | 101 ++++--- src/css/nitrocard/NitroCardView.css | 8 + 7 files changed, 334 insertions(+), 171 deletions(-) diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index eb16cc2..1ee04f2 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -1,9 +1,9 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect } from 'react'; -import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; -import { CatalogType, LocalizeText } from '../../api'; -import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks'; +import { FC, useEffect, useState } from 'react'; +import { FaBars, FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; +import { CatalogType, GetConfigurationValue, LocalizeShortNumber, LocalizeText } from '../../api'; +import { Column, Grid, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission, usePurse } from '../../hooks'; import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; @@ -31,6 +31,9 @@ const CatalogClassicViewInner: FC<{}> = () => const loading = catalogAdmin?.loading ?? false; const isMod = useHasPermission('acc_catalogfurni'); + const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); + const { purse = null } = usePurse(); + const displayedCurrencies = GetConfigurationValue('system.currency.types', []); const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; @@ -121,6 +124,42 @@ const CatalogClassicViewInner: FC<{}> = () => { isVisible && setIsVisible(false) } style={ buildersClubHeaderStyle } /> +
+ { isMod && +
+ + { mobileMenuOpen && +
+ + { adminMode && + } +
} +
} +
+
+ { LocalizeShortNumber(purse?.credits ?? 0) } + +
+ { displayedCurrencies.map(type => ( +
+ { LocalizeShortNumber(purse?.activityPoints?.get(type) ?? 0) } + +
+ )) } +
+
{ adminMode &&
Admin Mode @@ -148,7 +187,7 @@ const CatalogClassicViewInner: FC<{}> = () => } }>
- { child.localization } + { child.localization } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -172,7 +211,7 @@ const CatalogClassicViewInner: FC<{}> = () => ); }) } { isMod && - setAdminMode(!adminMode) }> + setAdminMode(!adminMode) }> } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 6866dbc..09411d1 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { FaEdit, FaPlus } from 'react-icons/fa'; +import { FaEdit, FaPlus, FaPowerOff, FaSyncAlt } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalogData } from '../../../../../hooks'; @@ -17,13 +17,12 @@ import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutDefaultView: FC = props => { const { page = null } = props; - const { currentOffer = null, currentPage = null } = useCatalogData(); + const { currentOffer = null, currentPage = null, roomPreviewer = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const adminMode = catalogAdmin?.adminMode ?? false; return (
- { /* Admin: quick actions */ } { adminMode && !catalogAdmin.editingPageData &&
} - - { /* Product detail card. shrink-0 + visible overflow so the Buy - button never gets squeezed off-screen when the grid below - holds a lot of items. */ } { currentOffer &&
- { /* Preview area */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) && <> + + } { (currentOffer.product.productType === ProductTypeEnum.BADGE) && }
- { /* Product info + purchase */ }
- { /* Title row */ }
{ currentOffer.localizationName } @@ -79,13 +77,9 @@ export const CatalogLayoutDefaultView: FC = props =>
}
- { /* Price */ } - { /* Spinner */ } - { /* Actions - natural flow, no mt-auto so they can't - be pushed past the panel's bottom edge. */ } -
+
diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index 027aa36..f63f721 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -19,6 +19,8 @@ export const CatalogViewProductWidgetView: FC<{}> = props => if(!product) return; roomPreviewer.reset(false); + roomPreviewer.updateObjectRoom('default', 'default', 'default'); + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); switch(product.productType) { @@ -68,6 +70,8 @@ export const CatalogViewProductWidgetView: FC<{}> = props => case ProductTypeEnum.WALL: { if(!product.furnitureData) return; + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); + switch(product.furnitureData.specialType) { case FurniCategory.FLOOR: diff --git a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx index 364cfe3..58a049f 100644 --- a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx +++ b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx @@ -1,7 +1,7 @@ import { InfiniteGrid } from '@layout/InfiniteGrid'; import { GetRoomEngine, GetSessionDataManager, IRoomSession, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { FaTrashAlt } from 'react-icons/fa'; +import { FaPowerOff, FaSyncAlt, FaTrashAlt } from 'react-icons/fa'; import { DispatchUiEvent, FurniCategory, GroupItem, LocalizeText, UnseenItemCategory, attemptItemPlacement } from '../../../../api'; import { LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomPreviewerView } from '../../../../common'; import { CatalogPostMarketplaceOfferEvent, DeleteItemConfirmEvent } from '../../../../events'; @@ -49,22 +49,24 @@ export const InventoryFurnitureView: FC<{ if(!furnitureItem) return; - const roomEngine = GetRoomEngine(); - - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - wallType = (wallType && wallType.length) ? wallType : '101'; - floorType = (floorType && floorType.length) ? floorType : '101'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; - roomPreviewer.reset(false); - roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); - roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - if((furnitureItem.category === FurniCategory.WALL_PAPER) || (furnitureItem.category === FurniCategory.FLOOR) || (furnitureItem.category === FurniCategory.LANDSCAPE)) + const isRoomDecoration = (furnitureItem.category === FurniCategory.WALL_PAPER) || (furnitureItem.category === FurniCategory.FLOOR) || (furnitureItem.category === FurniCategory.LANDSCAPE); + + if(isRoomDecoration) { + const roomEngine = GetRoomEngine(); + + let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); + let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); + let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); + + wallType = (wallType && wallType.length) ? wallType : '101'; + floorType = (floorType && floorType.length) ? floorType : '101'; + landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; + + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); + floorType = ((furnitureItem.category === FurniCategory.FLOOR) ? selectedItem.stuffData.getLegacyString() : floorType); wallType = ((furnitureItem.category === FurniCategory.WALL_PAPER) ? selectedItem.stuffData.getLegacyString() : wallType); landscapeType = ((furnitureItem.category === FurniCategory.LANDSCAPE) ? selectedItem.stuffData.getLegacyString() : landscapeType); @@ -77,17 +79,20 @@ export const InventoryFurnitureView: FC<{ if(data) roomPreviewer.addWallItemIntoRoom(data.id, new Vector3d(90, 0, 0), data.customParams); } + + return; + } + + roomPreviewer.updateObjectRoom('default', 'default', 'default'); + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); + + if(selectedItem.isWallItem) + { + roomPreviewer.addWallItemIntoRoom(selectedItem.type, new Vector3d(90), furnitureItem.stuffData.getLegacyString()); } else { - if(selectedItem.isWallItem) - { - roomPreviewer.addWallItemIntoRoom(selectedItem.type, new Vector3d(90), furnitureItem.stuffData.getLegacyString()); - } - else - { - roomPreviewer.addFurnitureIntoRoom(selectedItem.type, new Vector3d(90), selectedItem.stuffData, (furnitureItem.extra.toString())); - } + roomPreviewer.addFurnitureIntoRoom(selectedItem.type, new Vector3d(90), selectedItem.stuffData, (furnitureItem.extra.toString())); } }, [ roomPreviewer, selectedItem ]); @@ -129,6 +134,15 @@ export const InventoryFurnitureView: FC<{
+ { selectedItem && + <> + + + } { selectedItem && 1024px desktop, fixed 640x600 (default rules above) - * 641px - 1024px tablet, fluid card, single-column stage - * <= 640px phone, full-screen modal with stacked layout + - * larger touch targets and a horizontally - * scrollable tab strip - * - * A separate short-landscape branch trims chrome when height <= 480px - * (typical phone-in-landscape) so the grid still has room to breathe. - * ------------------------------------------------------------------------- */ - -/* Tablet (portrait + landscape between phone and desktop). */ @media (max-width: 1024px) and (min-width: 641px) { .nitro-catalog-classic-window { width: min(calc(100vw - 24px), 720px) !important; @@ -505,8 +458,6 @@ max-height: calc(100vh - 24px) !important; } - /* Drop the sidebar to a horizontal row above the content so the grid - * has the full card width on the narrower tablet viewports. */ .nitro-catalog-classic-stage { grid-template-columns: minmax(0, 1fr); } @@ -516,7 +467,139 @@ } } -/* Phone — portrait and landscape. */ +.nitro-catalog-classic-mobile-header { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 5; + display: flex; + align-items: center; + height: 38px; + padding: 0 44px 0 8px; + pointer-events: none; +} + +.nitro-catalog-classic-mobile-burger { + position: relative; + pointer-events: auto; +} + +.nitro-catalog-classic-burger-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: 0; + border-radius: 5px; + background: rgba(0, 0, 0, 0.2); + color: #fff; + font-size: 13px; + cursor: pointer; +} + +.nitro-catalog-classic-burger-btn:hover { + background: rgba(0, 0, 0, 0.3); +} + +.nitro-catalog-classic-burger-btn:active { + background: rgba(0, 0, 0, 0.36); +} + +.nitro-catalog-classic-burger-menu { + position: absolute; + top: 32px; + left: 0; + z-index: 60; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 150px; + padding: 6px; + border: 1px solid var(--cat-line); + border-radius: 6px; + background: #fff; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); +} + +.nitro-catalog-classic-burger-menu button { + padding: 8px 10px; + border: 0; + border-radius: 4px; + background: var(--cat-strip); + color: var(--cat-ink); + font-weight: 700; + text-align: left; + cursor: pointer; +} + +.nitro-catalog-classic-burger-menu button:disabled { + opacity: 0.6; +} + +.nitro-catalog-classic-mobile-currency { + margin-left: auto; + display: flex; + align-items: center; + gap: 5px; + pointer-events: auto; +} + +.nitro-catalog-classic-coin { + display: flex; + align-items: center; + gap: 3px; + padding: 3px 7px; + border-radius: 11px; + background: rgba(0, 0, 0, 0.25); + color: #fff; + font-size: 10px; + font-weight: 700; +} + +.nitro-catalog-classic-coin span { + color: #fff; +} + +.nitro-catalog-classic-admin-tab { + display: none !important; +} + +.nitro-catalog-classic-preview-btn { + position: absolute; + top: 8px; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 10px; + border: 1px solid #555; + border-radius: 5px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + cursor: pointer; +} + +.nitro-catalog-classic-preview-btn:hover { + background: rgba(0, 0, 0, 0.82); +} + +.nitro-catalog-classic-preview-btn:active { + background: rgba(0, 0, 0, 0.9); +} + +.nitro-catalog-classic-preview-rotate { + left: 8px; +} + +.nitro-catalog-classic-preview-state { + right: 8px; +} + @media (max-width: 640px) { .nitro-catalog-classic-window { width: 100vw !important; @@ -525,13 +608,26 @@ height: 100vh !important; min-height: 0 !important; max-height: 100vh !important; - /* Drop the soft drop-shadow / outer border on full-screen so it - * blends with the viewport edges. */ border-radius: 0 !important; } - /* Tabs become a horizontally scrollable strip with bigger tap targets - * (≥ 44px tall is the WCAG / iOS recommendation). */ + .draggable-window:has(> .nitro-catalog-classic-window) { + transform: none !important; + left: 0 !important; + top: 0 !important; + } + + .nitro-catalog-classic-window .nitro-card-title { + display: none; + } + + .nitro-catalog-classic-mobile-currency { + position: absolute; + left: 50%; + margin-left: 0; + transform: translateX(-50%); + } + .nitro-catalog-classic-tabs-shell { min-height: 44px; max-height: 44px; @@ -541,25 +637,43 @@ .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { min-height: 42px; - padding: 6px 14px; + padding: 6px 12px; font-size: 12px; + justify-content: center; + } + + .nitro-catalog-classic-tab-label { + display: none; } .nitro-catalog-classic-content-shell { padding: 6px !important; } + .nitro-catalog-classic-layout-hero { + display: none; + } + + .nitro-catalog-classic-offer-panel { + flex-direction: column; + } + + .nitro-catalog-classic-offer-preview { + width: 100%; + min-width: 0; + border-right: 0; + border-bottom: 1px solid var(--cat-line); + } + .nitro-catalog-classic-stage { grid-template-columns: minmax(0, 1fr); gap: 6px; } - /* Sidebar above content, capped so most of the viewport is the grid. */ .nitro-catalog-classic-sidebar { max-height: 33vh; } - /* Search input + nav items grow into real touch targets. */ .nitro-catalog-classic-search-shell input { height: 28px; font-size: 13px; @@ -574,21 +688,16 @@ font-size: 13px; } - /* Bigger furni icons in the grid so a fingertip can hit them. */ .nitro-catalog-classic-window .layout-grid-item { height: 64px; } - /* Modal corner radius cleanups so the full-screen look is consistent. */ .nitro-catalog-classic-window .nitro-card-header-shell, .nitro-catalog-classic-window .nitro-card-content-shell { border-radius: 0 !important; } } -/* Phone in landscape — short viewport. Trim header / hero so the grid keeps - * usable height. Triggers regardless of portrait/landscape on any short - * screen, which is also the right answer for very small laptop windows. */ @media (max-height: 480px) { .nitro-catalog-classic-window { height: 100vh !important; @@ -610,8 +719,6 @@ padding: 3px 6px; } - /* Hide the marketing hero image in landscape - it's the first thing to - * sacrifice when the user clearly wants more grid. */ .nitro-catalog-classic-layout-hero { display: none; } diff --git a/src/css/inventory/InventoryView.css b/src/css/inventory/InventoryView.css index 125f8bc..01c8d77 100644 --- a/src/css/inventory/InventoryView.css +++ b/src/css/inventory/InventoryView.css @@ -1,20 +1,3 @@ -/* --------------------------------------------------------------------------- - * Inventory window — Habbo mobile inventory skin. - * - * Matches the mobile inventory mockup and stays consistent with the catalog - * redesign: blue header, beige body, folder tabs, framed preview canvas, - * teal-accented item slots, and chunky green "place" / red "sell" buttons. - * - * Palette (from the mockup): - * --inv-blue #4a7d8c header - * --inv-beige #e2e0d6 window / body / slot fill - * --inv-tab #c7c5ba inactive tab fill - * --inv-line #919088 slot / preview outline - * --inv-accent #4a7d8c selected slot glow + count text - * --inv-place* green place button - * --inv-sell* red sell button - * ------------------------------------------------------------------------- */ - .nitro-inventory-window { --inv-blue: #4a7d8c; --inv-beige: #e2e0d6; @@ -32,7 +15,6 @@ background: var(--inv-beige) !important; } -/* Blue Habbo header. */ .nitro-inventory-window .nitro-card-header { background: var(--inv-blue); border-color: var(--inv-blue); @@ -40,7 +22,6 @@ box-shadow: inset 0 2px 0 #709da9, inset 0 -2px 0 #315863; } -/* Tab strip — beige shelf with folder tabs. Light, low-contrast outlines. */ .nitro-inventory-window .nitro-inventory-tabs-shell { background: var(--inv-beige); gap: 4px; @@ -71,7 +52,6 @@ top: 1px; } -/* Tabs show the icon first, then the text label (desktop). */ .nitro-inventory-window .nitro-inventory-tab-icon { display: inline-flex; align-items: center; @@ -81,12 +61,10 @@ line-height: 1; } -/* Body — grey-beige backdrop. */ .nitro-inventory-window .nitro-inventory-body { background: var(--inv-beige); } -/* Filter bar — white search box + beige select, hard outlines. */ .nitro-inventory-window .nitro-inventory-filter-bar { background: #fff; border: 2px solid var(--inv-border); @@ -100,7 +78,6 @@ font-weight: 700; } -/* Item slots — beige tiles, grey outline, teal glow when selected. */ .nitro-inventory-window .bg-card-grid-item { background-color: var(--inv-beige) !important; } @@ -118,20 +95,17 @@ box-shadow: 0 0 0 2px var(--inv-accent); } -/* Count badge — white pill with a teal number (mockup). */ .nitro-inventory-window .bg-red-700 { background-color: #fff !important; color: var(--inv-accent) !important; border-color: #75746e !important; } -/* Preview canvas — white framed box around the room previewer. */ .nitro-inventory-window .shadow-room-previewer { background-color: #fff; border: 2px solid var(--inv-line); } -/* Action buttons — chunky beveled Habbo buttons. */ .nitro-inventory-window .nitro-inventory-btn-place, .nitro-inventory-window .nitro-inventory-btn-sell { border: 2px solid var(--inv-border) !important; @@ -159,17 +133,10 @@ background: #db4d40 !important; } -/* --------------------------------------------------------------------------- - * Responsive layout - * - * The window ships a fixed 528x420 size (Tailwind utilities on the card). - * Mirror the catalog's three tiers so it stays usable down to phones: - * > 1024px desktop, fixed size (utilities) - * 641px - 1024px tablet, fluid card - * <= 640px phone, full-screen + stacked grid / preview - * ------------------------------------------------------------------------- */ +.nitro-inventory-preview-btn { + display: none; +} -/* Tablet. */ @media (max-width: 1024px) and (min-width: 641px) { .nitro-inventory-window { width: min(calc(100vw - 24px), 640px) !important; @@ -181,25 +148,28 @@ } } -/* Phone — full-screen, single-column stack (grid on top, preview below). */ @media (max-width: 640px) { - .nitro-inventory-window { - width: 100vw !important; + .draggable-window:has(> .nitro-inventory-window) { + inset: 0 !important; + left: 0 !important; + right: 0 !important; + top: 0 !important; + bottom: 0 !important; + width: auto !important; + height: auto !important; + transform: none !important; + } + + .nitro-card-shell.nitro-inventory-window { + width: calc(100% - 40px) !important; min-width: 0 !important; - max-width: 100vw !important; - height: 100vh !important; + max-width: calc(100% - 40px) !important; + height: calc(100% - 76px) !important; min-height: 0 !important; - max-height: 100vh !important; - border-radius: 0 !important; + max-height: calc(100% - 76px) !important; + margin: 8px auto 0 auto !important; } - .nitro-inventory-window .nitro-card-header-shell, - .nitro-inventory-window .nitro-card-content-shell { - border-radius: 0 !important; - } - - /* Icon-only tabs spread evenly across the width — text labels don't - * fit on a phone, so swap them for the per-tab glyph. */ .nitro-inventory-window .nitro-inventory-tabs-shell { flex-wrap: nowrap; overflow: hidden; @@ -221,7 +191,6 @@ font-size: 17px; } - /* Stack the item grid above the preview / action panel. */ .nitro-inventory-window .grid.grid-cols-12 { grid-template-columns: minmax(0, 1fr) !important; grid-template-rows: minmax(0, 1fr) auto !important; @@ -232,10 +201,38 @@ grid-column: 1 / -1 !important; } - /* Roomier touch targets for the action buttons. */ .nitro-inventory-window .nitro-inventory-btn-place, .nitro-inventory-window .nitro-inventory-btn-sell { padding-top: 0.5rem; padding-bottom: 0.5rem; } + + .nitro-inventory-window .nitro-inventory-preview-btn { + position: absolute; + top: 6px; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 8px; + border: 1px solid #555; + border-radius: 5px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 10px; + font-weight: 700; + cursor: pointer; + } + + .nitro-inventory-window .nitro-inventory-preview-btn:active { + background: rgba(0, 0, 0, 0.85); + } + + .nitro-inventory-window .nitro-inventory-preview-rotate { + left: 6px; + } + + .nitro-inventory-window .nitro-inventory-preview-state { + right: 6px; + } } diff --git a/src/css/nitrocard/NitroCardView.css b/src/css/nitrocard/NitroCardView.css index 296e8b7..0f37279 100644 --- a/src/css/nitrocard/NitroCardView.css +++ b/src/css/nitrocard/NitroCardView.css @@ -212,3 +212,11 @@ } } } + +@media (max-width: 640px) { + .draggable-window { + left: 50% !important; + top: 50% !important; + transform: translate(-50%, -50%) !important; + } +} From 74747b8aad3e7132b6b7fc8736ae12038d329097 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 16:18:01 +0200 Subject: [PATCH 06/24] =?UTF-8?q?=F0=9F=86=99=20fix=20the=20header=20for?= =?UTF-8?q?=20catalogue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/css/catalog/CatalogClassicView.css | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 498fa9c..48a6100 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -245,6 +245,7 @@ display: flex; flex-direction: column; gap: 3px; + flex-shrink: 0; min-height: 0; padding: 6px 8px; border-bottom: 1px solid var(--cat-line); @@ -262,10 +263,10 @@ .nitro-catalog-classic-layout-hero img { display: block; - width: 100%; - max-width: 100%; + width: auto; height: auto; - max-height: 80px; + max-width: 100%; + max-height: none; object-fit: contain; } @@ -281,7 +282,6 @@ gap: 8px; } -/* Offer / detail card — the mockup's preview + info panel. */ .nitro-catalog-classic-offer-panel, .nitro-catalog-classic-welcome { border: 1px solid var(--cat-line); @@ -372,21 +372,7 @@ } .nitro-catalog-classic-window .nitro-catalog-header { - justify-content: flex-start; - min-height: 56px; - margin-bottom: 6px; - padding: 4px 6px; - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #ffffff; -} - -.nitro-catalog-classic-window .nitro-catalog-header img { - max-width: 100%; - max-height: 48px; - width: auto; - height: auto; - object-fit: contain; + display: none; } .nitro-catalog-classic-offer-info .bg-\[\#00800b\] { @@ -417,7 +403,6 @@ color: #94a7b3; } -/* Scrollbars — blue-grey to match the chrome. */ .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { width: 12px; From dc0a8f965e7fcea0b5ec5ba4083ddf0e9a3dfdca Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 23:36:06 +0200 Subject: [PATCH 07/24] feat(catalog): toggle stile catalogo classico/moderno Aggiunge un checkbox nelle impostazioni utente per scegliere lo stile del catalogo (classico vs moderno) + flag globale catalog.classic.style in ui-config.json come default per tutti. Override per-utente in localStorage. --- public/configuration/ui-config.example | 1 + src/api/utils/LocalStorageKeys.ts | 1 + src/components/catalog/CatalogClassicView.tsx | 8 +++++--- src/components/user-settings/UserSettingsView.tsx | 7 ++++++- src/hooks/catalog/index.ts | 1 + src/hooks/catalog/useCatalogClassicStyle.ts | 14 ++++++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/hooks/catalog/useCatalogClassicStyle.ts diff --git a/public/configuration/ui-config.example b/public/configuration/ui-config.example index e65c501..796ac12 100644 --- a/public/configuration/ui-config.example +++ b/public/configuration/ui-config.example @@ -28,6 +28,7 @@ "housekeeping.enabled": true, "toolbar.hide.quests": true, "show.google.ads": false, + "catalog.classic.style": false, "loginview": { "images": { "background": "${asset.url}/c_images/reception/stretch_blue.png", diff --git a/src/api/utils/LocalStorageKeys.ts b/src/api/utils/LocalStorageKeys.ts index 847f3aa..75ecfe8 100644 --- a/src/api/utils/LocalStorageKeys.ts +++ b/src/api/utils/LocalStorageKeys.ts @@ -4,4 +4,5 @@ export class LocalStorageKeys public static CATALOG_SKIP_PURCHASE_CONFIRMATION: string = 'catalogSkipPurchaseConfirmation'; public static CHAT_WINDOW_ENABLED: string = 'chatWindowEnabled'; public static CHAT_TRANSLATION_SETTINGS: string = 'chatTranslationSettings'; + public static CATALOG_CLASSIC_STYLE: string = 'catalogClassicStyle'; } diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index 1ee04f2..7153c22 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useState } from 'react'; import { FaBars, FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; import { CatalogType, GetConfigurationValue, LocalizeShortNumber, LocalizeText } from '../../api'; import { Column, Grid, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission, usePurse } from '../../hooks'; +import { useCatalogActions, useCatalogClassicStyle, useCatalogData, useCatalogUiState, useHasPermission, usePurse } from '../../hooks'; import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; @@ -31,6 +31,7 @@ const CatalogClassicViewInner: FC<{}> = () => const loading = catalogAdmin?.loading ?? false; const isMod = useHasPermission('acc_catalogfurni'); + const [ catalogClassicStyle ] = useCatalogClassicStyle(); const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); const { purse = null } = usePurse(); const displayedCurrencies = GetConfigurationValue('system.currency.types', []); @@ -122,8 +123,9 @@ const CatalogClassicViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } /> + { !catalogClassicStyle &&
{ isMod &&
@@ -159,7 +161,7 @@ const CatalogClassicViewInner: FC<{}> = () =>
)) }
-
+
} { adminMode &&
Admin Mode diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 8857e1a..999ac3b 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -3,7 +3,7 @@ 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 { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; +import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; import { classNames } from '../../layout'; export const UserSettingsView: FC<{}> = props => @@ -13,6 +13,7 @@ export const UserSettingsView: FC<{}> = props => const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); const [ chatWindowEnabled, setChatWindowEnabled ] = useChatWindow(); + const [ catalogClassicStyle, setCatalogClassicStyle ] = useCatalogClassicStyle(); const processAction = (type: string, value?: boolean | number | string) => { @@ -156,6 +157,10 @@ export const UserSettingsView: FC<{}> = props => setChatWindowEnabled(event.target.checked) } /> Enable chat window
+
+ setCatalogClassicStyle(event.target.checked) } /> + Catalogo: stile classico +
{ LocalizeText('widget.memenu.settings.volume') } diff --git a/src/hooks/catalog/index.ts b/src/hooks/catalog/index.ts index 2a817fa..1b4b9bc 100644 --- a/src/hooks/catalog/index.ts +++ b/src/hooks/catalog/index.ts @@ -1,4 +1,5 @@ export * from './useCatalog'; +export * from './useCatalogClassicStyle'; export * from './useCatalogFavorites'; export * from './useCatalogPlaceMultipleItems'; export * from './useCatalogSkipPurchaseConfirmation'; diff --git a/src/hooks/catalog/useCatalogClassicStyle.ts b/src/hooks/catalog/useCatalogClassicStyle.ts new file mode 100644 index 0000000..a239a60 --- /dev/null +++ b/src/hooks/catalog/useCatalogClassicStyle.ts @@ -0,0 +1,14 @@ +import { useBetween } from 'use-between'; +import { GetConfigurationValue, LocalStorageKeys } from '../../api'; +import { useLocalStorage } from '../useLocalStorage'; + +// Per-user toggle for the catalog visual style. +// - true => classic (old) catalog look +// - false => modern (rebuilt) catalog look +// The default for users who never touched the toggle comes from the global +// `catalog.classic.style` flag in ui-config.json, so an admin can flip the +// default for everyone (true = classic for all, false = modern for all) +// while still letting each user override it from the settings panel. +const useCatalogClassicStyleState = () => useLocalStorage(LocalStorageKeys.CATALOG_CLASSIC_STYLE, GetConfigurationValue('catalog.classic.style', false)); + +export const useCatalogClassicStyle = () => useBetween(useCatalogClassicStyleState); From 32dcbaf265146d985b3168ce06dfc0da8c32035b Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 23:40:37 +0200 Subject: [PATCH 08/24] feat(catalog): stile classico completo via skin legacy Ripristina il look del catalogo pre-merge come CSS scoped sotto body.catalog-skin-legacy (CatalogClassicLegacy.css), attivato dal toggle 'stile classico'. CatalogView mette/toglie la classe sul body. --- src/components/catalog/CatalogView.tsx | 15 +- src/css/catalog/CatalogClassicLegacy.css | 354 +++++++++++++++++++++++ src/index.tsx | 1 + 3 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 src/css/catalog/CatalogClassicLegacy.css diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index fbc700e..2859f24 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -1,10 +1,21 @@ -import { FC } from 'react'; -import { useCatalogData } from '../../hooks'; +import { FC, useEffect } from 'react'; +import { useCatalogClassicStyle, useCatalogData } from '../../hooks'; import { CatalogClassicView } from './CatalogClassicView'; export const CatalogView: FC<{}> = () => { const { catalogLocalizationVersion = 0 } = useCatalogData(); + const [ catalogClassicStyle ] = useCatalogClassicStyle(); + + // Toggle the legacy-skin marker on so the scoped overrides in + // CatalogClassicLegacy.css (the pre-merge catalog look) take effect for + // every catalog element without touching the modern stylesheet. + useEffect(() => + { + document.body.classList.toggle('catalog-skin-legacy', !!catalogClassicStyle); + + return () => document.body.classList.remove('catalog-skin-legacy'); + }, [ catalogClassicStyle ]); return ( <> diff --git a/src/css/catalog/CatalogClassicLegacy.css b/src/css/catalog/CatalogClassicLegacy.css new file mode 100644 index 0000000..db5c6ef --- /dev/null +++ b/src/css/catalog/CatalogClassicLegacy.css @@ -0,0 +1,354 @@ +/* Auto-generated: pre-merge catalog CSS scoped under .catalog-skin-legacy (classic style toggle). Source: CatalogClassicView.css @ b360595. Do not edit by hand. */ +body.catalog-skin-legacy .nitro-catalog-classic-window { + width: 640px !important; + height: 600px !important; + max-width: 640px !important; + min-width: 640px !important; + min-height: 600px !important; + max-height: 600px !important; +} +body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-card-title { + font-size: 18px; + letter-spacing: 0.2px; +} +body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 38px; + max-height: 38px; +} +body.catalog-skin-legacy .nitro-catalog-classic-admin-banner { + border-bottom: 1px solid rgba(0, 0, 0, 0.18); + background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell { + flex-wrap: nowrap; + gap: 1px; + min-height: 30px; + max-height: 30px; + padding: 0 6px; + overflow-x: auto; + overflow-y: hidden; + align-items: end; + background: #e7e8df; + border-bottom: 1px solid #b8beb4; +} +body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { + min-height: 28px; + padding: 5px 10px 4px; + border: 1px solid #8f8f8b; + border-bottom: 0; + border-radius: 5px 5px 0 0; + background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + white-space: nowrap; +} +body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { + background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { + background: #f2f2eb; + transform: translateY(0); + position: relative; + top: 1px; +} +body.catalog-skin-legacy .nitro-catalog-classic-content-shell { + padding: 6px 8px 8px !important; +} +body.catalog-skin-legacy .nitro-catalog-classic-stage { + display: grid; + grid-template-columns: 196px minmax(0, 1fr); + gap: 8px; + min-height: 0; + height: 100%; +} +body.catalog-skin-legacy .nitro-catalog-classic-stage.is-navigation-hidden { + grid-template-columns: minmax(0, 1fr); +} +body.catalog-skin-legacy .nitro-catalog-classic-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + height: 100%; +} +body.catalog-skin-legacy .nitro-catalog-classic-search-shell { + padding: 3px; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-search-shell input { + height: 18px; + padding-top: 0 !important; + padding-bottom: 0 !important; + border-width: 1px !important; + border-color: #8f9588 !important; + border-radius: 3px !important; + background: #fff !important; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); +} +body.catalog-skin-legacy .nitro-catalog-classic-search-shell svg { + color: #61645b !important; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell { + flex: 1 1 auto; + min-height: 0; + padding: 3px 2px 3px 3px; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); + overflow: auto; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-list { + display: flex; + flex-direction: column; + gap: 2px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-node.is-child { + margin-left: 10px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-item { + display: flex; + align-items: center; + gap: 4px; + min-height: 21px; + padding: 1px 6px 1px 5px; + border: 1px solid #bdc2ba; + border-radius: 4px; + background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); + color: #2e2e2e; + cursor: pointer; + transition: background-color 0.12s ease, border-color 0.12s ease; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-item:hover { + background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); + border-color: #9ea79b; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-item.is-active { + background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); + border-color: #8e9ba5; + font-weight: 700; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-item.is-drag-over { + outline: 2px solid rgba(48, 114, 140, 0.35); + outline-offset: 1px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + min-width: 18px; + height: 18px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon img, +body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon canvas { + width: auto; + height: auto; + max-width: 18px; + max-height: 18px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-label { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + line-height: 1; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-caret, +body.catalog-skin-legacy .nitro-catalog-classic-navigation-favorite, +body.catalog-skin-legacy .nitro-catalog-classic-navigation-admin, +body.catalog-skin-legacy .nitro-catalog-classic-navigation-drag { + flex-shrink: 0; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-caret { + color: #676d66 !important; +} +body.catalog-skin-legacy .nitro-catalog-classic-layout-shell { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); + overflow: hidden; +} +body.catalog-skin-legacy .nitro-catalog-classic-layout-header-shell { + display: flex; + flex-direction: column; + gap: 3px; + min-height: 66px; + padding: 5px 7px; + border-bottom: 1px solid #c8cdc3; + background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-layout-hero { + display: flex; + align-items: center; + justify-content: center; + flex: 1 1 auto; + min-height: 32px; + overflow: hidden; +} +body.catalog-skin-legacy .nitro-catalog-classic-layout-hero img { + max-width: 100%; + max-height: 32px; + width: auto; + height: auto; + object-fit: contain; +} +body.catalog-skin-legacy .nitro-catalog-classic-layout-container { + flex: 1 1 auto; + min-height: 0; + padding: 6px; + background: #f2f2eb; + overflow: hidden; +} +body.catalog-skin-legacy .nitro-catalog-classic-default-layout { + gap: 8px; +} +body.catalog-skin-legacy .nitro-catalog-classic-offer-panel, +body.catalog-skin-legacy .nitro-catalog-classic-welcome { + border: 1px solid #bfc4bc; + border-radius: 6px; + background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); +} +body.catalog-skin-legacy .nitro-catalog-classic-offer-preview { + width: 136px; + min-width: 136px; + padding: 8px; + border-right: 1px solid #c9cec5; + background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-offer-info { + padding: 10px; +} +body.catalog-skin-legacy .nitro-catalog-classic-welcome { + min-height: 128px; + padding: 10px; +} +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell { + min-height: 150px; + padding: 4px; + border: 1px solid #bcc2b8; + border-radius: 6px; + background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); + height: auto; + flex: 1 1 auto; +} +body.catalog-skin-legacy .nitro-catalog-classic-grid { + gap: 4px !important; + align-content: start; +} +body.catalog-skin-legacy .nitro-catalog-classic-window .layout-grid-item { + height: 54px; + border: 1px solid #b8beb6 !important; + border-radius: 6px !important; + background-color: #d7dde2; + background-image: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); +} +body.catalog-skin-legacy .nitro-catalog-classic-window .layout-grid-item.is-active { + background-color: #e5ebef !important; + border-color: #8f978b !important; +} +body.catalog-skin-legacy .nitro-catalog-classic-grid-offer-icon { + position: absolute; + inset: 4px; + background-repeat: no-repeat; + background-position: center; + pointer-events: none; +} +body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-catalog-header { + justify-content: flex-start; + min-height: 56px; + margin-bottom: 6px; + padding: 4px 6px; + border: 1px solid #bec3ba; + border-radius: 6px; + background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-catalog-header img { + max-width: 100%; + max-height: 48px; + width: auto; + height: auto; + object-fit: contain; +} +body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb { + display: flex; + align-items: center; + gap: 5px; + min-height: 16px; + overflow: hidden; + color: #666a63; + font-size: 10px; + line-height: 1; + white-space: nowrap; +} +body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb-segment { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; +} +body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb-separator { + color: #9ea395; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, +body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, +body.catalog-skin-legacy .nitro-card-content-shell, +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { + width: 12px; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, +body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, +body.catalog-skin-legacy .nitro-card-content-shell, +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { + border-left: 1px solid #c2c6be; + background: #dde2d8; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, +body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, +body.catalog-skin-legacy .nitro-card-content-shell, +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { + border: 1px solid #7d8680; + border-radius: 6px; + background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, +body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, +body.catalog-skin-legacy .nitro-card-content-shell, +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { + height: 12px; + background: #dde2d8; +} +body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, +body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, +body.catalog-skin-legacy .nitro-card-content-shell, +body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { + height: 12px; + background: #dde2d8; +} +@media (max-width: 991.98px) { +body.catalog-skin-legacy .nitro-catalog-classic-window { + width: min(calc(100vw - 16px), 570px) !important; + min-width: 0 !important; + height: min(calc(100vh - 16px), 635px) !important; + min-height: 0 !important; + max-width: calc(100vw - 16px) !important; + } +body.catalog-skin-legacy .nitro-catalog-classic-stage { + grid-template-columns: minmax(0, 1fr); + } +body.catalog-skin-legacy .nitro-catalog-classic-sidebar { + max-height: 180px; + } + + +} + diff --git a/src/index.tsx b/src/index.tsx index 3825168..a9dacf1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,6 +20,7 @@ import './css/index.css'; import './css/backgrounds/BackgroundsView.css'; import './css/badges/BadgeLeaderboardView.css'; import './css/catalog/CatalogClassicView.css'; +import './css/catalog/CatalogClassicLegacy.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; From 2b8aca23b6f8fe1645d679f2bf869155d50917b6 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 23:49:10 +0200 Subject: [PATCH 09/24] revert(catalog): ripristina catalogo Hippiehotel.nl (Modern+Classic) Rimpiazza il catalogo del rebuild upstream con quello originale di Hippiehotel.nl Nitro-V3 (CatalogModernView ripristinato, ClassicView/ sub-views/CSS pre-merge). CatalogView sceglie Modern (default) o Classic via il toggle 'stile classico'. Rimosso l'hack CatalogClassicLegacy.css. --- src/components/catalog/CatalogClassicView.tsx | 57 +- src/components/catalog/CatalogModernView.tsx | 331 ++++++++++++ src/components/catalog/CatalogView.tsx | 25 +- .../page/layout/CatalogLayoutDefaultView.tsx | 25 +- .../page/layout/CatalogLayoutTrophiesView.tsx | 8 +- .../widgets/CatalogPurchaseWidgetView.tsx | 2 +- .../widgets/CatalogViewProductWidgetView.tsx | 4 - src/css/catalog/CatalogClassicLegacy.css | 354 ------------ src/css/catalog/CatalogClassicView.css | 510 ++++-------------- src/index.tsx | 1 - 10 files changed, 465 insertions(+), 852 deletions(-) create mode 100644 src/components/catalog/CatalogModernView.tsx delete mode 100644 src/css/catalog/CatalogClassicLegacy.css diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index 7153c22..a3f2a73 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -1,9 +1,9 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { FaBars, FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; -import { CatalogType, GetConfigurationValue, LocalizeShortNumber, LocalizeText } from '../../api'; -import { Column, Grid, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useCatalogActions, useCatalogClassicStyle, useCatalogData, useCatalogUiState, useHasPermission, usePurse } from '../../hooks'; +import { FC, useEffect } from 'react'; +import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; +import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api'; +import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks'; import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; @@ -31,10 +31,6 @@ const CatalogClassicViewInner: FC<{}> = () => const loading = catalogAdmin?.loading ?? false; const isMod = useHasPermission('acc_catalogfurni'); - const [ catalogClassicStyle ] = useCatalogClassicStyle(); - const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); - const { purse = null } = usePurse(); - const displayedCurrencies = GetConfigurationValue('system.currency.types', []); const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; @@ -123,45 +119,8 @@ const CatalogClassicViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } /> - { !catalogClassicStyle && -
- { isMod && -
- - { mobileMenuOpen && -
- - { adminMode && - } -
} -
} -
-
- { LocalizeShortNumber(purse?.credits ?? 0) } - -
- { displayedCurrencies.map(type => ( -
- { LocalizeShortNumber(purse?.activityPoints?.get(type) ?? 0) } - -
- )) } -
-
} { adminMode &&
Admin Mode @@ -189,7 +148,7 @@ const CatalogClassicViewInner: FC<{}> = () => } }>
- { child.localization } + { child.localization } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -213,7 +172,7 @@ const CatalogClassicViewInner: FC<{}> = () => ); }) } { isMod && - setAdminMode(!adminMode) }> + setAdminMode(!adminMode) }> } diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx new file mode 100644 index 0000000..1c4af9a --- /dev/null +++ b/src/components/catalog/CatalogModernView.tsx @@ -0,0 +1,331 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa'; +import { CatalogType, LocalizeText } from '../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { useCatalogActions, useCatalogData, useCatalogFavorites, useCatalogUiState, useHasPermission } from '../../hooks'; +import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; +import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; +import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; +import { CatalogBuildersClubStatusView } from './views/catalog-header/CatalogBuildersClubStatusView'; +import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; +import { CatalogFavoritesView } from './views/favorites/CatalogFavoritesView'; +import { CatalogGiftView } from './views/gift/CatalogGiftView'; +import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; +import { CatalogSearchView } from './views/page/common/CatalogSearchView'; +import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; +import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; + +const CatalogModernViewInner: FC<{}> = () => +{ + const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData(); + const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState(); + const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; + const setAdminMode = catalogAdmin?.setAdminMode ?? (() => + {}); + const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; + const publishCatalog = catalogAdmin?.publishCatalog ?? (() => + {}); + const loading = catalogAdmin?.loading ?? false; + const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites(); + const [ showFavorites, setShowFavorites ] = useState(false); + + const isMod = useHasPermission('acc_catalogfurni'); + const totalFavs = favoriteOfferIds.length + favoritePageIds.length; + const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) + ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } + : undefined; + + useEffect(() => + { + const getCatalogTypeFromLink = (type?: string) => + { + switch((type || '').toLowerCase()) + { + case 'bc': + case 'builder': + case 'buildersclub': + case 'builders_club': + return CatalogType.BUILDER; + default: + return CatalogType.NORMAL; + } + }; + + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + if(parts.length > 2) + { + openCatalogByType(getCatalogTypeFromLink(parts[2])); + + return; + } + + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + if(parts.length > 2) + { + toggleCatalogByType(getCatalogTypeFromLink(parts[2])); + + return; + } + + setIsVisible(prevValue => !prevValue); + return; + case 'open': + if(parts.length > 2) + { + if(parts.length === 4) + { + switch(parts[2]) + { + case 'offerId': + openPageByOfferId(parseInt(parts[3])); + return; + } + } + else + { + openPageByName(parts[2]); + } + } + else + { + setIsVisible(true); + } + + return; + } + }, + eventUrlPrefix: 'catalog/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]); + + return ( + <> + { isVisible && + + setIsVisible(false) } style={ buildersClubHeaderStyle } /> + + { /* Admin banner */ } + { adminMode && +
+ ⚙ Admin Mode + +
} + + +
+ { /* === LEFT SIDEBAR === */ } +
+ + { /* Favorites toggle */ } +
setShowFavorites(!showFavorites) } + > +
+ 0 ? 'text-danger' : 'text-muted' }` } /> + { totalFavs > 0 && + + { totalFavs } + } +
+ { LocalizeText('catalog.favorites') } +
+ +
+ + { /* Admin: root page actions */ } + { adminMode && rootNode && +
+ + +
} + + { /* Category icons */ } + { rootNode && rootNode.children.length > 0 && rootNode.children.map((child, index) => + { + if(!adminMode && !child.isVisible) return null; + + const isHidden = !child.isVisible; + + return ( +
+ { + if(searchResult) setSearchResult(null); + if(showFavorites) setShowFavorites(false); + activateNode(child); + } } + > +
+ + { isHidden && } +
+ + { child.localization } + + { /* Admin actions on each root category */ } + { adminMode && +
+
+ { + e.stopPropagation(); + catalogAdmin.setEditingPageNode(child); + catalogAdmin.setEditingRootPage(false); + catalogAdmin.setEditingPageData(true); + } } + > + +
+
+ { + e.stopPropagation(); + catalogAdmin.togglePageVisible(child.pageId); + } } + > + { isHidden + ? + : } +
+
+ { + e.stopPropagation(); + if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) + { + catalogAdmin.deletePage(child.pageId); + } + } } + > + +
+
} +
+ ); + }) } +
+ + { /* === MAIN AREA === */ } +
+ { /* Toolbar: search + admin */ } +
+ { /* Breadcrumb */ } +
+ + { activeNodes && activeNodes.length > 0 + ? activeNodes.map((node, i) => ( + + { i > 0 && } + activateNode(node) : undefined }> + { node.localization } + + + )) + : { LocalizeText('catalog.title') } } +
+ +
+ +
+ + { isMod && + } +
+ + { /* Content area */ } +
+ { showFavorites + ?
+ setShowFavorites(false) } /> +
+ : <> + { !navigationHidden && activeNodes && activeNodes.length > 0 && +
+ +
} +
+ { adminMode && } + { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } +
+ } +
+
+
+ + } + + + + + ); +}; + +export const CatalogModernView: FC<{}> = () => +{ + return ( + + + + ); +}; diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index 2859f24..a1d2d8b 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -1,26 +1,27 @@ -import { FC, useEffect } from 'react'; +import { FC } from 'react'; import { useCatalogClassicStyle, useCatalogData } from '../../hooks'; import { CatalogClassicView } from './CatalogClassicView'; +import { CatalogModernView } from './CatalogModernView'; export const CatalogView: FC<{}> = () => { const { catalogLocalizationVersion = 0 } = useCatalogData(); const [ catalogClassicStyle ] = useCatalogClassicStyle(); - // Toggle the legacy-skin marker on so the scoped overrides in - // CatalogClassicLegacy.css (the pre-merge catalog look) take effect for - // every catalog element without touching the modern stylesheet. - useEffect(() => - { - document.body.classList.toggle('catalog-skin-legacy', !!catalogClassicStyle); - - return () => document.body.classList.remove('catalog-skin-legacy'); - }, [ catalogClassicStyle ]); - - return ( + // Modern (Hippiehotel style) is the default; the "stile classico" toggle in + // user settings (or the global catalog.classic.style flag) switches to the + // classic catalog. Both views are the Hippiehotel.nl Nitro-V3 originals. + if(catalogClassicStyle) return ( <>
); + + return ( + <> +
+ + + ); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 09411d1..3490e60 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { FaEdit, FaPlus, FaPowerOff, FaSyncAlt } from 'react-icons/fa'; +import { FaEdit, FaPlus } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalogData } from '../../../../../hooks'; @@ -17,12 +17,13 @@ import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutDefaultView: FC = props => { const { page = null } = props; - const { currentOffer = null, currentPage = null, roomPreviewer = null } = useCatalogData(); + const { currentOffer = null, currentPage = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const adminMode = catalogAdmin?.adminMode ?? false; return (
+ { /* Admin: quick actions */ } { adminMode && !catalogAdmin.editingPageData &&
} + + { /* Product detail card */ } { currentOffer && -
+
+ { /* Preview area */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) && <> - - } { (currentOffer.product.productType === ProductTypeEnum.BADGE) && }
+ { /* Product info + purchase */ }
+ { /* Title row */ }
{ currentOffer.localizationName } @@ -77,16 +77,19 @@ export const CatalogLayoutDefaultView: FC = props =>
}
+ { /* Price */ } + { /* Spinner */ } -
+ { /* Actions */ } +
} { !currentOffer && -
+
{ !!page.localization.getImage(1) && } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx index 6bc187c..31299a0 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx @@ -58,11 +58,9 @@ export const CatalogLayoutTrophiesView: FC = props =>
} - { /* Selected trophy card. shrink-0 + no overflow-hidden so the - Buy button stays inside the panel even when the grid below - holds many trophies. */ } + { /* Selected trophy card */ } { currentOffer - ?
+ ?
{ /* Preview */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) @@ -92,7 +90,7 @@ export const CatalogLayoutTrophiesView: FC = props => { !canPurchase && { LocalizeText('catalog.trophies.write.hint') } } -
+
diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index e04cac0..5fe8ef8 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -240,7 +240,7 @@ export const CatalogPurchaseWidgetView: FC = pro return ; case CatalogPurchaseState.NONE: default: - return ; + return ; } }; diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index f63f721..027aa36 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -19,8 +19,6 @@ export const CatalogViewProductWidgetView: FC<{}> = props => if(!product) return; roomPreviewer.reset(false); - roomPreviewer.updateObjectRoom('default', 'default', 'default'); - roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); switch(product.productType) { @@ -70,8 +68,6 @@ export const CatalogViewProductWidgetView: FC<{}> = props => case ProductTypeEnum.WALL: { if(!product.furnitureData) return; - roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - switch(product.furnitureData.specialType) { case FurniCategory.FLOOR: diff --git a/src/css/catalog/CatalogClassicLegacy.css b/src/css/catalog/CatalogClassicLegacy.css deleted file mode 100644 index db5c6ef..0000000 --- a/src/css/catalog/CatalogClassicLegacy.css +++ /dev/null @@ -1,354 +0,0 @@ -/* Auto-generated: pre-merge catalog CSS scoped under .catalog-skin-legacy (classic style toggle). Source: CatalogClassicView.css @ b360595. Do not edit by hand. */ -body.catalog-skin-legacy .nitro-catalog-classic-window { - width: 640px !important; - height: 600px !important; - max-width: 640px !important; - min-width: 640px !important; - min-height: 600px !important; - max-height: 600px !important; -} -body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-card-title { - font-size: 18px; - letter-spacing: 0.2px; -} -body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-card-header-shell { - min-height: 38px; - max-height: 38px; -} -body.catalog-skin-legacy .nitro-catalog-classic-admin-banner { - border-bottom: 1px solid rgba(0, 0, 0, 0.18); - background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell { - flex-wrap: nowrap; - gap: 1px; - min-height: 30px; - max-height: 30px; - padding: 0 6px; - overflow-x: auto; - overflow-y: hidden; - align-items: end; - background: #e7e8df; - border-bottom: 1px solid #b8beb4; -} -body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 28px; - padding: 5px 10px 4px; - border: 1px solid #8f8f8b; - border-bottom: 0; - border-radius: 5px 5px 0 0; - background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); - white-space: nowrap; -} -body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #f2f2eb; - transform: translateY(0); - position: relative; - top: 1px; -} -body.catalog-skin-legacy .nitro-catalog-classic-content-shell { - padding: 6px 8px 8px !important; -} -body.catalog-skin-legacy .nitro-catalog-classic-stage { - display: grid; - grid-template-columns: 196px minmax(0, 1fr); - gap: 8px; - min-height: 0; - height: 100%; -} -body.catalog-skin-legacy .nitro-catalog-classic-stage.is-navigation-hidden { - grid-template-columns: minmax(0, 1fr); -} -body.catalog-skin-legacy .nitro-catalog-classic-sidebar { - display: flex; - flex-direction: column; - gap: 4px; - min-height: 0; - height: 100%; -} -body.catalog-skin-legacy .nitro-catalog-classic-search-shell { - padding: 3px; - border: 1px solid #a7aba1; - border-radius: 4px; - background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-search-shell input { - height: 18px; - padding-top: 0 !important; - padding-bottom: 0 !important; - border-width: 1px !important; - border-color: #8f9588 !important; - border-radius: 3px !important; - background: #fff !important; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); -} -body.catalog-skin-legacy .nitro-catalog-classic-search-shell svg { - color: #61645b !important; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell { - flex: 1 1 auto; - min-height: 0; - padding: 3px 2px 3px 3px; - border: 1px solid #a7aba1; - border-radius: 4px; - background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); - overflow: auto; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-list { - display: flex; - flex-direction: column; - gap: 2px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-node.is-child { - margin-left: 10px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-item { - display: flex; - align-items: center; - gap: 4px; - min-height: 21px; - padding: 1px 6px 1px 5px; - border: 1px solid #bdc2ba; - border-radius: 4px; - background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); - color: #2e2e2e; - cursor: pointer; - transition: background-color 0.12s ease, border-color 0.12s ease; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); - border-color: #9ea79b; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-item.is-active { - background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); - border-color: #8e9ba5; - font-weight: 700; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(48, 114, 140, 0.35); - outline-offset: 1px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon { - display: flex; - align-items: center; - justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon img, -body.catalog-skin-legacy .nitro-catalog-classic-navigation-icon canvas { - width: auto; - height: auto; - max-width: 18px; - max-height: 18px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-label { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 11px; - line-height: 1; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-caret, -body.catalog-skin-legacy .nitro-catalog-classic-navigation-favorite, -body.catalog-skin-legacy .nitro-catalog-classic-navigation-admin, -body.catalog-skin-legacy .nitro-catalog-classic-navigation-drag { - flex-shrink: 0; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-caret { - color: #676d66 !important; -} -body.catalog-skin-legacy .nitro-catalog-classic-layout-shell { - display: flex; - flex-direction: column; - min-width: 0; - min-height: 0; - height: 100%; - border: 1px solid #a7aba1; - border-radius: 4px; - background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); - overflow: hidden; -} -body.catalog-skin-legacy .nitro-catalog-classic-layout-header-shell { - display: flex; - flex-direction: column; - gap: 3px; - min-height: 66px; - padding: 5px 7px; - border-bottom: 1px solid #c8cdc3; - background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-layout-hero { - display: flex; - align-items: center; - justify-content: center; - flex: 1 1 auto; - min-height: 32px; - overflow: hidden; -} -body.catalog-skin-legacy .nitro-catalog-classic-layout-hero img { - max-width: 100%; - max-height: 32px; - width: auto; - height: auto; - object-fit: contain; -} -body.catalog-skin-legacy .nitro-catalog-classic-layout-container { - flex: 1 1 auto; - min-height: 0; - padding: 6px; - background: #f2f2eb; - overflow: hidden; -} -body.catalog-skin-legacy .nitro-catalog-classic-default-layout { - gap: 8px; -} -body.catalog-skin-legacy .nitro-catalog-classic-offer-panel, -body.catalog-skin-legacy .nitro-catalog-classic-welcome { - border: 1px solid #bfc4bc; - border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); -} -body.catalog-skin-legacy .nitro-catalog-classic-offer-preview { - width: 136px; - min-width: 136px; - padding: 8px; - border-right: 1px solid #c9cec5; - background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-offer-info { - padding: 10px; -} -body.catalog-skin-legacy .nitro-catalog-classic-welcome { - min-height: 128px; - padding: 10px; -} -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell { - min-height: 150px; - padding: 4px; - border: 1px solid #bcc2b8; - border-radius: 6px; - background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); - height: auto; - flex: 1 1 auto; -} -body.catalog-skin-legacy .nitro-catalog-classic-grid { - gap: 4px !important; - align-content: start; -} -body.catalog-skin-legacy .nitro-catalog-classic-window .layout-grid-item { - height: 54px; - border: 1px solid #b8beb6 !important; - border-radius: 6px !important; - background-color: #d7dde2; - background-image: none; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); -} -body.catalog-skin-legacy .nitro-catalog-classic-window .layout-grid-item.is-active { - background-color: #e5ebef !important; - border-color: #8f978b !important; -} -body.catalog-skin-legacy .nitro-catalog-classic-grid-offer-icon { - position: absolute; - inset: 4px; - background-repeat: no-repeat; - background-position: center; - pointer-events: none; -} -body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-catalog-header { - justify-content: flex-start; - min-height: 56px; - margin-bottom: 6px; - padding: 4px 6px; - border: 1px solid #bec3ba; - border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-window .nitro-catalog-header img { - max-width: 100%; - max-height: 48px; - width: auto; - height: auto; - object-fit: contain; -} -body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb { - display: flex; - align-items: center; - gap: 5px; - min-height: 16px; - overflow: hidden; - color: #666a63; - font-size: 10px; - line-height: 1; - white-space: nowrap; -} -body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb-segment { - display: inline-flex; - align-items: center; - gap: 5px; - min-width: 0; -} -body.catalog-skin-legacy .nitro-catalog-classic-breadcrumb-separator { - color: #9ea395; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, -body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, -body.catalog-skin-legacy .nitro-card-content-shell, -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { - width: 12px; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, -body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, -body.catalog-skin-legacy .nitro-card-content-shell, -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { - border-left: 1px solid #c2c6be; - background: #dde2d8; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, -body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, -body.catalog-skin-legacy .nitro-card-content-shell, -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { - border: 1px solid #7d8680; - border-radius: 6px; - background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, -body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, -body.catalog-skin-legacy .nitro-card-content-shell, -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { - height: 12px; - background: #dde2d8; -} -body.catalog-skin-legacy .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, -body.catalog-skin-legacy .nitro-catalog-classic-layout-container :is(.overflow-auto, -body.catalog-skin-legacy .nitro-card-content-shell, -body.catalog-skin-legacy .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { - height: 12px; - background: #dde2d8; -} -@media (max-width: 991.98px) { -body.catalog-skin-legacy .nitro-catalog-classic-window { - width: min(calc(100vw - 16px), 570px) !important; - min-width: 0 !important; - height: min(calc(100vh - 16px), 635px) !important; - min-height: 0 !important; - max-width: calc(100vw - 16px) !important; - } -body.catalog-skin-legacy .nitro-catalog-classic-stage { - grid-template-columns: minmax(0, 1fr); - } -body.catalog-skin-legacy .nitro-catalog-classic-sidebar { - max-height: 180px; - } - - -} - diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 48a6100..b1dff1b 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -1,29 +1,10 @@ .nitro-catalog-classic-window { - --cat-blue: #4b7a94; - --cat-blue-dark: #385d73; - --cat-ink: #233a47; - --cat-strip: #d9e2e8; - --cat-tab: #b7c7d1; - --cat-tab-border: #7a9cb0; - --cat-panel: #eef2f5; - --cat-sub: #e1e7ec; - --cat-line: #b7c7d1; - --cat-canvas: #d4dadf; - --cat-canvas-2: #c9cfd4; - --cat-select: #3a82a7; - --cat-select-bg: #f0f5f8; - --cat-gold: #f7d673; - --cat-gold-border: #d4af37; - --cat-gold-ink: #4a3300; - --cat-buy: #009900; - width: 640px !important; height: 600px !important; max-width: 640px !important; min-width: 640px !important; min-height: 600px !important; max-height: 600px !important; - background: #ffffff !important; } .nitro-catalog-classic-window .nitro-card-title { @@ -36,12 +17,6 @@ max-height: 38px; } -.nitro-catalog-classic-window .nitro-card-header { - background: var(--cat-blue); - border-color: var(--cat-blue); - border-bottom-color: var(--cat-ink); -} - .nitro-catalog-classic-admin-banner { border-bottom: 1px solid rgba(0, 0, 0, 0.18); background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); @@ -49,47 +24,41 @@ .nitro-catalog-classic-tabs-shell { flex-wrap: nowrap; - gap: 2px; - min-height: 32px; - max-height: 32px; - padding: 4px 6px 0; + gap: 1px; + min-height: 30px; + max-height: 30px; + padding: 0 6px; overflow-x: auto; overflow-y: hidden; align-items: end; - background: var(--cat-strip); - border-bottom: 2px solid var(--cat-ink); + background: #e7e8df; + border-bottom: 1px solid #b8beb4; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { min-height: 28px; - padding: 5px 12px 4px; - border: 1px solid var(--cat-tab-border); + padding: 5px 10px 4px; + border: 1px solid #8f8f8b; border-bottom: 0; border-radius: 5px 5px 0 0; - background: var(--cat-tab); - color: var(--cat-ink); - box-shadow: none; + background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); white-space: nowrap; - font-weight: 700; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: #c7d4dd; + background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #ffffff; - color: #000000; + background: #f2f2eb; + transform: translateY(0); position: relative; top: 1px; - border-color: var(--cat-ink); - box-shadow: inset 0 -1px 0 #ffffff; - font-weight: 700; } .nitro-catalog-classic-content-shell { padding: 6px 8px 8px !important; - background: #ffffff !important; } .nitro-catalog-classic-stage { @@ -113,82 +82,75 @@ } .nitro-catalog-classic-search-shell { - padding: 4px; - border: 1px solid var(--cat-line); + padding: 3px; + border: 1px solid #a7aba1; border-radius: 4px; - background: var(--cat-panel); + background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); } .nitro-catalog-classic-search-shell input { - height: 20px; + height: 18px; padding-top: 0 !important; padding-bottom: 0 !important; border-width: 1px !important; - border-color: var(--cat-tab-border) !important; + border-color: #8f9588 !important; border-radius: 3px !important; background: #fff !important; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.06); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); } .nitro-catalog-classic-search-shell svg { - color: #5b7080 !important; + color: #61645b !important; } .nitro-catalog-classic-navigation-shell { flex: 1 1 auto; min-height: 0; - padding: 4px 0; - border: 1px solid var(--cat-line); + padding: 3px 2px 3px 3px; + border: 1px solid #a7aba1; border-radius: 4px; - background: var(--cat-panel); + background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); overflow: auto; } .nitro-catalog-classic-navigation-list { display: flex; flex-direction: column; - gap: 0; + gap: 2px; } -.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - padding-left: 22px; - background: var(--cat-sub); +.nitro-catalog-classic-navigation-node.is-child { + margin-left: 10px; } .nitro-catalog-classic-navigation-item { display: flex; align-items: center; - gap: 6px; - min-height: 28px; - padding: 4px 10px; - border: 0; - border-left: 4px solid transparent; - border-radius: 0; - background: transparent; - color: var(--cat-ink); - font-weight: 700; + gap: 4px; + min-height: 21px; + padding: 1px 6px 1px 5px; + border: 1px solid #bdc2ba; + border-radius: 4px; + background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); + color: #2e2e2e; cursor: pointer; - transition: background-color 0.12s ease; -} - -.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - font-weight: 400; + transition: background-color 0.12s ease, border-color 0.12s ease; } .nitro-catalog-classic-navigation-item:hover { - background: #dde6ec; + background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); + border-color: #9ea79b; } .nitro-catalog-classic-navigation-item.is-active { - background: #ffffff; - border-left-color: var(--cat-blue); - color: #000000; + background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); + border-color: #8e9ba5; font-weight: 700; } .nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(58, 130, 167, 0.4); - outline-offset: -2px; + outline: 2px solid rgba(48, 114, 140, 0.35); + outline-offset: 1px; } .nitro-catalog-classic-navigation-icon { @@ -226,7 +188,7 @@ } .nitro-catalog-classic-navigation-caret { - color: #5b7080 !important; + color: #676d66 !important; } .nitro-catalog-classic-layout-shell { @@ -235,9 +197,9 @@ min-width: 0; min-height: 0; height: 100%; - border: 1px solid var(--cat-line); + border: 1px solid #a7aba1; border-radius: 4px; - background: #ffffff; + background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); overflow: hidden; } @@ -245,28 +207,26 @@ display: flex; flex-direction: column; gap: 3px; - flex-shrink: 0; - min-height: 0; - padding: 6px 8px; - border-bottom: 1px solid var(--cat-line); - background: #ffffff; + min-height: 66px; + padding: 5px 7px; + border-bottom: 1px solid #c8cdc3; + background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); } .nitro-catalog-classic-layout-hero { display: flex; align-items: center; justify-content: center; - flex: 0 0 auto; + flex: 1 1 auto; min-height: 32px; - overflow: visible; + overflow: hidden; } .nitro-catalog-classic-layout-hero img { - display: block; + max-width: 100%; + max-height: 32px; width: auto; height: auto; - max-width: 100%; - max-height: none; object-fit: contain; } @@ -274,7 +234,7 @@ flex: 1 1 auto; min-height: 0; padding: 6px; - background: #ffffff; + background: #f2f2eb; overflow: hidden; } @@ -284,42 +244,24 @@ .nitro-catalog-classic-offer-panel, .nitro-catalog-classic-welcome { - border: 1px solid var(--cat-line); + border: 1px solid #bfc4bc; border-radius: 6px; - background: #ffffff; -} - -.nitro-catalog-classic-offer-panel { - min-height: 132px; - overflow: hidden; + background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); } .nitro-catalog-classic-offer-preview { - width: 190px; - min-width: 190px; + width: 136px; + min-width: 136px; padding: 8px; - border-right: 1px solid var(--cat-line); - background-color: var(--cat-canvas); + border-right: 1px solid #c9cec5; + background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); } .nitro-catalog-classic-offer-info { padding: 10px; } -.nitro-catalog-classic-offer-actions { - justify-content: flex-end; -} - -.nitro-catalog-classic-offer-info .rounded-full { - background: var(--cat-gold) !important; - border-color: var(--cat-gold-border) !important; -} - -.nitro-catalog-classic-offer-info .rounded-full, -.nitro-catalog-classic-offer-info .rounded-full * { - color: var(--cat-gold-ink) !important; -} - .nitro-catalog-classic-welcome { min-height: 128px; padding: 10px; @@ -327,40 +269,31 @@ .nitro-catalog-classic-grid-shell { min-height: 150px; - padding: 6px; - border: 1px solid var(--cat-line); + padding: 4px; + border: 1px solid #bcc2b8; border-radius: 6px; - background: #ffffff; + background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); height: auto; flex: 1 1 auto; } .nitro-catalog-classic-grid { - gap: 6px !important; + gap: 4px !important; align-content: start; } .nitro-catalog-classic-window .layout-grid-item { height: 54px; - border: 1px solid var(--cat-line) !important; - border-radius: 4px !important; - background-color: #ffffff; + border: 1px solid #b8beb6 !important; + border-radius: 6px !important; + background-color: #d7dde2; background-image: none; - box-shadow: none; - transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; -} - -.nitro-catalog-classic-window .layout-grid-item:hover { - background-color: var(--cat-select-bg) !important; - border-color: var(--cat-select) !important; - box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); } .nitro-catalog-classic-window .layout-grid-item.is-active { - background-color: var(--cat-select-bg) !important; - border-color: var(--cat-select) !important; - border-width: 2px !important; - box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.35); + background-color: #e5ebef !important; + border-color: #8f978b !important; } .nitro-catalog-classic-grid-offer-icon { @@ -372,12 +305,21 @@ } .nitro-catalog-classic-window .nitro-catalog-header { - display: none; + justify-content: flex-start; + min-height: 56px; + margin-bottom: 6px; + padding: 4px 6px; + border: 1px solid #bec3ba; + border-radius: 6px; + background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); } -.nitro-catalog-classic-offer-info .bg-\[\#00800b\] { - background-color: var(--cat-buy) !important; - border-color: #007a00 !important; +.nitro-catalog-classic-window .nitro-catalog-header img { + max-width: 100%; + max-height: 48px; + width: auto; + height: auto; + object-fit: contain; } .nitro-catalog-classic-breadcrumb { @@ -386,7 +328,7 @@ gap: 5px; min-height: 16px; overflow: hidden; - color: #5b7080; + color: #666a63; font-size: 10px; line-height: 1; white-space: nowrap; @@ -400,7 +342,7 @@ } .nitro-catalog-classic-breadcrumb-separator { - color: #94a7b3; + color: #9ea395; } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, @@ -410,37 +352,36 @@ .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { - border-left: 1px solid var(--cat-line); - background: var(--cat-panel); + border-left: 1px solid #c2c6be; + background: #dde2d8; } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { - border: 1px solid var(--cat-tab-border); + border: 1px solid #7d8680; border-radius: 6px; - background: linear-gradient(180deg, #a9bcc9 0%, #89a0ae 100%); + background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { height: 12px; - background: var(--cat-panel); + background: #dde2d8; } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { height: 12px; - background: var(--cat-panel); + background: #dde2d8; } -@media (max-width: 1024px) and (min-width: 641px) { +@media (max-width: 991.98px) { .nitro-catalog-classic-window { - width: min(calc(100vw - 24px), 720px) !important; + width: min(calc(100vw - 16px), 570px) !important; min-width: 0 !important; - max-width: calc(100vw - 24px) !important; - height: min(calc(100vh - 24px), 720px) !important; + height: min(calc(100vh - 16px), 635px) !important; min-height: 0 !important; - max-height: calc(100vh - 24px) !important; + max-width: calc(100vw - 16px) !important; } .nitro-catalog-classic-stage { @@ -448,267 +389,6 @@ } .nitro-catalog-classic-sidebar { - max-height: 200px; - } -} - -.nitro-catalog-classic-mobile-header { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 5; - display: flex; - align-items: center; - height: 38px; - padding: 0 44px 0 8px; - pointer-events: none; -} - -.nitro-catalog-classic-mobile-burger { - position: relative; - pointer-events: auto; -} - -.nitro-catalog-classic-burger-btn { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: 0; - border-radius: 5px; - background: rgba(0, 0, 0, 0.2); - color: #fff; - font-size: 13px; - cursor: pointer; -} - -.nitro-catalog-classic-burger-btn:hover { - background: rgba(0, 0, 0, 0.3); -} - -.nitro-catalog-classic-burger-btn:active { - background: rgba(0, 0, 0, 0.36); -} - -.nitro-catalog-classic-burger-menu { - position: absolute; - top: 32px; - left: 0; - z-index: 60; - display: flex; - flex-direction: column; - gap: 4px; - min-width: 150px; - padding: 6px; - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #fff; - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); -} - -.nitro-catalog-classic-burger-menu button { - padding: 8px 10px; - border: 0; - border-radius: 4px; - background: var(--cat-strip); - color: var(--cat-ink); - font-weight: 700; - text-align: left; - cursor: pointer; -} - -.nitro-catalog-classic-burger-menu button:disabled { - opacity: 0.6; -} - -.nitro-catalog-classic-mobile-currency { - margin-left: auto; - display: flex; - align-items: center; - gap: 5px; - pointer-events: auto; -} - -.nitro-catalog-classic-coin { - display: flex; - align-items: center; - gap: 3px; - padding: 3px 7px; - border-radius: 11px; - background: rgba(0, 0, 0, 0.25); - color: #fff; - font-size: 10px; - font-weight: 700; -} - -.nitro-catalog-classic-coin span { - color: #fff; -} - -.nitro-catalog-classic-admin-tab { - display: none !important; -} - -.nitro-catalog-classic-preview-btn { - position: absolute; - top: 8px; - z-index: 4; - display: inline-flex; - align-items: center; - gap: 5px; - padding: 5px 10px; - border: 1px solid #555; - border-radius: 5px; - background: rgba(0, 0, 0, 0.7); - color: #fff; - font-size: 11px; - font-weight: 700; - white-space: nowrap; - cursor: pointer; -} - -.nitro-catalog-classic-preview-btn:hover { - background: rgba(0, 0, 0, 0.82); -} - -.nitro-catalog-classic-preview-btn:active { - background: rgba(0, 0, 0, 0.9); -} - -.nitro-catalog-classic-preview-rotate { - left: 8px; -} - -.nitro-catalog-classic-preview-state { - right: 8px; -} - -@media (max-width: 640px) { - .nitro-catalog-classic-window { - width: 100vw !important; - min-width: 0 !important; - max-width: 100vw !important; - height: 100vh !important; - min-height: 0 !important; - max-height: 100vh !important; - border-radius: 0 !important; - } - - .draggable-window:has(> .nitro-catalog-classic-window) { - transform: none !important; - left: 0 !important; - top: 0 !important; - } - - .nitro-catalog-classic-window .nitro-card-title { - display: none; - } - - .nitro-catalog-classic-mobile-currency { - position: absolute; - left: 50%; - margin-left: 0; - transform: translateX(-50%); - } - - .nitro-catalog-classic-tabs-shell { - min-height: 44px; - max-height: 44px; - padding: 4px 4px 0; - -webkit-overflow-scrolling: touch; - } - - .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 42px; - padding: 6px 12px; - font-size: 12px; - justify-content: center; - } - - .nitro-catalog-classic-tab-label { - display: none; - } - - .nitro-catalog-classic-content-shell { - padding: 6px !important; - } - - .nitro-catalog-classic-layout-hero { - display: none; - } - - .nitro-catalog-classic-offer-panel { - flex-direction: column; - } - - .nitro-catalog-classic-offer-preview { - width: 100%; - min-width: 0; - border-right: 0; - border-bottom: 1px solid var(--cat-line); - } - - .nitro-catalog-classic-stage { - grid-template-columns: minmax(0, 1fr); - gap: 6px; - } - - .nitro-catalog-classic-sidebar { - max-height: 33vh; - } - - .nitro-catalog-classic-search-shell input { - height: 28px; - font-size: 13px; - } - - .nitro-catalog-classic-navigation-item { - min-height: 40px; - padding: 6px 12px; - } - - .nitro-catalog-classic-navigation-label { - font-size: 13px; - } - - .nitro-catalog-classic-window .layout-grid-item { - height: 64px; - } - - .nitro-catalog-classic-window .nitro-card-header-shell, - .nitro-catalog-classic-window .nitro-card-content-shell { - border-radius: 0 !important; - } -} - -@media (max-height: 480px) { - .nitro-catalog-classic-window { - height: 100vh !important; - max-height: 100vh !important; - } - - .nitro-catalog-classic-window .nitro-card-header-shell { - min-height: 32px; - max-height: 32px; - } - - .nitro-catalog-classic-tabs-shell { - min-height: 38px; - max-height: 38px; - } - - .nitro-catalog-classic-layout-header-shell { - min-height: 0; - padding: 3px 6px; - } - - .nitro-catalog-classic-layout-hero { - display: none; - } - - .nitro-catalog-classic-sidebar { - max-height: 26vh; + max-height: 180px; } } diff --git a/src/index.tsx b/src/index.tsx index a9dacf1..3825168 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,7 +20,6 @@ import './css/index.css'; import './css/backgrounds/BackgroundsView.css'; import './css/badges/BadgeLeaderboardView.css'; import './css/catalog/CatalogClassicView.css'; -import './css/catalog/CatalogClassicLegacy.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; From 4f133abe33bffe31b89ef55c53b57b1996b2e43b Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 23:55:36 +0200 Subject: [PATCH 10/24] feat(catalog): default upstream, toggle classico = Hippiehotel Default catalog = rebuild upstream ultima release (CatalogClassicView). Il toggle 'stile classico' mostra il catalogo Hippiehotel (CatalogModernView). --- src/components/catalog/CatalogClassicView.tsx | 53 +- src/components/catalog/CatalogView.tsx | 10 +- .../page/layout/CatalogLayoutDefaultView.tsx | 25 +- .../page/layout/CatalogLayoutTrophiesView.tsx | 8 +- .../widgets/CatalogPurchaseWidgetView.tsx | 2 +- .../widgets/CatalogViewProductWidgetView.tsx | 4 + src/css/catalog/CatalogClassicView.css | 510 ++++++++++++++---- 7 files changed, 487 insertions(+), 125 deletions(-) diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index a3f2a73..1ee04f2 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -1,9 +1,9 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect } from 'react'; -import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; -import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api'; -import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks'; +import { FC, useEffect, useState } from 'react'; +import { FaBars, FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; +import { CatalogType, GetConfigurationValue, LocalizeShortNumber, LocalizeText } from '../../api'; +import { Column, Grid, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission, usePurse } from '../../hooks'; import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; @@ -31,6 +31,9 @@ const CatalogClassicViewInner: FC<{}> = () => const loading = catalogAdmin?.loading ?? false; const isMod = useHasPermission('acc_catalogfurni'); + const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); + const { purse = null } = usePurse(); + const displayedCurrencies = GetConfigurationValue('system.currency.types', []); const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; @@ -121,6 +124,42 @@ const CatalogClassicViewInner: FC<{}> = () => { isVisible && setIsVisible(false) } style={ buildersClubHeaderStyle } /> +
+ { isMod && +
+ + { mobileMenuOpen && +
+ + { adminMode && + } +
} +
} +
+
+ { LocalizeShortNumber(purse?.credits ?? 0) } + +
+ { displayedCurrencies.map(type => ( +
+ { LocalizeShortNumber(purse?.activityPoints?.get(type) ?? 0) } + +
+ )) } +
+
{ adminMode &&
Admin Mode @@ -148,7 +187,7 @@ const CatalogClassicViewInner: FC<{}> = () => } }>
- { child.localization } + { child.localization } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -172,7 +211,7 @@ const CatalogClassicViewInner: FC<{}> = () => ); }) } { isMod && - setAdminMode(!adminMode) }> + setAdminMode(!adminMode) }> } diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index a1d2d8b..600264b 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -8,20 +8,20 @@ export const CatalogView: FC<{}> = () => const { catalogLocalizationVersion = 0 } = useCatalogData(); const [ catalogClassicStyle ] = useCatalogClassicStyle(); - // Modern (Hippiehotel style) is the default; the "stile classico" toggle in - // user settings (or the global catalog.classic.style flag) switches to the - // classic catalog. Both views are the Hippiehotel.nl Nitro-V3 originals. + // Default = upstream rebuilt catalog (CatalogClassicView, latest release theme). + // The "stile classico" toggle (or global catalog.classic.style flag) switches + // to the Hippiehotel.nl catalog (CatalogModernView, self-contained tailwind). if(catalogClassicStyle) return ( <>
- + ); return ( <>
- + ); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 3490e60..09411d1 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { FaEdit, FaPlus } from 'react-icons/fa'; +import { FaEdit, FaPlus, FaPowerOff, FaSyncAlt } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalogData } from '../../../../../hooks'; @@ -17,13 +17,12 @@ import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutDefaultView: FC = props => { const { page = null } = props; - const { currentOffer = null, currentPage = null } = useCatalogData(); + const { currentOffer = null, currentPage = null, roomPreviewer = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const adminMode = catalogAdmin?.adminMode ?? false; return (
- { /* Admin: quick actions */ } { adminMode && !catalogAdmin.editingPageData &&
} - - { /* Product detail card */ } { currentOffer && -
- { /* Preview area */ } +
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) && <> + + } { (currentOffer.product.productType === ProductTypeEnum.BADGE) && }
- { /* Product info + purchase */ }
- { /* Title row */ }
{ currentOffer.localizationName } @@ -77,19 +77,16 @@ export const CatalogLayoutDefaultView: FC = props =>
}
- { /* Price */ } - { /* Spinner */ } - { /* Actions */ } -
+
} { !currentOffer && -
+
{ !!page.localization.getImage(1) && } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx index 31299a0..6bc187c 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx @@ -58,9 +58,11 @@ export const CatalogLayoutTrophiesView: FC = props =>
} - { /* Selected trophy card */ } + { /* Selected trophy card. shrink-0 + no overflow-hidden so the + Buy button stays inside the panel even when the grid below + holds many trophies. */ } { currentOffer - ?
+ ?
{ /* Preview */ }
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) @@ -90,7 +92,7 @@ export const CatalogLayoutTrophiesView: FC = props => { !canPurchase && { LocalizeText('catalog.trophies.write.hint') } } -
+
diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index 5fe8ef8..e04cac0 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -240,7 +240,7 @@ export const CatalogPurchaseWidgetView: FC = pro return ; case CatalogPurchaseState.NONE: default: - return ; + return ; } }; diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index 027aa36..f63f721 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -19,6 +19,8 @@ export const CatalogViewProductWidgetView: FC<{}> = props => if(!product) return; roomPreviewer.reset(false); + roomPreviewer.updateObjectRoom('default', 'default', 'default'); + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); switch(product.productType) { @@ -68,6 +70,8 @@ export const CatalogViewProductWidgetView: FC<{}> = props => case ProductTypeEnum.WALL: { if(!product.furnitureData) return; + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); + switch(product.furnitureData.specialType) { case FurniCategory.FLOOR: diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index b1dff1b..48a6100 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -1,10 +1,29 @@ .nitro-catalog-classic-window { + --cat-blue: #4b7a94; + --cat-blue-dark: #385d73; + --cat-ink: #233a47; + --cat-strip: #d9e2e8; + --cat-tab: #b7c7d1; + --cat-tab-border: #7a9cb0; + --cat-panel: #eef2f5; + --cat-sub: #e1e7ec; + --cat-line: #b7c7d1; + --cat-canvas: #d4dadf; + --cat-canvas-2: #c9cfd4; + --cat-select: #3a82a7; + --cat-select-bg: #f0f5f8; + --cat-gold: #f7d673; + --cat-gold-border: #d4af37; + --cat-gold-ink: #4a3300; + --cat-buy: #009900; + width: 640px !important; height: 600px !important; max-width: 640px !important; min-width: 640px !important; min-height: 600px !important; max-height: 600px !important; + background: #ffffff !important; } .nitro-catalog-classic-window .nitro-card-title { @@ -17,6 +36,12 @@ max-height: 38px; } +.nitro-catalog-classic-window .nitro-card-header { + background: var(--cat-blue); + border-color: var(--cat-blue); + border-bottom-color: var(--cat-ink); +} + .nitro-catalog-classic-admin-banner { border-bottom: 1px solid rgba(0, 0, 0, 0.18); background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); @@ -24,41 +49,47 @@ .nitro-catalog-classic-tabs-shell { flex-wrap: nowrap; - gap: 1px; - min-height: 30px; - max-height: 30px; - padding: 0 6px; + gap: 2px; + min-height: 32px; + max-height: 32px; + padding: 4px 6px 0; overflow-x: auto; overflow-y: hidden; align-items: end; - background: #e7e8df; - border-bottom: 1px solid #b8beb4; + background: var(--cat-strip); + border-bottom: 2px solid var(--cat-ink); } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { min-height: 28px; - padding: 5px 10px 4px; - border: 1px solid #8f8f8b; + padding: 5px 12px 4px; + border: 1px solid var(--cat-tab-border); border-bottom: 0; border-radius: 5px 5px 0 0; - background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + background: var(--cat-tab); + color: var(--cat-ink); + box-shadow: none; white-space: nowrap; + font-weight: 700; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); + background: #c7d4dd; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #f2f2eb; - transform: translateY(0); + background: #ffffff; + color: #000000; position: relative; top: 1px; + border-color: var(--cat-ink); + box-shadow: inset 0 -1px 0 #ffffff; + font-weight: 700; } .nitro-catalog-classic-content-shell { padding: 6px 8px 8px !important; + background: #ffffff !important; } .nitro-catalog-classic-stage { @@ -82,75 +113,82 @@ } .nitro-catalog-classic-search-shell { - padding: 3px; - border: 1px solid #a7aba1; + padding: 4px; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); + background: var(--cat-panel); } .nitro-catalog-classic-search-shell input { - height: 18px; + height: 20px; padding-top: 0 !important; padding-bottom: 0 !important; border-width: 1px !important; - border-color: #8f9588 !important; + border-color: var(--cat-tab-border) !important; border-radius: 3px !important; background: #fff !important; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.06); } .nitro-catalog-classic-search-shell svg { - color: #61645b !important; + color: #5b7080 !important; } .nitro-catalog-classic-navigation-shell { flex: 1 1 auto; min-height: 0; - padding: 3px 2px 3px 3px; - border: 1px solid #a7aba1; + padding: 4px 0; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); + background: var(--cat-panel); overflow: auto; } .nitro-catalog-classic-navigation-list { display: flex; flex-direction: column; - gap: 2px; + gap: 0; } -.nitro-catalog-classic-navigation-node.is-child { - margin-left: 10px; +.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { + padding-left: 22px; + background: var(--cat-sub); } .nitro-catalog-classic-navigation-item { display: flex; align-items: center; - gap: 4px; - min-height: 21px; - padding: 1px 6px 1px 5px; - border: 1px solid #bdc2ba; - border-radius: 4px; - background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); - color: #2e2e2e; + gap: 6px; + min-height: 28px; + padding: 4px 10px; + border: 0; + border-left: 4px solid transparent; + border-radius: 0; + background: transparent; + color: var(--cat-ink); + font-weight: 700; cursor: pointer; - transition: background-color 0.12s ease, border-color 0.12s ease; + transition: background-color 0.12s ease; +} + +.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { + font-weight: 400; } .nitro-catalog-classic-navigation-item:hover { - background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); - border-color: #9ea79b; + background: #dde6ec; } .nitro-catalog-classic-navigation-item.is-active { - background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); - border-color: #8e9ba5; + background: #ffffff; + border-left-color: var(--cat-blue); + color: #000000; font-weight: 700; } .nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(48, 114, 140, 0.35); - outline-offset: 1px; + outline: 2px solid rgba(58, 130, 167, 0.4); + outline-offset: -2px; } .nitro-catalog-classic-navigation-icon { @@ -188,7 +226,7 @@ } .nitro-catalog-classic-navigation-caret { - color: #676d66 !important; + color: #5b7080 !important; } .nitro-catalog-classic-layout-shell { @@ -197,9 +235,9 @@ min-width: 0; min-height: 0; height: 100%; - border: 1px solid #a7aba1; + border: 1px solid var(--cat-line); border-radius: 4px; - background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); + background: #ffffff; overflow: hidden; } @@ -207,26 +245,28 @@ display: flex; flex-direction: column; gap: 3px; - min-height: 66px; - padding: 5px 7px; - border-bottom: 1px solid #c8cdc3; - background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); + flex-shrink: 0; + min-height: 0; + padding: 6px 8px; + border-bottom: 1px solid var(--cat-line); + background: #ffffff; } .nitro-catalog-classic-layout-hero { display: flex; align-items: center; justify-content: center; - flex: 1 1 auto; + flex: 0 0 auto; min-height: 32px; - overflow: hidden; + overflow: visible; } .nitro-catalog-classic-layout-hero img { - max-width: 100%; - max-height: 32px; + display: block; width: auto; height: auto; + max-width: 100%; + max-height: none; object-fit: contain; } @@ -234,7 +274,7 @@ flex: 1 1 auto; min-height: 0; padding: 6px; - background: #f2f2eb; + background: #ffffff; overflow: hidden; } @@ -244,24 +284,42 @@ .nitro-catalog-classic-offer-panel, .nitro-catalog-classic-welcome { - border: 1px solid #bfc4bc; + border: 1px solid var(--cat-line); border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); + background: #ffffff; +} + +.nitro-catalog-classic-offer-panel { + min-height: 132px; + overflow: hidden; } .nitro-catalog-classic-offer-preview { - width: 136px; - min-width: 136px; + width: 190px; + min-width: 190px; padding: 8px; - border-right: 1px solid #c9cec5; - background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); + border-right: 1px solid var(--cat-line); + background-color: var(--cat-canvas); } .nitro-catalog-classic-offer-info { padding: 10px; } +.nitro-catalog-classic-offer-actions { + justify-content: flex-end; +} + +.nitro-catalog-classic-offer-info .rounded-full { + background: var(--cat-gold) !important; + border-color: var(--cat-gold-border) !important; +} + +.nitro-catalog-classic-offer-info .rounded-full, +.nitro-catalog-classic-offer-info .rounded-full * { + color: var(--cat-gold-ink) !important; +} + .nitro-catalog-classic-welcome { min-height: 128px; padding: 10px; @@ -269,31 +327,40 @@ .nitro-catalog-classic-grid-shell { min-height: 150px; - padding: 4px; - border: 1px solid #bcc2b8; + padding: 6px; + border: 1px solid var(--cat-line); border-radius: 6px; - background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); + background: #ffffff; height: auto; flex: 1 1 auto; } .nitro-catalog-classic-grid { - gap: 4px !important; + gap: 6px !important; align-content: start; } .nitro-catalog-classic-window .layout-grid-item { height: 54px; - border: 1px solid #b8beb6 !important; - border-radius: 6px !important; - background-color: #d7dde2; + border: 1px solid var(--cat-line) !important; + border-radius: 4px !important; + background-color: #ffffff; background-image: none; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); + box-shadow: none; + transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; +} + +.nitro-catalog-classic-window .layout-grid-item:hover { + background-color: var(--cat-select-bg) !important; + border-color: var(--cat-select) !important; + box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.2); } .nitro-catalog-classic-window .layout-grid-item.is-active { - background-color: #e5ebef !important; - border-color: #8f978b !important; + background-color: var(--cat-select-bg) !important; + border-color: var(--cat-select) !important; + border-width: 2px !important; + box-shadow: 0 0 0 1px rgba(58, 130, 167, 0.35); } .nitro-catalog-classic-grid-offer-icon { @@ -305,21 +372,12 @@ } .nitro-catalog-classic-window .nitro-catalog-header { - justify-content: flex-start; - min-height: 56px; - margin-bottom: 6px; - padding: 4px 6px; - border: 1px solid #bec3ba; - border-radius: 6px; - background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); + display: none; } -.nitro-catalog-classic-window .nitro-catalog-header img { - max-width: 100%; - max-height: 48px; - width: auto; - height: auto; - object-fit: contain; +.nitro-catalog-classic-offer-info .bg-\[\#00800b\] { + background-color: var(--cat-buy) !important; + border-color: #007a00 !important; } .nitro-catalog-classic-breadcrumb { @@ -328,7 +386,7 @@ gap: 5px; min-height: 16px; overflow: hidden; - color: #666a63; + color: #5b7080; font-size: 10px; line-height: 1; white-space: nowrap; @@ -342,7 +400,7 @@ } .nitro-catalog-classic-breadcrumb-separator { - color: #9ea395; + color: #94a7b3; } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, @@ -352,36 +410,37 @@ .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { - border-left: 1px solid #c2c6be; - background: #dde2d8; + border-left: 1px solid var(--cat-line); + background: var(--cat-panel); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { - border: 1px solid #7d8680; + border: 1px solid var(--cat-tab-border); border-radius: 6px; - background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); + background: linear-gradient(180deg, #a9bcc9 0%, #89a0ae 100%); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { height: 12px; - background: #dde2d8; + background: var(--cat-panel); } .nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, .nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { height: 12px; - background: #dde2d8; + background: var(--cat-panel); } -@media (max-width: 991.98px) { +@media (max-width: 1024px) and (min-width: 641px) { .nitro-catalog-classic-window { - width: min(calc(100vw - 16px), 570px) !important; + width: min(calc(100vw - 24px), 720px) !important; min-width: 0 !important; - height: min(calc(100vh - 16px), 635px) !important; + max-width: calc(100vw - 24px) !important; + height: min(calc(100vh - 24px), 720px) !important; min-height: 0 !important; - max-width: calc(100vw - 16px) !important; + max-height: calc(100vh - 24px) !important; } .nitro-catalog-classic-stage { @@ -389,6 +448,267 @@ } .nitro-catalog-classic-sidebar { - max-height: 180px; + max-height: 200px; + } +} + +.nitro-catalog-classic-mobile-header { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 5; + display: flex; + align-items: center; + height: 38px; + padding: 0 44px 0 8px; + pointer-events: none; +} + +.nitro-catalog-classic-mobile-burger { + position: relative; + pointer-events: auto; +} + +.nitro-catalog-classic-burger-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: 0; + border-radius: 5px; + background: rgba(0, 0, 0, 0.2); + color: #fff; + font-size: 13px; + cursor: pointer; +} + +.nitro-catalog-classic-burger-btn:hover { + background: rgba(0, 0, 0, 0.3); +} + +.nitro-catalog-classic-burger-btn:active { + background: rgba(0, 0, 0, 0.36); +} + +.nitro-catalog-classic-burger-menu { + position: absolute; + top: 32px; + left: 0; + z-index: 60; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 150px; + padding: 6px; + border: 1px solid var(--cat-line); + border-radius: 6px; + background: #fff; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); +} + +.nitro-catalog-classic-burger-menu button { + padding: 8px 10px; + border: 0; + border-radius: 4px; + background: var(--cat-strip); + color: var(--cat-ink); + font-weight: 700; + text-align: left; + cursor: pointer; +} + +.nitro-catalog-classic-burger-menu button:disabled { + opacity: 0.6; +} + +.nitro-catalog-classic-mobile-currency { + margin-left: auto; + display: flex; + align-items: center; + gap: 5px; + pointer-events: auto; +} + +.nitro-catalog-classic-coin { + display: flex; + align-items: center; + gap: 3px; + padding: 3px 7px; + border-radius: 11px; + background: rgba(0, 0, 0, 0.25); + color: #fff; + font-size: 10px; + font-weight: 700; +} + +.nitro-catalog-classic-coin span { + color: #fff; +} + +.nitro-catalog-classic-admin-tab { + display: none !important; +} + +.nitro-catalog-classic-preview-btn { + position: absolute; + top: 8px; + z-index: 4; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 10px; + border: 1px solid #555; + border-radius: 5px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 11px; + font-weight: 700; + white-space: nowrap; + cursor: pointer; +} + +.nitro-catalog-classic-preview-btn:hover { + background: rgba(0, 0, 0, 0.82); +} + +.nitro-catalog-classic-preview-btn:active { + background: rgba(0, 0, 0, 0.9); +} + +.nitro-catalog-classic-preview-rotate { + left: 8px; +} + +.nitro-catalog-classic-preview-state { + right: 8px; +} + +@media (max-width: 640px) { + .nitro-catalog-classic-window { + width: 100vw !important; + min-width: 0 !important; + max-width: 100vw !important; + height: 100vh !important; + min-height: 0 !important; + max-height: 100vh !important; + border-radius: 0 !important; + } + + .draggable-window:has(> .nitro-catalog-classic-window) { + transform: none !important; + left: 0 !important; + top: 0 !important; + } + + .nitro-catalog-classic-window .nitro-card-title { + display: none; + } + + .nitro-catalog-classic-mobile-currency { + position: absolute; + left: 50%; + margin-left: 0; + transform: translateX(-50%); + } + + .nitro-catalog-classic-tabs-shell { + min-height: 44px; + max-height: 44px; + padding: 4px 4px 0; + -webkit-overflow-scrolling: touch; + } + + .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { + min-height: 42px; + padding: 6px 12px; + font-size: 12px; + justify-content: center; + } + + .nitro-catalog-classic-tab-label { + display: none; + } + + .nitro-catalog-classic-content-shell { + padding: 6px !important; + } + + .nitro-catalog-classic-layout-hero { + display: none; + } + + .nitro-catalog-classic-offer-panel { + flex-direction: column; + } + + .nitro-catalog-classic-offer-preview { + width: 100%; + min-width: 0; + border-right: 0; + border-bottom: 1px solid var(--cat-line); + } + + .nitro-catalog-classic-stage { + grid-template-columns: minmax(0, 1fr); + gap: 6px; + } + + .nitro-catalog-classic-sidebar { + max-height: 33vh; + } + + .nitro-catalog-classic-search-shell input { + height: 28px; + font-size: 13px; + } + + .nitro-catalog-classic-navigation-item { + min-height: 40px; + padding: 6px 12px; + } + + .nitro-catalog-classic-navigation-label { + font-size: 13px; + } + + .nitro-catalog-classic-window .layout-grid-item { + height: 64px; + } + + .nitro-catalog-classic-window .nitro-card-header-shell, + .nitro-catalog-classic-window .nitro-card-content-shell { + border-radius: 0 !important; + } +} + +@media (max-height: 480px) { + .nitro-catalog-classic-window { + height: 100vh !important; + max-height: 100vh !important; + } + + .nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 32px; + max-height: 32px; + } + + .nitro-catalog-classic-tabs-shell { + min-height: 38px; + max-height: 38px; + } + + .nitro-catalog-classic-layout-header-shell { + min-height: 0; + padding: 3px 6px; + } + + .nitro-catalog-classic-layout-hero { + display: none; + } + + .nitro-catalog-classic-sidebar { + max-height: 26vh; } } From 4e96355a948bd63eccfd9b0d88c73cd0d7f7d515 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 00:15:07 +0200 Subject: [PATCH 11/24] perf(boot): non pre-fetchare le URL gamedata directory preloadUrl salta le URL che finiscono con '/' (le gamedata split sono directory): prima 404avano e sprecavano connessioni all'avvio. --- src/App.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 55fce02..4b799ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,11 @@ const preloadUrl = async (url: string): Promise => { if(!url) return; + // Split gamedata URLs are directories (end with '/'); fetching them as a + // file just 404s and wastes a connection at startup. The real split loader + // handles those — only warm up actual file URLs here. + if(url.split('?')[0].split('#')[0].endsWith('/')) return; + try { const response = await fetch(url, { cache: 'force-cache' }); From 72952318c0785a305f2fdd3b0b29eb878cab8fd5 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 00:26:59 +0200 Subject: [PATCH 12/24] fix(catalog-admin): non re-includere ' (pageId)' nella caption editata Il form di modifica pagina precompilava la caption con la localization che include il suffisso ' (id)' aggiunto dal server ai mod, e salvandola l'id si accumulava (Wired (1114) (1114) ...). Ora striscia quel suffisso. --- .../catalog/views/admin/CatalogAdminPageEditView.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx index b85393a..94b2326 100644 --- a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx +++ b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx @@ -67,8 +67,14 @@ export const CatalogAdminPageEditView: FC<{}> = () => { if(!editingPageData || !targetNode) return; - setCaption(targetNode.localization || ''); - setCaptionSave(targetNode.pageName || targetNode.localization || ''); + // The server appends " (pageId)" to the caption for mods/admins (see + // CatalogPagesListComposer). Strip that exact suffix before seeding the + // edit field, otherwise saving folds the id back into the stored + // caption and it multiplies on every edit ("Wired (1114) (1114) ..."). + const rawCaption = (targetNode.localization || '').replace(new RegExp(`\\s*\\(${ targetNode.pageId }\\)\\s*$`), ''); + + setCaption(rawCaption); + setCaptionSave(targetNode.pageName || rawCaption); setCatalogMode(currentType === CatalogType.BUILDER ? 'BUILDER' : 'NORMAL'); setPageLayout(currentPage?.layoutCode || 'default_3x3'); setIconImage(targetNode.iconId ?? 0); From 91f114bb3dd9e8657fc7dce59bbf0a57fbed6dc8 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 02:02:43 +0200 Subject: [PATCH 13/24] fix(config): pet.asset.url usa /pet/ non /pets/ (cartella bundled e 'pet') --- public/configuration/renderer-config.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/configuration/renderer-config.example b/public/configuration/renderer-config.example index c40cdee..8438c97 100644 --- a/public/configuration/renderer-config.example +++ b/public/configuration/renderer-config.example @@ -27,7 +27,7 @@ "avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro", "furni.asset.url": "${asset.url}/furniture/%libname%.nitro", "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", - "pet.asset.url": "${asset.url}/pets/%libname%.nitro", + "pet.asset.url": "${asset.url}/pet/%libname%.nitro", "generic.asset.url": "${asset.url}/generic/%libname%.nitro", "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", "radio.url": "${gamedata.url}/radio-stations.json5?t=%timestamp%", From 6db8d3e1910579e6e671f41852170eb29eb80297 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 02:34:25 +0200 Subject: [PATCH 14/24] feat(catalog): layout info BC (logo 70% + box testo nero) via info_duckets --- .../page/layout/CatalogLayoutBcInfoView.tsx | 32 +++++++++++++++++++ .../views/page/layout/GetCatalogLayout.tsx | 3 ++ 2 files changed, 35 insertions(+) create mode 100644 src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx new file mode 100644 index 0000000..13c5aca --- /dev/null +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +// Info/landing layout: a big logo box (70% of the page height) on top and a +// smaller box below holding the page text in black. Logo = page headline +// image (getImage(0)), text = page text 1 (getText(0)). Set both from +// catalog admin (Gestione -> Modifica pagina). +export const CatalogLayoutBcInfoView: FC = props => +{ + const { page = null } = props; + + const logo = page?.localization?.getImage(0) || ''; + const text = page?.localization?.getText(0) || ''; + + return ( +
+
+ { logo + ? + : Logo — imposta l'immagine headline da Gestione } +
+
+
+
+
+ ); +}; diff --git a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx index 6ccf2a1..ce68f37 100644 --- a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx +++ b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx @@ -1,6 +1,7 @@ import { ICatalogPage } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView'; +import { CatalogLayoutBcInfoView } from './CatalogLayoutBcInfoView'; import { CatalogLayoutBuildersClubBuyView } from './CatalogLayoutBuildersClubBuyView'; import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView'; import { CatalogLayoutCustomPrefixView } from './CatalogLayoutCustomPrefixView'; @@ -34,6 +35,8 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) { case 'frontpage_featured': return null; + case 'info_duckets': + return ; case 'frontpage4': return ; case 'pets': From 552cd1f5387118ede8246963aac74a626165e96e Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 02:43:07 +0200 Subject: [PATCH 15/24] fix(catalog): layout info BC nasconde la navigazione vuota a sinistra --- .../views/page/layout/CatalogLayoutBcInfoView.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx index 13c5aca..dee8094 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -1,18 +1,24 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; // Info/landing layout: a big logo box (70% of the page height) on top and a // smaller box below holding the page text in black. Logo = page headline // image (getImage(0)), text = page text 1 (getText(0)). Set both from -// catalog admin (Gestione -> Modifica pagina). +// catalog admin (Gestione -> Modifica pagina). Hides the (empty) navigation +// sidebar so the content uses the full width. export const CatalogLayoutBcInfoView: FC = props => { - const { page = null } = props; + const { page = null, hideNavigation = null } = props; const logo = page?.localization?.getImage(0) || ''; const text = page?.localization?.getText(0) || ''; + useEffect(() => + { + hideNavigation?.(); + }, [ page, hideNavigation ]); + return (
Date: Sat, 30 May 2026 03:03:28 +0200 Subject: [PATCH 16/24] fix(catalog): immagini pagina supportano URL completi e estensioni non-gif --- src/api/catalog/PageLocalization.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/api/catalog/PageLocalization.ts b/src/api/catalog/PageLocalization.ts index f24ae87..824683d 100644 --- a/src/api/catalog/PageLocalization.ts +++ b/src/api/catalog/PageLocalization.ts @@ -27,8 +27,16 @@ export class PageLocalization implements IPageLocalization if(!imageName || !imageName.length) return null; + // Already a full URL (any extension) -> use it directly. + if(/^https?:\/\//i.test(imageName)) return imageName; + let assetUrl = GetConfigurationValue('catalog.asset.image.url'); + // The template forces ".gif" (.../%name%.gif). If the image name + // already carries its own extension (png/jpg/webp/gif), don't append + // the forced .gif so non-gif catalog images work too. + if(/\.[a-z0-9]+$/i.test(imageName)) assetUrl = assetUrl.replace(/\.gif(?=$|\?)/i, ''); + assetUrl = assetUrl.replace('%name%', imageName); return assetUrl; From 9f737c512984d144208145e74617fb87cff26980 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:08:44 +0200 Subject: [PATCH 17/24] style(catalog): logo info a tutta larghezza (no crop, no margini) --- .../page/layout/CatalogLayoutBcInfoView.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx index dee8094..750c638 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -2,11 +2,11 @@ import { FC, useEffect } from 'react'; import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; -// Info/landing layout: a big logo box (70% of the page height) on top and a -// smaller box below holding the page text in black. Logo = page headline -// image (getImage(0)), text = page text 1 (getText(0)). Set both from -// catalog admin (Gestione -> Modifica pagina). Hides the (empty) navigation -// sidebar so the content uses the full width. +// Info/landing layout: a logo box on top (image fills the full width, natural +// height, no crop) and a smaller box below with the page text in black. +// Logo = page headline image (getImage(0)), text = page text 1 (getText(0)), +// set from catalog admin (Gestione -> Modifica pagina). Hides the (empty) +// navigation sidebar so the content uses the full width. export const CatalogLayoutBcInfoView: FC = props => { const { page = null, hideNavigation = null } = props; @@ -22,10 +22,10 @@ export const CatalogLayoutBcInfoView: FC = props => return (
+ className="bg-white rounded border border-card-grid-item-border overflow-hidden flex items-center justify-center" + style={ logo ? undefined : { minHeight: '70%' } }> { logo - ? + ? : Logo — imposta l'immagine headline da Gestione }
From 624a310c5946237ec61f7ae0a86ec2ce9e0a35ae Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:14:36 +0200 Subject: [PATCH 18/24] style(catalog): logo info massimizzato senza crop, testo box piu piccolo --- .../views/page/layout/CatalogLayoutBcInfoView.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx index 750c638..793801f 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -2,8 +2,8 @@ import { FC, useEffect } from 'react'; import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; -// Info/landing layout: a logo box on top (image fills the full width, natural -// height, no crop) and a smaller box below with the page text in black. +// Info/landing layout: a logo box on top (image scaled to fit the available +// space, no crop) and a smaller box below with the page text in black. // Logo = page headline image (getImage(0)), text = page text 1 (getText(0)), // set from catalog admin (Gestione -> Modifica pagina). Hides the (empty) // navigation sidebar so the content uses the full width. @@ -21,14 +21,12 @@ export const CatalogLayoutBcInfoView: FC = props => return (
-
+
{ logo - ? + ? : Logo — imposta l'immagine headline da Gestione }
-
+
From a034517143ea1d85992f719a3ea93682ce8ce2af Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:17:32 +0200 Subject: [PATCH 19/24] style(catalog): logo info edge-to-edge (object-cover) --- .../catalog/views/page/layout/CatalogLayoutBcInfoView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx index 793801f..f7fc770 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -23,7 +23,7 @@ export const CatalogLayoutBcInfoView: FC = props =>
{ logo - ? + ? : Logo — imposta l'immagine headline da Gestione }
From 8e95ca9570f2082ff5ce0bdddb0ae1de7f0eccf0 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:22:48 +0200 Subject: [PATCH 20/24] style(catalog): logo info torna a contain (immagine intera con bordi) --- .../catalog/views/page/layout/CatalogLayoutBcInfoView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx index f7fc770..793801f 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBcInfoView.tsx @@ -23,7 +23,7 @@ export const CatalogLayoutBcInfoView: FC = props =>
{ logo - ? + ? : Logo — imposta l'immagine headline da Gestione }
From 52a7dc16048f4ede87def7e736434c60adb5550a Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:32:40 +0200 Subject: [PATCH 21/24] fix(catalog): scrollbar-gutter stable sulla nav, proporzioni costanti con/senza scroll --- src/css/catalog/CatalogClassicView.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 48a6100..a595965 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -142,6 +142,10 @@ border-radius: 4px; background: var(--cat-panel); overflow: auto; + /* Reserve the scrollbar space at all times so the rows (and the active + highlight) keep the same width/proportions whether or not the list + overflows into a scrollbar (e.g. many sub-pages). */ + scrollbar-gutter: stable; } .nitro-catalog-classic-navigation-list { From d79bdd33e1f09b095475f6a4cf379d0ac3146466 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 03:42:58 +0200 Subject: [PATCH 22/24] fix(catalog): scrollbar-gutter both-edges, icona nav centrata con/senza scroll --- src/css/catalog/CatalogClassicView.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index a595965..dc99550 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -142,10 +142,10 @@ border-radius: 4px; background: var(--cat-panel); overflow: auto; - /* Reserve the scrollbar space at all times so the rows (and the active - highlight) keep the same width/proportions whether or not the list + /* Reserve the scrollbar space at all times (both edges) so the rows and + the active highlight stay centered/consistent whether or not the list overflows into a scrollbar (e.g. many sub-pages). */ - scrollbar-gutter: stable; + scrollbar-gutter: stable both-edges; } .nitro-catalog-classic-navigation-list { From f7e78674c697378329e4083607bcced24a01a5df Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sat, 30 May 2026 05:49:04 +0200 Subject: [PATCH 23/24] fix(client): catalogo mobile, BC/navigator/profilo/amici - Catalogo Hippiehotel responsive su mobile (finestra 96vw/72vh, rail tappabile, sotto-pannelli ristretti); duckie ripristinato come entita separata (revert modifiche scrollbar sul suo CSS) - BC e catalogo normale seguono entrambi il toggle tema; il duckie non duplica piu il logo nelle pagine info_duckets - Navigator: highlight della tab segue currentTabCode (era bloccato su Pubblici mentre il contenuto cambiava) - Inventario: link inventory/show/ per deep-link a una scheda - User Profile: avatar visibile e allineato a bg/stand, finestra piu grande, bordi puliti, Created/Last login mostrano il valore, bottoni Change Looks/Rooms/Change Badges/Add friends/Achievement funzionanti - Amici: header non piu sovradimensionati e teste avatar inquadrate (regole flat: quelle annidate .nitro-friends{&...} non si applicavano) --- src/components/catalog/CatalogClassicView.tsx | 3 +- src/components/catalog/CatalogModernView.tsx | 12 ++-- src/components/catalog/CatalogView.tsx | 1 + src/components/inventory/InventoryView.tsx | 9 +++ src/components/navigator/NavigatorView.tsx | 4 +- .../RelationshipsContainerView.tsx | 6 +- .../user-profile/UserContainerView.tsx | 26 ++++---- .../user-profile/UserProfileView.tsx | 15 ++--- src/css/catalog/CatalogClassicView.css | 4 -- src/css/friends/FriendsView.css | 50 +++++++++++++++- src/css/user-profile/UserProfileView.css | 60 +++++++++++-------- 11 files changed, 125 insertions(+), 65 deletions(-) diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index 1ee04f2..9df060e 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -252,7 +252,8 @@ const CatalogClassicViewInner: FC<{}> = () =>
- { !!currentPage?.localization?.getImage(0) && } + { /* info_duckets renders its own logo in the body (BcInfoView) — don't duplicate it in the hero */ } + { (currentPage?.layoutCode !== 'info_duckets') && !!currentPage?.localization?.getImage(0) && }
diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx index 1c4af9a..ea1c2de 100644 --- a/src/components/catalog/CatalogModernView.tsx +++ b/src/components/catalog/CatalogModernView.tsx @@ -37,6 +37,10 @@ const CatalogModernViewInner: FC<{}> = () => const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; + // Desktop = fixed 780x520. On mobile the window clamps below the viewport so + // it reads as a dialog (with margins) instead of filling the whole phone + // screen — applies to both the normal catalog and the Builders Club. + const catalogCardSize = 'w-[780px] h-[520px] max-w-[96vw] max-h-[72vh] sm:max-w-[100vw] sm:max-h-[92vh]'; useEffect(() => { @@ -122,7 +126,7 @@ const CatalogModernViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } /> { /* Admin banner */ } @@ -141,7 +145,7 @@ const CatalogModernViewInner: FC<{}> = () =>
{ /* === LEFT SIDEBAR === */ } -
+
{ /* Favorites toggle */ }
= () => : { LocalizeText('catalog.title') } }
-
+
@@ -301,7 +305,7 @@ const CatalogModernViewInner: FC<{}> = () =>
: <> { !navigationHidden && activeNodes && activeNodes.length > 0 && -
+
}
diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index 600264b..87344fe 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -11,6 +11,7 @@ export const CatalogView: FC<{}> = () => // Default = upstream rebuilt catalog (CatalogClassicView, latest release theme). // The "stile classico" toggle (or global catalog.classic.style flag) switches // to the Hippiehotel.nl catalog (CatalogModernView, self-contained tailwind). + // Both the normal catalog and the Builders Club follow this toggle. if(catalogClassicStyle) return ( <>
diff --git a/src/components/inventory/InventoryView.tsx b/src/components/inventory/InventoryView.tsx index 4c95c35..881ddce 100644 --- a/src/components/inventory/InventoryView.tsx +++ b/src/components/inventory/InventoryView.tsx @@ -19,6 +19,13 @@ const TAB_PETS: string = 'inventory.furni.tab.pets'; const TAB_BADGES: string = 'inventory.badges'; const TAB_PREFIXES: string = 'inventory.prefixes'; const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_PREFIXES, TAB_BOTS ]; +// Maps an optional link code (inventory/show/) to a tab so other views +// (e.g. the profile "Change Badges" button) can deep-link to a specific tab. +const TAB_BY_CODE: Record = { + furni: TAB_FURNITURE, furniture: TAB_FURNITURE, + pets: TAB_PETS, badges: TAB_BADGES, + prefixes: TAB_PREFIXES, bots: TAB_BOTS +}; const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.PREFIX, UnseenItemCategory.BOT ]; const TAB_ICONS: Record = { [TAB_FURNITURE]: , @@ -94,12 +101,14 @@ export const InventoryView: FC<{}> = props => { case 'show': setIsVisible(true); + if(parts[2] && TAB_BY_CODE[parts[2]]) setCurrentTab(TAB_BY_CODE[parts[2]]); return; case 'hide': setIsVisible(false); return; case 'toggle': setIsVisible(prevValue => !prevValue); + if(parts[2] && TAB_BY_CODE[parts[2]]) setCurrentTab(TAB_BY_CODE[parts[2]]); return; } }, diff --git a/src/components/navigator/NavigatorView.tsx b/src/components/navigator/NavigatorView.tsx index b93b8d5..8daf79b 100644 --- a/src/components/navigator/NavigatorView.tsx +++ b/src/components/navigator/NavigatorView.tsx @@ -22,7 +22,7 @@ export const NavigatorView: FC<{}> = props => { const { topLevelContext, topLevelContexts, navigatorData, navigatorSearches } = useNavigatorData(); const { searchResult, isFetching } = useNavigatorSearch(); - const { isVisible, isCreatorOpen, isRoomInfoOpen, isRoomLinkOpen, isOpenSavesSearches, needsInit } = useNavigatorUiState(); + const { isVisible, isCreatorOpen, isRoomInfoOpen, isRoomLinkOpen, isOpenSavesSearches, needsInit, currentTabCode } = useNavigatorUiState(); const elementRef = useRef(null); useNitroEvent(RoomSessionEvent.CREATED, event => @@ -122,7 +122,7 @@ export const NavigatorView: FC<{}> = props => { topLevelContexts && topLevelContexts.length > 0 && topLevelContexts.map((context, index) => useNavigatorUiStore.getState().setTab(context.code) }> { LocalizeText('navigator.toplevelview.' + context.code) } ) } diff --git a/src/components/user-profile/RelationshipsContainerView.tsx b/src/components/user-profile/RelationshipsContainerView.tsx index 4de625a..204eb2b 100644 --- a/src/components/user-profile/RelationshipsContainerView.tsx +++ b/src/components/user-profile/RelationshipsContainerView.tsx @@ -1,6 +1,6 @@ import { RelationshipStatusEnum, RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { GetUserProfile, LocalizeText } from '../../api'; +import { CreateLinkEvent, GetUserProfile, LocalizeText } from '../../api'; import { Flex, LayoutAvatarImageView } from '../../common'; interface RelationshipsContainerViewProps @@ -29,7 +29,7 @@ export const RelationshipsContainerView: FC = p
-

(relationshipInfo && (relationshipInfo.randomFriendId >= 1) && GetUserProfile(relationshipInfo.randomFriendId)) }> +

((relationshipInfo && (relationshipInfo.randomFriendId >= 1)) ? GetUserProfile(relationshipInfo.randomFriendId) : CreateLinkEvent('friends/toggle')) }> { (!relationshipInfo || (relationshipInfo.friendCount === 0)) && LocalizeText('extendedprofile.add.friends') } { (relationshipInfo && (relationshipInfo.friendCount >= 1)) && @@ -37,7 +37,7 @@ export const RelationshipsContainerView: FC = p

{ (relationshipInfo && (relationshipInfo.friendCount >= 1)) &&
- +
}

diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index 73b3ca2..e4e6b43 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -60,18 +60,12 @@ export const UserContainerView: FC = props => prefixText={ userProfile.prefixText } username={ userProfile.username } />

{ userProfile.motto || '\u00A0' }

-

-

+

+ { LocalizeText('extendedprofile.created').replace(/%\w+%/g, '').trim() } { userProfile.registration } +

+

+ { LocalizeText('extendedprofile.last.login').replace(/%\w+%/g, '').trim() } { FriendlyTime.format(userProfile.secondsSinceLastVisit, '.ago', 2) } +

{ LocalizeText('extendedprofile.achievementscore') } { userProfile.achievementPoints }

@@ -100,10 +94,10 @@ export const UserContainerView: FC = props => { isOwnProfile &&
- -
} @@ -148,11 +142,11 @@ export const UserContainerView: FC = props => { LocalizeText('inventory.badges') } { totalBadges } -
+
+
); diff --git a/src/components/user-profile/UserProfileView.tsx b/src/components/user-profile/UserProfileView.tsx index fd5a3b5..801fd4d 100644 --- a/src/components/user-profile/UserProfileView.tsx +++ b/src/components/user-profile/UserProfileView.tsx @@ -1,6 +1,6 @@ -import { ExtendedProfileChangedMessageEvent, GetSessionDataManager, NavigatorSearchComposer, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { ExtendedProfileChangedMessageEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; -import { GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; +import { CreateLinkEvent, GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { NitroCard } from '../../layout'; import { GroupsContainerView } from './GroupsContainerView'; @@ -28,10 +28,11 @@ export const UserProfileView: FC<{}> = () => const onOpenRooms = () => { - if(userProfile) - { - SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`)); - } + if(!userProfile) return; + + // Open the navigator AND run the owner search (the composer alone never + // showed the navigator window, so the button looked dead). + CreateLinkEvent(`navigator/search/hotel_view/owner:${ userProfile.username }`); }; useMessageEvent(UserCurrentBadgesEvent, event => @@ -100,7 +101,7 @@ export const UserProfileView: FC<{}> = () => if(!userProfile) return null; return ( - + diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index dc99550..48a6100 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -142,10 +142,6 @@ border-radius: 4px; background: var(--cat-panel); overflow: auto; - /* Reserve the scrollbar space at all times (both edges) so the rows and - the active highlight stay centered/consistent whether or not the list - overflows into a scrollbar (e.g. many sub-pages). */ - scrollbar-gutter: stable both-edges; } .nitro-catalog-classic-navigation-list { diff --git a/src/css/friends/FriendsView.css b/src/css/friends/FriendsView.css index 2b2ea17..3786920 100644 --- a/src/css/friends/FriendsView.css +++ b/src/css/friends/FriendsView.css @@ -176,9 +176,15 @@ border: 0 !important; } - & .nitro-card-accordion-set-header span { - font-size: 12px; + /* The header title is rendered by the shared component, which is a +
(not a ) — so target the div too, otherwise it keeps the app + default size and shows as oversized "testoni". */ + & .nitro-card-accordion-set-header span, + & .nitro-card-accordion-set-header > div { + font-size: 12px !important; + font-weight: 700; color: #111 !important; + line-height: 1.1; } & .nitro-card-accordion-set-header .fa-icon { @@ -800,3 +806,43 @@ } } } + +/* ------------------------------------------------------------------ * + * Flat (non-nested) overrides. The rules above live inside a nested + * `.nitro-friends { & ... }` block; these are written flat so they + * apply reliably and win by source order. They fix two things the + * nested rules didn't: the oversized/overflowing friend-list heads and + * the oversized accordion section titles ("testoni"). + * ------------------------------------------------------------------ */ + +/* Friend-list avatar: clip a small box and centre the head, same proven + recipe as the messenger head. `inset: auto` cancels the component's + `inset-0`, otherwise the 130px head fills the row and overflows. */ +.nitro-friends .friends-list-avatar { + position: relative !important; + width: 34px; + height: 36px; + flex-shrink: 0; + overflow: hidden; +} + +.nitro-friends .friends-list-avatar .avatar-image { + position: absolute !important; + inset: auto !important; + left: 50% !important; + top: 56% !important; + width: 54px !important; + height: 54px !important; + margin: 0 !important; + background-position: center center !important; + transform: translate(-50%, -50%) scale(.95) !important; +} + +/* Accordion section titles are rendered by (a
, not a ), + so size the div too — otherwise they show oversized. */ +.nitro-friends .nitro-card-accordion-set-header > div, +.nitro-friends .nitro-card-accordion-set-header span { + font-size: 12px !important; + font-weight: 700 !important; + line-height: 1.15 !important; +} diff --git a/src/css/user-profile/UserProfileView.css b/src/css/user-profile/UserProfileView.css index 3f3e47b..66bbec6 100644 --- a/src/css/user-profile/UserProfileView.css +++ b/src/css/user-profile/UserProfileView.css @@ -1,7 +1,3 @@ -.nitro-extended-profile-window { - border-radius: 0 !important; -} - .nitro-extended-profile-window .nitro-card-header-shell { min-height: 34px; max-height: 34px; @@ -46,32 +42,31 @@ .nitro-extended-profile__identity { display: grid; - grid-template-columns: 56px minmax(0, 1fr); - gap: 8px; + grid-template-columns: 68px minmax(0, 1fr); + gap: 10px; } +/* Mirror the room infostand exactly: a 68x135 flex column (= profile-background) + that centres the avatar horizontally and clips it; the stand/overlay sit on + top as absolute layers. The avatar keeps its component default classes + (relative w-[90px] h-[130px] left-[-2px]) so it lines up with bg + stand and + isn't crooked. Do NOT absolutely position or force width/height on it. */ .nitro-extended-profile__avatar-shell { - width: 56px; - height: 113px; + width: 68px; + height: 135px; position: relative; overflow: hidden; - background-size: cover; - background-position: center; + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; } .nitro-extended-profile__avatar-stand, .nitro-extended-profile__avatar-overlay { position: absolute; - inset: 0; -} - -.nitro-extended-profile__avatar-image { - position: absolute !important; - left: 50% !important; - bottom: -4px; - transform: translateX(-50%); - width: auto !important; - height: auto !important; + top: 0; + left: 0; } .nitro-extended-profile__identity-copy { @@ -253,14 +248,27 @@ .nitro-extended-profile__relationship-head { position: absolute; - right: -2px; + right: 3px; top: 50%; - width: 34px; - height: 34px; + width: 30px; + height: 32px; transform: translateY(-50%); - display: flex; - align-items: center; - justify-content: center; + overflow: hidden; +} + +/* Same proven recipe as the messenger head: clip a small box and centre a + 54x54 avatar in it. `inset: auto` cancels the component's `inset-0` so the + width/position take effect (otherwise the head overflows huge). */ +.nitro-extended-profile__relationship-head .avatar-image { + position: absolute !important; + inset: auto !important; + left: 50% !important; + top: 54% !important; + width: 50px !important; + height: 50px !important; + margin: 0 !important; + background-position: center center !important; + transform: translate(-50%, -50%) scale(.95) !important; } .nitro-extended-profile__relationship-subcopy { From fa71e8eb4aa1529ce843be6ef523612189115f94 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 30 May 2026 07:18:05 +0200 Subject: [PATCH 24/24] =?UTF-8?q?=F0=9F=86=99=20Toolbar=20fix=20in=20rare?= =?UTF-8?q?=20cases=20the=20room-tools=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/toolbar/ToolbarView.tsx | 31 +++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7ce61b9..2c2dd26 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -34,6 +34,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = 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); const [ youtubeEnabled, setYoutubeEnabled ] = useState(false); const { userFigure = null } = useSessionInfo(); @@ -115,6 +116,33 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => return () => query.removeEventListener('change', updateTouchLayout); }, []); + // Keep the left staff-tools stack pinned 15px above the room tools rail + // (its height is dynamic, so measure it). Falls back to null (CSS + // default) when the room tools aren't present, e.g. outside a room. + useEffect(() => + { + const measure = () => + { + const roomTools = document.querySelector('.nitro-room-tools-container') as HTMLElement | null; + const next = roomTools + ? Math.max(8, Math.round(window.innerHeight - roomTools.getBoundingClientRect().top + 15)) + : null; + + setStaffStackBottom(prevValue => (prevValue === next ? prevValue : next)); + }; + + measure(); + + const interval = window.setInterval(measure, 400); + window.addEventListener('resize', measure); + + return () => + { + window.clearInterval(interval); + window.removeEventListener('resize', measure); + }; + }, [ isInRoom ]); + const openYouTubePlayer = () => window.dispatchEvent(new CustomEvent('youtube:toggle')); useMessageEvent(PerkAllowancesMessageEvent, event => @@ -405,7 +433,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => animate={ visibilityVariant } variants={ mobileNavVariants } transition={ NAV_TRANSITION } - className={ `fixed left-1 top-1/2 z-40 flex -translate-y-1/2 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)] ${ mobileOnlyClasses }` }> + 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 }` }> CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />