@rpg-engine/long-bow 0.8.35 → 0.8.36

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
+ // Center in the screen for mobile
117
+ setButtonPosition({
118
+ top: Math.max(50, window.innerHeight / 2 - 150),
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,34 @@ 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.top}px`,
193
+ left: `${buttonPosition.left}px`,
194
+ transform: 'translateX(-50%)',
195
+ zIndex: 9999,
196
+ }}
197
+ $isMobile={buttonPosition.isMobile}
198
+ >
199
+ <FilterHeader>
200
+ <FilterTitle>Advanced Filters</FilterTitle>
201
+ {buttonPosition.isMobile && (
202
+ <CloseButton onClick={onToggle}>×</CloseButton>
203
+ )}
204
+ </FilterHeader>
205
+
206
+ {sections.map(renderFilterSection)}
207
+
208
+ {hasActiveFilters && (
209
+ <ClearFiltersButton onClick={onClearAll}>
210
+ Clear All Filters
211
+ </ClearFiltersButton>
212
+ )}
213
+ </FiltersPanel>
214
+ </Portal>
138
215
  )}
139
216
  </Container>
140
217
  );
@@ -184,32 +261,37 @@ const FilterCount = styled.div<{ $visible: boolean }>`
184
261
  transition: all 0.2s;
185
262
  `;
186
263
 
187
- const FiltersPanel = styled.div`
188
- position: absolute;
189
- top: calc(100% + 0.75rem);
190
- right: -8px;
264
+ const FiltersPanel = styled.div<{ $isMobile?: boolean }>`
191
265
  background: #1a1a1a;
192
266
  border: 1px solid #333;
193
267
  border-radius: 6px;
194
268
  padding: 1rem;
195
- z-index: 1000;
196
- min-width: 280px;
269
+ width: 280px;
270
+ max-width: calc(100vw - 20px);
197
271
  display: flex;
198
272
  flex-direction: column;
199
273
  gap: 1rem;
200
274
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
275
+ font-family: 'Press Start 2P', cursive;
276
+
277
+ @media (max-width: 320px) {
278
+ width: 250px;
279
+ padding: 0.75rem;
280
+ gap: 0.75rem;
281
+ }
201
282
 
202
283
  &:before {
203
284
  content: '';
204
285
  position: absolute;
205
286
  top: -6px;
206
- right: 16px;
287
+ left: 50%;
288
+ transform: translateX(-50%) rotate(45deg);
207
289
  width: 12px;
208
290
  height: 12px;
209
291
  background: #1a1a1a;
210
292
  border-left: 1px solid #333;
211
293
  border-top: 1px solid #333;
212
- transform: rotate(45deg);
294
+ display: ${props => (props.$isMobile ? 'none' : 'block')};
213
295
  }
214
296
  `;
215
297
 
@@ -225,7 +307,7 @@ const FilterHeader = styled.div`
225
307
  const FilterTitle = styled.div`
226
308
  font-weight: 600;
227
309
  color: #ffd700;
228
- font-size: 0.875rem;
310
+ font-size: 0.75rem;
229
311
  `;
230
312
 
231
313
  const FilterSection = styled.div`
@@ -236,7 +318,7 @@ const FilterSection = styled.div`
236
318
 
237
319
  const Label = styled.div`
238
320
  color: #999;
239
- font-size: 0.75rem;
321
+ font-size: 0.65rem;
240
322
  text-transform: uppercase;
241
323
  letter-spacing: 0.05em;
242
324
  `;
@@ -249,8 +331,22 @@ const RangeInputs = styled.div`
249
331
  input {
250
332
  width: 80px;
251
333
  background: #262626 !important;
252
- border: 1px solid #333 !important;
253
- color: #fff !important;
334
+ border: 1px solid #444 !important;
335
+ border-radius: 4px !important;
336
+ color: #ddd !important;
337
+ font-size: 0.65rem !important;
338
+ padding: 0.5rem !important;
339
+ height: auto !important;
340
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2) !important;
341
+
342
+ &::placeholder {
343
+ color: #666 !important;
344
+ }
345
+
346
+ &:focus {
347
+ border-color: #f59e0b !important;
348
+ outline: none !important;
349
+ }
254
350
  }
