diff --git a/src/assets/images/catalog/bitmap/catalog_skin1.png b/src/assets/images/catalog/bitmap/catalog_skin1.png new file mode 100644 index 0000000..388d851 Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin1.png differ diff --git a/src/assets/images/catalog/bitmap/catalog_skin2.png b/src/assets/images/catalog/bitmap/catalog_skin2.png new file mode 100644 index 0000000..a8df6c3 Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin2.png differ diff --git a/src/assets/images/catalog/bitmap/catalog_skin3.png b/src/assets/images/catalog/bitmap/catalog_skin3.png new file mode 100644 index 0000000..f79cf9c Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin3.png differ diff --git a/src/assets/images/catalog/bitmap/catalog_skin4.png b/src/assets/images/catalog/bitmap/catalog_skin4.png new file mode 100644 index 0000000..f79cf9c Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin4.png differ diff --git a/src/assets/images/catalog/bitmap/catalog_skin5.png b/src/assets/images/catalog/bitmap/catalog_skin5.png new file mode 100644 index 0000000..f79cf9c Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin5.png differ diff --git a/src/assets/images/catalog/bitmap/catalog_skin6.png b/src/assets/images/catalog/bitmap/catalog_skin6.png new file mode 100644 index 0000000..b89f908 Binary files /dev/null and b/src/assets/images/catalog/bitmap/catalog_skin6.png differ diff --git a/src/assets/images/catalog/buttons/btn_secondary.png b/src/assets/images/catalog/buttons/btn_secondary.png new file mode 100644 index 0000000..39a109b Binary files /dev/null and b/src/assets/images/catalog/buttons/btn_secondary.png differ diff --git a/src/assets/images/catalog/buttons/btn_secondary_disabled.png b/src/assets/images/catalog/buttons/btn_secondary_disabled.png new file mode 100644 index 0000000..c410600 Binary files /dev/null and b/src/assets/images/catalog/buttons/btn_secondary_disabled.png differ diff --git a/src/assets/images/catalog/buttons/btn_secondary_hover.png b/src/assets/images/catalog/buttons/btn_secondary_hover.png new file mode 100644 index 0000000..5c71eca Binary files /dev/null and b/src/assets/images/catalog/buttons/btn_secondary_hover.png differ diff --git a/src/assets/images/catalog/buttons/btn_secondary_pressed.png b/src/assets/images/catalog/buttons/btn_secondary_pressed.png new file mode 100644 index 0000000..7b4ad96 Binary files /dev/null and b/src/assets/images/catalog/buttons/btn_secondary_pressed.png differ diff --git a/src/assets/images/catalog/buttons/buy.png b/src/assets/images/catalog/buttons/buy.png new file mode 100644 index 0000000..de4bf35 Binary files /dev/null and b/src/assets/images/catalog/buttons/buy.png differ diff --git a/src/assets/images/catalog/buttons/buy_disabled.png b/src/assets/images/catalog/buttons/buy_disabled.png new file mode 100644 index 0000000..2dbb3c6 Binary files /dev/null and b/src/assets/images/catalog/buttons/buy_disabled.png differ diff --git a/src/assets/images/catalog/buttons/buy_hover.png b/src/assets/images/catalog/buttons/buy_hover.png new file mode 100644 index 0000000..de4bf35 Binary files /dev/null and b/src/assets/images/catalog/buttons/buy_hover.png differ diff --git a/src/assets/images/catalog/buttons/buy_pressed.png b/src/assets/images/catalog/buttons/buy_pressed.png new file mode 100644 index 0000000..de4bf35 Binary files /dev/null and b/src/assets/images/catalog/buttons/buy_pressed.png differ diff --git a/src/assets/images/catalog/buttons/close.png b/src/assets/images/catalog/buttons/close.png new file mode 100644 index 0000000..2478b0a Binary files /dev/null and b/src/assets/images/catalog/buttons/close.png differ diff --git a/src/assets/images/catalog/buttons/close_hover.png b/src/assets/images/catalog/buttons/close_hover.png new file mode 100644 index 0000000..d331448 Binary files /dev/null and b/src/assets/images/catalog/buttons/close_hover.png differ diff --git a/src/assets/images/catalog/buttons/close_pressed.png b/src/assets/images/catalog/buttons/close_pressed.png new file mode 100644 index 0000000..1bc2abf Binary files /dev/null and b/src/assets/images/catalog/buttons/close_pressed.png differ diff --git a/src/assets/images/catalog/buttons/help.png b/src/assets/images/catalog/buttons/help.png new file mode 100644 index 0000000..7cd4f14 Binary files /dev/null and b/src/assets/images/catalog/buttons/help.png differ diff --git a/src/assets/images/catalog/buttons/help_hover.png b/src/assets/images/catalog/buttons/help_hover.png new file mode 100644 index 0000000..901d0e9 Binary files /dev/null and b/src/assets/images/catalog/buttons/help_hover.png differ diff --git a/src/assets/images/catalog/buttons/help_pressed.png b/src/assets/images/catalog/buttons/help_pressed.png new file mode 100644 index 0000000..71477b6 Binary files /dev/null and b/src/assets/images/catalog/buttons/help_pressed.png differ diff --git a/src/assets/images/catalog/buttons/minus.png b/src/assets/images/catalog/buttons/minus.png new file mode 100644 index 0000000..0a64845 Binary files /dev/null and b/src/assets/images/catalog/buttons/minus.png differ diff --git a/src/assets/images/catalog/buttons/minus_disabled.png b/src/assets/images/catalog/buttons/minus_disabled.png new file mode 100644 index 0000000..ab8f0e1 Binary files /dev/null and b/src/assets/images/catalog/buttons/minus_disabled.png differ diff --git a/src/assets/images/catalog/buttons/minus_hover.png b/src/assets/images/catalog/buttons/minus_hover.png new file mode 100644 index 0000000..c3108ba Binary files /dev/null and b/src/assets/images/catalog/buttons/minus_hover.png differ diff --git a/src/assets/images/catalog/buttons/minus_pressed.png b/src/assets/images/catalog/buttons/minus_pressed.png new file mode 100644 index 0000000..9410231 Binary files /dev/null and b/src/assets/images/catalog/buttons/minus_pressed.png differ diff --git a/src/assets/images/catalog/buttons/plus.png b/src/assets/images/catalog/buttons/plus.png new file mode 100644 index 0000000..a81fd05 Binary files /dev/null and b/src/assets/images/catalog/buttons/plus.png differ diff --git a/src/assets/images/catalog/buttons/plus_disabled.png b/src/assets/images/catalog/buttons/plus_disabled.png new file mode 100644 index 0000000..77817b9 Binary files /dev/null and b/src/assets/images/catalog/buttons/plus_disabled.png differ diff --git a/src/assets/images/catalog/buttons/plus_hover.png b/src/assets/images/catalog/buttons/plus_hover.png new file mode 100644 index 0000000..842451e Binary files /dev/null and b/src/assets/images/catalog/buttons/plus_hover.png differ diff --git a/src/assets/images/catalog/buttons/plus_pressed.png b/src/assets/images/catalog/buttons/plus_pressed.png new file mode 100644 index 0000000..f04fe26 Binary files /dev/null and b/src/assets/images/catalog/buttons/plus_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_left.png b/src/assets/images/catalog/scrollbar/scroll_h_left.png new file mode 100644 index 0000000..9b55972 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_left.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_left_disabled.png b/src/assets/images/catalog/scrollbar/scroll_h_left_disabled.png new file mode 100644 index 0000000..ea9dcd6 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_left_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_left_pressed.png b/src/assets/images/catalog/scrollbar/scroll_h_left_pressed.png new file mode 100644 index 0000000..b1fbb2b Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_left_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_right.png b/src/assets/images/catalog/scrollbar/scroll_h_right.png new file mode 100644 index 0000000..4219e42 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_right.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_right_disabled.png b/src/assets/images/catalog/scrollbar/scroll_h_right_disabled.png new file mode 100644 index 0000000..e5ae9e0 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_right_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png b/src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png new file mode 100644 index 0000000..7444586 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_right_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_thumb.png b/src/assets/images/catalog/scrollbar/scroll_h_thumb.png new file mode 100644 index 0000000..d4818bf Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_thumb.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png b/src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png new file mode 100644 index 0000000..d75689d Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_thumb_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png b/src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png new file mode 100644 index 0000000..0627403 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_down.png b/src/assets/images/catalog/scrollbar/scroll_v_down.png new file mode 100644 index 0000000..3962e1c Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_down.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_down_disabled.png b/src/assets/images/catalog/scrollbar/scroll_v_down_disabled.png new file mode 100644 index 0000000..5af6c6b Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_down_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_down_pressed.png b/src/assets/images/catalog/scrollbar/scroll_v_down_pressed.png new file mode 100644 index 0000000..ead6309 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_down_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_thumb.png b/src/assets/images/catalog/scrollbar/scroll_v_thumb.png new file mode 100644 index 0000000..7c1670e Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_thumb.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_thumb_disabled.png b/src/assets/images/catalog/scrollbar/scroll_v_thumb_disabled.png new file mode 100644 index 0000000..e4ffdcb Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_thumb_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png b/src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png new file mode 100644 index 0000000..7fbaffb Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_up.png b/src/assets/images/catalog/scrollbar/scroll_v_up.png new file mode 100644 index 0000000..1fb6071 Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_up.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_up_disabled.png b/src/assets/images/catalog/scrollbar/scroll_v_up_disabled.png new file mode 100644 index 0000000..c79977e Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_up_disabled.png differ diff --git a/src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png b/src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png new file mode 100644 index 0000000..aa2a47d Binary files /dev/null and b/src/assets/images/catalog/scrollbar/scroll_v_up_pressed.png differ diff --git a/src/components/catalog/CatalogAdminContext.tsx b/src/components/catalog/CatalogAdminContext.tsx index a0d70fb..8de4180 100644 --- a/src/components/catalog/CatalogAdminContext.tsx +++ b/src/components/catalog/CatalogAdminContext.tsx @@ -1,4 +1,4 @@ -import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer'; +import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminLoadOfferComposer, CatalogAdminLoadPageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminOfferDetailsEvent, CatalogAdminPageDetailsEvent, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer'; import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ICatalogNode, IPurchasableOffer, NotificationAlertType, SendMessageComposer } from '../../api'; import { useCatalogUiState, useMessageEvent, useNotification } from '../../hooks'; @@ -44,12 +44,34 @@ export interface IOfferEditData orderNumber: number; } +export interface IEditingOfferDetails +{ + offerId: number; + offerIdGroup: number; + limitedStack: number; + orderNumber: number; +} + +export interface IEditingPageDetails +{ + pageId: number; + caption: string; + captionSave: string; + minRank: number; + orderNum: number; + visible: boolean; + enabled: boolean; +} + interface ICatalogAdminContext { adminMode: boolean; setAdminMode: (value: boolean) => void; editingOffer: IPurchasableOffer | null; setEditingOffer: (offer: IPurchasableOffer | null) => void; + editingOfferDetails: IEditingOfferDetails | null; + editingPageDetails: IEditingPageDetails | null; + requestPageDetails: (pageId: number) => void; editingPageData: boolean; setEditingPageData: (value: boolean) => void; editingRootPage: boolean; @@ -80,7 +102,9 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) { const { currentType } = useCatalogUiState(); const [ adminMode, setAdminMode ] = useState(false); - const [ editingOffer, setEditingOffer ] = useState(null); + const [ editingOffer, setEditingOfferState ] = useState(null); + const [ editingOfferDetails, setEditingOfferDetails ] = useState(null); + const [ editingPageDetails, setEditingPageDetails ] = useState(null); const [ editingPageData, setEditingPageData ] = useState(false); const [ editingRootPage, setEditingRootPage ] = useState(false); const [ editingPageNode, setEditingPageNode ] = useState(null); @@ -90,6 +114,51 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) const pendingActionRef = useRef(null); const { simpleAlert = null } = useNotification(); + const setEditingOffer = useCallback((offer: IPurchasableOffer | null) => + { + setEditingOfferState(offer); + setEditingOfferDetails(null); + + if(offer && offer.offerId !== -1) + { + SendMessageComposer(new CatalogAdminLoadOfferComposer(offer.offerId, currentType)); + } + }, [ currentType ]); + + useMessageEvent(CatalogAdminOfferDetailsEvent, (event: CatalogAdminOfferDetailsEvent) => + { + const parser = event.getParser(); + + setEditingOfferDetails({ + offerId: parser.offerId, + offerIdGroup: parser.offerIdGroup, + limitedStack: parser.limitedStack, + orderNumber: parser.orderNumber + }); + }); + + useMessageEvent(CatalogAdminPageDetailsEvent, (event: CatalogAdminPageDetailsEvent) => + { + const parser = event.getParser(); + + setEditingPageDetails({ + pageId: parser.pageId, + caption: parser.caption, + captionSave: parser.captionSave, + minRank: parser.minRank, + orderNum: parser.orderNum, + visible: parser.visible, + enabled: parser.enabled + }); + }); + + const requestPageDetails = useCallback((pageId: number) => + { + setEditingPageDetails(null); + if(pageId == null || pageId < 0) return; + SendMessageComposer(new CatalogAdminLoadPageComposer(pageId, currentType)); + }, [ currentType ]); + useEffect(() => { if(!adminMode) return; @@ -288,7 +357,8 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) return ( = () => { - const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData(); + const { rootNode = null, currentPage = null, currentOffer = 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(); @@ -34,6 +34,11 @@ const CatalogClassicViewInner: FC<{}> = () => const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false); const { purse = null } = usePurse(); const displayedCurrencies = GetConfigurationValue('system.currency.types', []); + const activeCatalogNode = activeNodes?.[activeNodes.length - 1] ?? null; + // Strip SWF-style suffixes like "(BC)" or "(Hot)" but keep the + // pageId hint the gameserver appends when the viewer has + // ACC_CATALOG_IDS - that's a pure-numeric "(6)" trailer. + const getSwfTabLabel = (label: string) => (label || '').replace(/\s*\(\D[^)]*\)\s*$/g, '').trim(); const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER) ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' } : undefined; @@ -122,7 +127,7 @@ const CatalogClassicViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } />
{ isMod && @@ -161,20 +166,19 @@ const CatalogClassicViewInner: FC<{}> = () =>
{ adminMode && -
- Admin Mode - -
} + } { rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) => { if(!adminMode && !child.isVisible) return null; + if(!adminMode && (index === 0) && getSwfTabLabel(child.localization).toLowerCase().includes('rari')) return null; const isHidden = !child.isVisible; @@ -186,8 +190,9 @@ const CatalogClassicViewInner: FC<{}> = () => activateNode(child); } }>
- - { child.localization } + { (child.iconId > 0) && + } + { getSwfTabLabel(child.localization) } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -215,6 +220,20 @@ const CatalogClassicViewInner: FC<{}> = () => } +
+
+
+ +
+
+
+ { currentType === CatalogType.BUILDER ? LocalizeText('builder.header.title') : getSwfTabLabel(activeCatalogNode?.localization ?? LocalizeText('catalog.title')) } +
+ { currentType === CatalogType.BUILDER + ?
{ LocalizeText('builder.header.status.membership') }
+ :
} +
+
{ adminMode && rootNode && @@ -252,8 +271,7 @@ const CatalogClassicViewInner: FC<{}> = () =>
- { /* 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) && } + { !!currentPage?.localization?.getImage(0) && }
diff --git a/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx index c61c14b..0259589 100644 --- a/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx +++ b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx @@ -10,6 +10,7 @@ export const CatalogAdminOfferEditView: FC<{}> = () => const { currentPage = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const editingOffer = catalogAdmin?.editingOffer ?? null; + const editingOfferDetails = catalogAdmin?.editingOfferDetails ?? null; const setEditingOffer = catalogAdmin?.setEditingOffer; const saveOffer = catalogAdmin?.saveOffer; const deleteOffer = catalogAdmin?.deleteOffer; @@ -62,12 +63,21 @@ export const CatalogAdminOfferEditView: FC<{}> = () => setClubOnly(editingOffer.clubLevel > 0 ? '1' : '0'); setExtradata(editingOffer.product?.extraParam || ''); setHaveOffer(editingOffer.haveOffer ? '1' : '0'); - setOfferIdGroup(editingOffer.offerId || -1); + setOfferIdGroup(0); setLimitedStack(0); setOrderNumber(0); } }, [ editingOffer ]); + useEffect(() => + { + if(!editingOfferDetails) return; + + setOfferIdGroup(editingOfferDetails.offerIdGroup); + setLimitedStack(editingOfferDetails.limitedStack); + setOrderNumber(editingOfferDetails.orderNumber); + }, [ editingOfferDetails ]); + if(!editingOffer) return null; const handleSave = async () => diff --git a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx index 94b2326..1b4c201 100644 --- a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx +++ b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx @@ -28,6 +28,8 @@ export const CatalogAdminPageEditView: FC<{}> = () => const editingPageData = catalogAdmin?.editingPageData ?? false; const editingRootPage = catalogAdmin?.editingRootPage ?? false; const editingPageNode = catalogAdmin?.editingPageNode ?? null; + const editingPageDetails = catalogAdmin?.editingPageDetails ?? null; + const requestPageDetails = catalogAdmin?.requestPageDetails; const loading = catalogAdmin?.loading ?? false; const [ caption, setCaption ] = useState(''); @@ -67,21 +69,22 @@ export const CatalogAdminPageEditView: FC<{}> = () => { if(!editingPageData || !targetNode) return; - // 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*$`), ''); + // Don't read the decorated caption out of the catalog index - + // the gameserver appends " (id)" when ACC_CATALOG_IDS is on and + // we don't want that round-tripping back into the DB. Wait for + // the admin page-details event to land instead; it carries the + // raw caption / caption_save / min_rank / order_num / enabled. + setCaption(''); + setCaptionSave(''); + setMinRank(1); + setOrderNum(0); + setEnabled('1'); - setCaption(rawCaption); - setCaptionSave(targetNode.pageName || rawCaption); setCatalogMode(currentType === CatalogType.BUILDER ? 'BUILDER' : 'NORMAL'); setPageLayout(currentPage?.layoutCode || 'default_3x3'); setIconImage(targetNode.iconId ?? 0); setVisible(targetNode.isVisible ? '1' : '0'); - setEnabled('1'); - setMinRank(1); - setOrderNum(0); + const matchesLoadedPage = currentPage && targetPageId === currentPage.pageId; const existingText1 = matchesLoadedPage && currentPage.localization ? currentPage.localization.getText(0) @@ -94,7 +97,22 @@ export const CatalogAdminPageEditView: FC<{}> = () => setParentId(typeof wireParentId === 'number' && wireParentId !== -1 ? wireParentId : (targetNode.parent ? targetNode.parent.pageId : -1)); - }, [ editingPageData, targetNode, currentPage, currentType ]); + + if(targetPageId != null && targetPageId >= 0) requestPageDetails?.(targetPageId); + }, [ editingPageData, targetNode, currentPage, currentType, targetPageId, requestPageDetails ]); + + useEffect(() => + { + if(!editingPageDetails) return; + if(targetPageId != null && editingPageDetails.pageId !== targetPageId) return; + + setCaption(editingPageDetails.caption); + setCaptionSave(editingPageDetails.captionSave); + setMinRank(editingPageDetails.minRank); + setOrderNum(editingPageDetails.orderNum); + setVisible(editingPageDetails.visible ? '1' : '0'); + setEnabled(editingPageDetails.enabled ? '1' : '0'); + }, [ editingPageDetails, targetPageId ]); if(!editingPageData || !targetNode) return null; @@ -168,7 +186,7 @@ export const CatalogAdminPageEditView: FC<{}> = () => const handleDelete = async () => { if(!catalogAdmin?.deletePage || isRoot) return; - if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ targetNode.localization ]))) return; + if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ editingPageDetails?.caption ?? '' ]))) return; catalogAdmin.deletePage(targetPageId); @@ -179,7 +197,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
- { isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ targetNode.localization }` } + { isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ editingPageDetails?.caption ?? '' }` }
diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index 973179c..18e68af 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -23,6 +23,10 @@ export const CatalogNavigationItemView: FC = pro const isFav = node ? isFavoritePage(node.pageId) : false; const [ isDragOver, setIsDragOver ] = useState(false); const dragRef = useRef(null); + // Strip SWF-style suffixes like "(BC)" or "(Hot)" but keep the + // pageId hint the gameserver appends when the viewer has + // ACC_CATALOG_IDS - that's a pure-numeric "(6)" trailer. + const swfLabel = (node?.localization || '').replace(/\s*\(\D[^)]*\)\s*$/g, '').trim(); const handleDragStart = useCallback((e: React.DragEvent) => { @@ -90,7 +94,7 @@ export const CatalogNavigationItemView: FC = pro
- { node.localization } + { swfLabel } { adminMode &&
= props => return null; } - return offer.product?.getIconUrl(offer) ?? null; + const product = offer.product; + + if(!product) return null; + + if((product.productType === ProductTypeEnum.FLOOR) || (product.productType === ProductTypeEnum.WALL)) + { + const className = product.furnitureData?.className; + + if(className?.length) + { + const param = (product.productType === ProductTypeEnum.WALL && product.extraParam?.length) ? `_${ product.extraParam }` : ''; + const configuredIconUrl = GetConfigurationValue('furni.asset.icon.url', ''); + + if(configuredIconUrl?.length) + { + return configuredIconUrl + .replace('%libname%', className) + .replace('%param%', param); + } + } + } + + return product.getIconUrl(offer) ?? null; }, [ offer ]); + const prices = useMemo(() => + { + if(!offer) return []; + + const values: { amount: number; type: number }[] = []; + + if(offer.priceInCredits > 0) values.push({ amount: offer.priceInCredits, type: -1 }); + if(offer.priceInActivityPoints > 0) values.push({ amount: offer.priceInActivityPoints, type: offer.activityPointType }); + + return values; + }, [ offer ]); + + const getCurrencyIconUrl = (type: number) => + { + const configuredCurrencyUrl = GetConfigurationValue('currency.asset.icon.url', ''); + + return configuredCurrencyUrl.replace('%type%', type.toString()); + }; + const onMouseEvent = (event: MouseEvent) => { switch(event.type) @@ -74,9 +115,30 @@ export const CatalogGridOfferView: FC = props => { ...rest } > { iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) && -
} + + { + const fallbackIconUrl = product.getIconUrl(offer); + + if(fallbackIconUrl && (event.currentTarget.src !== fallbackIconUrl)) event.currentTarget.src = fallbackIconUrl; + } } /> } { (offer.product.productType === ProductTypeEnum.ROBOT) && } + { (prices.length > 0) && + 1 ? 'is-multi-price' : 'is-single-price' }` }> + { prices.map((price, index) => + + { index > 0 && + } + { price.amount } + + ) } + }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 09411d1..d8517f5 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, FaExchangeAlt, FaPlus, FaSyncAlt } from 'react-icons/fa'; import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalogData } from '../../../../../hooks'; @@ -20,6 +20,7 @@ export const CatalogLayoutDefaultView: FC = props => const { currentOffer = null, currentPage = null, roomPreviewer = null } = useCatalogData(); const catalogAdmin = useCatalogAdmin(); const adminMode = catalogAdmin?.adminMode ?? false; + const offerName = currentOffer?.localizationName?.replace(/\s*\([^)]*\)\s*$/g, ''); return (
@@ -40,63 +41,87 @@ export const CatalogLayoutDefaultView: FC = props => > { LocalizeText('catalog.admin.offer.new') } + { currentOffer && + }
} - { currentOffer && -
-
- { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && - <> - - - - - } - { (currentOffer.product.productType === ProductTypeEnum.BADGE) && - } -
-
-
-
- { currentOffer.localizationName } +
+ { currentOffer && +
+
+ { offerName } + { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && + <> + + + + + } + { (currentOffer.product.productType === ProductTypeEnum.BADGE) && + } +
+
+
+
+ { offerName } + { adminMode && + catalogAdmin.setEditingOffer(currentOffer) } + /> } +
{ adminMode && - catalogAdmin.setEditingOffer(currentOffer) } - /> } +
+ ID: { currentOffer.product.productClassId } + Offer: { currentOffer.offerId } + { currentOffer.product.productType.toUpperCase() } +
} +
- { adminMode && -
- ID: { currentOffer.product.productClassId } - Offer: { currentOffer.offerId } - { currentOffer.product.productType.toUpperCase() } -
} -
- - -
- -
-
-
} +
} - { !currentOffer && -
- { !!page.localization.getImage(1) && - } - -
} + { !currentOffer && +
+ { !!page.localization.getImage(1) && + } + +
} +
{ GetConfigurationValue('catalog.headers') && } - +
+ + { currentOffer && +
+
+ +
+
+ +
+
} + + { currentOffer && +
+
+ +
+
}
); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx index 6d28e1e..145620d 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -17,14 +17,14 @@ export const CatalogLayouGuildCustomFurniView: FC = props => return ( - - + + - + { !currentOffer && <> - { !!page.localization.getImage(1) && } - + { !!page.localization.getImage(1) && } + } { currentOffer && <> @@ -33,7 +33,7 @@ export const CatalogLayouGuildCustomFurniView: FC = props =>
- { currentOffer.localizationName } + { currentOffer.localizationName }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx index ade3125..5432bb2 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx @@ -20,6 +20,7 @@ export const CatalogLayoutRoomAdsView: FC = props => const { categories } = useNavigatorData(); const { setIsVisible = null } = useCatalogUiState(); const { promoteInformation, isExtended, setIsExtended } = useRoomPromote(); + const promoteData = promoteInformation?.data ?? null; const { data: availableRooms = [] } = useNitroQuery({ key: [ 'nitro', 'catalog', 'room-ad-purchase-info' ], @@ -31,17 +32,17 @@ export const CatalogLayoutRoomAdsView: FC = props => useEffect(() => { - if(isExtended) + if(isExtended && promoteData) { - setRoomId(promoteInformation.data.flatId); - setEventName(promoteInformation.data.eventName); - setEventDesc(promoteInformation.data.eventDescription); - setCategoryId(promoteInformation.data.categoryId); + setRoomId(promoteData.flatId); + setEventName(promoteData.eventName); + setEventDesc(promoteData.eventDescription); + setCategoryId(promoteData.categoryId); setExtended(isExtended); // This is for sending to packet setIsExtended(false); // This is from hook useRoomPromotte } - }, [ isExtended, eventName, eventDesc, categoryId, promoteInformation.data, setIsExtended ]); + }, [ isExtended, promoteData, setIsExtended ]); const resetData = () => { diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx index 7b8f1ec..a3cab10 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx @@ -28,7 +28,7 @@ export const CatalogLayoutSingleBundleView: FC = props => { page.localization.getText(1) } } { !!page.localization.getImage(1) && - } + } diff --git a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx index ce68f37..6ccf2a1 100644 --- a/src/components/catalog/views/page/layout/GetCatalogLayout.tsx +++ b/src/components/catalog/views/page/layout/GetCatalogLayout.tsx @@ -1,7 +1,6 @@ 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'; @@ -35,8 +34,6 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) { case 'frontpage_featured': return null; - case 'info_duckets': - return ; case 'frontpage4': return ; case 'pets': diff --git a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx index 7081428..96dff71 100644 --- a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx +++ b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx @@ -206,7 +206,7 @@ export const CatalogLayoutPetView: FC = props =>
} { /* Top card: preview + name + purchase */ } -
+
{ /* Pet preview */ }
@@ -240,12 +240,12 @@ export const CatalogLayoutPetView: FC = props => Offer: { currentOffer.offerId }
} { !!page.localization.getText(0) && -

} +

}

