From d0c11f047aa2bc72e80e7528ffe4ec93a5612a23 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 11:30:17 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Complete=20rebuild=20of=20toolba?= =?UTF-8?q?r=20/=20catalog=20/=20inventory=20make=20it=20100%=20mobile=20f?= =?UTF-8?q?riendly=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';