255
351
 
256
352
  svg {
@@ -267,13 +363,42 @@ const ClearFiltersButton = styled.button`
267
363
  padding: 0.75rem 0;
268
364
  margin-top: 0.5rem;
269
365
  cursor: pointer;
270
- font-size: 0.75rem;
366
+ font-size: 0.65rem;
271
367
  transition: all 0.2s;
272
368
  border-top: 1px solid #333;
273
369
  text-transform: uppercase;
274
370
  letter-spacing: 0.05em;
371
+ font-family: 'Press Start 2P', cursive;
372
+
373
+ &:hover {
374
+ color: #ffd700;
375
+ }
376
+ `;
377
+
378
+ const CloseButton = styled.button`
379
+ background: transparent;
380
+ border: none;
381
+ color: #999;
382
+ font-size: 1.5rem;
383
+ line-height: 1;
384
+ cursor: pointer;
385
+ padding: 0;
386
+ margin: 0;
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ width: 24px;
391
+ height: 24px;
275
392
 
276
393
  &:hover {
277
394
  color: #ffd700;
278
395
  }
279
396
  `;
397
+
398
+ const StyledDropdownWrapper = styled.div`
399
+ .rpgui-dropdown-imp {
400
+ font-size: 0.8rem;
401
+ min-height: unset;
402
+ padding-top: 0.3rem;
403
+ }
404
+ `;
@@ -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,69 @@ 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
116
  max-height: ${props => props.$maxHeight};
113
117
  overflow-y: ${props => (props.$maxHeight ? 'auto' : 'visible')};
118
+ box-sizing: border-box;
119
+
120
+ @media (min-width: 360px) {
121
+ padding: 0.5rem;
122
+ }
123
+
124
+ @media (min-width: 480px) {
125
+ padding: 1rem;
126
+ }
114
127
 
115
128
  &.grid {
116
129
  display: grid;
117
- grid-template-columns: repeat(
118
- ${props => props.$gridColumns},
119
- minmax(0, 1fr)
120
- );
121
- gap: 1rem;
130
+ grid-template-columns: 1fr;
131
+ gap: 0.5rem;
122
132
  align-items: start;
133
+ justify-content: center;
134
+ width: 100%;
135
+ overflow-x: hidden;
136
+
137
+ @media (min-width: 320px) {
138
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
139
+ }
140
+
141
+ @media (min-width: 400px) {
142
+ grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
143
+ gap: 0.75rem;
144
+ }
145
+
146
+ @media (min-width: 480px) {
147
+ grid-template-columns: repeat(2, minmax(135px, 1fr));
148
+ gap: 1rem;
149
+ }
150
+
151
+ @media (min-width: 768px) {
152
+ grid-template-columns: repeat(3, minmax(135px, 1fr));
153
+ }
154
+
155
+ @media (min-width: ${UI_BREAKPOINT_MOBILE}) {
156
+ grid-template-columns: repeat(
157
+ ${props => Math.min(props.$gridColumns, 4)},
158
+ minmax(135px, 1fr)
159
+ );
160
+ }
161
+
162
+ @media (min-width: ${UI_BREAKPOINT_SMALL_LAPTOP}) {
163
+ grid-template-columns: repeat(
164
+ ${props => props.$gridColumns},
165
+ minmax(135px, 1fr)
166
+ );
167
+ }
123
168
 
124
169
  .PaginatedContent-item {
125
170
  display: flex;
126
171
  align-items: flex-start;
172
+ justify-content: center;
127
173
  height: ${props => props.$itemHeight ?? 'auto'};
174
+ width: 100%;
175
+ box-sizing: border-box;
176
+ min-height: 135px;
128
177
  }
129
178
  }
130
179
 
@@ -138,7 +187,11 @@ const Content = styled.div<{
138
187
  const PaginationContainer = styled.div`
139
188
  display: flex;
140
189
  justify-content: center;
141
- padding: 1rem;
190
+ padding: 0.5rem;
191
+
192
+ @media (min-width: 480px) {
193
+ padding: 1rem;
194
+ }
142
195
  `;
143
196
 
144
197
  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