@rpg-engine/long-bow 0.8.30 → 0.8.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/components/InformationCenter/sections/bestiary/BestiaryAdvancedFilters.d.ts +13 -0
  2. package/dist/components/InformationCenter/sections/bestiary/InformationCenterBestiarySection.d.ts +2 -2
  3. package/dist/components/InformationCenter/sections/items/ItemsAdvancedFilters.d.ts +11 -0
  4. package/dist/components/shared/AdvancedFilters/AdvancedFilters.d.ts +23 -0
  5. package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +1 -0
  6. package/dist/components/shared/SearchBar/SearchBar.d.ts +1 -0
  7. package/dist/components/shared/SearchHeader/SearchHeader.d.ts +1 -0
  8. package/dist/hooks/useTooltipPosition.d.ts +15 -0
  9. package/dist/long-bow.cjs.development.js +758 -509
  10. package/dist/long-bow.cjs.development.js.map +1 -1
  11. package/dist/long-bow.cjs.production.min.js +1 -1
  12. package/dist/long-bow.cjs.production.min.js.map +1 -1
  13. package/dist/long-bow.esm.js +665 -416
  14. package/dist/long-bow.esm.js.map +1 -1
  15. package/package.json +2 -2
  16. package/src/components/InformationCenter/InformationCenter.tsx +5 -19
  17. package/src/components/InformationCenter/sections/bestiary/BestiaryAdvancedFilters.tsx +95 -0
  18. package/src/components/InformationCenter/sections/bestiary/InformationCenterBestiarySection.tsx +124 -84
  19. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.tsx +31 -7
  20. package/src/components/InformationCenter/sections/items/InformationCenterItemsSection.tsx +76 -78
  21. package/src/components/InformationCenter/sections/items/ItemsAdvancedFilters.tsx +80 -0
  22. package/src/components/InformationCenter/shared/BaseInformationDetails.tsx +34 -11
  23. package/src/components/Item/Cards/ItemInfo.tsx +1 -18
  24. package/src/components/Item/Inventory/ItemSlot.tsx +3 -15
  25. package/src/components/Item/Inventory/ItemSlotRenderer.tsx +2 -6
  26. package/src/components/shared/AdvancedFilters/AdvancedFilters.tsx +279 -0
  27. package/src/components/shared/Collapsible/Collapsible.tsx +1 -1
  28. package/src/components/shared/PaginatedContent/PaginatedContent.tsx +1 -0
  29. package/src/components/shared/SearchBar/SearchBar.tsx +15 -5
  30. package/src/components/shared/SearchHeader/SearchHeader.tsx +2 -0
  31. package/src/hooks/useTooltipPosition.ts +73 -0
  32. package/src/mocks/itemContainer.mocks.ts +0 -7
  33. package/dist/components/Item/Inventory/ItemSlotQuality.d.ts +0 -2
  34. package/src/components/Item/Inventory/ItemSlotQuality.ts +0 -18
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  IInformationCenterItem,
3
3
  IInformationCenterNPC,
4
- ItemType,
4
+ isMobileOrTablet,
5
5
  } from '@rpg-engine/shared';
6
- import React, { useState } from 'react';
6
+ import React, { useMemo, useState } from 'react';
7
7
  import styled from 'styled-components';
8
- import { IOptionsProps } from '../../../Dropdown';
8
+ import { useTooltipPosition } from '../../../../hooks/useTooltipPosition';
9
9
  import { PaginatedContent } from '../../../shared/PaginatedContent/PaginatedContent';
10
+ import { Portal } from '../../../shared/Portal/Portal';
10
11
  import { InformationCenterCell } from '../../InformationCenterCell';
11
12
  import { InformationCenterItemDetails } from './InformationCenterItemDetails';
12
13
  import { InformationCenterItemTooltip } from './InformationCenterItemTooltip';
13
-
14
- const TOOLTIP_OFFSET = 200;
14
+ import { ItemsAdvancedFilters } from './ItemsAdvancedFilters';
15
15
 
