Merge pull request #167 from duckietm/Dev

Dev
This commit is contained in:
DuckieTM
2026-05-27 15:37:26 +02:00
committed by GitHub
6 changed files with 92 additions and 66 deletions
+39 -11
View File
@@ -12,35 +12,51 @@ export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
headOnly?: boolean; headOnly?: boolean;
direction?: number; direction?: number;
scale?: number; scale?: number;
fit?: boolean;
} }
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props => export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
{ {
const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, fit = false, classNames = [], style = {}, ...rest } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(null); const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
const [ isReady, setIsReady ] = useState<boolean>(false); const [ isReady, setIsReady ] = useState<boolean>(false);
const isDisposed = useRef(false); const isDisposed = useRef(false);
// Request id bumped on every prop change. The SDK can call
// resetFigure asynchronously when server-side figure data lands;
// if props change in quick succession the older callback could
// otherwise overwrite the newer image. The closure captures the
// id and bails when stale.
const requestIdRef = useRef(0); const requestIdRef = useRef(0);
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
const newClassNames: string[] = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ]; let newClassNames: string[];
if(fit)
{
newClassNames = [ 'avatar-image absolute inset-0 pointer-events-none' ];
}
else if(headOnly)
{
newClassNames = [ 'avatar-image absolute inset-0 bg-no-repeat pointer-events-none' ];
}
else
{
newClassNames = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ];
}
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
}, [ classNames ]); }, [ classNames, headOnly, fit ]);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
{ {
let newStyle: CSSProperties = {}; let newStyle: CSSProperties = {};
if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`; if(!fit && avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
if(headOnly && !fit)
{
newStyle.backgroundSize = '130px auto';
newStyle.backgroundPosition = '51% 40%';
newStyle.imageRendering = 'pixelated';
}
if(scale !== 1) if(scale !== 1)
{ {
@@ -52,7 +68,7 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle; return newStyle;
}, [ avatarUrl, scale, style ]); }, [ avatarUrl, scale, style, headOnly, fit ]);
useEffect(() => useEffect(() =>
{ {
@@ -116,5 +132,17 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
}; };
}, []); }, []);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />; return (
<Base classNames={ getClassNames } style={ getStyle } { ...rest }>
{ fit && avatarUrl && avatarUrl.length > 0 && (
<img
src={ avatarUrl }
alt=""
draggable={ false }
className="absolute inset-0 w-full h-full object-contain"
style={ { imageRendering: 'pixelated', transform: 'translateY(-20%)' } }
/>
) }
</Base>
);
}; };
@@ -76,7 +76,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
{ iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) && { iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) &&
<div className="nitro-catalog-classic-grid-offer-icon" style={ { backgroundImage: `url(${ iconUrl })` } } /> } <div className="nitro-catalog-classic-grid-offer-icon" style={ { backgroundImage: `url(${ iconUrl })` } } /> }
{ (offer.product.productType === ProductTypeEnum.ROBOT) && { (offer.product.productType === ProductTypeEnum.ROBOT) &&
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> } <LayoutAvatarImageView direction={ 2 } figure={ offer.product.extraParam } fit /> }
<div <div
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` } className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
onClick={ e => onClick={ e =>
@@ -88,7 +88,6 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
</div> </div>
</div> } </div> }
{ /* Welcome/description card */ }
{ !currentOffer && { !currentOffer &&
<div className="nitro-catalog-classic-welcome flex items-center gap-3"> <div className="nitro-catalog-classic-welcome flex items-center gap-3">
{ !!page.localization.getImage(1) && { !!page.localization.getImage(1) &&
@@ -96,11 +95,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
<Text className="text-[11px]! text-muted" dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } /> <Text className="text-[11px]! text-muted" dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
</div> } </div> }
{ /* Item grid */ }
<div className="nitro-catalog-classic-grid-shell flex-1 overflow-auto min-h-0"> <div className="nitro-catalog-classic-grid-shell flex-1 overflow-auto min-h-0">
{ GetConfigurationValue('catalog.headers') && { GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> } <CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView className="nitro-catalog-classic-grid" columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } /> <CatalogItemGridWidgetView className="nitro-catalog-classic-grid" columnCount={ 7 } columnMinHeight={ currentPage.layoutCode === 'bots' ? 65 : 50 } columnMinWidth={ currentPage.layoutCode === 'bots' ? 65 : 50 } />
</div> </div>
</div> </div>
); );
@@ -38,8 +38,8 @@ export const InventoryBotItemView: FC<PropsWithChildren<{
}; };
return ( return (
<InfiniteGrid.Item itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onDoubleClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest } className="*:[background-position-y:-32px]"> <InfiniteGrid.Item itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onDoubleClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest } className="aspect-[2/3]">
<LayoutAvatarImageView direction={ 3 } figure={ botItem.botData.figure } headOnly={ true } /> <LayoutAvatarImageView direction={ 2 } figure={ botItem.botData.figure } fit />
{ children } { children }
</InfiniteGrid.Item> </InfiniteGrid.Item>
); );
@@ -68,7 +68,7 @@ export const InventoryBotView: FC<{
<div className="grid h-full grid-cols-12 gap-2"> <div className="grid h-full grid-cols-12 gap-2">
<div className="flex flex-col col-span-7 gap-1 overflow-hidden"> <div className="flex flex-col col-span-7 gap-1 overflow-hidden">
<InfiniteGrid<IBotItem> <InfiniteGrid<IBotItem>
columnCount={ 6 } columnCount={ 4 }
itemRender={ item => <InventoryBotItemView botItem={ item } /> } itemRender={ item => <InventoryBotItemView botItem={ item } /> }
items={ botItems } /> items={ botItems } />
</div> </div>
+48 -48
View File
@@ -69,10 +69,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS); toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS);
}, []); }, []);
const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1540px]:bottom-0' : 'bottom-0'; const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1700px]:bottom-0' : 'bottom-0';
const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1540px]:hidden'; const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1700px]:hidden';
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:block'; const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:block';
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:flex'; const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:flex';
const leftNavVariants = useMemo<Variants>(() => ({ const leftNavVariants = useMemo<Variants>(() => ({
hidden: { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8, pointerEvents: 'none' }, hidden: { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8, pointerEvents: 'none' },
visible: { opacity: 1, x: 0, y: 0, pointerEvents: 'auto' } visible: { opacity: 1, x: 0, y: 0, pointerEvents: 'auto' }
@@ -216,14 +216,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> <ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
</motion.div> </motion.div>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
</motion.div>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
</motion.div>
<motion.div variants={ itemVariants } className="relative"> <motion.div variants={ itemVariants } className="relative">
<AnimatePresence> <AnimatePresence>
{ isMeExpanded && { isMeExpanded &&
@@ -237,7 +229,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
</motion.div> } </motion.div> }
</AnimatePresence> </AnimatePresence>
<motion.div <motion.div
className="cursor-pointer" className="cursor-pointer relative h-[40px] w-[40px] overflow-hidden"
whileHover={ { scale: 1.08 } } whileHover={ { scale: 1.08 } }
whileTap={ { scale: 0.95 } } whileTap={ { scale: 0.95 } }
onClick={ event => onClick={ event =>
@@ -245,11 +237,19 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
setMeExpanded(value => !value); setMeExpanded(value => !value);
event.stopPropagation(); event.stopPropagation();
} }> } }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[64px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: "8px" } } /> <LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon" style={ { backgroundSize: 'auto', backgroundPosition: '-25px -38px' } } />
</motion.div> </motion.div>
{ (getTotalUnseen > 0) && { (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> } <LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div> </motion.div>
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
</motion.div>
<motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
</motion.div>
{ (isInRoom && showToolbarButton) && { (isInRoom && showToolbarButton) &&
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="wired-tools" onClick={ openMonitor } className="tb-icon" /> <ToolbarItemView icon="wired-tools" onClick={ openMonitor } className="tb-icon" />
@@ -268,14 +268,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{ (openTicketsCount > 0) && { (openTicketsCount > 0) &&
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> } <LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div> } </motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
{ (isHk && hkEnabled) && { (isHk && hkEnabled) &&
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> <ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
</motion.div> } </motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
</motion.div> </motion.div>
</motion.div> </motion.div>
<motion.div <motion.div
@@ -324,6 +324,32 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> <ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
</motion.div> </motion.div>
<motion.div variants={ itemVariants } className="relative shrink-0">
<AnimatePresence>
{ isMeExpanded &&
<motion.div
initial={ { opacity: 0, y: 6, scale: 0.97 } }
animate={ { opacity: 1, y: 0, scale: 1 } }
exit={ { opacity: 0, y: 6, scale: 0.97 } }
transition={ ME_POPOVER_TRANSITION }
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> }
</AnimatePresence>
<motion.div
className="cursor-pointer relative h-[40px] w-[40px] overflow-hidden"
whileHover={ { scale: 1.08 } }
whileTap={ { scale: 0.95 } }
onClick={ event =>
{
setMeExpanded(value => !value);
event.stopPropagation();
} }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon" style={ { backgroundSize: 'auto', backgroundPosition: '-25px -38px' } } />
</motion.div>
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div>
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> <ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
</motion.div> </motion.div>
@@ -333,32 +359,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> } <LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
</motion.div> </motion.div>
</motion.div> </motion.div>
<motion.div variants={ itemVariants } className="relative mx-[2px] shrink-0">
<AnimatePresence>
{ isMeExpanded &&
<motion.div
initial={ { opacity: 0, y: 6, scale: 0.97 } }
animate={ { opacity: 1, y: 0, scale: 1 } }
exit={ { opacity: 0, y: 6, scale: 0.97 } }
transition={ ME_POPOVER_TRANSITION }
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> }
</AnimatePresence>
<motion.div
className="cursor-pointer"
whileHover={ { scale: 1.08 } }
whileTap={ { scale: 0.95 } }
onClick={ event =>
{
setMeExpanded(value => !value);
event.stopPropagation();
} }>
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[64px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: "8px" } } />
</motion.div>
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div>
<motion.div <motion.div
variants={ containerVariants } variants={ containerVariants }
className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1"> className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1">
@@ -380,14 +380,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{ (openTicketsCount > 0) && { (openTicketsCount > 0) &&
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> } <LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
</motion.div> } </motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
{ (isHk && hkEnabled) && { (isHk && hkEnabled) &&
<motion.div variants={ itemVariants }> <motion.div variants={ itemVariants }>
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" /> <ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
</motion.div> } </motion.div> }
{ isMod &&
<motion.div variants={ itemVariants }>
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
</motion.div> }
<motion.div variants={ itemVariants } className="relative"> <motion.div variants={ itemVariants } className="relative">
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" /> <ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
{ (requests.length > 0) && { (requests.length > 0) &&