@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
@@ -0,0 +1,279 @@
1
+ import React from 'react';
2
+ import {
3
+ AiFillCaretRight,
4
+ AiFillFilter,
5
+ AiOutlineFilter,
6
+ } from 'react-icons/ai';
7
+ import styled from 'styled-components';
8
+ import { Dropdown } from '../../Dropdown';
9
+ import { Input } from '../../Input';
10
+
11
+ export interface IFilterOption {
12
+ id: number;
13
+ value: string;
14
+ option: string;
15
+ }
16
+
17
+ export interface IFilterSection {
18
+ type: 'range' | 'dropdown';
19
+ label: string;
20
+ key: string;
21
+ options?: IFilterOption[];
22
+ value?: string | [number | undefined, number | undefined];
23
+ onChange: (value: any) => void;
24
+ }
25
+
26
+ interface IAdvancedFiltersProps {
27
+ isOpen: boolean;
28
+ onToggle: () => void;
29
+ sections: IFilterSection[];
30
+ onClearAll: () => void;
31
+ hasActiveFilters: boolean;
32
+ }
33
+
34
+ export const AdvancedFilters: React.FC<IAdvancedFiltersProps> = ({
35
+ isOpen,
36
+ onToggle,
37
+ sections,
38
+ onClearAll,
39
+ hasActiveFilters,
40
+ }) => {
41
+ const renderFilterSection = (section: IFilterSection) => {
42
+ switch (section.type) {
43
+ case 'range':
44
+ const rangeValue = section.value as [
45
+ number | undefined,
46
+ number | undefined
47
+ ];
48
+ return (
49
+ <FilterSection key={section.key}>
50
+ <Label>{section.label}</Label>
51
+ <RangeInputs>
52
+ <Input
53
+ type="number"
54
+ min={0}
55
+ placeholder="Min"
56
+ value={rangeValue[0] || ''}
57
+ onChange={e =>
58
+ section.onChange([
59
+ e.target.value ? Number(e.target.value) : undefined,
60
+ rangeValue[1],
61
+ ])
62
+ }
63
+ />
64
+ <AiFillCaretRight />
65
+ <Input
66
+ type="number"
67
+ min={0}
68
+ placeholder="Max"
69
+ value={rangeValue[1] || ''}
70
+ onChange={e =>
71
+ section.onChange([
72
+ rangeValue[0],
73
+ e.target.value ? Number(e.target.value) : undefined,
74
+ ])
75
+ }
76
+ />
77
+ </RangeInputs>
78
+ </FilterSection>
79
+ );
80
+
81
+ case 'dropdown':
82
+ return (
83
+ <FilterSection key={section.key}>
84
+ <Label>{section.label}</Label>
85
+ <Dropdown
86
+ options={section.options || []}
87
+ onChange={section.onChange}
88
+ width="100%"
89
+ />
90
+ </FilterSection>
91
+ );
92
+
93
+ default:
94
+ return null;
95
+ }
96
+ };
97
+
98
+ return (
99
+ <Container>
100
+ <FilterButton onClick={onToggle} $hasActiveFilters={hasActiveFilters}>
101
+ {hasActiveFilters ? (
102
+ <AiFillFilter size={20} />
103
+ ) : (
104
+ <AiOutlineFilter size={20} />
105
+ )}
106
+ <FilterCount $visible={hasActiveFilters}>
107
+ {
108
+ sections.filter(section => {
109
+ if (section.type === 'range') {
110
+ const rangeValue = section.value as [
111
+ number | undefined,
112
+ number | undefined
113
+ ];
114
+ return (
115
+ rangeValue[0] !== undefined || rangeValue[1] !== undefined
116
+ );
117
+ }
118
+ return section.value !== 'all';
119
+ }).length
120
+ }
121
+ </FilterCount>
122
+ </FilterButton>
123
+
124
+ {isOpen && (
125
+ <FiltersPanel>
126
+ <FilterHeader>
127
+ <FilterTitle>Advanced Filters</FilterTitle>
128
+ </FilterHeader>
129
+
130
+ {sections.map(renderFilterSection)}
131
+
132
+ {hasActiveFilters && (
133
+ <ClearFiltersButton onClick={onClearAll}>
134
+ Clear All Filters
135
+ </ClearFiltersButton>
136
+ )}
137
+ </FiltersPanel>
138
+ )}
139
+ </Container>
140
+ );
141
+ };
142
+
143
+ const Container = styled.div`
144
+ position: relative;
145
+ margin-left: 0.5rem;
146
+ `;
147
+
148
+ const FilterButton = styled.button<{ $hasActiveFilters: boolean }>`
149
+ position: relative;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ width: 36px;
154
+ height: 36px;
155
+ background: transparent;
156
+ border: none;
157
+ color: ${({ $hasActiveFilters }) =>
158
+ $hasActiveFilters ? '#ffd700' : 'rgba(255, 255, 255, 0.8)'};
159
+ cursor: pointer;
160
+ transition: all 0.2s;
161
+
162
+ &:hover {
163
+ color: ${({ $hasActiveFilters }) =>
164
+ $hasActiveFilters ? '#ffd700' : '#ffffff'};
165
+ }
166
+ `;
167
+
168
+ const FilterCount = styled.div<{ $visible: boolean }>`
169
+ position: absolute;
170
+ top: -4px;
171
+ right: -4px;
172
+ background: #ffd700;
173
+ color: #000;
174
+ border-radius: 50%;
175
+ width: 16px;
176
+ height: 16px;
177
+ font-size: 10px;
178
+ font-weight: bold;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ opacity: ${({ $visible }) => ($visible ? 1 : 0)};
183
+ transform: scale(${({ $visible }) => ($visible ? 1 : 0.5)});
184
+ transition: all 0.2s;
185
+ `;
186
+
187
+ const FiltersPanel = styled.div`
188
+ position: absolute;
189
+ top: calc(100% + 0.75rem);
190
+ right: -8px;
191
+ background: #1a1a1a;
192
+ border: 1px solid #333;
193
+ border-radius: 6px;
194
+ padding: 1rem;
195
+ z-index: 1000;
196
+ min-width: 280px;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 1rem;
200
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
201
+
202
+ &:before {
203
+ content: '';
204
+ position: absolute;
205
+ top: -6px;
206
+ right: 16px;
207
+ width: 12px;
208
+ height: 12px;
209
+ background: #1a1a1a;
210
+ border-left: 1px solid #333;
211
+ border-top: 1px solid #333;
212
+ transform: rotate(45deg);
213
+ }
214
+ `;
215
+
216
+ const FilterHeader = styled.div`
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: space-between;
220
+ padding-bottom: 0.75rem;
221
+ margin-bottom: 0.5rem;
222
+ border-bottom: 1px solid #333;
223
+ `;
224
+
225
+ const FilterTitle = styled.div`
226
+ font-weight: 600;
227
+ color: #ffd700;
228
+ font-size: 0.875rem;
229
+ `;
230
+
231
+ const FilterSection = styled.div`
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 0.5rem;
235
+ `;
236
+
237
+ const Label = styled.div`
238
+ color: #999;
239
+ font-size: 0.75rem;
240
+ text-transform: uppercase;
241
+ letter-spacing: 0.05em;
242
+ `;
243
+
244
+ const RangeInputs = styled.div`
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 0.5rem;
248
+
249
+ input {
250
+ width: 80px;
251
+ background: #262626 !important;
252
+ border: 1px solid #333 !important;
253
+ color: #fff !important;
254
+ }
255
+
256
+ svg {
257
+ color: #666;
258
+ font-size: 0.875rem;
259
+ }
260
+ `;
261
+
262
+ const ClearFiltersButton = styled.button`
263
+ width: 100%;
264
+ background: transparent;
265
+ color: #666;
266
+ border: none;
267
+ padding: 0.75rem 0;
268
+ margin-top: 0.5rem;
269
+ cursor: pointer;
270
+ font-size: 0.75rem;
271
+ transition: all 0.2s;
272
+ border-top: 1px solid #333;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.05em;
275
+
276
+ &:hover {
277
+ color: #ffd700;
278
+ }
279
+ `;
@@ -66,5 +66,5 @@ const Icon = styled.span`
66
66
  `;
