Tile context menu tweaks (Loot panel) (#89114)

## About The Pull Request


https://github.com/user-attachments/assets/7e81b46b-1ed7-4ef1-b246-0188846711a5


Changed the loot panel style to show items in a similar style to the
regular shift context menu.

Also made the window resize with contents.

## Why It's Good For The Game

Better UX

## Changelog

🆑
qol: Tile context menu tweaks (Loot panel)
/🆑
This commit is contained in:
Andrew
2025-02-07 10:55:15 -08:00
committed by GitHub
parent 0724d6aa3c
commit 25cfb96dfd
7 changed files with 114 additions and 138 deletions
@@ -1,47 +1,26 @@
import { useMemo } from 'react';
import { Flex } from 'tgui-core/components';
import { Box } from 'tgui-core/components';
import { createSearch } from 'tgui-core/string';
import { LootBox } from './LootBox';
import { SearchGroup, SearchItem } from './types';
type Props = {
contents: SearchItem[];
contents: Record<string, SearchItem[]>;
searchText: string;
};
export function GroupedContents(props: Props) {
const { contents, searchText } = props;
// limitations: items with different stack counts, charges etc.
const contentsByPath = useMemo(() => {
const acc: Record<string, SearchItem[]> = {};
for (let i = 0; i < contents.length; i++) {
const item = contents[i];
if (item.path) {
if (!acc[item.path]) {
acc[item.path] = [];
}
acc[item.path].push(item);
} else {
acc[item.ref] = [item];
}
}
return acc;
}, [contents]);
const filteredContents: SearchGroup[] = Object.entries(contentsByPath)
const filteredContents: SearchGroup[] = Object.entries(contents)
.filter(createSearch(searchText, ([_, items]) => items[0].name))
.map(([_, items]) => ({ amount: items.length, item: items[0] }));
return (
<Flex wrap>
<Box m={-0.5}>
{filteredContents.map((group) => (
<Flex.Item key={group.item.name} m={1}>
<LootBox group={group} />
</Flex.Item>
<LootBox key={group.item.name} group={group} />
))}
</Flex>
</Box>
);
}
@@ -18,14 +18,14 @@ export function IconDisplay(props: Props) {
size: { height, width },
} = props;
const fallback = <Icon name="spinner" size={2.2} spin color="gray" />;
const fallback = <Icon name="spinner" size={1.5} spin color="gray" />;
if (!icon) {
return fallback;
}
if (icon === 'n/a') {
return <Icon name="dumpster-fire" size={2} color="gray" />;
return <Icon name="dumpster-fire" size={1.5} color="gray" />;
}
if (icon_state) {
@@ -1,6 +1,6 @@
import { Tooltip } from 'tgui-core/components';
import { Button, Stack } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
import { capitalizeAll, capitalizeFirst } from 'tgui-core/string';
import { capitalizeFirst } from 'tgui-core/string';
import { useBackend } from '../../backend';
import { IconDisplay } from './IconDisplay';
@@ -31,39 +31,48 @@ export function LootBox(props: Props) {
item = props.item;
}
const name = !item.name
? '???'
: capitalizeFirst(item.name.split(' ')[0]).slice(0, 5);
const name = !item.name ? '???' : capitalizeFirst(item.name);
// So we can conditionally wrap tooltip
const content = (
<div className="SearchItem">
<div
className="SearchItem--box"
onClick={(event) =>
act('grab', {
alt: event.altKey,
ctrl: event.ctrlKey,
ref: item.ref,
shift: event.shiftKey,
})
}
onContextMenu={(event) => {
event.preventDefault();
act('grab', {
right: true,
ref: item.ref,
});
}}
>
<IconDisplay item={item} size={{ height: 3, width: 3 }} />
{amount > 1 && <div className="SearchItem--amount">{amount}</div>}
</div>
{!is_blind && <span className="SearchItem--text">{name}</span>}
</div>
<Button
p={0}
fluid
color="transparent"
onClick={(event) =>
act('grab', {
alt: event.altKey,
ctrl: event.ctrlKey,
ref: item.ref,
shift: event.shiftKey,
})
}
onContextMenu={(event) => {
event.preventDefault();
act('grab', {
right: true,
ref: item.ref,
});
}}
>
<Stack>
<Stack.Item mb={-1} minWidth={'36px'} minHeight={'42px'}>
<IconDisplay item={item} size={{ height: 3, width: 3 }} />
</Stack.Item>
<Stack.Item
lineHeight="34px"
overflow="hidden"
style={{ textOverflow: 'ellipsis' }}
>
{!is_blind && name}
</Stack.Item>
<Stack.Item lineHeight="34px" pr={1}>
{amount > 1 && 'x' + amount}
</Stack.Item>
</Stack>
</Button>
);
if (is_blind) return content;
return <Tooltip content={capitalizeAll(item.name)}>{content}</Tooltip>;
return content;
}
@@ -1,4 +1,4 @@
import { Flex } from 'tgui-core/components';
import { Box } from 'tgui-core/components';
import { createSearch } from 'tgui-core/string';
import { LootBox } from './LootBox';
@@ -17,12 +17,10 @@ export function RawContents(props: Props) {
);
return (
<Flex wrap>
<Box m={-0.5}>
{filteredContents.map((item) => (
<Flex.Item key={item.ref} m={1}>
<LootBox item={item} />
</Flex.Item>
<LootBox key={item.ref} item={item} />
))}
</Flex>
</Box>
);
}
@@ -1,6 +1,8 @@
import { useState } from 'react';
import { Button, Input, Section, Stack } from 'tgui-core/components';
import { useMemo } from 'react';
import { Box, Button, Input, Section } from 'tgui-core/components';
import { isEscape } from 'tgui-core/keys';
import { clamp } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../../backend';
@@ -18,54 +20,78 @@ export function LootPanel(props) {
const { act, data } = useBackend<Data>();
const { contents = [], searching } = data;
// limitations: items with different stack counts, charges etc.
const contentsByPathName = useMemo(() => {
const acc: Record<string, SearchItem[]> = {};
for (let i = 0; i < contents.length; i++) {
const item = contents[i];
if (item.path) {
if (!acc[item.path + item.name]) {
acc[item.path + item.name] = [];
}
acc[item.path + item.name].push(item);
} else {
acc[item.ref] = [item];
}
}
return acc;
}, [contents]);
const [grouping, setGrouping] = useState(true);
const [searchText, setSearchText] = useState('');
const total = contents.length ? contents.length - 1 : 0;
const headerHeight = 38;
const itemHeight = 38;
const minHeight = headerHeight + itemHeight;
const maxHeight = headerHeight + itemHeight * 10;
const height: number = clamp(
headerHeight +
(!grouping ? contents.length : Object.keys(contentsByPathName).length) *
itemHeight,
minHeight,
maxHeight,
);
return (
<Window height={275} width={190} title={`Contents: ${total}`}>
<Window
width={300}
height={height}
buttons={
<Box align={'left'}>
<Input
onInput={(event, value) => setSearchText(value)}
placeholder={`Search items...`}
/>
<Button
icon={grouping ? 'layer-group' : 'object-ungroup'}
selected={grouping}
onClick={() => setGrouping(!grouping)}
tooltip="Toggle Grouping"
/>
<Button
icon="sync"
onClick={() => act('refresh')}
tooltip="Refresh"
/>
</Box>
}
>
<Window.Content
fitted
scrollable={height === maxHeight}
onKeyDown={(event) => {
if (isEscape(event.key)) {
Byond.sendMessage('close');
}
}}
>
<Section
fill
scrollable
title={
<Stack>
<Stack.Item grow>
<Input
autoFocus
fluid
onInput={(event, value) => setSearchText(value)}
placeholder="Search"
/>
</Stack.Item>
<Stack.Item>
<Button
icon={grouping ? 'layer-group' : 'object-ungroup'}
selected={grouping}
onClick={() => setGrouping(!grouping)}
tooltip="Toggle Grouping"
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={!!searching}
icon="sync"
onClick={() => act('refresh')}
tooltip="Refresh"
/>
</Stack.Item>
</Stack>
}
>
<Section>
{grouping ? (
<GroupedContents contents={contents} searchText={searchText} />
<GroupedContents
contents={contentsByPathName}
searchText={searchText}
/>
) : (
<RawContents contents={contents} searchText={searchText} />
)}
@@ -1,35 +0,0 @@
@use '../colors.scss';
.SearchItem {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.SearchItem--box {
background: black;
border: thin solid hsl(0, 0%, 12.9%);
display: flex;
align-items: center;
justify-content: center;
height: 3rem;
position: relative;
width: 3rem;
}
.SearchItem--amount {
bottom: -4px;
color: colors.$teal;
font-family: 'Roboto', sans-serif;
font-size: 1.5rem;
position: absolute;
right: -6px;
}
.SearchItem--text {
color: colors.$label;
font-family: 'Roboto', sans-serif;
font-size: 1rem;
z-index: 1;
}
-1
View File
@@ -35,7 +35,6 @@
@include meta.load-css('./interfaces/LibraryAdmin.scss');
@include meta.load-css('./interfaces/LibraryComputer.scss');
@include meta.load-css('./interfaces/ListInput.scss');
@include meta.load-css('./interfaces/LootPanel.scss');
@include meta.load-css('./interfaces/Mecha.scss');
@include meta.load-css('./interfaces/NtosMessenger.scss');
@include meta.load-css('./interfaces/NtosNotepad.scss');