diff --git a/src/common/layout/LayoutGridItem.tsx b/src/common/layout/LayoutGridItem.tsx index e8d66ef..54a7ad4 100644 --- a/src/common/layout/LayoutGridItem.tsx +++ b/src/common/layout/LayoutGridItem.tsx @@ -24,10 +24,10 @@ export const LayoutGridItem: FC = props => const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-[#c4cabf]', 'rounded-[6px]' ]; + const newClassNames: string[] = [ 'layout-grid-item' ]; - if(itemActive) newClassNames.push('bg-[#e4e7df]! border-[#aeb7aa]!'); + if(itemActive) newClassNames.push('is-grid-active', 'bg-[#e4e7df]!'); if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item'); diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index 3926e42..1927a1c 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -9,11 +9,12 @@ interface CatalogGridOfferViewProps extends LayoutGridItemProps { offer: IPurchasableOffer; selectOffer: (offer: IPurchasableOffer) => void; + tintColor?: string; } export const CatalogGridOfferView: FC = props => { - const { offer = null, selectOffer = null, itemActive = false, ...rest } = props; + const { offer = null, selectOffer = null, itemActive = false, tintColor = null, ...rest } = props; const [ isMouseDown, setMouseDown ] = useState(false); const { requestOfferToMover = null } = useCatalogActions(); const { currentType = CatalogType.NORMAL } = useCatalogUiState(); @@ -130,6 +131,7 @@ export const CatalogGridOfferView: FC = props => className="nitro-catalog-classic-grid-offer-icon" src={ iconUrl } draggable={ false } + style={ tintColor ? { filter: 'url(#guild-furni-recolor)', transform: 'translateZ(0)' } : undefined } onError={ event => { const fallbackIconUrl = product.getIconUrl(offer); diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 917666b..f41ff31 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -112,6 +112,7 @@ export const CatalogLayoutDefaultView: FC = props =>
+ { LocalizeText('catalog.bundlewidget.price') }
} diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx index 863b39e..f946726 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -1,9 +1,11 @@ -import { FC } from 'react'; +import { StringDataType } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; import { FaExchangeAlt, FaSyncAlt } from 'react-icons/fa'; import { Column } from '../../../../../common'; -import { useCatalogData, useUserGroups } from '../../../../../hooks'; +import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks'; import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView'; +import { CatalogGuildFurniRecolorFilter } from '../widgets/CatalogGuildFurniRecolorFilter'; import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView'; import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; @@ -14,11 +16,43 @@ import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayouGuildCustomFurniView: FC = () => { const { currentOffer = null, roomPreviewer = null } = useCatalogData(); + const { purchaseOptions = null } = useCatalogUiState(); const { data: groups = null } = useUserGroups(); const hasGroups = !!(groups && groups.length); + const [ groupColors, setGroupColors ] = useState<{ colorA: string; colorB: string } | null>(null); + + useEffect(() => + { + const previewStuffData = purchaseOptions?.previewStuffData ?? null; + + if(!previewStuffData) return; + + const colorA = (previewStuffData as StringDataType).getValue(3); + const colorB = (previewStuffData as StringDataType).getValue(4); + + if(!colorA || !colorA.length) return; + + const next = { colorA, colorB: (colorB && colorB.length) ? colorB : colorA }; + + setGroupColors(prev => (prev && (prev.colorA === next.colorA) && (prev.colorB === next.colorB)) ? prev : next); + }, [ purchaseOptions ]); + + + const tintColor = useMemo(() => + { + if(!groupColors) return null; + + const { colorA, colorB } = groupColors; + + if(colorB && (colorB !== colorA)) return `linear-gradient(90deg, #${ colorA } 0 50%, #${ colorB } 50% 100%)`; + + return `#${ colorA }`; + }, [ groupColors ]); return ( <> + { !!groupColors && + } { !!currentOffer && @@ -38,7 +72,7 @@ export const CatalogLayouGuildCustomFurniView: FC = () => }
- +
{ !!currentOffer &&
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx index 0145230..07ec5de 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx @@ -33,7 +33,9 @@ export const CatalogLayouGuildForumView: FC = props =>
{ hasGroups && - } +
+ +
} }
diff --git a/src/components/catalog/views/page/widgets/CatalogGuildFurniRecolorFilter.tsx b/src/components/catalog/views/page/widgets/CatalogGuildFurniRecolorFilter.tsx new file mode 100644 index 0000000..3de982b --- /dev/null +++ b/src/components/catalog/views/page/widgets/CatalogGuildFurniRecolorFilter.tsx @@ -0,0 +1,52 @@ +import { memo, useMemo } from 'react'; + +interface CatalogGuildFurniRecolorFilterProps +{ + colorA?: string; + colorB?: string; +} + +export const GUILD_FURNI_RECOLOR_FILTER_ID = 'guild-furni-recolor'; +const OUTLINE_LEVEL = 0.08; + +const toUnit = (hex: string, offset: number): number => +{ + const value = parseInt(hex.substr(offset, 2), 16); + + return (isNaN(value) ? 0 : value) / 255; +}; + +export const CatalogGuildFurniRecolorFilter = memo((props: CatalogGuildFurniRecolorFilterProps) => +{ + const { colorA = null, colorB = null } = props; + + const tables = useMemo(() => + { + if(!colorA || (colorA.length < 6) || !colorB || (colorB.length < 6)) return null; + + const aR = toUnit(colorA, 0), aG = toUnit(colorA, 2), aB = toUnit(colorA, 4); + const bR = toUnit(colorB, 0), bG = toUnit(colorB, 2), bB = toUnit(colorB, 4); + + return { + r: `${ OUTLINE_LEVEL } ${ bR } ${ bR } ${ aR } ${ aR } ${ aR }`, + g: `${ OUTLINE_LEVEL } ${ bG } ${ bG } ${ aG } ${ aG } ${ aG }`, + b: `${ OUTLINE_LEVEL } ${ bB } ${ bB } ${ aB } ${ aB } ${ aB }` + }; + }, [ colorA, colorB ]); + + if(!tables) return null; + + return ( + + ); +}); + +CatalogGuildFurniRecolorFilter.displayName = 'CatalogGuildFurniRecolorFilter'; diff --git a/src/components/catalog/views/page/widgets/CatalogItemGridWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogItemGridWidgetView.tsx index e34dbb8..c46e4e3 100644 --- a/src/components/catalog/views/page/widgets/CatalogItemGridWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogItemGridWidgetView.tsx @@ -7,12 +7,12 @@ import { CatalogGridOfferView } from '../common/CatalogGridOfferView'; interface CatalogItemGridWidgetViewProps extends AutoGridProps { - + tintColor?: string; } export const CatalogItemGridWidgetView: FC = props => { - const { columnCount = 5, children = null, ...rest } = props; + const { columnCount = 5, tintColor = null, children = null, ...rest } = props; const { currentOffer = null, currentPage = null } = useCatalogData(); const { selectCatalogOffer = null } = useCatalogActions(); const catalogAdmin = useCatalogAdmin(); @@ -26,13 +26,6 @@ export const CatalogItemGridWidgetView: FC = pro if(elementRef && elementRef.current) elementRef.current.scrollTop = 0; }, [ currentPage ]); - // Drag-and-drop handlers — hooks MUST run unconditionally so the - // hook order stays stable when currentPage flips from null to a - // real value (the `if(!currentPage) return null` below would - // otherwise hide these from the first render and React would flag - // "Rendered more hooks than during the previous render"). Bodies - // are safe to evaluate pre-load: currentPage? optional chaining - // already guards the only access inside handleDrop. const handleDragStart = useCallback((index: number) => { setDragIndex(index); @@ -96,6 +89,7 @@ export const CatalogItemGridWidgetView: FC = pro itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } + tintColor={ tintColor } /> ); diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index 5f602b3..57c4a81 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -827,40 +827,45 @@ overflow: visible !important; } -.nitro-catalog-classic-window .layout-grid-item { - 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-image: none !important; - box-shadow: none !important; - overflow: visible !important; -} +@layer utilities { + .nitro-catalog-classic-window .layout-grid-item { + 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-image: none !important; + box-shadow: none !important; + overflow: visible !important; + } -.nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { - background-color: transparent !important; -} + .nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { + background-color: #e4e7df !important; + border: 2px solid transparent !important; + border-radius: 4px !important; + box-shadow: none !important; + } -.nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item { - width: 84px !important; - min-width: 84px !important; - height: 74px !important; - min-height: 74px !important; -} + .nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item { + width: 84px !important; + min-width: 84px !important; + height: 74px !important; + min-height: 74px !important; + } -.nitro-catalog-classic-window .layout-grid-item:hover { - background-image: none !important; - box-shadow: inset 0 0 0 1px #a1a19b !important; -} + .nitro-catalog-classic-window .layout-grid-item:not(.has-highlight):not(.is-active):hover { + background-image: none !important; + border-color: transparent !important; + box-shadow: none !important; + } -.nitro-catalog-classic-window .layout-grid-item.is-active { - 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-window .layout-grid-item.is-active { + background-color: #ffffff !important; + background-image: none !important; + border: 2px solid #62c4e8 !important; + box-shadow: none !important; + } } .nitro-catalog-classic-window .layout-grid-item.has-highlight { @@ -901,13 +906,15 @@ pointer-events: none; } + .nitro-catalog-classic-grid-price { display: flex; flex-direction: row; align-items: center; - justify-content: center; + justify-content: flex-end; gap: 3px; width: 100%; + padding-right: 4px; color: #000; font-size: 11px; font-weight: 700; @@ -963,7 +970,8 @@ .nitro-catalog-classic-grid-price.is-multi-price { height: auto; min-height: 0; - flex-wrap: wrap; + flex-direction: column; + align-items: flex-end; row-gap: 1px; } @@ -971,7 +979,7 @@ display: inline-flex; align-items: center; justify-content: center; - gap: 1px; + gap: 3px; height: 13px; white-space: nowrap; } @@ -1022,8 +1030,7 @@ padding-right: 2px; } -.nitro-catalog-classic-total-price-slot::before { - content: "Prezzo"; +.nitro-catalog-classic-total-price-label { color: #666; font-size: 11px; line-height: 17px; @@ -1463,9 +1470,6 @@ } .nitro-catalog-classic-window *::-webkit-scrollbar-thumb { - /* Grip: a single 2px #a0a0a0 stripe in an 8px-wide centered band, - repeated every 5px (2px stripe + 3px body gap). - Outline: 1px black border, then a 2px white inset frame inside it. */ min-height: 24px !important; background: url("data:image/svg+xml;utf8,") center top / 8px 5px repeat-y, @@ -1487,9 +1491,6 @@ #bcbcbc !important; } -/* Arrow buttons: light grey cap with rounded OUTER corners (up button - rounded at the top, down button rounded at the bottom), 1px black - border, dark chevron via inline SVG. */ .nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { display: block !important; width: 18px !important; @@ -1606,13 +1607,6 @@ display: none !important; } - /* Stack the navigation above the furni/preview layout and let the - whole content area scroll. The previous grid used - `grid-template-rows: auto minmax(0, 1fr)`, but on iOS Safari the - flex height chain is indefinite, so the 1fr layout-shell row - collapsed to 0 and only the sidebar (category list) was visible. - A flex column sized to content + a scrollable content-shell is - device-robust. */ .nitro-catalog-classic-stage, .nitro-catalog-classic-stage.is-navigation-hidden { display: flex; @@ -1628,9 +1622,6 @@ max-height: 30vh; } - /* The default layout's children (preview, grid, buy bar) are - absolutely positioned against a fixed ~460px box, so give the - shell a definite height and never clip it on mobile. */ .nitro-catalog-classic-layout-shell { flex: 0 0 auto; width: 100%; diff --git a/src/css/index.css b/src/css/index.css index 46f219d..1a11ad5 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -116,97 +116,128 @@ body { } *::-webkit-scrollbar { - width: 17px !important; - height: 17px !important; + width: 18px !important; + height: 18px !important; + background-color: #bdbbb3 !important; } *::-webkit-scrollbar:horizontal { - height: 17px !important; + height: 18px !important; } *::-webkit-scrollbar:not(:horizontal) { - width: 17px !important; + width: 18px !important; } -/* 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; + background-color: #bdbbb3 !important; + box-shadow: inset 1px 0 0 #000000 !important; border: 0 !important; border-radius: 0 !important; } *::-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; + background: + url("data:image/svg+xml;utf8,") center top / 8px 5px repeat-y, + #d9d9d9 !important; + border: 1px solid #000000 !important; + border-radius: 3px !important; + box-shadow: inset 0 0 0 2px #ffffff !important; image-rendering: pixelated !important; } -*::-webkit-scrollbar-thumb:hover, +*::-webkit-scrollbar-thumb:hover { + background: + url("data:image/svg+xml;utf8,") center top / 8px 5px repeat-y, + #e3e3e3 !important; +} + *::-webkit-scrollbar-thumb:active { - background-image: url("../assets/images/catalog/scrollbar/scroll_v_thumb_pressed.png") !important; + background: + url("data:image/svg+xml;utf8,") center top / 8px 5px repeat-y, + #bcbcbc !important; } *::-webkit-scrollbar-corner { background: transparent !important; } -*::-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:vertical:decrement { - background-image: url("../assets/images/catalog/scrollbar/scroll_v_up.png") !important; + display: block !important; + width: 18px !important; + height: 18px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + #d9d9d9 !important; + border: 1px solid #000000 !important; + border-radius: 3px 3px 0 0 !important; + box-shadow: + inset 0 1px 0 #ffffff, + inset 1px 0 0 rgba(255, 255, 255, 0.6) !important; } *::-webkit-scrollbar-button:single-button:vertical:decrement:active { - background-image: url("../assets/images/catalog/scrollbar/scroll_v_up_pressed.png") !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + #bcbcbc !important; } *::-webkit-scrollbar-button:single-button:vertical:increment { - background-image: url("../assets/images/catalog/scrollbar/scroll_v_down.png") !important; + display: block !important; + width: 18px !important; + height: 18px !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + #d9d9d9 !important; + border: 1px solid #000000 !important; + border-radius: 0 0 3px 3px !important; + box-shadow: + inset 0 1px 0 #ffffff, + inset 1px 0 0 rgba(255, 255, 255, 0.6) !important; } *::-webkit-scrollbar-button:single-button:vertical:increment:active { - background-image: url("../assets/images/catalog/scrollbar/scroll_v_down_pressed.png") !important; + background: + url("data:image/svg+xml;utf8,") center center / 9px 6px no-repeat, + #bcbcbc !important; } *::-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; + display: block !important; + width: 18px !important; + height: 18px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + #d9d9d9 !important; + border: 1px solid #000000 !important; + border-radius: 3px 0 0 3px !important; + box-shadow: + inset 0 1px 0 #ffffff, + inset 1px 0 0 rgba(255, 255, 255, 0.6) !important; } *::-webkit-scrollbar-button:single-button:horizontal:decrement:active { - background-image: url("../assets/images/catalog/scrollbar/scroll_h_left_pressed.png") !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + #bcbcbc !important; } *::-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; + display: block !important; + width: 18px !important; + height: 18px !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + #d9d9d9 !important; + border: 1px solid #000000 !important; + border-radius: 0 3px 3px 0 !important; + box-shadow: + inset 0 1px 0 #ffffff, + inset 1px 0 0 rgba(255, 255, 255, 0.6) !important; } *::-webkit-scrollbar-button:single-button:horizontal:increment:active { - background-image: url("../assets/images/catalog/scrollbar/scroll_h_right_pressed.png") !important; + background: + url("data:image/svg+xml;utf8,") center center / 6px 9px no-repeat, + #bcbcbc !important; } @layer components { @@ -714,6 +745,12 @@ body { background-position: center; background-repeat: no-repeat; background-color: #cdd3d9; + border: 2px solid #c4cabf; + border-radius: 6px; +} + +.layout-grid-item.is-grid-active { + border-color: #aeb7aa; } .nitro-friends-spritesheet { @@ -816,8 +853,6 @@ body { } } -/* Avatar editor icons are now rendered as tags via AvatarEditorIcon.tsx */ - .nitro-avatar-editor-wardrobe-figure-preview { background-color: #677181; overflow: hidden; @@ -940,10 +975,9 @@ body { } } -/* Font Size */ .fs-custom { - font-size: var(--font-size, 16px); /* Fallback to 16px if not set */ + font-size: var(--font-size, 16px); } .nitro-wired { @@ -1898,8 +1932,6 @@ body { box-shadow: none !important; } -/* ── Avatar Editor ─────────────────────────────────────────────────────── */ - .color-picker-frame { border-image-source: url('@/assets/images/avatareditor/color_frame.png'); border-image-slice: 6 6 6 6 fill; @@ -1977,8 +2009,6 @@ body { gap: 5px; } -/* ── Avatar Editor tab icons ───────────────────────────────────────────── */ - .avatar-editor-tabs { position: relative; @@ -2045,11 +2075,6 @@ body { box-shadow: none !important; } -/* ── Avatar Editor misc ─────────────────────────────────────────────────── */ - - -/* ── Pet Companion ─────────────────────────────────────────────────────── */ - .pet-equipped-bar { display: flex; align-items: center;