67
67
 
68
68
  const Content = styled.div`
69
- padding: 12px;
69
+ padding: 6px;
70
70
  `;
@@ -20,6 +20,7 @@ interface IPaginatedContentProps<T> {
20
20
  value: string;
21
21
  onChange: (value: string) => void;
22
22
  placeholder?: string;
23
+ rightElement?: React.ReactNode;
23
24
  };
24
25
  dependencies?: any[];
25
26
  tabId?: string;
@@ -7,6 +7,7 @@ interface ISearchBarProps {
7
7
  onChange: (value: string) => void;
8
8
  placeholder?: string;
9
9
  className?: string;
10
+ rightElement?: React.ReactNode;
10
11
  }
11
12
 
12
13
  export const SearchBar: React.FC<ISearchBarProps> = ({
@@ -14,7 +15,10 @@ export const SearchBar: React.FC<ISearchBarProps> = ({
14
15
  onChange,
15
16
  placeholder,
16
17
  className,
18
+ rightElement,
17
19
  }) => {
20
+ const hasRightElement = Boolean(rightElement);
21
+
18
22
  return (
19
23
  <Container className={className}>
20
24
  <Input
@@ -23,9 +27,11 @@ export const SearchBar: React.FC<ISearchBarProps> = ({
23
27
  onChange={e => onChange(e.target.value)}
24
28
  placeholder={placeholder}
25
29
  className="rpgui-input"
30
+ $hasRightElement={hasRightElement}
26
31
  />
27
32
  <IconContainer>
28
33
  <SearchIcon />
34
+ {rightElement}
29
35
  </IconContainer>
30
36
  </Container>
31
37
  );
@@ -36,9 +42,10 @@ const Container = styled.div`
36
42
  width: 100%;
37
43
  `;
38
44
 
39
- const Input = styled.input`
45
+ const Input = styled.input<{ $hasRightElement: boolean }>`
40
46
  width: 100%;
41
- padding-right: 2.5rem !important;
47
+ padding-right: ${props =>
48
+ props.$hasRightElement ? '6rem' : '2.5rem'} !important;
42
49
  background: rgba(0, 0, 0, 0.2) !important;
43
50
  border: 2px solid #f59e0b !important;
44
51
  box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
@@ -58,10 +65,13 @@ const IconContainer = styled.div`
58
65
  transform: translateY(-50%);
59
66
  display: flex;
60
67
  align-items: center;
61
- justify-content: center;
62
- width: 24px;
63
- height: 24px;
68
+ gap: 0.5rem;
64
69
  pointer-events: none;
70
+ z-index: 1;
71
+
72
+ > * {
73
+ pointer-events: auto;
74
+ }
65
75
  `;
66
76
 
67
77
  const SearchIcon = styled(FaSearch)`
@@ -8,6 +8,7 @@ interface ISearchHeaderProps {
8
8
  value: string;
9
9
  onChange: (value: string) => void;
10
10
  placeholder?: string;
11
+ rightElement?: React.ReactNode;
11
12
  };
12
13
  filterOptions?: {
13
14
  options: IOptionsProps[];
@@ -33,6 +34,7 @@ export const SearchHeader: React.FC<ISearchHeaderProps> = ({
33
34
  value={searchOptions.value}
34
35
  onChange={searchOptions.onChange}
35
36
  placeholder={searchOptions.placeholder || 'Search...'}
37
+ rightElement={searchOptions.rightElement}
36
38
  />
37
39
  </SearchContainer>
38
40
  )}
@@ -0,0 +1,73 @@
1
+ import { useState } from 'react';
2
+
3
+ const TOOLTIP_WIDTH = 300;
4
+ const TOOLTIP_OFFSET = 10;
5
+ const MIN_VISIBLE_HEIGHT = 100;
6
+
7
+ interface ITooltipPosition {
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ interface ITooltipState<T> {
13
+ item: T | null;
14
+ position: ITooltipPosition;
15
+ }
16
+
17
+ export const useTooltipPosition = <T>() => {
18
+ const [tooltipState, setTooltipState] = useState<ITooltipState<T> | null>(
19
+ null
20
+ );
21
+
22
+ const calculateTooltipPosition = (rect: DOMRect): ITooltipPosition => {
23
+ const viewportWidth = window.innerWidth;
24
+ const viewportHeight = window.innerHeight;
25
+
26
+ // Try to position to the right first
27
+ let x = rect.right + TOOLTIP_OFFSET;
28
+
29
+ // If it would overflow right, try positioning to the left
30
+ if (x + TOOLTIP_WIDTH > viewportWidth - TOOLTIP_OFFSET) {
31
+ x = rect.left - TOOLTIP_WIDTH - TOOLTIP_OFFSET;
32
+ }
33
+
34
+ // If left positioning would go off screen, position relative to viewport
35
+ if (x < TOOLTIP_OFFSET) {
36
+ x = TOOLTIP_OFFSET;
37
+ }
38
+
39
+ // Position vertically aligned with the top of the element
40
+ let y = rect.top;
41
+
42
+ // Ensure tooltip doesn't go above viewport
43
+ if (y < TOOLTIP_OFFSET) {
44
+ y = TOOLTIP_OFFSET;
45
+ }
46
+
47
+ // Ensure some part of tooltip is always visible if element is near bottom
48
+ if (y > viewportHeight - MIN_VISIBLE_HEIGHT) {
49
+ y = viewportHeight - MIN_VISIBLE_HEIGHT;
50
+ }
51
+
52
+ return { x, y };
53
+ };
54
+
55
+ const handleMouseEnter = (item: T, event: React.MouseEvent) => {
56
+ const rect = event.currentTarget.getBoundingClientRect();
57
+ setTooltipState({
58
+ item,
59
+ position: calculateTooltipPosition(rect),
60
+ });
61
+ };
62
+
63
+ const handleMouseLeave = () => {
64
+ setTooltipState(null);
65
+ };
66
+
67
+ return {
68
+ tooltipState,
69
+ handleMouseEnter,
70
+ handleMouseLeave,
71
+ TOOLTIP_WIDTH,
72
+ };
73
+ };
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  IItem,
3
3
  IItemContainer,
4
- ItemQualityLevel,
5
4
  ItemRarities,
6
5
  ItemSlotType,
7
6
  ItemSubType,
@@ -82,7 +81,6 @@ export const items: IItem[] = [
82
81
  createdAt: '2022-06-04T03:18:09.335Z',
83
82
  updatedAt: '2022-06-04T18:16:49.056Z',
84
83
  rarity: ItemRarities.Legendary,
85
- quality: ItemQualityLevel.Mythic,
86
84
  canBePurchasedOnlyByPremiumPlans: [
87
85
  UserAccountTypes.PremiumGold,
88
86
  UserAccountTypes.PremiumUltimate,
@@ -169,7 +167,6 @@ export const items: IItem[] = [
169
167
  createdAt: '2022-06-04T03:18:09.335Z',
170
168
  updatedAt: '2022-06-04T18:16:49.056Z',
171
169
  rarity: ItemRarities.Epic,
172
- quality: ItemQualityLevel.Normal,
173
170
  attachedGems: [
174
171
  {
175
172
  gemEntityEffectsAdd: ['poison'],
@@ -224,7 +221,6 @@ export const items: IItem[] = [
224
221
  createdAt: '2022-06-04T03:18:09.335Z',
225
222
  updatedAt: '2022-06-04T18:16:49.056Z',
226
223
  rarity: ItemRarities.Uncommon,
227
- quality: ItemQualityLevel.HighQuality,
228
224
  },
229
225
  {
230
226
  _id: '629acek4j7c8e8002ff60034',
@@ -259,7 +255,6 @@ export const items: IItem[] = [
259
255
  createdAt: '2022-06-04T03:18:09.335Z',
260
256
  updatedAt: '2022-06-04T18:16:49.056Z',
261
257
  rarity: ItemRarities.Rare,
262
- quality: ItemQualityLevel.Ancient,
263
258
  },
264
259
  {
265
260
  _id: '629acek4j7c8e8002fg60034',
@@ -294,7 +289,6 @@ export const items: IItem[] = [
294
289
  createdAt: '2022-06-04T03:18:09.335Z',
295
290
  updatedAt: '2022-06-04T18:16:49.056Z',
296
291
  rarity: ItemRarities.Common,
297
- quality: ItemQualityLevel.Mastercrafted,
298
292
  },
299
293
  {
300
294
  _id: '392acek4j7c8e8002ff60403',
@@ -329,7 +323,6 @@ export const items: IItem[] = [
329
323
  createdAt: '2022-06-04T03:18:09.335Z',
330
324
  updatedAt: '2022-06-04T18:16:49.056Z',
331
325
  rarity: ItemRarities.Common,
332
- quality: ItemQualityLevel.Exceptional,
333
326
  },
334
327
  createBagItem('392acek4j7c8e8002ff60404', '#FF0000'), // red
335
328
  createBagItem('392acek4j7c8e8002ff60405', '#0000FF'), // blue
@@ -1,2 +0,0 @@
1
- import { IItem } from '@rpg-engine/shared';
2
- export declare const qualityColorHex: (item: IItem | null) => "#00bfff" | "#ff8c00" | "#ff00ff" | "#ffd700" | "#ff0080" | "transparent";
@@ -1,18 +0,0 @@
1
- import { IItem, ItemQualityLevel } from '@rpg-engine/shared';
2
-
3
- export const qualityColorHex = (item: IItem | null) => {
4
- switch (item?.quality) {
5
- case ItemQualityLevel.HighQuality:
6
- return '#00bfff';
7
- case ItemQualityLevel.Exceptional:
8
- return '#ff8c00';
9
- case ItemQualityLevel.Mastercrafted:
10
- return '#ff00ff';
11
- case ItemQualityLevel.Ancient:
12
- return '#ffd700';
13
- case ItemQualityLevel.Mythic:
14
- return '#ff0080';
15
- default:
16
- return 'transparent';
17
- }
18
- };