{ /* Name input */ }
- +
0 ? 'border-danger bg-danger/5' : approvalResult === 0 ? 'border-success bg-success/5' : 'border-card-grid-item-border focus:border-primary bg-white' }` } @@ -267,7 +267,7 @@ export const CatalogLayoutPetView: FC = props =>
diff --git a/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx index a83a981..80bcd00 100644 --- a/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx @@ -19,17 +19,17 @@ export const CatalogPriceDisplayWidgetView: FC +
{ (offer.priceInCredits > 0) && -
- { (offer.priceInCredits * quantity) } +
+ { (offer.priceInCredits * quantity) }
} { separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) && - } + } { (offer.priceInActivityPoints > 0) && -
- { (offer.priceInActivityPoints * quantity) } +
+ { (offer.priceInActivityPoints * quantity) }
}
diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index e04cac0..44feade 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -171,6 +171,8 @@ export const CatalogPurchaseWidgetView: FC = pro const PurchaseButton = () => { + const swfButtonClassNames = [ 'nitro-catalog-swf-button' ]; + if(isBuildersClubPlaceable) { const hasMissingExtraParam = (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)); @@ -198,10 +200,10 @@ export const CatalogPurchaseWidgetView: FC = pro return (
- -
@@ -220,37 +222,37 @@ export const CatalogPurchaseWidgetView: FC = pro const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity); const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity); - if(GetClubMemberLevel() < currentOffer.clubLevel) return ; + if(GetClubMemberLevel() < currentOffer.clubLevel) return ; - if(isLimitedSoldOut) return ; + if(isLimitedSoldOut) return ; - if(priceCredits > getCurrencyAmount(-1)) return ; + if(priceCredits > getCurrencyAmount(-1)) return ; - if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ; + if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ; switch(purchaseState) { case CatalogPurchaseState.CONFIRM: - return ; + return ; case CatalogPurchaseState.PURCHASE: - return ; + return ; case CatalogPurchaseState.FAILED: - return ; + return ; case CatalogPurchaseState.SOLD_OUT: - return ; + return ; case CatalogPurchaseState.NONE: default: - return ; + return ; } }; return ( <> - { (!isBuildersClubOffer && !noGiftOption && !currentOffer.isRentOffer) && - } + ); }; diff --git a/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx index 573465e..6ba296b 100644 --- a/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx @@ -34,26 +34,26 @@ export const CatalogSpinnerWidgetView: FC<{}> = props => if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null; return ( -
- { LocalizeText('catalog.bundlewidget.spinner.select.amount') } -
+
+ { LocalizeText('catalog.bundlewidget.quantity') } +
updateQuantity(event.target.valueAsNumber) } />
diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index f63f721..8beb08e 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -119,5 +119,5 @@ export const CatalogViewProductWidgetView: FC<{}> = props => ); } - return ; + return ; }; diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx index 5eae3d2..f3458c5 100644 --- a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx @@ -7,11 +7,25 @@ interface ChatInputCommandSelectorViewProps selectedIndex: number; onSelect: (command: CommandDefinition) => void; onHover: (index: number) => void; + /** + * When true, render the flat minimalist look (gray list, dark-blue + * selection). When false / undefined (default) the picker wears the + * Habbo NitroCard chrome with the green :command header strip. + */ + newStyle?: boolean; } +/** + * :command autocomplete popover. Two visual modes, both driven by the + * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style): + * + * - newStyle = false (default): cream cardstock, habbo-green header, + * UbuntuCondensed names, green ":" tile, custom Habbo scrollbar. + * - newStyle = true: flat gray list, dark-blue selection, plain text rows. + */ export const ChatInputCommandSelectorView: FC = props => { - const { commands = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const { commands = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props; const listRef = useRef(null); useEffect(() => @@ -23,19 +37,57 @@ export const ChatInputCommandSelectorView: FC if(selected) selected.scrollIntoView({ block: 'nearest' }); }, [ selectedIndex ]); + if(newStyle) + { + return ( +
+ { commands.map((cmd, index) => ( +
onSelect(cmd) } + onMouseEnter={ () => onHover(index) } + > + :{ cmd.key } + { cmd.description } +
+ )) } +
+ ); + } + return ( -
- { commands.map((cmd, index) => ( -
onSelect(cmd) } - onMouseEnter={ () => onHover(index) } - > - :{ cmd.key } - { cmd.description } -
- )) } +
+
+ + : Command +
+
+ { commands.map((cmd, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = [ + 'chat-input-command-row', + isSelected ? 'is-selected' : '' + ].filter(Boolean).join(' '); + + return ( +
onSelect(cmd) } + onMouseEnter={ () => onHover(index) } + > +
:
+
+ :{ cmd.key } + { cmd.description && + { cmd.description } } +
+
+ ); + }) } +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx index 370786f..0c860ad 100644 --- a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx @@ -19,11 +19,28 @@ interface ChatInputMentionSelectorViewProps selectedIndex: number; onSelect: (suggestion: MentionSuggestion) => void; onHover: (index: number) => void; + /** + * When true, render the flat minimalist look (gray list, dark-blue + * selection, no header / no kind chip). When false / undefined (default) + * the picker wears the Habbo NitroCard chrome. + */ + newStyle?: boolean; } +/** + * @-mention autocomplete popover. Two visual modes, both driven by the + * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style): + * + * - newStyle = false (default): cream cardstock, habbo-blue header, + * UbuntuCondensed names, kind chips, custom Habbo scrollbar. + * - newStyle = true: flat gray list, dark-blue selection, plain text rows. + * + * Both modes share the same suggestion structure and keyboard contract - + * the difference is purely cosmetic. + */ export const ChatInputMentionSelectorView: FC = props => { - const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props; const listRef = useRef(null); useEffect(() => @@ -37,39 +54,92 @@ export const ChatInputMentionSelectorView: FC if(suggestions.length === 0) return null; - return ( -
- { suggestions.map((suggestion, index) => - { - const isSelected = (index === selectedIndex); - const rowClass = `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ isSelected ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }`; + if(newStyle) + { + return ( +
+ { suggestions.map((suggestion, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ isSelected ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }`; - return ( -
onSelect(suggestion) } - onMouseEnter={ () => onHover(index) } - > - { suggestion.kind === 'user' && suggestion.figure - ? ( -
- -
- ) - : ( -
@
- ) } - @{ suggestion.name } - { suggestion.description && { suggestion.description } } -
- ); - }) } + return ( +
onSelect(suggestion) } + onMouseEnter={ () => onHover(index) } + > + { suggestion.kind === 'user' && suggestion.figure + ? ( +
+ +
+ ) + : ( +
@
+ ) } + @{ suggestion.name } + { suggestion.description && { suggestion.description } } +
+ ); + }) } +
+ ); + } + + return ( +
+
+ + @ Mention +
+
+ { suggestions.map((suggestion, index) => + { + const isSelected = (index === selectedIndex); + const rowClass = [ + 'chat-input-mention-row', + isSelected ? 'is-selected' : '' + ].filter(Boolean).join(' '); + + return ( +
onSelect(suggestion) } + onMouseEnter={ () => onHover(index) } + > + { suggestion.kind === 'user' && suggestion.figure + ? ( +
+ +
+ ) + : ( +
@
+ ) } +
+ @{ suggestion.name } + { suggestion.description && + { suggestion.description } } +
+ + { suggestion.kind === 'alias' ? 'Broadcast' : 'User' } + +
+ ); + }) } +
); }; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 85d3198..551795a 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; import { Text } from '../../../../common'; -import { useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; +import { useCatalogClassicStyle, useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; import { useRoomUserListSnapshot } from '../../../../hooks/session/useSessionSnapshots'; import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView'; import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; @@ -58,6 +58,11 @@ export const ChatInputView: FC<{}> = props => const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); + // The "New style" user-setting (memenu.settings.other.catalog.classic.style) + // drives BOTH the catalog layout and the mention-picker chrome: + // false (default) = Habbo old-school NitroCard cardstock look + // true = flat minimalist gray look + const [ newStyle ] = useCatalogClassicStyle(); const mentionContext = useMemo(() => { @@ -485,6 +490,7 @@ export const ChatInputView: FC<{}> = props => setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } onHover={ setSelectedIndex } + newStyle={ newStyle } /> } { mentionSelectorVisible && !commandSelectorVisible && = props => selectedIndex={ mentionSelectedIndex } onSelect={ applyMentionSuggestion } onHover={ setMentionSelectedIndex } + newStyle={ newStyle } /> }
{ !floodBlocked && diff --git a/src/css/backgrounds/BackgroundsView.css b/src/css/backgrounds/BackgroundsView.css index d08b2b1..a5e1fa8 100644 --- a/src/css/backgrounds/BackgroundsView.css +++ b/src/css/backgrounds/BackgroundsView.css @@ -1629,6 +1629,9 @@ .infostand-border.border-15 { border-color: #cbd5e1; } /* Silver */ .infostand-border.border-16 { border-color: #1f2937; } /* Black */ +/* Image-based borders (17-25). These override the colour-border insets and + strip the CSS border so the artwork sits ~22px outside the card and + stretches to fill the frame area. */ .infostand-border.border-17, .infostand-border.border-18, .infostand-border.border-19, @@ -1659,6 +1662,8 @@ .infostand-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); } .infostand-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } +/* Picker thumbnails inside the BackgroundsView "Borders" tab. + Each thumbnail is a small rounded box outlined in its border colour. */ .profile-border { width: 60px; height: 76px; @@ -1669,7 +1674,9 @@ background: rgba(255, 255, 255, 0.05); } +/* border-0 = no border (default) — show as a dashed translucent outline */ .profile-border.border-0 { border: 2px dashed rgba(255, 255, 255, 0.25); } + .profile-border.border-1 { border-color: #ef4444; } .profile-border.border-2 { border-color: #f97316; } .profile-border.border-3 { border-color: #eab308; } @@ -1686,6 +1693,8 @@ .profile-border.border-14 { border-color: #d4a020; } .profile-border.border-15 { border-color: #cbd5e1; } .profile-border.border-16 { border-color: #1f2937; } + +/* Image-border picker thumbnails — drop the CSS frame and show the artwork. */ .profile-border.border-17, .profile-border.border-18, .profile-border.border-19, @@ -1710,61 +1719,4 @@ .profile-border.border-22 { background-image: url('@/assets/images/backgrounds/borders/border_22.webp'); } .profile-border.border-23 { background-image: url('@/assets/images/backgrounds/borders/border_23.webp'); } .profile-border.border-24 { background-image: url('@/assets/images/backgrounds/borders/border_24.webp'); } -.profile-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } - -.card-background-2, -.card-background-3, -.card-background-4, -.card-background-5, -.card-background-6, -.card-background-7, -.card-background-12, -.card-background-13, -.card-background-28, -.card-background-30, -.card-background-35, -.card-background-52, -.card-background-55, -.card-background-56, -.card-background-58, -.card-background-60, -.card-background-95, -.card-background-116, -.card-background-122, -.card-background-127, -.card-background-129, -.card-background-131, -.card-background-144, -.card-background-149, -.card-background-150, -.card-background-161, -.card-background-185, -.card-background-187 { - --profile-card-text: #1a1a1a; - --profile-card-shadow: 0 1px 1px rgba(255, 255, 255, 0.65); -} - -[class*="card-background-"] .nitro-extended-profile__username .username, -[class*="card-background-"] .nitro-extended-profile__motto, -[class*="card-background-"] .nitro-extended-profile__meta, -[class*="card-background-"] .nitro-extended-profile__meta b, -[class*="card-background-"] .nitro-extended-profile__meta span, -[class*="card-background-"] .nitro-extended-profile__status-text, -[class*="card-background-"] .nitro-extended-profile__relationships-label, -[class*="card-background-"] .nitro-extended-profile__relationship-subcopy, -[class*="card-background-"] .nitro-extended-profile__link, -[class*="card-background-"] .nitro-extended-profile__right > p, -[class*="card-background-"] .nitro-extended-profile__right > p b { - color: var(--profile-card-text, #fff) !important; - text-shadow: var(--profile-card-shadow, 0 1px 2px rgba(0, 0, 0, 0.55)); -} - -.profile-card-background[class*="card-background-"]:not(.nitro-extended-profile-window__content) { - color: var(--profile-card-text, #fff); - text-shadow: var(--profile-card-shadow, 0 1px 2px rgba(0, 0, 0, 0.55)); -} - -.profile-card-background[class*="card-background-"]:not(.nitro-extended-profile-window__content) .text-white { - color: var(--profile-card-text, #fff) !important; -} - +.profile-border.border-25 { background-image: url('@/assets/images/backgrounds/borders/border_25.webp'); } \ No newline at end of file diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 0a146e7..6a1f23e 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -1,110 +1,532 @@ .nitro-catalog-classic-window { - --cat-blue: #4a7d8c; - --cat-blue-dark: #315863; - --cat-ink: #2c2c2a; - --cat-strip: #e2e0d6; - --cat-tab: #c7c5ba; - --cat-tab-border: #8f8f8b; - --cat-panel: #e8e7df; - --cat-sub: #dedcd2; - --cat-line: #8899a2; - --cat-canvas: #d8d8d2; - --cat-canvas-2: #ccccc4; - --cat-select: #4a7d8c; - --cat-select-bg: #f1efe6; - --cat-gold: #f7d673; - --cat-gold-border: #d4af37; - --cat-gold-ink: #4a3300; - --cat-buy: #5ca843; + --catalog-swf-bg: #ecece4; + --catalog-swf-panel: #f7f7f2; + --catalog-swf-panel-2: #e7e7df; + --catalog-swf-border: #9d9d96; + --catalog-swf-border-dark: #6f6f6a; + --catalog-swf-text: #222222; + --catalog-swf-muted: #666666; + --catalog-swf-blue: #2f8097; + --catalog-swf-blue-dark: #1c596c; + --catalog-swf-select: #63c5e9; + --catalog-swf-select-outer: #82d1ed; + --catalog-swf-bc: #ff8d00; + --catalog-swf-bc-outer: #ffb53c; + --habbo-skin-ubuntu: url("../../assets/images/catalog/swf/skins/habbo_skin_ubuntu.png"); + --habbo-skin-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); + --habbo-skin-illumina-light: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_light.png"); + --habbo-skin-illumina-dark: url("../../assets/images/catalog/swf/skins/habbo_skin_illumina_dark.png"); + --habbo-slice-frame: url("../../assets/images/catalog/swf/ubuntu_frame3_26x55.png"); + --habbo-slice-tab-default: url("../../assets/images/catalog/swf/ubuntu_tab3_default_22x32.png"); + --habbo-slice-tab-selected: url("../../assets/images/catalog/swf/ubuntu_tab3_selected_22x32.png"); + --habbo-slice-tab-hover: url("../../assets/images/catalog/swf/ubuntu_tab3_hover_22x32.png"); + /* Light gray secondary button - cropped from catalog_skin1.png + at (10, 190, 25x22). Drives the gift button "Cadeau", the + preview-room control button and the generic .nitro-catalog-swf- + button via border-image-slice 3 3 3 3 fill. */ + --habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png"); + --habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); + --habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); + --habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); + /* Classic Habbo "Osta!" Buy button - cropped from catalog_skin3.png + yellow band. The historical name says "green" but the user's + skin sheet ships yellow for the action colour, so that's what + we paint. The 27x34 sprite border-image-slices nicely at 6px + since the rounded corner is ~5px. */ + --habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); + --habbo-slice-button-large-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png"); + --habbo-slice-button-large-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png"); + --habbo-button-green: url("../../assets/images/catalog/buttons/buy.png"); + --habbo-button-green-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); + --habbo-button-green-pressed: url("../../assets/images/catalog/buttons/buy_pressed.png"); + --habbo-button-green-disabled: url("../../assets/images/catalog/buttons/buy_disabled.png"); + --habbo-grid-default: url("../../assets/images/catalog/swf/habbo_grid.png"); + --habbo-grid-hover: url("../../assets/images/catalog/swf/habbo_grid_hover.png"); + --habbo-grid-selected: url("../../assets/images/catalog/swf/habbo_grid_selected.png"); + --habbo-grid-selected-inactive: url("../../assets/images/catalog/swf/habbo_grid_selected_inactive.png"); + --habbo-close: url("../../assets/images/catalog/buttons/close.png"); + --habbo-close-hover: url("../../assets/images/catalog/buttons/close_hover.png"); + --habbo-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png"); + --habbo-help: url("../../assets/images/catalog/buttons/help.png"); + --habbo-help-hover: url("../../assets/images/catalog/buttons/help_hover.png"); + --habbo-help-pressed: url("../../assets/images/catalog/buttons/help_pressed.png"); + --habbo-stepper-plus: url("../../assets/images/catalog/buttons/plus.png"); + --habbo-stepper-plus-hover: url("../../assets/images/catalog/buttons/plus_hover.png"); + --habbo-stepper-plus-pressed: url("../../assets/images/catalog/buttons/plus_pressed.png"); + --habbo-stepper-plus-disabled: url("../../assets/images/catalog/buttons/plus_disabled.png"); + --habbo-stepper-minus: url("../../assets/images/catalog/buttons/minus.png"); + --habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png"); + --habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png"); + --habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png"); + /* Scrollbar sprites cropped from catalog_skin1.png. The single-piece + thumb has caps + grip baked into one 17x34 image - stretch it + full-height with background-size: 17px 100%. */ + --habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png"); + --habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png"); + --habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png"); + --habbo-scrollbar-down-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_down_pressed.png"); + --habbo-scrollbar-thumb-v: url("../../assets/images/catalog/scrollbar/scroll_v_thumb.png"); + --habbo-scrollbar-thumb-v-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png"); + --habbo-scrollbar-h-left: url("../../assets/images/catalog/scrollbar/scroll_h_left.png"); + --habbo-scrollbar-h-left-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_left_pressed.png"); + --habbo-scrollbar-h-right: url("../../assets/images/catalog/scrollbar/scroll_h_right.png"); + --habbo-scrollbar-h-right-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_right_pressed.png"); + --habbo-scrollbar-thumb-h: url("../../assets/images/catalog/scrollbar/scroll_h_thumb.png"); + --habbo-scrollbar-thumb-h-pressed: url("../../assets/images/catalog/scrollbar/scroll_h_thumb_pressed.png"); - width: 640px !important; - height: 600px !important; - max-width: 640px !important; - min-width: 640px !important; - min-height: 600px !important; - max-height: 600px !important; - background: var(--cat-strip) !important; - border-radius: 10px !important; + width: 660px !important; + height: 720px !important; + min-width: 660px !important; + max-width: 660px !important; + min-height: 640px !important; + max-height: calc(100vh - 24px) !important; + position: relative; + color: var(--catalog-swf-text); + background: var(--catalog-swf-bg) !important; + border: 1px solid #000 !important; + border-radius: 7px 7px 0 0 !important; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35); overflow: hidden; + box-sizing: border-box; } -.nitro-catalog-classic-window .nitro-card-title { - font-size: 18px; - letter-spacing: 0.2px; +.draggable-window:has(> .nitro-catalog-classic-window) { + width: 660px !important; + height: 720px !important; + min-width: 660px !important; + max-width: 660px !important; } -.nitro-catalog-classic-window .nitro-card-header-shell { - min-height: 38px; - max-height: 38px; +.nitro-catalog-classic-window::before { + display: none; +} + +.nitro-catalog-classic-window > * { + position: relative; + z-index: 4; +} + +.nitro-catalog-classic-window, +.nitro-catalog-classic-window * { + image-rendering: pixelated; } .nitro-catalog-classic-window .nitro-card-header { - background: var(--cat-blue); - border-color: var(--cat-blue); - border-bottom-color: var(--cat-blue); - box-shadow: inset 0 2px 0 #709da9, inset 0 -2px 0 var(--cat-blue-dark); + height: 35px; + min-height: 35px; + background: var(--catalog-swf-blue) !important; + border: 0 !important; + border-bottom: 1px solid #000 !important; } -.nitro-catalog-classic-admin-banner { - border-bottom: 1px solid rgba(0, 0, 0, 0.18); - background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); +.nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 35px; + max-height: 35px; +} + +.nitro-catalog-classic-window .nitro-card-title { + color: #ffffff; + font-size: 16px; + font-weight: 700; + line-height: 35px; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.8); +} + +.nitro-catalog-classic-window .nitro-card-close-button { + top: 7px !important; + right: 7px !important; + width: 19px !important; + min-width: 19px !important; + height: 20px !important; + min-height: 20px !important; + padding: 0 !important; + background-image: url("../../assets/images/catalog/buttons/close.png") !important; + background-color: transparent !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 19px 20px !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + image-rendering: pixelated !important; + opacity: 1 !important; + visibility: visible !important; + display: block !important; +} + +.nitro-catalog-classic-window .nitro-card-close-button:hover { + background-image: url("../../assets/images/catalog/buttons/close_hover.png") !important; + filter: none; +} + +.nitro-catalog-classic-window .nitro-card-close-button:active { + background-image: url("../../assets/images/catalog/buttons/close_pressed.png") !important; +} + +.nitro-catalog-classic-mobile-header, +.nitro-catalog-classic-admin-banner, +.nitro-catalog-classic-layout-header-shell, +.nitro-catalog-classic-layout-hero, +.nitro-catalog-classic-window .nitro-catalog-header, +.nitro-catalog-classic-window .builders-club-status-shell { + display: none !important; +} + +/* Publish button: lives inside the catalog window, absolutely + positioned in the header just to the left of the close X. Renders + only when adminMode is true (see CatalogClassicView.tsx). Uses the + Habbo yellow buy-button skin so it matches the Koop button. */ +.nitro-catalog-classic-window .nitro-catalog-classic-header-publish { + position: absolute !important; + top: 5px !important; + right: 32px !important; + width: auto !important; + min-width: 0 !important; + height: 22px !important; + padding: 0 10px !important; + font-size: 10px !important; + font-weight: 700 !important; + letter-spacing: 0.5px !important; + line-height: 22px !important; + z-index: 10 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-header-publish.has-pending { + animation: nitroPublishPulse 1.4s ease-in-out infinite; +} + +@keyframes nitroPublishPulse { + 0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 -2px 0 rgba(140, 75, 0, 0.35), 0 0 0 rgba(255, 200, 0, 0); } + 50% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 -2px 0 rgba(140, 75, 0, 0.35), 0 0 8px rgba(255, 200, 0, 0.75); } +} + +/* Catalog default-layout admin row (Pagina bewerken / Nieuwe + aanbieding / Aanbieding bewerken). These are inline text buttons + but the .habbo-swf-window button + .habbo-swf-window + button[class*="success"] global rules were dressing them up as + SWF skin buttons (one yellow!) and forcing min-height: 22px which + broke the layout. Reset and re-skin as compact pill chips. */ +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin { + /* Keep all admin buttons on one row so the product-view doesn't + get pushed down into the absolutely-positioned grid-shell. If + a future label makes them overflow, the row scrolls + horizontally instead of wrapping. */ + flex-wrap: nowrap !important; + align-items: center !important; + gap: 6px !important; + margin-bottom: 4px !important; + overflow-x: auto !important; + overflow-y: visible !important; + scrollbar-width: thin; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin::-webkit-scrollbar { + height: 4px !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button { + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + height: 22px !important; + min-height: 22px !important; + padding: 0 10px !important; + border: 1px solid #6f8db5 !important; + border-image: none !important; + border-image-source: none !important; + border-radius: 4px !important; + background: linear-gradient(180deg, #ffffff 0%, #e7eef8 100%) !important; + background-image: linear-gradient(180deg, #ffffff 0%, #e7eef8 100%) !important; + color: #1a3a6b !important; + font-size: 10px !important; + font-weight: 600 !important; + line-height: 1 !important; + text-shadow: none !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 1px 0 rgba(0, 0, 0, 0.12) !important; + cursor: pointer !important; + white-space: nowrap !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button:hover { + background: linear-gradient(180deg, #ffffff 0%, #d6e2f3 100%) !important; + background-image: linear-gradient(180deg, #ffffff 0%, #d6e2f3 100%) !important; + color: #0b2347 !important; + border-color: #4a72b8 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button:active { + background: linear-gradient(180deg, #d6e2f3 0%, #ffffff 100%) !important; + background-image: linear-gradient(180deg, #d6e2f3 0%, #ffffff 100%) !important; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08), 0 0 0 rgba(0, 0, 0, 0) !important; +} + +/* The "Nieuwe aanbieding" button uses text-success - give it the + habbo-yellow buy-button palette to mark it as the create action. */ +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button.text-success, +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button[class*="success"] { + border-color: #8a5b00 !important; + background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + background-image: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button.text-success:hover, +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button[class*="success"]:hover { + background: linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + background-image: linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + color: #4a2b00 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button svg { + width: 11px !important; + height: 11px !important; + fill: currentColor !important; + flex-shrink: 0 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-classic-default-admin button .font-mono { + color: #4a4a44 !important; + font-weight: 600 !important; + margin-left: 4px !important; +} + +/* Admin cog tab at the end of the tab strip - only renders when the + user is a mod, so leaving it visible at all times is safe. Style + it as a compact square that sits flush with the other tabs + instead of stretching to flex: 1 like a category tab. */ +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item.nitro-catalog-classic-admin-tab { + flex: 0 0 auto !important; + width: 32px !important; + min-width: 32px !important; + max-width: 32px !important; + padding: 6px 4px 7px !important; + margin-left: 4px !important; +} + +.nitro-catalog-classic-swf-header { + position: relative; + flex: 0 0 90px; + height: 90px; + margin: 0 1px; + border: 1px solid #376275; + background: #0e3f52; + overflow: hidden; +} + +.nitro-catalog-classic-swf-header-bg { + position: absolute; + inset: 0; + opacity: 0.1; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + background: + linear-gradient(135deg, transparent 0 46%, rgba(255, 255, 255, 0.35) 47% 49%, transparent 50%), + radial-gradient(circle at 18% 28%, rgba(255, 255, 255, 0.55), transparent 18%), + linear-gradient(180deg, #60a6bd, #0e3f52); + filter: grayscale(1); +} + +.nitro-catalog-classic-swf-header-icon { + position: absolute; + left: 24px; + top: 30px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 35px; + transform: scale(2); + transform-origin: center; +} + +.nitro-catalog-classic-swf-header-copy { + position: absolute; + left: 80px; + top: 11px; + right: 15px; + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.7); +} + +.nitro-catalog-classic-swf-header-title { + min-height: 24px; + font-size: 24px; + font-weight: 700; + line-height: 24px; +} + +.nitro-catalog-classic-swf-header-description { + margin-top: 5px; + font-size: 12px; + line-height: 15px; } .nitro-catalog-classic-tabs-shell { - flex-wrap: nowrap; - gap: 2px; - min-height: 32px; - max-height: 32px; - padding: 4px 6px 0; + /* Strip just tall enough to hold the 36px tab + 4px of breathing + room above. Trims the dead blue band between the header and + the tabs so the catalog body doesn't lose vertical space. */ + flex: 0 0 40px; + height: 40px; + min-height: 40px; + max-height: 40px; + gap: 0; + padding: 4px 6px 0 !important; + align-items: flex-end; + /* Horizontal scroll so every category tab stays reachable when the + card is narrower than the total tab width. */ overflow-x: auto; overflow-y: hidden; - align-items: end; - background: var(--cat-strip); - border-bottom: 2px solid var(--cat-ink); + flex-wrap: nowrap; + scrollbar-width: thin; + scrollbar-color: #8da3b3 transparent; + background: var(--catalog-swf-bg) !important; + border: 0 !important; + border-bottom: 1px solid #c8c8bf; +} + +/* The tabs strip uses a slim 6px scrollbar - opt it out of the + 17px Habbo-sprite scrollbar applied to the rest of the catalog. */ +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar { + width: 6px !important; + height: 6px !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-thumb { + background: #8da3b3 !important; + background-image: none !important; + border-radius: 3px !important; + box-shadow: none !important; + min-height: 0 !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-track { + background: transparent !important; + background-image: none !important; + box-shadow: none !important; +} + +.nitro-catalog-classic-tabs-shell::-webkit-scrollbar-button:single-button { + display: none !important; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 28px; - padding: 5px 12px 4px; - border: 1px solid var(--cat-tab-border); - border-bottom: 0; - border-radius: 5px 5px 0 0; - background: var(--cat-tab); - color: var(--cat-ink); - box-shadow: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 36px; + min-width: 0; + /* Equal-width tabs that share the strip exactly - the right + edge of the last tab now lines up with the right edge of the + catalog window, no trailing gap. */ + flex: 1 1 0; + max-width: none; + padding: 6px 6px 7px; + margin: 0 2px 0 0; + flex-shrink: 1; + /* Classic Habbo tab: gray rounded-top rectangle with a 1px black + outline. ubuntu_tab3_*.png isn't shipped, so we draw the + habbo-look ourselves instead of border-image-slicing a missing + sprite. */ + border: 1px solid #000 !important; + border-bottom: 0 !important; + border-radius: 6px 6px 0 0 !important; + background: + linear-gradient(180deg, #d6d6cc 0%, #c1c1b7 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; + color: #2a2a2a !important; + font-size: 12px; + font-weight: 400; white-space: nowrap; - font-weight: 700; + overflow: hidden; + border-image-source: none !important; +} + +/* Bring the last tab flush with whatever follows (admin cog, edge), + instead of leaving the negative right margin tugging on empty + space. */ +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item:last-of-type { + margin-right: 0; +} + +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item > div { + align-items: center !important; + justify-content: center !important; + gap: 4px; + min-width: 0; + width: auto; + height: 20px; + line-height: 17px; +} + +/* Category icon that sits before the label. The blanket "hide every + img/svg inside a tab" rule is gone - we explicitly size the + classic tab icon and let everything else fall through. */ +.nitro-catalog-classic-tab-icon { + width: 18px; + height: 18px; + object-fit: contain; + image-rendering: pixelated; + flex-shrink: 0; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { - background: #d2d0c6; + background: linear-gradient(180deg, #e3e3d9 0%, #d0d0c5 100%) !important; + color: #000 !important; } .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { - background: #ffffff; - color: #000000; - position: relative; - top: 1px; - border-color: var(--cat-ink); - box-shadow: inset 0 -1px 0 #ffffff; + z-index: 2; + /* Active tab: the catalog-header habbo-blue with the cream catalog + body bleeding up into it. Drop the bottom border so the tab + "merges" with the panel below. */ + background: + linear-gradient(180deg, #4fb3ff 0%, var(--catalog-swf-blue) 100%) !important; + color: #ffffff !important; font-weight: 700; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -2px 0 var(--catalog-swf-blue-dark) !important; +} + +.nitro-catalog-classic-tab-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; } .nitro-catalog-classic-content-shell { + position: relative; + flex: 1 1 auto; + min-height: 0; padding: 6px 8px 8px !important; - background: #ffffff !important; + background: var(--catalog-swf-bg) !important; + overflow: hidden; } .nitro-catalog-classic-stage { display: grid; - grid-template-columns: 196px minmax(0, 1fr); + /* Sidebar pinned at 184px; the layout column takes the rest of + the stage row so the right edge of the right column lines up + with the right edge of the catalog window instead of leaving a + wide cream strip. */ + grid-template-columns: 184px 1fr; gap: 8px; - min-height: 0; + width: 100%; height: 100%; + min-height: 0; } .nitro-catalog-classic-stage.is-navigation-hidden { - grid-template-columns: minmax(0, 1fr); + grid-template-columns: 1fr; } .nitro-catalog-classic-sidebar { @@ -112,103 +534,159 @@ flex-direction: column; gap: 4px; min-height: 0; - height: 100%; + overflow: hidden; } .nitro-catalog-classic-search-shell { - padding: 4px; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: var(--cat-panel); + position: relative; + /* Use flex so the input vertically centers inside the 24px shell + regardless of the input's own intrinsic baseline. */ + display: flex; + align-items: center; + height: 24px; + /* Outer padding 0, negative horizontal margin bleeds the shell a + couple of pixels past its sidebar column on each side without + touching the grid template - cheap way to look ~4px wider. */ + padding: 0; + margin: -2px -1px 0 -1px; + border: 1px solid #b7b7ae; + border-radius: 3px; + background: #f7f7f2; } +/* Clear the magnifying-glass on the left and the X-clear button on the + right. The shell's own outer padding is essentially zero, so the + input claims the full sidebar column width minus just enough to + keep the icons from overlapping the text. */ .nitro-catalog-classic-search-shell input { - height: 20px; - padding-top: 0 !important; - padding-bottom: 0 !important; - border-width: 1px !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.06); + flex: 1 1 auto; + width: 100% !important; + height: 22px !important; + min-height: 22px !important; + line-height: 22px !important; + padding: 0 16px 0 18px !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + color: var(--catalog-swf-muted) !important; + font-size: 11px !important; + box-shadow: none !important; + vertical-align: middle !important; +} + +/* The wrapping div the React component renders is 100% wide so the + input fills the shell instead of shrinking to content. */ +.nitro-catalog-classic-search-shell > div { + width: 100% !important; + height: 100% !important; } .nitro-catalog-classic-search-shell svg { - color: #5b7080 !important; + color: #888 !important; + font-size: 11px !important; +} + +/* The search icon ships with absolute + left-2; nudge it tight to the + shell edge so the input can keep its left padding small. */ +.nitro-catalog-classic-search-shell svg:first-child { + left: 4px !important; +} + +/* X-clear button on the right edge - keep it tight too. */ +.nitro-catalog-classic-search-shell button { + right: 4px !important; } .nitro-catalog-classic-navigation-shell { flex: 1 1 auto; min-height: 0; - padding: 4px 0; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: var(--cat-panel); + padding: 5px 3px; + border: 1px solid #b4b4ae; + border-radius: 3px; + background: rgba(236, 236, 228, 0.5); overflow: auto; } .nitro-catalog-classic-navigation-list { display: flex; flex-direction: column; - gap: 0; } -.nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - padding-left: 22px; - background: var(--cat-sub); +.nitro-catalog-classic-navigation-node { + min-height: 21px; } .nitro-catalog-classic-navigation-item { + position: relative; display: flex; align-items: center; gap: 6px; - min-height: 28px; - padding: 4px 10px; - border: 0; - border-left: 4px solid transparent; - border-radius: 0; + height: 21px; + min-height: 21px; + padding: 1px 18px 1px 0; + border: 0 !important; + border-radius: 0 !important; background: transparent; - color: var(--cat-ink); + color: var(--catalog-swf-muted); + font-size: 11px; font-weight: 700; + line-height: 17px; cursor: pointer; - transition: background-color 0.12s ease; + text-shadow: 0 1px 0 #b4b4ae; } .nitro-catalog-classic-navigation-node.is-child .nitro-catalog-classic-navigation-item { - font-weight: 400; + padding-left: 15px; + color: #52819a; + font-style: italic; } -.nitro-catalog-classic-navigation-item:hover { - background: #dcdacf; +.nitro-catalog-classic-navigation-item::before { + position: absolute; + inset: 0 1px 1px 1px; + z-index: 0; + display: none; + content: ""; + border: 1px solid var(--catalog-swf-select-outer); + background: linear-gradient(180deg, var(--catalog-swf-select-outer) 0 2px, var(--catalog-swf-select) 2px calc(100% - 2px), var(--catalog-swf-select-outer) calc(100% - 2px)); } -.nitro-catalog-classic-navigation-item.is-active { - background: #ffffff; - border-left-color: var(--cat-blue); - color: #000000; - font-weight: 700; +.nitro-catalog-classic-window .builders-club-card-header ~ .nitro-catalog-classic-tabs-shell, +.nitro-catalog-classic-window:has(.builders-club-card-header) .nitro-catalog-classic-navigation-item::before { + --catalog-swf-select: var(--catalog-swf-bc); + --catalog-swf-select-outer: var(--catalog-swf-bc-outer); } -.nitro-catalog-classic-navigation-item.is-drag-over { - outline: 2px solid rgba(74, 125, 140, 0.4); - outline-offset: -2px; +.nitro-catalog-classic-navigation-item:hover::before, +.nitro-catalog-classic-navigation-item.is-active::before { + display: block; +} + +.nitro-catalog-classic-navigation-icon, +.nitro-catalog-classic-navigation-label, +.nitro-catalog-classic-navigation-caret, +.nitro-catalog-classic-navigation-favorite, +.nitro-catalog-classic-navigation-admin, +.nitro-catalog-classic-navigation-drag { + position: relative; + z-index: 1; } .nitro-catalog-classic-navigation-icon { display: flex; align-items: center; justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; + width: 20px; + min-width: 20px; + height: 20px; } .nitro-catalog-classic-navigation-icon img, .nitro-catalog-classic-navigation-icon canvas { width: auto; height: auto; - max-width: 18px; - max-height: 18px; + max-width: 20px; + max-height: 20px; } .nitro-catalog-classic-navigation-label { @@ -217,19 +695,14 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 11px; - line-height: 1; -} - -.nitro-catalog-classic-navigation-caret, -.nitro-catalog-classic-navigation-favorite, -.nitro-catalog-classic-navigation-admin, -.nitro-catalog-classic-navigation-drag { - flex-shrink: 0; } .nitro-catalog-classic-navigation-caret { - color: #5b7080 !important; + position: absolute; + right: 5px; + top: 5px; + color: #777 !important; + font-size: 9px; } .nitro-catalog-classic-layout-shell { @@ -238,378 +711,879 @@ min-width: 0; min-height: 0; height: 100%; - border: 1px solid var(--cat-line); - border-radius: 4px; - background: #ffffff; + background: transparent; + border: 0; overflow: hidden; } -.nitro-catalog-classic-layout-header-shell { - 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; -} - -.nitro-catalog-classic-layout-hero { - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - min-height: 32px; - overflow: visible; -} - -.nitro-catalog-classic-layout-hero img { - display: block; - width: auto; - height: auto; - max-width: 100%; - max-height: none; - object-fit: contain; -} - .nitro-catalog-classic-layout-container { flex: 1 1 auto; min-height: 0; - padding: 6px; - background: #ffffff; + padding: 0; + background: transparent; overflow: hidden; } .nitro-catalog-classic-default-layout { - gap: 8px; + position: relative; + display: block !important; + /* Fill the layout container in both axes - the stage was + previously 552px wide and this column was pinned at 360px, but + now that the stage uses 1fr, hardcoding 360px would leave a + wide blank strip on the right of every default-layout catalog + page. */ + width: 100%; + height: 100%; + min-height: 460px; } -.nitro-catalog-classic-offer-panel, -.nitro-catalog-classic-welcome { - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #ffffff; +/* The product-view, grid-shell, price-row and purchase-row inside + the default-layout were each pinned at 360px to match the old + stage width. Widen them in lockstep so they fill the new + 1fr-sized container. */ +.nitro-catalog-classic-product-view, +.nitro-catalog-classic-grid-shell, +.nitro-catalog-classic-price-row, +.nitro-catalog-classic-purchase-row { + width: 100% !important; } -.nitro-catalog-classic-offer-panel { - min-height: 132px; +.nitro-catalog-classic-product-view { + width: calc(100% + 3px) !important; +} + +.nitro-catalog-classic-product-view, +.nitro-catalog-classic-grid-shell { + border: 1px solid #b4b4ae; + border-radius: 5px; + background: var(--catalog-swf-panel); +} + +.nitro-catalog-classic-product-view { + position: relative; + top: 0; + left: 0; + width: 363px; + height: 240px; + min-height: 0; overflow: hidden; } +.nitro-catalog-classic-product-view::before { + display: none; +} + +.nitro-catalog-classic-offer-panel { + position: relative; + z-index: 1; + width: 100%; + height: 100%; +} + .nitro-catalog-classic-offer-preview { - width: 190px; - min-width: 190px; - padding: 8px; - border-right: 1px solid var(--cat-line); - background-color: var(--cat-canvas); + position: relative; + width: 360px; + min-width: 360px; + height: 100%; + padding: 0; + overflow: hidden; + background: #000; +} + +.nitro-catalog-classic-preview-title { + position: absolute; + top: 12px; + left: 12px; + z-index: 5; + color: #ffffff !important; + font-size: 12px !important; + font-weight: 700 !important; + line-height: 15px !important; + text-shadow: 0 1px 0 #000; +} + +.nitro-catalog-classic-offer-preview .shadow-room-previewer, +.nitro-catalog-classic-offer-preview canvas, +.nitro-catalog-classic-offer-preview img { + width: 100% !important; + height: 100% !important; + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 0 !important; + box-shadow: none !important; + background-position: center bottom !important; + background-repeat: no-repeat !important; + background-size: contain !important; } .nitro-catalog-classic-offer-info { - padding: 10px; + display: none !important; } -.nitro-catalog-classic-offer-actions { - justify-content: flex-end; +.nitro-catalog-classic-offer-info .nitro-text, +.nitro-catalog-classic-offer-info span, +.nitro-catalog-classic-offer-info div { + color: inherit; } -.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; + position: relative; + z-index: 1; + width: 100%; + height: 100%; + padding: 18px; + color: var(--catalog-swf-muted); +} + +.nitro-catalog-classic-welcome img { + width: auto !important; + height: auto !important; + max-width: 96px; + max-height: 120px; + border: 0; + background: transparent; } .nitro-catalog-classic-grid-shell { - min-height: 150px; - padding: 6px; - border: 1px solid var(--cat-line); - border-radius: 6px; - background: #ffffff; - height: auto; - flex: 1 1 auto; + position: absolute; + left: 0; + top: 245px; + /* Stretch down to just above the price + purchase rows so the + grid soaks up any extra height the bigger window gives us. */ + bottom: 68px; + width: 360px; + min-height: 0; + padding: 3px 2px 3px 4px; + overflow: auto; +} + +/* When the admin row is rendered above the product-view it adds + ~30px (22px button + flex gap) to the flex column, but the + grid-shell is absolutely positioned and doesn't shift on its own. + Push it (and the bottom-anchored price/purchase rows stay put) + down so the preview panel no longer bleeds into the grid. */ +.nitro-catalog-classic-default-layout:has(.nitro-catalog-classic-default-admin) .nitro-catalog-classic-grid-shell { + top: 280px !important; } .nitro-catalog-classic-grid { - gap: 6px !important; + /* Don't pin a fixed column track here - AutoGrid sets the inline + grid-template-columns from its columnMinWidth prop. The earlier + `repeat(6, 53px) !important` was clobbering that and freezing + the row at 6 tiles regardless of what the React layout passed. */ + grid-auto-rows: var(--nitro-grid-column-min-height, 70px); align-content: start; + justify-content: start; + gap: 3px !important; + overflow: visible !important; } .nitro-catalog-classic-window .layout-grid-item { - height: 54px; - border: 1px solid var(--cat-line) !important; - border-radius: 4px !important; - background-color: #ffffff; - background-image: none; - box-shadow: none; - transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; + /* Let the tile flex to whatever min/max width the AutoGrid sets + via repeat(auto-fill, minmax(N, 1fr)) - hard-pinning 53x74 was + overriding the layout's columnMinWidth prop, so the row count + never changed when we reduced it. Width is now 100% of the + column cell, height tracks --nitro-grid-column-min-height. */ + width: 100% !important; + height: var(--nitro-grid-column-min-height, 70px) !important; + min-width: 0 !important; + min-height: var(--nitro-grid-column-min-height, 70px) !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + overflow: visible !important; } .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(74, 125, 140, 0.2); + background-image: none !important; + box-shadow: inset 0 0 0 1px #a1a19b !important; } .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(74, 125, 140, 0.35); + background-image: none !important; + box-shadow: + inset 0 0 0 1px #63c5e9, + inset 2px 2px 0 #ecece4, + inset -2px -2px 0 #ecece4 !important; } .nitro-catalog-classic-grid-offer-icon { position: absolute; - inset: 4px; - background-repeat: no-repeat; - background-position: center; + left: 50%; + top: 20px; + width: auto !important; + height: auto !important; + max-width: 36px; + max-height: 36px; + object-fit: contain; + transform: translate(-50%, -50%); pointer-events: none; } -.nitro-catalog-classic-window .nitro-catalog-header { - display: none; -} - -.nitro-catalog-classic-offer-info .bg-\[\#00800b\] { - background-color: var(--cat-buy) !important; - border-color: #007a00 !important; -} - -.nitro-catalog-classic-breadcrumb { +.nitro-catalog-classic-grid-price { + position: absolute; + left: 2px; + right: 2px; + top: 36px; + bottom: auto; display: flex; + flex-direction: column; align-items: center; - gap: 5px; - min-height: 16px; - overflow: hidden; - color: #5b7080; - font-size: 10px; - line-height: 1; + justify-content: flex-start; + gap: 0; + min-height: 24px; + color: #000; + font-size: 11px; + font-weight: 700; + line-height: 12px; + pointer-events: none; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75); +} + +/* When the tile shows a full-tile bot/pet avatar (instead of a small + icon), pin the price strip to the bottom of the tile and give it a + translucent backdrop so it doesn't overlap with the avatar body. */ +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price, +.nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price { + top: auto !important; + /* Re-anchor horizontally too: the parent rule's left: 2px / + right: 2px combined with content-sized inner flex was visually + parking the pill at the left side of the tile. Center it via + explicit left/right + transform so it lands smack in the + middle regardless of inner content width. */ + left: 50% !important; + right: auto !important; + bottom: 4px !important; + transform: translateX(-50%) !important; + width: auto !important; + background: rgba(255, 255, 255, 0.85) !important; + border-radius: 3px !important; + padding: 2px 6px !important; + min-height: 0 !important; + height: auto !important; + display: inline-flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: center !important; + z-index: 5 !important; +} + +/* Tighten the price entry inside the avatar-tile pill so the number + and currency icon center on the same baseline (the global + .grid-price-entry height: 13px clipped the 15px wallet icon and + pushed it visually below the number). */ +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry { + height: auto !important; + line-height: 1 !important; + align-items: center !important; + justify-content: center !important; + gap: 3px !important; +} + +.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-currency, +.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-currency { + width: 13px !important; + height: 13px !important; + object-fit: contain !important; + vertical-align: middle !important; + display: inline-block !important; +} + +.nitro-catalog-classic-grid-price.is-single-price { + height: 19px; +} + +.nitro-catalog-classic-grid-price.is-multi-price { + height: 38px; +} + +.nitro-catalog-classic-grid-price-entry { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 1px; + height: 13px; white-space: nowrap; } -.nitro-catalog-classic-breadcrumb-segment { +.nitro-catalog-classic-grid-price-plus { + padding-right: 1px; +} + +.nitro-catalog-classic-grid-price-currency { + width: auto !important; + height: auto !important; + max-width: none !important; + max-height: none !important; + min-width: 0 !important; + min-height: 0 !important; + object-fit: none !important; +} + +.nitro-catalog-classic-price-row { + position: absolute; + left: 0; + /* Anchored from the bottom so the Aantal/Prezzo row sits just + above the Cadeau/Koop buttons regardless of layout height. */ + bottom: 38px; + width: 360px; + height: 25px; + padding: 0; + overflow: visible; +} + +.nitro-catalog-classic-spinner-slot { + position: absolute; + left: 0; + top: 0; + width: 200px; + height: 25px; +} + +.nitro-catalog-classic-total-price-slot { + position: absolute; + /* Anchored to the right of the now-100% wide price row so the + Prezzo + amount stays flush with the right edge of the panel. */ + right: 2px; + top: 0; + width: auto; + min-width: 180px; + height: 25px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding-right: 2px; +} + +.nitro-catalog-classic-total-price-slot::before { + content: "Prezzo"; + color: #666; + font-size: 11px; + line-height: 17px; +} + +.nitro-catalog-classic-purchase-row { + position: absolute; + left: 0; + /* Anchored to the bottom of the panel with a 4px breathing strip + so the Cadeau / Koop buttons stay flush at the bottom of the + window no matter how tall the catalog is. */ + bottom: 4px; + width: 360px; + height: 30px; + padding: 0; + overflow: hidden; +} + +.nitro-catalog-classic-offer-actions { + display: flex !important; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + gap: 10px !important; + /* Fill the now-100% wide purchase row instead of staying pinned at + 330px (which used to match the old 360px column - 15px each + side). */ + width: auto; + height: 24px; + margin-left: 15px; + margin-right: 15px; + padding: 0; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 22px; + padding: 2px 12px !important; + border: 3px solid transparent !important; + border-radius: 0 !important; + border-image-source: var(--habbo-slice-button-default) !important; + border-image-slice: 3 3 3 3 fill !important; + border-image-width: 3px !important; + border-image-repeat: stretch !important; + background: transparent !important; + background-color: transparent !important; + background-image: none !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + box-shadow: none !important; + transition: none !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn *, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn svg { + border-radius: 0 !important; + box-shadow: none !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button:hover, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:hover, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:hover { + border-image-source: var(--habbo-slice-button-hover) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button:active, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:active, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:active { + border-image-source: var(--habbo-slice-button-pressed) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-button.pointer-events-none, +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button:disabled, +.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn:disabled { + border-image-source: var(--habbo-slice-button-disabled) !important; + color: #888 !important; + opacity: 1 !important; +} + +/* Buy / Gift buttons - pure CSS. border-image-slicing the bitmap + sprites produced thin highlight/shadow stripes at the top and + bottom because the source rounded corners are ~5-6px tall but the + buttons render at 22-24px, so the slice rows stretched into a + visible band. CSS gradients give a crisp pixel-art classic-habbo + look without those artefacts. */ + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button { + width: 160px !important; + min-width: 160px !important; + height: 24px !important; + padding: 0 8px !important; + border: 1px solid #000 !important; + border-radius: 4px !important; + border-image: none !important; + border-image-source: none !important; + /* Yellow body with the same #f0a318 / #ffd54d tones as the + skin3-yellow Buy sprite. */ + background: + linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.65), + inset 0 -2px 0 rgba(140, 75, 0, 0.35) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + font-weight: 700 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:hover { + background: + linear-gradient(180deg, #fff080 0%, #ffd54d 45%, #f5ab1c 100%) !important; + filter: brightness(1.04); +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:active { + background: + linear-gradient(180deg, #f0a318 0%, #d98c0a 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + inset 0 2px 0 rgba(140, 75, 0, 0.45) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none, +.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled { + /* Stay yellow when disabled - the user wants the action colour + to be recognisable regardless of state. Drop opacity + flip + the cursor so it still reads as non-interactive. */ + background: + linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; + color: #4a2b00 !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + opacity: 0.55 !important; + cursor: not-allowed !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button { + width: 160px !important; + min-width: 160px !important; + height: 22px !important; + padding: 0 8px !important; + border: 1px solid #000 !important; + border-radius: 4px !important; + border-image: none !important; + border-image-source: none !important; + /* Cream / light-gray body matching the catalog cardstock. */ + background: + linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; + color: #2a2a2a !important; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55) !important; + font-weight: 700 !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:hover { + background: + linear-gradient(180deg, #f4f4ed 0%, #dcdcd0 100%) !important; + filter: brightness(1.02); +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:active { + background: + linear-gradient(180deg, #c0c0b6 0%, #aaaaa0 100%) !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.2), + inset 0 2px 0 rgba(0, 0, 0, 0.25) !important; +} + +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button.pointer-events-none, +.nitro-catalog-classic-window .nitro-catalog-swf-gift-button:disabled { + background: + linear-gradient(180deg, #e3e3dc 0%, #c5c5bb 100%) !important; + color: #6a6a64 !important; + text-shadow: none !important; +} + +/* Pet purchase card lives in a tight flex row alongside the price, + so the main 160px Buy button doesn't fit. Shrink it down here. */ +.nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button { + width: auto !important; + min-width: 0 !important; + padding: 0 14px !important; +} + +/* All catalog grids must scroll vertically only - horizontal overflow + produces a stray horizontal scrollbar at the bottom of the items + strip on narrow columns (e.g. guild_furni). minmax(N, 1fr) usually + contains content but the safety net stops any odd item from + triggering a horizontal bar. */ +.nitro-catalog-classic-window .layout-grid, +.nitro-catalog-classic-window [class*="grid-cols-["] { + overflow-x: hidden !important; +} + +.nitro-catalog-swf-spinner { + display: flex; + align-items: center; + gap: 5px; + height: 25px; + color: #666; + font-size: 11px; +} + +.nitro-catalog-swf-spinner-label { + max-width: 62px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.nitro-catalog-swf-spinner-control { + display: grid; + grid-template-columns: 22px 34px 22px; + align-items: center; + height: 25px; + gap: 2px; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button { + width: 22px !important; + min-width: 22px !important; + height: 22px !important; + min-height: 22px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + border-image: none !important; + background-color: transparent !important; + background-repeat: no-repeat !important; + background-position: center center !important; + background-size: auto !important; + box-shadow: none !important; + image-rendering: pixelated !important; +} + +/* react-icons FaMinus/FaPlus glyphs ride inside these buttons; hide + them - the sprite already contains the +/- mark. */ +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg { + display: none !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less { + background-image: var(--habbo-stepper-minus) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:hover { + background-image: var(--habbo-stepper-minus-hover) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:active { + background-image: var(--habbo-stepper-minus-pressed) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-less:disabled { + background-image: var(--habbo-stepper-minus-disabled) !important; +} + +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more { + background-image: var(--habbo-stepper-plus) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:hover { + background-image: var(--habbo-stepper-plus-hover) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:active { + background-image: var(--habbo-stepper-plus-pressed) !important; +} +.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button-more:disabled { + background-image: var(--habbo-stepper-plus-disabled) !important; +} + +.nitro-catalog-swf-spinner-input { + width: 30px !important; + height: 25px !important; + padding: 4px 2px !important; + border: 1px solid #9d9d96 !important; + border-radius: 0 !important; + background: #fff !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 15px !important; + text-align: center !important; + outline: none !important; + appearance: textfield; +} + +.nitro-catalog-swf-spinner-input::-webkit-outer-spin-button, +.nitro-catalog-swf-spinner-input::-webkit-inner-spin-button { + appearance: none; + margin: 0; +} + +.nitro-catalog-swf-price-display { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 3px; + min-height: 24px; + color: #222; + font-size: 11px; +} + +.nitro-catalog-swf-price-pill { display: inline-flex; align-items: center; - gap: 5px; - min-width: 0; + gap: 2px; + height: 22px; + min-width: 34px; + padding: 0 2px; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; } -.nitro-catalog-classic-breadcrumb-separator { - color: #94a7b3; +.nitro-catalog-swf-price-text { + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 17px !important; } -.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; -} - -.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); -} - -.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-radius: 6px; - 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: 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: var(--cat-panel); -} - -@media (max-width: 1024px) and (min-width: 641px) { - .nitro-catalog-classic-window { - width: min(calc(100vw - 24px), 720px) !important; - min-width: 0 !important; - max-width: calc(100vw - 24px) !important; - height: min(calc(100vh - 24px), 720px) !important; - min-height: 0 !important; - max-height: calc(100vh - 24px) !important; - } - - .nitro-catalog-classic-stage { - grid-template-columns: minmax(0, 1fr); - } - - .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-swf-price-plus { + width: 7px; + height: 7px; + color: #666; } .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; + width: 25px; + height: 24px; + min-width: 25px; + padding: 0 !important; + overflow: hidden; + /* font-size: 0 was killing the SVG: react-icons emits + , so 0em -> 0x0. Use a real + font-size and pin the SVG to explicit pixels below. */ + font-size: 14px !important; + line-height: 1 !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; } -.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-btn svg { + width: 14px !important; + height: 14px !important; + color: #111 !important; + fill: #111 !important; + flex-shrink: 0 !important; + display: block !important; } .nitro-catalog-classic-preview-rotate { - left: 8px; + top: 8px; + right: 35px; } .nitro-catalog-classic-preview-state { - right: 8px; + top: 8px; + right: 6px; } -@media (max-width: 640px) { - .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-select: #3a82a7; - --cat-select-bg: #f0f5f8; - --cat-buy: #009900; +.nitro-catalog-classic-window .nitro-catalog-classic-navigation-shell::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-navigation-list::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-grid-shell::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-grid::-webkit-scrollbar, +.nitro-catalog-classic-window .nitro-catalog-classic-layout-container::-webkit-scrollbar { + width: 17px; + height: 17px; +} +/* ===== Classic catalog scrollbar (pure CSS, no sprites) ===== + Drew this with CSS gradients instead of stretching the 17x34 + skin1 thumb sprite. The sprite version pixelated into visible + horizontal bands on tall scroll areas because every source row + stretched 5-10x. CSS gradients stay crisp at any height. */ + +.nitro-catalog-classic-window * { + scrollbar-color: auto !important; + scrollbar-width: auto; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; + background-color: #e7e5d8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-track { + background-image: none !important; + background-color: #e7e5d8 !important; + box-shadow: inset 1px 0 0 #b9b6a5, inset -1px 0 0 #ffffff !important; + border: 0 !important; +} + +/* Habbo thumb: symmetric light-edges -> darker-middle gradient (the + "pinched in the middle" look of the classic Ubuntu scrollbar), + 1px near-black outline, three central grip lines via SVG centered + no-repeat. */ +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb { + min-height: 28px !important; + border: 1px solid #2a2a26 !important; + border-radius: 2px !important; + background: + url("data:image/svg+xml;utf8,") center center / 10px 9px no-repeat, + linear-gradient(180deg, #d6d6cc 0%, #b4b4aa 30%, #9a9a90 50%, #b4b4aa 70%, #d6d6cc 100%) !important; + background-color: #a8a89e !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.55), + inset 0 -1px 0 rgba(255, 255, 255, 0.4) !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb:hover { + background: + url("data:image/svg+xml;utf8,") center center / 10px 9px no-repeat, + linear-gradient(180deg, #e0e0d6 0%, #bebeb4 30%, #a4a49a 50%, #bebeb4 70%, #e0e0d6 100%) !important; + background-color: #b2b2a8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-thumb:active { + background: + linear-gradient(180deg, #c6c6bc 0%, #a4a49a 30%, #8a8a82 50%, #a4a49a 70%, #c6c6bc 100%) !important; + background-color: #9a9a90 !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.35), + inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important; +} + +/* Arrow buttons: cream cap with a 1px black outline + dark inset + chevron. SVG glyphs so they stay crisp at any zoom. */ +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { + display: block !important; + width: 17px !important; + height: 16px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement:active { + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #c7c5b8 0%, #aeaca0 100%) !important; + background-color: #c7c5b8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:increment { + display: block !important; + width: 17px !important; + height: 16px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:increment:active { + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + linear-gradient(180deg, #c7c5b8 0%, #aeaca0 100%) !important; + background-color: #c7c5b8 !important; +} + +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:horizontal:decrement { + display: block !important; + width: 16px !important; + height: 17px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} +.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:horizontal:increment { + display: block !important; + width: 16px !important; + height: 17px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + linear-gradient(180deg, #f3f1e6 0%, #d8d6c8 100%) !important; + background-color: #e7e5d8 !important; + border: 1px solid #0b2d3a !important; + border-radius: 2px !important; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + inset 0 -1px 0 rgba(0, 0, 0, 0.18) !important; +} + +.nitro-catalog-classic-breadcrumb { + display: none; +} + +@media (max-width: 640px), (orientation: landscape) and (max-height: 520px) { + .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: 10px !important; + border-radius: 0 !important; } .draggable-window:has(> .nitro-catalog-classic-window) { @@ -618,131 +1592,38 @@ top: 0 !important; } - .nitro-catalog-classic-window .nitro-card-title { - display: block; - } - - .nitro-catalog-classic-window .nitro-card-header { - border-bottom-color: transparent; - box-shadow: none; - } - - .nitro-catalog-classic-mobile-currency { - position: absolute; - top: 38px; - left: 0; - right: 0; - height: 30px; - margin: 0; - transform: none; - justify-content: center; - gap: 6px; - background: #30728c; - z-index: 5; - } - .nitro-catalog-classic-tabs-shell { - margin-top: 30px; - min-height: 56px; - max-height: 56px; - padding: 4px 4px 0; - -webkit-overflow-scrolling: touch; - } - - .nitro-catalog-classic-tabs-shell .nitro-card-tab-item { - min-height: 52px; - padding: 5px 8px; - justify-content: center; - } - - .nitro-catalog-classic-tabs-shell .nitro-card-tab-item div:has(> .nitro-catalog-classic-tab-label) { - flex-direction: column; - gap: 2px; - } - - .nitro-catalog-classic-tab-label { - font-size: 9px; - line-height: 1; + overflow-x: auto; + overflow-y: hidden; } .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 { + .nitro-catalog-classic-stage, + .nitro-catalog-classic-stage.is-navigation-hidden { grid-template-columns: minmax(0, 1fr); - gap: 6px; + grid-template-rows: auto minmax(0, 1fr); + width: 100%; } .nitro-catalog-classic-sidebar { - max-height: 33vh; + max-height: 34vh; } - .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 { + .nitro-catalog-classic-layout-shell, + .nitro-catalog-classic-default-layout { + width: 100%; + height: 100%; min-height: 0; - padding: 3px 6px; } - .nitro-catalog-classic-layout-hero { - display: none; + .nitro-catalog-classic-default-layout { + grid-template-rows: minmax(150px, 1fr) minmax(120px, 38vh) 30px; } - .nitro-catalog-classic-sidebar { - max-height: 26vh; + .nitro-catalog-classic-grid { + grid-template-columns: repeat(auto-fill, minmax(47px, 47px)) !important; } } diff --git a/src/css/chat/ChatInputMentionSelectorView.css b/src/css/chat/ChatInputMentionSelectorView.css new file mode 100644 index 0000000..d9b0cc8 --- /dev/null +++ b/src/css/chat/ChatInputMentionSelectorView.css @@ -0,0 +1,366 @@ +/* ============================================================================ + Chat-bar @-mention autocomplete - Habbo style + ---------------------------------------------------------------------------- + Mirrors the NitroCard look (cream cardstock, habbo-blue header, black 2px + border, drop shadow) and the in-room infostand row chrome. The popover + appears above the chat input, anchored to its bottom-left and the same + width as the input, with the bottom corners flush so it visually merges + with the input edge. + ============================================================================ */ + +.chat-input-mention-popover { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + margin-bottom: 4px; + background: #f2f2eb; + border: 2px solid #000; + border-radius: 10px; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28); + overflow: hidden; + z-index: 1070; + font-family: Volter, Volter_Goldfish, "Ubuntu", sans-serif; + image-rendering: pixelated; +} + +.chat-input-mention-popover-header { + height: 24px; + padding: 0 8px; + background: #30728c; + border-bottom: 2px solid #000; + color: #fff; + text-shadow: 1px 1px 1px #000; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 13px; + line-height: 22px; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 6px; +} + +.chat-input-mention-popover-header-dot { + width: 8px; + height: 8px; + background: #ffdc4c; + border: 1px solid #000; + border-radius: 50%; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.55); +} + +.chat-input-mention-popover-list { + max-height: 220px; + overflow-y: auto; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-input-mention-row { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + background: transparent; + transition: background 0.08s linear, border-color 0.08s linear; +} + +.chat-input-mention-row:hover { + background: rgba(48, 114, 140, 0.12); +} + +.chat-input-mention-row.is-selected { + background: #30728c; + border-color: #000; + color: #fff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18); +} + +.chat-input-mention-row-tile { + position: relative; + width: 36px; + height: 36px; + flex-shrink: 0; + border-radius: 6px; + border: 1px solid #000; + background: #cfcfc4; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.12); + overflow: hidden; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-tile { + background: #1f5a72; +} + +.chat-input-mention-row-tile.is-alias { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(180deg, #ffdc4c 0%, #f0a91c 100%); + color: #4a2b00; + font-weight: 700; + font-size: 16px; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.45); +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-tile.is-alias { + background: linear-gradient(180deg, #ffe97a 0%, #f9bd44 100%); +} + +.chat-input-mention-row-tile .avatar-image { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + left: 0 !important; + background-repeat: no-repeat; + background-position: -22px -32px; + background-size: auto; +} + +.chat-input-mention-row-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; + gap: 0; +} + +.chat-input-mention-row-name { + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 14px; + line-height: 1.1; + color: #2c2c2c; + font-weight: 700; + letter-spacing: 0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-name { + color: #fff; +} + +.chat-input-mention-row-desc { + font-size: 10px; + line-height: 1.1; + color: #6b6b6b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-desc { + color: #d6e6ee; +} + +.chat-input-mention-row-kind { + font-size: 9px; + line-height: 1; + padding: 2px 5px; + border-radius: 8px; + border: 1px solid #000; + background: #cfe6ef; + color: #1c3d4c; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6); +} + +.chat-input-mention-row-kind.is-alias { + background: #ffe97a; + color: #5a3a00; +} + +.chat-input-mention-row.is-selected .chat-input-mention-row-kind { + background: #ffdc4c; + color: #4a2b00; +} + +/* Habbo-style scrollbar, matching NitroCardContentView scroll chrome. */ +.chat-input-mention-popover-list::-webkit-scrollbar { + width: 8px; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-track { + background: #d8d8cf; + border-left: 1px solid #000; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-thumb { + background: #30728c; + border: 1px solid #000; + border-radius: 3px; +} + +.chat-input-mention-popover-list::-webkit-scrollbar-thumb:hover { + background: #3c88a6; +} + +/* ============================================================================ + :command popover - same Habbo NitroCard chrome as the @-mention picker. + Header is green to distinguish it visually from the mention popover, + which uses the standard habbo-blue. + ============================================================================ */ + +.chat-input-command-popover { + position: absolute; + bottom: 100%; + left: 0; + width: 100%; + margin-bottom: 4px; + background: #f2f2eb; + border: 2px solid #000; + border-radius: 10px; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28); + overflow: hidden; + z-index: 1070; + font-family: Volter, Volter_Goldfish, "Ubuntu", sans-serif; + image-rendering: pixelated; +} + +.chat-input-command-popover-header { + height: 24px; + padding: 0 8px; + background: #2f8d4a; + border-bottom: 2px solid #000; + color: #fff; + text-shadow: 1px 1px 1px #000; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 13px; + line-height: 22px; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 6px; +} + +.chat-input-command-popover-header-dot { + width: 8px; + height: 8px; + background: #ffdc4c; + border: 1px solid #000; + border-radius: 50%; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.55); +} + +.chat-input-command-popover-list { + max-height: 220px; + overflow-y: auto; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-input-command-row { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + background: transparent; + transition: background 0.08s linear, border-color 0.08s linear; +} + +.chat-input-command-row:hover { + background: rgba(47, 141, 74, 0.12); +} + +.chat-input-command-row.is-selected { + background: #2f8d4a; + border-color: #000; + color: #fff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18); +} + +.chat-input-command-row-tile { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: 6px; + border: 1px solid #000; + background: linear-gradient(180deg, #b6e6c4 0%, #5ec07d 100%); + color: #1a4a28; + font-weight: 700; + font-size: 14px; + font-family: UbuntuCondensed, Ubuntu, sans-serif; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.45); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.12); +} + +.chat-input-command-row.is-selected .chat-input-command-row-tile { + background: linear-gradient(180deg, #c8f0d5 0%, #7ed79a 100%); +} + +.chat-input-command-row-body { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; + gap: 0; +} + +.chat-input-command-row-name { + font-family: UbuntuCondensed, Ubuntu, sans-serif; + font-size: 14px; + line-height: 1.1; + color: #2c2c2c; + font-weight: 700; + letter-spacing: 0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-command-row.is-selected .chat-input-command-row-name { + color: #fff; +} + +.chat-input-command-row-desc { + font-size: 10px; + line-height: 1.1; + color: #6b6b6b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-input-command-row.is-selected .chat-input-command-row-desc { + color: #d9efde; +} + +.chat-input-command-popover-list::-webkit-scrollbar { + width: 8px; +} + +.chat-input-command-popover-list::-webkit-scrollbar-track { + background: #d8d8cf; + border-left: 1px solid #000; +} + +.chat-input-command-popover-list::-webkit-scrollbar-thumb { + background: #2f8d4a; + border: 1px solid #000; + border-radius: 3px; +} + +.chat-input-command-popover-list::-webkit-scrollbar-thumb:hover { + background: #3aa55b; +} diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 8dd0096..43f093f 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -807,435 +807,6 @@ } } - &.bubble-253 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_253.png'); - border-image-slice: 16 22 15 27 fill; - border-image-width: 16px 22px 15px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_253_pointer.png'); - } - } - - &.bubble-254 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_254.png'); - border-image-slice: 7 28 15 25 fill; - border-image-width: 7px 28px 15px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_254_pointer.png'); - } - } - - &.bubble-255 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_255.png'); - border-image-slice: 12 19 22 30 fill; - border-image-width: 12px 19px 22px 30px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_255_pointer.png'); - } - } - - &.bubble-256 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_256.png'); - border-image-slice: 24 18 10 31 fill; - border-image-width: 24px 18px 10px 31px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_256_pointer.png'); - } - } - - &.bubble-257 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_257.png'); - border-image-slice: 6 17 19 36 fill; - border-image-width: 6px 17px 19px 36px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_257_pointer.png'); - } - } - - &.bubble-258 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_258.png'); - border-image-slice: 22 27 10 27 fill; - border-image-width: 22px 27px 10px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_258_pointer.png'); - } - } - - &.bubble-259 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_259.png'); - border-image-slice: 21 27 18 37 fill; - border-image-width: 21px 27px 18px 37px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_259_pointer.png'); - } - } - - &.bubble-260 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_260.png'); - border-image-slice: 6 22 16 27 fill; - border-image-width: 6px 22px 16px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_260_pointer.png'); - } - } - - &.bubble-261 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_261.png'); - border-image-slice: 18 27 5 22 fill; - border-image-width: 18px 27px 5px 22px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_261_pointer.png'); - } - } - - &.bubble-262 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_262.png'); - border-image-slice: 33 31 11 34 fill; - border-image-width: 33px 31px 11px 34px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_262_pointer.png'); - } - } - - &.bubble-263 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_263.png'); - border-image-slice: 15 19 10 32 fill; - border-image-width: 15px 19px 10px 32px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_263_pointer.png'); - } - } - - &.bubble-264 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_264.png'); - border-image-slice: 18 24 16 25 fill; - border-image-width: 18px 24px 16px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_264_pointer.png'); - } - } - - &.bubble-265 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_265.png'); - border-image-slice: 41 40 17 18 fill; - border-image-width: 41px 40px 17px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_265_pointer.png'); - } - } - - &.bubble-266 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_266.png'); - border-image-slice: 13 34 22 27 fill; - border-image-width: 13px 34px 22px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_266_pointer.png'); - } - } - - &.bubble-267 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_267.png'); - border-image-slice: 17 30 22 25 fill; - border-image-width: 17px 30px 22px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_267_pointer.png'); - } - } - - &.bubble-268 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_268.png'); - border-image-slice: 7 30 21 24 fill; - border-image-width: 7px 30px 21px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_268_pointer.png'); - } - } - - &.bubble-269 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_269.png'); - border-image-slice: 10 23 25 35 fill; - border-image-width: 10px 23px 25px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_269_pointer.png'); - } - } - - &.bubble-270 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_270.png'); - border-image-slice: 13 30 14 26 fill; - border-image-width: 13px 30px 14px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_270_pointer.png'); - } - } - - &.bubble-271 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_271.png'); - border-image-slice: 23 23 9 35 fill; - border-image-width: 23px 23px 9px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_271_pointer.png'); - } - } - - &.bubble-272 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_272.png'); - border-image-slice: 9 31 24 25 fill; - border-image-width: 9px 31px 24px 25px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_272_pointer.png'); - } - } - - &.bubble-273 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_273.png'); - border-image-slice: 11 16 25 37 fill; - border-image-width: 11px 16px 25px 37px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_273_pointer.png'); - } - } - - &.bubble-274 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_274.png'); - border-image-slice: 7 22 19 27 fill; - border-image-width: 7px 22px 19px 27px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_274_pointer.png'); - } - } - - &.bubble-275 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_275.png'); - border-image-slice: 8 23 14 26 fill; - border-image-width: 8px 23px 14px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_275_pointer.png'); - } - } - - &.bubble-276 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_276.png'); - border-image-slice: 12 40 17 17 fill; - border-image-width: 12px 40px 17px 17px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_276_pointer.png'); - } - } - - &.bubble-277 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_277.png'); - border-image-slice: 6 39 18 17 fill; - border-image-width: 6px 39px 18px 17px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_277_pointer.png'); - } - } - - &.bubble-278 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_278.png'); - border-image-slice: 16 38 6 19 fill; - border-image-width: 16px 38px 6px 19px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_278_pointer.png'); - } - } - - &.bubble-279 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_279.png'); - border-image-slice: 6 26 16 23 fill; - border-image-width: 6px 26px 16px 23px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_279_pointer.png'); - } - } - - &.bubble-280 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_280.png'); - border-image-slice: 23 29 6 15 fill; - border-image-width: 23px 29px 6px 15px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_280_pointer.png'); - } - } - - &.bubble-281 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_281.png'); - border-image-slice: 18 42 9 18 fill; - border-image-width: 18px 42px 9px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_281_pointer.png'); - } - } - - &.bubble-282 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_282.png'); - border-image-slice: 18 42 9 18 fill; - border-image-width: 18px 42px 9px 18px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_282_pointer.png'); - } - } - - &.bubble-283 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_283.png'); - border-image-slice: 17 26 13 31 fill; - border-image-width: 17px 26px 13px 31px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_283_pointer.png'); - } - } - - &.bubble-284 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_284.png'); - border-image-slice: 9 26 23 26 fill; - border-image-width: 9px 26px 23px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_284_pointer.png'); - } - } - - &.bubble-285 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_285.png'); - border-image-slice: 16 35 15 15 fill; - border-image-width: 16px 35px 15px 15px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_285_pointer.png'); - } - } - - &.bubble-286 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_286.png'); - border-image-slice: 18 22 4 23 fill; - border-image-width: 18px 22px 4px 23px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_286_pointer.png'); - } - } - - &.bubble-287 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_287.png'); - border-image-slice: 6 22 18 26 fill; - border-image-width: 6px 22px 18px 26px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_287_pointer.png'); - } - } - - &.bubble-288 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_288.png'); - border-image-slice: 18 31 11 24 fill; - border-image-width: 18px 31px 11px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_288_pointer.png'); - } - } - - &.bubble-289 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_289.png'); - border-image-slice: 7 54 17 24 fill; - border-image-width: 7px 54px 17px 24px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_289_pointer.png'); - } - } - - &.bubble-290 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_290.png'); - border-image-slice: 18 24 14 29 fill; - border-image-width: 18px 24px 14px 29px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_290_pointer.png'); - } - } - - &.bubble-291 { - border-image-source: url('@/assets/images/chat/chatbubbles/bubble_291.png'); - border-image-slice: 9 26 11 35 fill; - border-image-width: 9px 26px 11px 35px; - border-image-repeat: stretch stretch; - - .pointer { - background: url('@/assets/images/chat/chatbubbles/bubble_291_pointer.png'); - } - } - &.bubble-200, &.bubble-201, &.bubble-202, @@ -2239,169 +1810,4 @@ background: center / contain no-repeat url('@/assets/images/chat/chatbubbles/bubble_252_extra.png'); } } - &.bubble-253 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_253.png'); - } - - &.bubble-254 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_254.png'); - } - - &.bubble-255 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_255.png'); - } - - &.bubble-256 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_256.png'); - } - - &.bubble-257 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_257.png'); - } - - &.bubble-258 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_258.png'); - } - - &.bubble-259 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_259.png'); - } - - &.bubble-260 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_260.png'); - } - - &.bubble-261 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_261.png'); - } - - &.bubble-262 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_262.png'); - } - - &.bubble-263 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_263.png'); - } - - &.bubble-264 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_264.png'); - } - - &.bubble-265 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_265.png'); - } - - &.bubble-266 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_266.png'); - } - - &.bubble-267 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_267.png'); - } - - &.bubble-268 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_268.png'); - } - - &.bubble-269 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_269.png'); - } - - &.bubble-270 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_270.png'); - } - - &.bubble-271 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_271.png'); - } - - &.bubble-272 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_272.png'); - } - - &.bubble-273 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_273.png'); - } - - &.bubble-274 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_274.png'); - } - - &.bubble-275 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_275.png'); - } - - &.bubble-276 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_276.png'); - } - - &.bubble-277 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_277.png'); - } - - &.bubble-278 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_278.png'); - } - - &.bubble-279 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_279.png'); - } - - &.bubble-280 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_280.png'); - } - - &.bubble-281 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_281.png'); - } - - &.bubble-282 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_282.png'); - } - - &.bubble-283 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_283.png'); - } - - &.bubble-284 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_284.png'); - } - - &.bubble-285 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_285.png'); - } - - &.bubble-286 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_286.png'); - } - - &.bubble-287 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_287.png'); - } - - &.bubble-288 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_288.png'); - } - - &.bubble-289 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_289.png'); - } - - &.bubble-290 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_290.png'); - } - - &.bubble-291 { - background-image: url('@/assets/images/chat/chatbubbles/bubble_291.png'); - } - -} - -/* Mention highlight inside chat bubbles (cosmetic) */ -.mention-highlight { - font-weight: 700; - color: #1e7295; - background-color: rgba(30, 114, 149, 0.16); - border-radius: 3px; - padding: 0 2px; } diff --git a/src/css/common/Buttons.css b/src/css/common/Buttons.css index 106a3cc..7188a50 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -1,3 +1,70 @@ +.nitro-swf-button { + min-height: 22px !important; + height: 22px; + padding: 2px 10px !important; + border: 3px solid transparent !important; + border-radius: 0 !important; + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_default_9x22.png") !important; + border-image-slice: 3 3 3 3 fill !important; + border-image-width: 3px !important; + border-image-repeat: stretch !important; + background: transparent !important; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + color: #222 !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 16px !important; + text-shadow: 0 1px 0 rgba(255,255,255,.75) !important; + transition: none !important; +} + +.nitro-swf-button:hover { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_hover_9x22.png") !important; + background: transparent !important; + background-color: transparent !important; +} + +.nitro-swf-button:active, +.nitro-swf-button.active { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_pressed_9x22.png") !important; + background: transparent !important; + background-color: transparent !important; +} + +.nitro-swf-button.pointer-events-none, +.nitro-swf-button:disabled { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_disabled_9x22.png") !important; + color: #888 !important; + opacity: 1 !important; +} + +.nitro-swf-button-success { + height: 24px; + min-height: 24px !important; + border: 6px solid transparent !important; + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_24x24.png") !important; + border-image-slice: 6 6 6 6 fill !important; + border-image-width: 6px !important; + color: #fff !important; + text-shadow: 0 1px 0 rgba(0,0,0,.55) !important; +} + +.nitro-swf-button-success:hover { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_hover_24x24.png") !important; +} + +.nitro-swf-button-success:active, +.nitro-swf-button-success.active { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_pressed_24x24.png") !important; +} + +.nitro-swf-button-success.pointer-events-none, +.nitro-swf-button-success:disabled { + border-image-source: url("../../assets/images/catalog/swf/habbo_skin_button_green_disabled_24x24.png") !important; +} + .btn-sm { min-height: 28px; } diff --git a/src/css/friends/FriendsView.css b/src/css/friends/FriendsView.css index 33a625d..43be89c 100644 --- a/src/css/friends/FriendsView.css +++ b/src/css/friends/FriendsView.css @@ -1,3 +1,4 @@ +/* ── Friends spritesheet icons ── */ .nitro-friends-spritesheet { background: url('@/assets/images/friends/friends-spritesheet.png') transparent no-repeat; @@ -138,8 +139,8 @@ & .nitro-card-accordion-set-content, & .nitro-card-content-shell { - scrollbar-width: thin; - scrollbar-color: #6d7b84 #cdd4d8; + scrollbar-width: auto; + scrollbar-color: auto; } & .nitro-card-accordion-set-content::-webkit-scrollbar, @@ -175,12 +176,9 @@ border: 0 !important; } - & .nitro-card-accordion-set-header span, - & .nitro-card-accordion-set-header > div { - font-size: 12px !important; - font-weight: 700; + & .nitro-card-accordion-set-header span { + font-size: 12px; color: #111 !important; - line-height: 1.1; } & .nitro-card-accordion-set-header .fa-icon { @@ -463,8 +461,8 @@ & .nitro-card-content-shell, & .chat-messages { - scrollbar-width: thin; - scrollbar-color: #6d7b84 #cdd4d8; + scrollbar-width: auto; + scrollbar-color: auto; } & .nitro-card-content-shell::-webkit-scrollbar, @@ -802,29 +800,3 @@ } } } - -.nitro-friends .friends-list-avatar { - position: relative !important; - width: 32px; - height: 36px; - flex-shrink: 0; - overflow: hidden; -} - -.nitro-friends .friends-list-avatar .avatar-image { - position: absolute !important; - inset: 0 !important; - width: 100% !important; - height: 100% !important; - margin: 0 !important; - background-size: 66px auto !important; - background-position: -16px -21px !important; - transform: none !important; -} - -.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/habbo/HabboSwfSkin.css b/src/css/habbo/HabboSwfSkin.css new file mode 100644 index 0000000..7b2d7d9 --- /dev/null +++ b/src/css/habbo/HabboSwfSkin.css @@ -0,0 +1,168 @@ +.habbo-swf-window { + --habbo-swf-ubuntu: url("../../assets/images/catalog/swf/habbo_skin_ubuntu.png"); + --habbo-swf-blue: url("../../assets/images/catalog/swf/skins/habbo_skin_blue.png"); + --habbo-swf-bg: #ecece4; + --habbo-swf-panel: #f7f7f2; + --habbo-swf-border: #9d9d96; + --habbo-swf-header: #2f8097; + --habbo-swf-header-dark: #1a5364; + --habbo-swf-text: #111; + --habbo-swf-close: url("../../assets/images/catalog/buttons/close.png"); + --habbo-swf-close-hover: url("../../assets/images/catalog/buttons/close_hover.png"); + --habbo-swf-close-pressed: url("../../assets/images/catalog/buttons/close_pressed.png"); + --habbo-swf-button: url("../../assets/images/catalog/buttons/btn_secondary.png"); + --habbo-swf-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); + --habbo-swf-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); + --habbo-swf-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); + --habbo-swf-button-green: url("../../assets/images/catalog/buttons/buy.png"); + color: var(--habbo-swf-text) !important; + background: var(--habbo-swf-bg) !important; + border: 1px solid #000 !important; + border-radius: 7px 7px 0 0 !important; + font-family: Ubuntu, Arial, sans-serif !important; + image-rendering: pixelated; +} + +.habbo-swf-window, +.habbo-swf-window * { + box-sizing: border-box; + image-rendering: pixelated; +} + +.habbo-swf-window .nitro-card-header-shell, +.habbo-swf-window .nitro-card-header { + min-height: 35px !important; + max-height: 35px !important; + height: 35px !important; + background: var(--habbo-swf-header) !important; + border: 0 !important; + border-bottom: 1px solid #000 !important; + border-radius: 6px 6px 0 0 !important; +} + +.habbo-swf-window .nitro-card-title { + color: #fff !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 16px !important; + font-weight: 700 !important; + line-height: 35px !important; + text-align: center !important; + text-shadow: 0 1px 0 #000 !important; +} + +.habbo-swf-window .nitro-card-close-button { + top: 7px !important; + right: 7px !important; + width: 19px !important; + min-width: 19px !important; + height: 20px !important; + min-height: 20px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + /* Direct URL instead of var(--habbo-swf-close) - some browser / + bundler combinations don't resolve relative url()s inside CSS + custom properties consistently (they're spec'd to resolve from + the document, not the stylesheet). Inlining the path makes + this immune. */ + background-image: url("../../assets/images/catalog/buttons/close.png") !important; + background-repeat: no-repeat !important; + background-position: center !important; + background-size: 19px 20px !important; + box-shadow: none !important; + image-rendering: pixelated !important; + opacity: 1 !important; + visibility: visible !important; + display: block !important; +} + +.habbo-swf-window .nitro-card-close-button:hover { + background-image: url("../../assets/images/catalog/buttons/close_hover.png") !important; +} + +.habbo-swf-window .nitro-card-close-button:active { + background-image: url("../../assets/images/catalog/buttons/close_pressed.png") !important; +} + +.habbo-swf-window .nitro-card-close-button::before, +.habbo-swf-window .nitro-card-close-button::after { + display: none !important; +} + +.habbo-swf-window .nitro-card-content-shell, +.habbo-swf-window .nitro-card-content { + background: var(--habbo-swf-bg) !important; + color: var(--habbo-swf-text) !important; + border-radius: 0 !important; +} + +.habbo-swf-window button, +.habbo-swf-window .btn, +.habbo-swf-window .nitro-swf-button { + min-height: 22px !important; + border: 4px solid transparent !important; + border-radius: 0 !important; + border-image-source: var(--habbo-swf-button) !important; + border-image-slice: 4 4 4 4 fill !important; + border-image-width: 4px !important; + border-image-repeat: stretch !important; + background: transparent !important; + box-shadow: none !important; + color: #111 !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 12px !important; + font-weight: 700 !important; + line-height: 14px !important; +} + +.habbo-swf-window button:hover, +.habbo-swf-window .btn:hover, +.habbo-swf-window .nitro-swf-button:hover { + border-image-source: var(--habbo-swf-button-hover) !important; +} + +.habbo-swf-window button:active, +.habbo-swf-window .btn:active, +.habbo-swf-window .nitro-swf-button:active { + border-image-source: var(--habbo-swf-button-pressed) !important; +} + +.habbo-swf-window button:disabled, +.habbo-swf-window .btn:disabled, +.habbo-swf-window .nitro-swf-button:disabled { + border-image-source: var(--habbo-swf-button-disabled) !important; + color: #8d8d87 !important; +} + +.habbo-swf-window .btn-success, +.habbo-swf-window button[class*="success"], +.habbo-swf-window .nitro-swf-button-success { + border-image-source: var(--habbo-swf-button-green) !important; + color: #fff !important; + text-shadow: 0 1px 0 #004b00 !important; +} + +.habbo-swf-window input, +.habbo-swf-window textarea, +.habbo-swf-window select { + min-height: 22px !important; + border: 1px solid #b7b7ae !important; + border-radius: 3px !important; + background: #fff !important; + color: #333 !important; + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08) !important; + font-family: Ubuntu, Arial, sans-serif !important; + font-size: 12px !important; +} + +.habbo-swf-window .nitro-card-tabs-shell, +.habbo-swf-window .nitro-catalog-classic-tabs-shell { + background: var(--habbo-swf-bg) !important; + border-bottom: 1px solid #000 !important; +} + +.habbo-swf-window .nitro-card-tab-item { + color: #111 !important; + text-shadow: none !important; +} diff --git a/src/css/icons/icons.css b/src/css/icons/icons.css index 7245da9..17a903f 100644 --- a/src/css/icons/icons.css +++ b/src/css/icons/icons.css @@ -229,13 +229,6 @@ height: 32px; } -.nitro-icon.icon-mentions { - background-image: url("@/assets/images/toolbar/icons/mentions.png"); - background-size: contain; - width: 36px; - height: 32px; -} - .nitro-icon.icon-wired-tools { background-image: url("@/assets/images/wiredtools/wired_menu.png"); background-size: contain; diff --git a/src/css/index.css b/src/css/index.css index 8be4f78..46f219d 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -51,8 +51,8 @@ body { -webkit-user-select: none; user-select: none; overscroll-behavior: none; - scrollbar-width: thin; - scrollbar-color: #6d7b84 #c8d0d4; + scrollbar-width: auto; + scrollbar-color: auto; } #root { @@ -115,77 +115,98 @@ body { @apply outline-0; } -::-webkit-scrollbar { - width: .875rem; +*::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; } -::-webkit-scrollbar:horizontal { - height: .875rem; +*::-webkit-scrollbar:horizontal { + height: 17px !important; } -::-webkit-scrollbar:not(:horizontal) { - width: .875rem; +*::-webkit-scrollbar:not(:horizontal) { + width: 17px !important; } -::-webkit-scrollbar-track { - background: linear-gradient(180deg, #dfe5e8 0%, #c9d1d5 100%); - border-left: 1px solid #7a858b; - border-right: 1px solid #eef3f5; - border-radius: 0; +/* App-wide Habbo scrollbar (sprites cropped from catalog_skin1.png). + Thumb sprite is 17x34 with caps + grip baked in; stretched full + height via background-size: 17px 100%. Arrow buttons are natural + 17x16 sprites. */ + +*::-webkit-scrollbar-track { + background-color: #e7e5d8 !important; + background-image: none !important; + box-shadow: inset 1px 0 0 #b9b6a5, inset -1px 0 0 #ffffff !important; + border: 0 !important; + border-radius: 0 !important; } -::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, #8fb5c7 0%, #5d8ea5 100%); - border: 1px solid #446879; - border-radius: 2px; - box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.28); +*::-webkit-scrollbar-thumb { + min-height: 24px !important; + background-color: transparent !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_v_thumb.png") !important; + background-repeat: no-repeat !important; + background-position: center center !important; + background-size: 17px 100% !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + image-rendering: pixelated !important; } -::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, #99c2d5 0%, #689ab0 100%); +*::-webkit-scrollbar-thumb:hover, +*::-webkit-scrollbar-thumb:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png") !important; } -::-webkit-scrollbar-thumb:active { - background: linear-gradient(180deg, #5c889d 0%, #436977 100%); +*::-webkit-scrollbar-corner { + background: transparent !important; } -::-webkit-scrollbar-corner { - background: #c9d1d5; +*::-webkit-scrollbar-button:single-button { + display: block !important; + width: 17px !important; + height: 16px !important; + /* Cream fill so the arrow sprite's transparent rounded corners + paint over the track colour, not whatever is behind the + scrollbar (which can render black). */ + background-color: #e7e5d8 !important; + background-repeat: no-repeat !important; + background-position: center !important; + border: 0 !important; + image-rendering: pixelated !important; } -::-webkit-scrollbar-button:single-button { - display: block; - width: .875rem; - height: .875rem; - background-color: #d8dfe3; - background-repeat: no-repeat; - background-position: center; - border-left: 1px solid #7a858b; - border-right: 1px solid #eef3f5; +*::-webkit-scrollbar-button:single-button:vertical:decrement { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_up.png") !important; +} +*::-webkit-scrollbar-button:single-button:vertical:decrement:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_up_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:vertical:decrement { - background-image: linear-gradient(135deg, transparent 50%, #35586a 50%), linear-gradient(225deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: calc(50% - 3px) 55%, calc(50% + 3px) 55%; +*::-webkit-scrollbar-button:single-button:vertical:increment { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_down.png") !important; +} +*::-webkit-scrollbar-button:single-button:vertical:increment:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_v_down_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:vertical:increment { - background-image: linear-gradient(315deg, transparent 50%, #35586a 50%), linear-gradient(45deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: calc(50% - 3px) 45%, calc(50% + 3px) 45%; +*::-webkit-scrollbar-button:single-button:horizontal:decrement { + width: 16px !important; + height: 17px !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_h_left.png") !important; +} +*::-webkit-scrollbar-button:single-button:horizontal:decrement:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_h_left_pressed.png") !important; } -::-webkit-scrollbar-button:single-button:horizontal:decrement { - background-image: linear-gradient(45deg, transparent 50%, #35586a 50%), linear-gradient(135deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: 58% calc(50% - 3px), 58% calc(50% + 3px); +*::-webkit-scrollbar-button:single-button:horizontal:increment { + width: 16px !important; + height: 17px !important; + background-image: url("../assets/images/catalog/scrollbar/scroll_h_right.png") !important; } - -::-webkit-scrollbar-button:single-button:horizontal:increment { - background-image: linear-gradient(225deg, transparent 50%, #35586a 50%), linear-gradient(315deg, transparent 50%, #35586a 50%); - background-size: 6px 6px; - background-position: 42% calc(50% - 3px), 42% calc(50% + 3px); +*::-webkit-scrollbar-button:single-button:horizontal:increment:active { + background-image: url("../assets/images/catalog/scrollbar/scroll_h_right_pressed.png") !important; } @layer components { @@ -306,7 +327,7 @@ body { .nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell { border: 2px solid #3c88a6; - border-bottom-color: #30728c; + border-bottom-color: #000; border-radius: 8px 8px 0 0; background: #30728c; padding: 5px; @@ -314,7 +335,7 @@ body { .nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell.builders-club-card-header { border-color: #d79d2e; - border-bottom-color: #c68515; + border-bottom-color: #000; background: linear-gradient(180deg, #d89f2d 0%, #c68515 100%); } diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index fae3328..af2263d 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -1190,7 +1190,7 @@ overflow-x: hidden; padding-right: 4px; margin-top: 2px; - scrollbar-width: thin; + scrollbar-width: auto; } .nitro-login-card .room-template-option { diff --git a/src/css/navigator/HabboNavigatorDesktop.css b/src/css/navigator/HabboNavigatorDesktop.css new file mode 100644 index 0000000..dd75c44 --- /dev/null +++ b/src/css/navigator/HabboNavigatorDesktop.css @@ -0,0 +1,242 @@ +.habbo-navigator-desktop { + border: 1px solid #000; + border-radius: 7px; + background: #e9e9e1; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35); + color: #111; + font-family: Ubuntu, Arial, sans-serif; +} + +.habbo-navigator-desktop .nitro-card-header-shell { + min-height: 32px; + max-height: 32px; + background: #418db0; + border-bottom: 1px solid #000; + border-radius: 6px 6px 0 0; +} + +.habbo-navigator-desktop .nitro-card-title { + color: #fff; + font-family: UbuntuCondensed, Ubuntu, Arial, sans-serif; + font-size: 18px; + font-weight: 700; + line-height: 1; + text-shadow: 1px 1px 0 #000; +} + +.habbo-navigator-desktop .nitro-card-close-button { + width: 19px; + height: 20px; + right: 6px; + border: 2px solid #000; + border-radius: 4px; + background: #c73a32; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35); +} + +.habbo-navigator-desktop .nitro-card-close-button::before, +.habbo-navigator-desktop .nitro-card-close-button::after { + content: ""; + position: absolute; + width: 10px; + height: 2px; + background: #fff; + box-shadow: 1px 1px 0 #64120f; +} + +.habbo-navigator-desktop .nitro-card-close-button::before { + transform: rotate(45deg); +} + +.habbo-navigator-desktop .nitro-card-close-button::after { + transform: rotate(-45deg); +} + +.habbo-navigator-desktop .nitro-card-tabs-shell { + justify-content: flex-start; + gap: 0; + min-height: 27px; + max-height: 27px; + padding: 4px 8px 0; + background: #e9e9e1; + border-bottom: 1px solid #b8b8ad; +} + +.habbo-navigator-desktop .nitro-card-tab-item { + min-height: 23px; + margin-right: -1px; + padding: 4px 12px 3px; + border: 1px solid #555; + border-bottom: 0; + border-radius: 6px 6px 0 0; + background-color: #d5d8cf; + background-image: url("../../assets/images/navigator/swf/tab_bg_unsel.png"); + background-repeat: repeat-x; + background-size: auto 100%; + color: #111; + font-size: 12px; + font-weight: 400; + line-height: 1; + box-shadow: inset 1px 1px 0 #fff; +} + +.habbo-navigator-desktop .nitro-card-tab-item:hover { + background-color: #e7e8df; + background-image: url("../../assets/images/navigator/swf/tab_bg_hilite.png"); +} + +.habbo-navigator-desktop .nitro-card-tab-item-active { + z-index: 2; + margin-bottom: -1px; + background-color: #f4f4ed; + background-image: url("../../assets/images/navigator/swf/tab_bg_sel.png"); + border-bottom: 1px solid #f4f4ed; + font-weight: 700; +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content { + padding: 8px 9px 9px; + overflow: hidden; + background: #f4f4ed; + border-radius: 0 0 6px 6px; + color: #111; +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content input, +.habbo-navigator-desktop .habbo-navigator-desktop-content select, +.habbo-navigator-desktop .habbo-navigator-desktop-content textarea { + height: 22px; + border: 1px solid #a0a49c; + border-radius: 3px; + background-color: #fff; + background-image: url("../../assets/images/navigator/swf/hdr_search.png"); + background-repeat: repeat-x; + color: #333; + font-size: 12px; + box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08); +} + +.habbo-navigator-desktop .habbo-navigator-desktop-content button, +.habbo-navigator-desktop .habbo-navigator-desktop-content .btn { + min-height: 22px; + border: 1px solid #3a3a3a; + border-radius: 4px; + background-color: #d6d6d1; + background-image: url("../../assets/images/navigator/swf/button.png"); + background-repeat: repeat-x; + background-size: auto 100%; + color: #111; + font-size: 12px; + font-weight: 700; + box-shadow: inset 1px 1px 0 #fff; +} + +.habbo-navigator-desktop .nitro-card-panel { + border: 1px solid #babdb4; + border-radius: 6px; + background: #efefe8; + box-shadow: inset 1px 1px 0 #fff; + overflow: hidden; +} + +.habbo-navigator-desktop .nitro-card-panel > .flex:first-child { + min-height: 28px; + padding: 5px 8px; + border-bottom: 1px solid #d3d5cd; + background: #efefe8; +} + +.habbo-navigator-desktop .navigator-grid { + padding: 0 4px 5px; + background: #fff; +} + +.habbo-navigator-desktop .navigator-item { + min-height: 28px; + margin: 2px 0; + border: 1px solid transparent; + border-radius: 5px; + background: #f5f5ef; + color: #111; +} + +.habbo-navigator-desktop .navigator-item:nth-child(even) { + background: #e7e8e0; +} + +.habbo-navigator-desktop .navigator-item:hover { + border-color: #777; + background: #fff; +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result { + width: 155px; + min-width: 155px; + height: 100%; + border: 1px solid #babdb4; + border-radius: 6px; + background: #efefe8; + padding: 4px; +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result > .flex:first-child { + min-height: 24px; + border: 1px solid #d58e00; + border-radius: 4px; + background: #f8a900; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.45); +} + +.habbo-navigator-desktop .nitro-navigator-search-saves-result span { + color: #111; + font-weight: 700; +} + +.habbo-navigator-desktop .nitro-icon.icon-navigator-info { + width: 16px; + height: 16px; +} + +.habbo-navigator-desktop ::-webkit-scrollbar { + width: 17px !important; + height: 17px !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-track { + border: 0 !important; + background-color: transparent !important; + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_track_v_17x2.png") !important; + background-repeat: repeat-y !important; + background-position: center top !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-thumb { + min-height: 24px !important; + border: 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + background-image: + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_grip_7x10.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_top_17x2.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_bottom_17x2.png"), + url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_mid_17x1.png") !important; + background-repeat: repeat-y, no-repeat, no-repeat, repeat-y !important; + background-position: center center, center top, center bottom, center top !important; + background-size: 7px 10px, 17px 2px, 17px 2px, 17px 1px !important; + box-shadow: none !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button { + width: 17px !important; + height: 16px !important; + background-color: transparent !important; + border: 0 !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:decrement { + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_up_17x16.png") !important; +} + +.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:increment { + background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_down_17x16.png") !important; +} diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index e935296..c26057a 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -12,6 +12,19 @@ const MESSENGER_HISTORY_MAX = 1000; let CHAT_HISTORY_COUNTER: number = 0; let MESSENGER_HISTORY_COUNTER: number = 0; +/** + * Project a list of chat entries to the slim shape we want to persist in + * localStorage. `imageUrl` is a base64 data URL of the avatar / pet head + * (10-50 KB each) - keeping it in storage blows past the browser quota + * inside minutes in a pet-heavy room. The avatar can always be re-rendered + * from `look` via ChatBubbleUtilities.getUserImage(), and pet images are + * regenerated from the bubble flow when needed; we just don't restore + * head thumbnails for entries loaded from a previous session. + * + * `style` / `chatType` / `color` are kept because they're tiny but + * meaningful for re-rendering the bubble. Translation fields are kept + * because they're already text. + */ const slimChatEntriesForStorage = (entries: IChatEntry[]): IChatEntry[] => entries.map(entry => entry.imageUrl ? { ...entry, imageUrl: undefined } : entry); diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index ea61c6f..ed0b9ed 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -23,6 +23,11 @@ const useChatWidgetState = () => const { addChatEntry, updateChatEntry } = useChatHistory(); const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation(); const isDisposed = useRef(false); + // Reactive: re-renders if the session-data snapshot flips (e.g. + // reconnect under a different user id). Safe to call here — + // useChatWidget is NOT wrapped in useBetween (see export below), + // so the real React dispatcher is in scope and + // useSyncExternalStore installs correctly. const ownUserId = (useUserDataSnapshot().userId || -1); const applyTranslationToBubble = useCallback((chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => @@ -226,6 +231,12 @@ const useChatWidgetState = () => return newValue; }); + // Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"): + // the live bubble already rendered above, but we deliberately skip + // addChatEntry so the entry never lands in localStorage. A pet-heavy + // room used to push 30+ KB per message (base64 head data URL) into + // the chat history, exhausting the localStorage quota in minutes. + // Real users still go through the full persisted path. const chatEntryId = (userType === RoomObjectType.USER) ? addChatEntry({ id: -1, diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 1e760c8..13c00cc 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -13,6 +13,7 @@ const isQuotaError = (error: unknown): boolean => if(!error || typeof error !== 'object') return false; const name = (error as { name?: string }).name; if(name === 'QuotaExceededError') return true; + // Firefox legacy: if(name === 'NS_ERROR_DOM_QUOTA_REACHED') return true; return false; }; @@ -27,6 +28,12 @@ const trimArrayForQuota = (value: T): T => interface UseLocalStorageOptions { + /** + * Optional projection applied right before the value is written to + * localStorage. The in-memory React state is unaffected. Use this to + * strip heavy ephemeral fields (e.g. base64 image URLs) that would + * otherwise blow past the storage quota. + */ toStorage?: (value: T) => unknown; } @@ -52,6 +59,7 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal const writeTimerRef = useRef | null>(null); const optionsRef = useRef(options); + // Keep the latest toStorage projection without re-running effects. optionsRef.current = options; const flushWrite = (value: T) => @@ -75,6 +83,8 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal } } + // Quota exceeded - trim and retry once. Anything that isn't an + // array gets cleared, since we have no generic trimming rule. try { const trimmed = trimArrayForQuota(projected as T); @@ -84,10 +94,14 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal catch(retryError) { NitroLogger.error(retryError); - try { window.localStorage.removeItem(key); } catch(_) { } + // Last resort: drop the key entirely so future writes have room. + try { window.localStorage.removeItem(key); } catch(_) { /* ignore */ } } }; + // Debounce: high-frequency chat would otherwise trigger one full + // JSON.stringify + setItem per message. We coalesce bursts into one + // write per STORAGE_WRITE_DEBOUNCE_MS window with the latest value. const scheduleWrite = (value: T) => { pendingWriteRef.current = value; @@ -103,6 +117,8 @@ const useLocalStorageState = (key: string, initialValue: T, options: UseLocal }, STORAGE_WRITE_DEBOUNCE_MS); }; + // Flush a pending write on tab close / hide so we don't lose the last + // burst of activity. useEffect(() => { const flushOnLeave = () => diff --git a/src/index.tsx b/src/index.tsx index d1f1b0f..6fb5997 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import './css/catalog/CatalogClassicView.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; +import './css/chat/ChatInputMentionSelectorView.css'; import './css/mentions/MentionToasts.css'; import './css/common/Buttons.css'; @@ -32,6 +33,8 @@ import './css/forms/form_select.css'; import './css/friends/FriendsView.css'; +import './css/habbo/HabboSwfSkin.css'; + import './css/hotelview/HotelView.css'; import './css/login/LoginView.css'; @@ -43,6 +46,8 @@ import './css/inventory/InventoryView.css'; import './css/layout/LayoutTrophy.css'; +import './css/navigator/HabboNavigatorDesktop.css'; + import './css/nitrocard/NitroCardView.css';