@rpg-engine/long-bow 0.8.35 → 0.8.37

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.
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import {
3
3
  AiFillCaretRight,
4
4
  AiFillFilter,
@@ -7,6 +7,7 @@ import {
7
7
  import styled from 'styled-components';
8
8
  import { Dropdown } from '../../Dropdown';
9
9
  import { Input } from '../../Input';
10
+ import { Portal } from '../Portal/Portal';
10
11
 
11
12
  export interface IFilterOption {
12
13
  id: number;
@@ -82,11 +83,13 @@ export const AdvancedFilters: React.FC<IAdvancedFiltersProps> = ({
82
83
  return (
83
84
  <FilterSection key={section.key}>
84
85
  <Label>{section.label}</Label>
85
- <Dropdown
86
- options={section.options || []}
87
- onChange={section.onChange}
88
- width="100%"
89
- />
86
+ <StyledDropdownWrapper>
87
+ <Dropdown
88
+ options={section.options || []}
89
+ onChange={section.onChange}
90
+ width="100%"
91
+ />
92
+ </StyledDropdownWrapper>
90
93
  </FilterSection>
91
94
  );
92
95
 
@@ -95,9 +98,68 @@ export const AdvancedFilters: React.FC<IAdvancedFiltersProps> = ({
95
98
  }
96
99
  };
97
100
 
101
+ // Calculate button position for the portal
102
+ const [buttonPosition, setButtonPosition] = React.useState({
103
+ top: 0,
104
+ left: 0,
105
+ isMobile: false,
106
+ });
107
+ const buttonRef = useRef<HTMLButtonElement>(null);
108
+ const panelRef = useRef<HTMLDivElement>(null);
109
+
110
+ useEffect(() => {
111
+ if (isOpen && buttonRef.current) {
112
+ const rect = buttonRef.current.getBoundingClientRect();
113
+ const isMobileView = window.innerWidth < 480;
114
+
115
+ if (isMobileView) {
116
+ // For mobile, position in center of screen
117
+ setButtonPosition({
118
+ top: window.innerHeight / 2,
119
+ left: window.innerWidth / 2,
120
+ isMobile: true,
121
+ });
122
+ } else {
123
+ // Position below the button for larger screens
124
+ setButtonPosition({
125
+ top: rect.bottom + window.scrollY,
126
+ left: rect.left + rect.width / 2 + window.scrollX,
127
+ isMobile: false,
128
+ });
129
+ }
130
+ }
131
+ }, [isOpen]);
132
+
133
+ // Handle click outside to close the panel
134
+ useEffect(() => {
135
+ const handleClickOutside = (event: MouseEvent) => {
136
+ if (
137
+ isOpen &&
138
+ panelRef.current &&
139
+ !panelRef.current.contains(event.target as Node) &&
140
+ buttonRef.current &&
141
+ !buttonRef.current.contains(event.target as Node)
142
+ ) {
143
+ onToggle();
144
+ }
145
+ };
146
+
147
+ if (isOpen) {
148
+ document.addEventListener('mousedown', handleClickOutside);
149
+ }
150
+
151
+ return () => {
152
+ document.removeEventListener('mousedown', handleClickOutside);
153
+ };
154
+ }, [isOpen, onToggle]);
155
+
98
156
  return (
99
157
  <Container>
100
- <FilterButton onClick={onToggle} $hasActiveFilters={hasActiveFilters}>
158
+ <FilterButton
159
+ onClick={onToggle}
160
+ $hasActiveFilters={hasActiveFilters}
161
+ ref={buttonRef}
162
+ >
101
163
  {hasActiveFilters ? (
102
164
  <AiFillFilter size={20} />
103
165
  ) : (
@@ -122,19 +184,36 @@ export const AdvancedFilters: React.FC<IAdvancedFiltersProps> = ({
122
184
  </FilterButton>
123
185
 
124
186
  {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>
187
+ <Portal>
188
+ <FiltersPanel
189
+ ref={panelRef}
190
+ style={{
191
+ position: 'fixed',
192
+ top: buttonPosition.isMobile ? '50vh' : `${buttonPosition.top}px`,
193
+ left: buttonPosition.isMobile
194
+ ? '50vw'
195
+ : `${buttonPosition.left}px`,
196
+ transform: buttonPosition.isMobile
197
+ ? 'translate(-50%, -50%)'
198
+ : 'translateX(-50%)',
199
+ zIndex: 9999,
200
+ }}
201
+ $isMobile={buttonPosition.isMobile}
202
+ >
203
+ <FilterHeader>
204
+ <FilterTitle>Advanced Filters</FilterTitle>
205
+ <CloseButton onClick={onToggle}>×</CloseButton>
206
+ </FilterHeader>
207
+
208
+ {sections.map(renderFilterSection)}
209
+
210
+ {hasActiveFilters && (
211
+ <ClearFiltersButton onClick={onClearAll}>
212
+ Clear All Filters
213
+ </ClearFiltersButton>
214
+ )}
215
+ </FiltersPanel>
216
+ </Portal>
138
217
  )}
139
218
  </Container>
140
219
  );
@@ -184,32 +263,37 @@ const FilterCount = styled.div<{ $visible: boolean }>`
184
263
  transition: all 0.2s;
185
264
  `;
186
265
 
187
- const FiltersPanel = styled.div`
188
- position: absolute;
189
- top: calc(100% + 0.75rem);
190
- right: -8px;
266
+ const FiltersPanel = styled.div<{ $isMobile?: boolean }>`
191
267
  background: #1a1a1a;
192
268
  border: 1px solid #333;
193
269
  border-radius: 6px;
194
270
  padding: 1rem;
195
- z-index: 1000;
196
- min-width: 280px;
271
+ width: 280px;
272
+ max-width: calc(100vw - 20px);
197
273
  display: flex;
198
274
  flex-direction: column;
199
275
  gap: 1rem;
200
276
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
277
+ font-family: 'Press Start 2P', cursive;
278
+
279
+ @media (max-width: 320px) {
280
+ width: 250px;
281
+ padding: 0.75rem;
282
+ gap: 0.75rem;
283
+ }
201
284
 
202
285
  &:before {
203
286
  content: '';
204
287
  position: absolute;
205
288
  top: -6px;
206
- right: 16px;
289
+ left: 50%;
290
+ transform: translateX(-50%) rotate(45deg);
207
291
  width: 12px;
208
292
  height: 12px;
209
293
  background: #1a1a1a;
210
294
  border-left: 1px solid #333;
211
295
  border-top: 1px solid #333;
212
- transform: rotate(45deg);
296
+ display: ${props => (props.$isMobile ? 'none' : 'block')};
213
297
  }
214
298
  `;
215
299
 
@@ -225,7 +309,7 @@ const FilterHeader = styled.div`
225
309
  const FilterTitle = styled.div`
226
310
  font-weight: 600;
227
311
  color: #ffd700;
228
- font-size: 0.875rem;
312
+ font-size: 0.75rem;
229
313
  `;
230
314
 
231
315
  const FilterSection = styled.div`
@@ -236,7 +320,7 @@ const FilterSection = styled.div`
236
320
 
237
321
  const Label = styled.div`
238
322
  color: #999;
239
- font-size: 0.75rem;
323
+ font-size: 0.65rem;
240
324
  text-transform: uppercase;
241
325
  letter-spacing: 0.05em;
242
326
  `;
@@ -249,8 +333,22 @@ const RangeInputs = styled.div`
249
333
  input {
250
334
  width: 80px;
251
335
  background: #262626 !important;
252
- border: 1px solid #333 !important;
253
- color: #fff !important;
336
+ border: 1px solid #444 !important;
337
+ border-radius: 4px !important;
338
+ color: #ddd !important;
339
+ font-size: 0.65rem !important;
340
+ padding: 0.5rem !important;
341
+ height: auto !important;
342
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2) !important;
343
+
344
+ &::placeholder {
345
+ color: #666 !important;
346
+ }
347
+
348
+ &:focus {
349
+ border-color: #f59e0b !important;
350
+ outline: none !important;
351
+ }
254
352
  }
255
353
 
256
354
  svg {
@@ -267,13 +365,42 @@ const ClearFiltersButton = styled.button`
267
365
  padding: 0.75rem 0;
268
366
  margin-top: 0.5rem;
269
367
  cursor: pointer;
270
- font-size: 0.75rem;
368
+ font-size: 0.65rem;
271
369
  transition: all 0.2s;
272
370
  border-top: 1px solid #333;
273
371
  text-transform: uppercase;
274
372
  letter-spacing: 0.05em;
373
+ font-family: 'Press Start 2P', cursive;
374
+
375
+ &:hover {
376
+ color: #ffd700;
377
+ }
378
+ `;
379
+
380
+ const CloseButton = styled.button`
381
+ background: transparent;
382
+ border: none;
383
+ color: #999;
384
+ font-size: 1.5rem;
385
+ line-height: 1;
386
+ cursor: pointer;
387
+ padding: 0;
388
+ margin: 0;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ width: 24px;
393
+ height: 24px;
275
394
 
276
395
  &:hover {
277
396
  color: #ffd700;
278
397
  }
279
398
  `;
399
+
400
+ const StyledDropdownWrapper = styled.div`
401
+ .rpgui-dropdown-imp {
402
+ font-size: 0.8rem;
403
+ min-height: unset;
404
+ padding-top: 0.3rem;
405
+ }
406
+ `;
@@ -1,5 +1,9 @@
1
1
  import React from 'react';
2
2
  import styled from 'styled-components';
3
+ import {
4
+ UI_BREAKPOINT_MOBILE,
5
+ UI_BREAKPOINT_SMALL_LAPTOP,
6
+ } from '../../../constants/uiBreakpoints';
3
7
  import { usePagination } from '../../CraftBook/hooks/usePagination';
4
8
  import { IOptionsProps } from '../../Dropdown';
5
9
  import { Pagination } from '../Pagination/Pagination';
@@ -93,7 +97,7 @@ export const PaginatedContent = <T extends unknown>({
93
97
  const Container = styled.div`
94
98
  display: flex;
95
99
  flex-direction: column;
96
- gap: 1rem;
100
+ gap: 0.5rem;
97
101
  min-height: 400px;
98
102
  width: 100%;
99
103
  `;
@@ -107,24 +111,73 @@ const Content = styled.div<{
107
111
  flex-direction: column;
108
112
  gap: 0.5rem;
109
113
  flex: 1;
110
- padding: 1rem;
114
+ padding: 0.25rem;
111
115
  min-height: 200px;
112
- max-height: ${props => props.$maxHeight};
113
- overflow-y: ${props => (props.$maxHeight ? 'auto' : 'visible')};
116
+ max-height: 60vh;
117
+ height: 100%;
118
+ overflow-y: auto;
119
+ overflow-x: hidden;
120
+ box-sizing: border-box;
121
+
122
+ scrollbar-width: thin;
123
+ scrollbar-color: rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.2);
124
+
125
+ @media (min-width: 360px) {
126
+ padding: 0.5rem;
127
+ }
128
+
129
+ @media (min-width: 480px) {
130
+ padding: 1rem;
131
+ }
114
132
 
115
133
  &.grid {
116
134
  display: grid;
117
- grid-template-columns: repeat(
118
- ${props => props.$gridColumns},
119
- minmax(0, 1fr)
120
- );
121
- gap: 1rem;
135
+ grid-template-columns: 1fr;
136
+ gap: 0.5rem;
122
137
  align-items: start;
138
+ justify-content: center;
139
+ width: 100%;
140
+
141
+ @media (min-width: 320px) {
142
+ grid-template-columns: repeat(2, minmax(100px, 1fr));
143
+ }
144
+
145
+ @media (min-width: 400px) {
146
+ grid-template-columns: repeat(2, minmax(120px, 1fr));
147
+ gap: 0.75rem;
148
+ }
149
+
150
+ @media (min-width: 480px) {
151
+ grid-template-columns: repeat(3, minmax(120px, 1fr));
152
+ gap: 1rem;
153
+ }
154
+
155
+ @media (min-width: 768px) {
156
+ grid-template-columns: repeat(4, minmax(120px, 1fr));
157
+ }
158
+
159
+ @media (min-width: ${UI_BREAKPOINT_MOBILE}) {
160
+ grid-template-columns: repeat(
161
+ ${props => Math.min(props.$gridColumns, 4)},
162
+ minmax(120px, 1fr)
163
+ );
164
+ }
165
+
166
+ @media (min-width: ${UI_BREAKPOINT_SMALL_LAPTOP}) {
167
+ grid-template-columns: repeat(
168
+ ${props => props.$gridColumns},
169
+ minmax(120px, 1fr)
170
+ );
171
+ }
123
172
 
124
173
  .PaginatedContent-item {
125
174
  display: flex;
126
175
  align-items: flex-start;
176
+ justify-content: center;
127
177
  height: ${props => props.$itemHeight ?? 'auto'};
178
+ width: 100%;
179
+ box-sizing: border-box;
180
+ min-height: 120px;
128
181
  }
129
182
  }
130
183
 
@@ -138,7 +191,11 @@ const Content = styled.div<{
138
191
  const PaginationContainer = styled.div`
139
192
  display: flex;
140
193
  justify-content: center;
141
- padding: 1rem;
194
+ padding: 0.5rem;
195
+
196
+ @media (min-width: 480px) {
197
+ padding: 1rem;
198
+ }
142
199
  `;
143
200
 
144
201
  const EmptyMessage = styled.div`
@@ -1,6 +1,5 @@
1
1
  import { useState } from 'react';
2
2
 
3
- const TOOLTIP_WIDTH = 300;
4
3
  const TOOLTIP_OFFSET = 10;
5
4
  const MIN_VISIBLE_HEIGHT = 100;
6
5
 
@@ -15,6 +14,20 @@ interface ITooltipState<T> {
15
14
  }
16
15
 
17
16
  export const useTooltipPosition = <T>() => {
17
+ // Dynamically calculate tooltip width based on screen size
18
+ const getTooltipWidth = (): number => {
19
+ const viewportWidth = window.innerWidth;
20
+ if (viewportWidth < 360) {
21
+ return Math.min(280, viewportWidth - 20);
22
+ }
23
+ if (viewportWidth < 480) {
24
+ return 300;
25
+ }
26
+ return 320;
27
+ };
28
+
29
+ const TOOLTIP_WIDTH = getTooltipWidth();
30
+
18
31
  const [tooltipState, setTooltipState] = useState<ITooltipState<T> | null>(
19
32
  null
20
33
  );
@@ -22,18 +35,24 @@ export const useTooltipPosition = <T>() => {
22
35
  const calculateTooltipPosition = (rect: DOMRect): ITooltipPosition => {
23
36
  const viewportWidth = window.innerWidth;
24
37
  const viewportHeight = window.innerHeight;
38
+ const tooltipWidth = getTooltipWidth();
25
39
 
26
40
  // Try to position to the right first
27
41
  let x = rect.right + TOOLTIP_OFFSET;
28
42
 
29
43
  // 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;
44
+ if (x + tooltipWidth > viewportWidth - TOOLTIP_OFFSET) {
45
+ x = rect.left - tooltipWidth - TOOLTIP_OFFSET;
32
46
  }
33
47
 
34
- // If left positioning would go off screen, position relative to viewport
48
+ // If left positioning would go off screen, center it or position relative to viewport
35
49
  if (x < TOOLTIP_OFFSET) {
36
- x = TOOLTIP_OFFSET;
50
+ if (viewportWidth < 400) {
51
+ // For very small screens, center the tooltip
52
+ x = Math.max(TOOLTIP_OFFSET, (viewportWidth - tooltipWidth) / 2);
53
+ } else {
54
+ x = TOOLTIP_OFFSET;
55
+ }
37
56
  }
38
57
 
39
58
  // Position vertically aligned with the top of the element
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  IItem,
3
3
  IItemContainer,
4
+ ItemQualityLevel,
4
5
  ItemRarities,
5
6
  ItemSlotType,
6
7
  ItemSubType,
@@ -81,6 +82,7 @@ export const items: IItem[] = [
81
82
  createdAt: '2022-06-04T03:18:09.335Z',
82
83
  updatedAt: '2022-06-04T18:16:49.056Z',
83
84
  rarity: ItemRarities.Legendary,
85
+ quality: ItemQualityLevel.Mythic,
84
86
  canBePurchasedOnlyByPremiumPlans: [
85
87
  UserAccountTypes.PremiumGold,
86
88
  UserAccountTypes.PremiumUltimate,
@@ -167,6 +169,7 @@ export const items: IItem[] = [
167
169
  createdAt: '2022-06-04T03:18:09.335Z',
168
170
  updatedAt: '2022-06-04T18:16:49.056Z',
169
171
  rarity: ItemRarities.Epic,
172
+ quality: ItemQualityLevel.Normal,
170
173
  attachedGems: [
171
174
  {
172
175
  gemEntityEffectsAdd: ['poison'],
@@ -221,6 +224,7 @@ export const items: IItem[] = [
221
224
  createdAt: '2022-06-04T03:18:09.335Z',
222
225
  updatedAt: '2022-06-04T18:16:49.056Z',
223
226
  rarity: ItemRarities.Uncommon,
227
+ quality: ItemQualityLevel.HighQuality,
224
228
  },
225
229
  {
226
230
  _id: '629acek4j7c8e8002ff60034',
@@ -255,6 +259,7 @@ export const items: IItem[] = [
255
259
  createdAt: '2022-06-04T03:18:09.335Z',
256
260
  updatedAt: '2022-06-04T18:16:49.056Z',
257
261
  rarity: ItemRarities.Rare,
262
+ quality: ItemQualityLevel.Ancient,
258
263
  },
259
264
  {
260
265
  _id: '629acek4j7c8e8002fg60034',
@@ -289,6 +294,7 @@ export const items: IItem[] = [
289
294
  createdAt: '2022-06-04T03:18:09.335Z',
290
295
  updatedAt: '2022-06-04T18:16:49.056Z',
291
296
  rarity: ItemRarities.Common,
297
+ quality: ItemQualityLevel.Mastercrafted,
292
298
  },
293
299
  {
294
300
  _id: '392acek4j7c8e8002ff60403',
@@ -323,6 +329,7 @@ export const items: IItem[] = [
323
329
  createdAt: '2022-06-04T03:18:09.335Z',
324
330
  updatedAt: '2022-06-04T18:16:49.056Z',
325
331
  rarity: ItemRarities.Common,
332
+ quality: ItemQualityLevel.Exceptional,
326
333
  },
327
334
  createBagItem('392acek4j7c8e8002ff60404', '#FF0000'), // red
328
335
  createBagItem('392acek4j7c8e8002ff60405', '#0000FF'), // blue