mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-31 09:08:30 +01:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user