16
16
  interface IItemsSectionProps {
17
17
  items: IInformationCenterItem[];
@@ -47,33 +47,38 @@ export const InformationCenterItemsSection: React.FC<IItemsSectionProps> = ({
47
47
  initialSearchQuery,
48
48
  tabId,
49
49
  }) => {
50
+ const isMobile = isMobileOrTablet();
50
51
  const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
51
52
  const [selectedItemCategory, setSelectedItemCategory] = useState<string>(
52
53
  'all'
53
54
  );
54
- const [hoveredItem, setHoveredItem] = useState<IInformationCenterItem | null>(
55
- null
56
- );
57
- const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
55
+ const [selectedTier, setSelectedTier] = useState<string>('all');
56
+ const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState(false);
58
57
  const [
59
58
  selectedItem,
60
59
  setSelectedItem,
61
60
  ] = useState<IInformationCenterItem | null>(null);
62
61
 
63
- const itemCategoryOptions: IOptionsProps[] = [
64
- { id: 0, value: 'all', option: 'All Items' },
65
- ...Object.values(ItemType).map((type, index) => ({
66
- id: index + 1,
67
- value: type,
68
- option: formatItemType(type),
69
- })),
70
- ];
71
-
72
- const filteredItems = items.filter(
73
- item =>
74
- (selectedItemCategory === 'all' || item.type === selectedItemCategory) &&
75
- item.name.toLowerCase().includes(searchQuery.toLowerCase())
76
- );
62
+ const {
63
+ tooltipState,
64
+ handleMouseEnter,
65
+ handleMouseLeave,
66
+ TOOLTIP_WIDTH,
67
+ } = useTooltipPosition<IInformationCenterItem>();
68
+
69
+ const filteredItems = useMemo<IInformationCenterItem[]>(() => {
70
+ return items.filter(item => {
71
+ const matchesSearch = item.name
72
+ .toLowerCase()
73
+ .includes(searchQuery.toLowerCase());
74
+ const matchesCategory =
75
+ selectedItemCategory === 'all' || item.type === selectedItemCategory;
76
+ const matchesTier =
77
+ selectedTier === 'all' || String(item.tier) === selectedTier;
78
+
79
+ return matchesSearch && matchesCategory && matchesTier;
80
+ });
81
+ }, [items, searchQuery, selectedItemCategory, selectedTier]);
77
82
 
78
83
  const getDroppedByNPCs = (
79
84
  itemId: string,
@@ -86,45 +91,16 @@ export const InformationCenterItemsSection: React.FC<IItemsSectionProps> = ({
86
91
  );
87
92
  };
88
93
 
89
- const handleMouseEnter = (
90
- e: React.MouseEvent,
91
- item: IInformationCenterItem
92
- ) => {
93
- setTooltipPosition({
94
- x: e.clientX + TOOLTIP_OFFSET,
95
- y: e.clientY,
96
- });
97
- setHoveredItem(item);
98
- };
99
-
100
- const handleMouseMove = (e: React.MouseEvent) => {
101
- if (hoveredItem) {
102
- setTooltipPosition({
103
- x: e.clientX + TOOLTIP_OFFSET,
104
- y: e.clientY,
105
- });
106
- }
107
- };
108
-
109
- const handleMouseLeave = () => {
110
- setHoveredItem(null);
111
- };
112
-
113
94
  const handleTouchStart = (
114
95
  e: React.TouchEvent,
115
96
  item: IInformationCenterItem
116
97
  ) => {
117
- const touch = e.touches[0];
118
- setTooltipPosition({
119
- x: touch.clientX + TOOLTIP_OFFSET,
120
- y: touch.clientY,
121
- });
122
- setHoveredItem(item);
98
+ e.preventDefault();
99
+ setSelectedItem(item);
123
100
  };
124
101
 
125
102
  const handleItemClick = (item: IInformationCenterItem) => {
126
103
  setSelectedItem(item);
127
- setHoveredItem(null);
128
104
  };
129
105
 
130
106
  const handleSearchChange = (newQuery: string) => {
@@ -141,58 +117,80 @@ export const InformationCenterItemsSection: React.FC<IItemsSectionProps> = ({
141
117
  spriteKey={item.texturePath}
142
118
  atlasJSON={itemsAtlasJSON}
143
119
  atlasIMG={itemsAtlasIMG}
144
- onMouseEnter={e => handleMouseEnter(e, item)}
145
- onMouseMove={handleMouseMove}
120
+ onMouseEnter={e => handleMouseEnter(item, e)}
146
121
  onMouseLeave={handleMouseLeave}
147
122
  onTouchStart={e => handleTouchStart(e, item)}
148
123
  onClick={() => handleItemClick(item)}
149
124
  />
150
125
  );
151
126
 
127
+ const SearchBarRightElement = (
128
+ <SearchBarActions>
129
+ <ItemsAdvancedFilters
130
+ isOpen={isAdvancedFiltersOpen}
131
+ onToggle={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
132
+ onTierChange={setSelectedTier}
133
+ onTypeChange={setSelectedItemCategory}
134
+ selectedTier={selectedTier}
135
+ selectedType={selectedItemCategory}
136
+ />
137
+ </SearchBarActions>
138
+ );
139
+
152
140
  return (
153
141
  <>
154
142
  <PaginatedContent<IInformationCenterItem>
155
143
  items={filteredItems}
156
144
  renderItem={renderItem}
157
145
  emptyMessage="No items found"
158
- filterOptions={{
159
- options: itemCategoryOptions,
160
- selectedOption: selectedItemCategory,
161
- onOptionChange: setSelectedItemCategory,
162
- }}
163
146
  searchOptions={{
164
147
  value: searchQuery,
165
148
  onChange: handleSearchChange,
166
149
  placeholder: 'Search items...',
150
+ rightElement: SearchBarRightElement,
167
151
  }}
168
- dependencies={[selectedItemCategory]}
152
+ dependencies={[selectedItemCategory, selectedTier]}
169
153
  tabId={tabId}
170
154
  layout="grid"
171
155
  itemHeight="180px"
172
156
  />
173
- {hoveredItem && (
174
- <TooltipWrapper
175
- style={{ top: tooltipPosition.y, left: tooltipPosition.x }}
176
- >
177
- <InformationCenterItemTooltip item={hoveredItem} />
178
- </TooltipWrapper>
157
+ {!isMobile && tooltipState && tooltipState.item && (
158
+ <Portal>
159
+ <TooltipWrapper
160
+ width={TOOLTIP_WIDTH}
161
+ style={{
162
+ position: 'fixed',
163
+ left: `${tooltipState.position.x}px`,
164
+ top: `${tooltipState.position.y}px`,
165
+ }}
166
+ >
167
+ <InformationCenterItemTooltip item={tooltipState.item} />
168
+ </TooltipWrapper>
169
+ </Portal>
179
170
  )}
180
171
  {selectedItem && (
181
- <InformationCenterItemDetails
182
- item={selectedItem}
183
- itemsAtlasJSON={itemsAtlasJSON}
184
- itemsAtlasIMG={itemsAtlasIMG}
185
- droppedBy={getDroppedByNPCs(selectedItem.key, bestiaryItems)}
186
- onBack={() => setSelectedItem(null)}
187
- />
172
+ <Portal>
173
+ <InformationCenterItemDetails
174
+ item={selectedItem}
175
+ itemsAtlasJSON={itemsAtlasJSON}
176
+ itemsAtlasIMG={itemsAtlasIMG}
177
+ droppedBy={getDroppedByNPCs(selectedItem.key, bestiaryItems)}
178
+ onBack={() => setSelectedItem(null)}
179
+ />
180
+ </Portal>
188
181
  )}
189
182
  </>
190
183
  );
191
184
  };
192
185
 
193
- const TooltipWrapper = styled.div`
194
- position: fixed;
186
+ const TooltipWrapper = styled.div<{ width: number }>`
195
187
  z-index: 1000;
196
188
  pointer-events: none;
197
- transition: transform 0.1s ease;
189
+ width: ${props => `${props.width}px`};
190
+ `;
191
+
192
+ const SearchBarActions = styled.div`
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 0.5rem;
198
196
  `;
@@ -0,0 +1,80 @@
1
+ import { ItemType } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import {
4
+ AdvancedFilters,
5
+ IFilterOption,
6
+ IFilterSection,
7
+ } from '../../../shared/AdvancedFilters/AdvancedFilters';
8
+ import { formatItemType } from './InformationCenterItemsSection';
9
+
10
+ interface IItemsAdvancedFiltersProps {
11
+ isOpen: boolean;
12
+ onToggle: () => void;
13
+ onTierChange: (value: string) => void;
14
+ onTypeChange: (value: string) => void;
15
+ selectedTier: string;
16
+ selectedType: string;
17
+ }
18
+
19
+ export const ItemsAdvancedFilters = ({
20
+ isOpen,
21
+ onToggle,
22
+ onTierChange,
23
+ onTypeChange,
24
+ selectedTier,
25
+ selectedType,
26
+ }: IItemsAdvancedFiltersProps): JSX.Element => {
27
+ const tierOptions: IFilterOption[] = [
28
+ { id: 0, value: 'all', option: 'All Tiers' },
29
+ ...Array.from({ length: 5 }, (_, i) => ({
30
+ id: i + 1,
31
+ value: String(i + 1),
32
+ option: `Tier ${i + 1}`,
33
+ })),
34
+ ];
35
+
36
+ const typeOptions: IFilterOption[] = [
37
+ { id: 0, value: 'all', option: 'All Types' },
38
+ ...Object.entries(ItemType).map(([, value], index) => ({
39
+ id: index + 1,
40
+ value: value as string,
41
+ option: formatItemType(value as string),
42
+ })),
43
+ ];
44
+
45
+ const hasActiveFilters = selectedTier !== 'all' || selectedType !== 'all';
46
+
47
+ const handleClearFilters = () => {
48
+ onTierChange('all');
49
+ onTypeChange('all');
50
+ };
51
+
52
+ const sections: IFilterSection[] = [
53
+ {
54
+ type: 'dropdown',
55
+ label: 'Tier',
56
+ key: 'tier',
57
+ options: tierOptions,
58
+ value: selectedTier,
59
+ onChange: onTierChange,
60
+ },
61
+ {
62
+ type: 'dropdown',
63
+ label: 'Item Type',
64
+ key: 'type',
65
+ options: typeOptions,
66
+ value: selectedType,
67
+ onChange: onTypeChange,
68
+ },
69
+ ];
70
+
71
+ return (
72
+ <AdvancedFilters
73
+ isOpen={isOpen}
74
+ onToggle={onToggle}
75
+ sections={sections}
76
+ onClearAll={handleClearFilters}
77
+ hasActiveFilters={hasActiveFilters}
78
+ />
79
+ );
80
+ };
@@ -49,32 +49,45 @@ export const BaseInformationDetails: React.FC<IBaseInformationDetailsProps> = ({
49
49
  };
50
50
 
51
51
  const Container = styled.div`
52
- position: absolute;
52
+ position: fixed;
53
53
  inset: 0;
54
54
  display: flex;
55
55
  justify-content: center;
56
56
  align-items: center;
57
- z-index: 1000;
57
+ z-index: 9999;
58
58
  `;
59
59
 
60
60
  const Overlay = styled.div`
61
- position: absolute;
61
+ position: fixed;
62
62
  inset: 0;
63
63
  background-color: rgba(0, 0, 0, 0.8);
64
64
  `;
65
65
 
66
66
  const Modal = styled.div`
67
- position: relative;
68
- width: 90%;
69
- height: 90%;
67
+ position: fixed;
70
68
  background-color: rgba(0, 0, 0, 0.95);
71
69
  border-radius: 4px;
72
- padding: 16px;
73
- overflow-y: auto;
70
+ padding: 12px;
71
+ overflow: hidden;
74
72
  z-index: 1;
75
73
  font-family: 'Press Start 2P', cursive;
76
74
  border: 1px solid ${uiColors.darkGray};
77
75
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
76
+ width: 90%;
77
+ height: 90%;
78
+ top: 5%;
79
+ left: 5%;
80
+ max-width: 800px;
81
+ margin: 0 auto;
82
+
83
+ @media (max-width: 768px) {
84
+ width: 100%;
85
+ height: 100%;
86
+ top: 0;
87
+ left: 0;
88
+ border-radius: 0;
89
+ padding: 8px;
90
+ }
78
91
 
79
92
  &::-webkit-scrollbar {
80
93
  width: 2px;
@@ -126,9 +139,19 @@ const Header = styled.div`
126
139
  const Content = styled.div`
127
140
  display: flex;
128
141
  flex-direction: column;
129
- gap: 16px;
130
- height: 100%;
131
- overflow: auto;
142
+ gap: 12px;
143
+ height: calc(100% - 80px);
144
+ overflow-y: auto;
145
+ overflow-x: hidden;
146
+ padding-right: 6px;
147
+ margin-right: -6px;
148
+
149
+ @media (max-width: 768px) {
150
+ height: calc(100% - 64px);
151
+ gap: 8px;
152
+ padding-right: 4px;
153
+ margin-right: -4px;
154
+ }
132
155
  `;
133
156
 
134
157
  const Title = styled.h2`
@@ -1,4 +1,4 @@
1
- import { IItem, ItemQualityLevel } from '@rpg-engine/shared';
1
+ import { IItem } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { uiColors } from '../../../constants/uiColors';
@@ -6,7 +6,6 @@ import { uiFonts } from '../../../constants/uiFonts';
6
6
  import { SpriteFromAtlas } from '../../shared/SpriteFromAtlas';
7
7
  import { ErrorBoundary } from '../Inventory/ErrorBoundary';
8
8
  import { EquipmentSlotSpriteByType } from '../Inventory/ItemSlot';
9
- import { qualityColorHex } from '../Inventory/ItemSlotQuality';
10
9
  import { rarityColor } from '../Inventory/ItemSlotRarity';
11
10
 
12
11
  interface IItemInfoProps {
@@ -192,24 +191,8 @@ const Container = styled.div<{ item: IItem }>`
192
191
  padding: 0.5rem;
193
192
  font-size: ${uiFonts.size.small};
194
193
  border: 3px solid ${({ item }) => rarityColor(item) ?? uiColors.lightGray};
195
- box-shadow: ${({ item }) => `0 0 5px 2px ${rarityColor(item)}`};
196
194
  height: max-content;
197
195
  width: 18rem;
198
- position: relative;
199
-
200
- ${({ item }) =>
201
- item?.quality && item.quality !== ItemQualityLevel.Normal &&
202
- `
203
- &::before {
204
- content: '★';
205
- position: absolute;
206
- top: 0.2rem;
207
- left: 0.5rem;
208
- font-size: 1.2rem;
209
- color: ${qualityColorHex(item)};
210
- text-shadow: 0 0 3px black;
211
- }
212
- `}
213
196
 
214
197
  @media (max-width: 640px) {
215
198
  width: 80vw;
@@ -6,7 +6,7 @@ import {
6
6
  ItemContainerType,
7
7
  ItemSlotType,
8
8
  ItemSubType,
9
- ItemType
9
+ ItemType,
10
10
  } from '@rpg-engine/shared';
11
11
 
12
12
  import { observer } from 'mobx-react-lite';
@@ -15,11 +15,10 @@ import Draggable, { DraggableEventHandler } from 'react-draggable';
15
15
  import styled from 'styled-components';
16
16
  import useTouchTarget from '../../../hooks/useTouchTarget';
17
17
  import { IPosition } from '../../../types/eventTypes';
18
- import { useItemSlotDetails } from './context/ItemSlotDetailsContext';
19
- import { useItemSlotDragging } from './context/ItemSlotDraggingContext';
20
- import { qualityColorHex } from './ItemSlotQuality';
21
18
  import { rarityColor } from './ItemSlotRarity';
22
19
  import { ItemSlotRenderer } from './ItemSlotRenderer';
20
+ import { useItemSlotDetails } from './context/ItemSlotDetailsContext';
21
+ import { useItemSlotDragging } from './context/ItemSlotDraggingContext';
23
22
 
24
23
  export const EquipmentSlotSpriteByType: any = {
25
24
  Neck: 'accessories/corruption-necklace.png',
@@ -540,17 +539,6 @@ const Container = styled.div<ContainerTypes>`
540
539
  ${({ item }) => `0 0 4px 3px ${rarityColor(item)}`};
541
540
  }
542
541
 
543
- .quality-star {
544
- position: absolute;
545
- top: 0.5rem;
546
- left: 0.5rem;
547
- font-size: 1.2rem;
548
- z-index: 2;
549
- text-shadow: 0 0 3px black;
550
- pointer-events: none;
551
- color: ${({ item }) => qualityColorHex(item)};
552
- }
553
-
554
542
  &::before {
555
543
  content: '';
556
544
  position: absolute;
@@ -1,9 +1,8 @@
1
1
  import {
2
- getItemTextureKeyPath,
3
2
  IItem,
4
3
  ItemContainerType,
5
- ItemQualityLevel,
6
4
  ItemSlotType,
5
+ getItemTextureKeyPath,
7
6
  } from '@rpg-engine/shared';
8
7
  import React from 'react';
9
8
  import { v4 as uuidv4 } from 'uuid';
@@ -47,9 +46,6 @@ export const ItemSlotRenderer: React.FC<IProps> = ({
47
46
 
48
47
  return (
49
48
  <ErrorBoundary key={item._id}>
50
- {item.quality && item.quality !== ItemQualityLevel.Normal && (
51
- <div className="quality-star">★</div>
52
- )}
53
49
  <SpriteFromAtlas
54
50
  atlasIMG={atlasIMG}
55
51
  atlasJSON={atlasJSON}
@@ -105,5 +101,5 @@ export const ItemSlotRenderer: React.FC<IProps> = ({
105
101
  }
106
102
  };
107
103
 
108
- return onRenderSlot(item);
104
+ return <>{onRenderSlot(item)}</>;
109
105
  };