@rpg-engine/long-bow 0.8.31 → 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 (24) 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 +467 -163
  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 +468 -164
  14. package/dist/long-bow.esm.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/InformationCenter/sections/bestiary/BestiaryAdvancedFilters.tsx +95 -0
  17. package/src/components/InformationCenter/sections/bestiary/InformationCenterBestiarySection.tsx +103 -61
  18. package/src/components/InformationCenter/sections/items/InformationCenterItemsSection.tsx +62 -69
  19. package/src/components/InformationCenter/sections/items/ItemsAdvancedFilters.tsx +80 -0
  20. package/src/components/shared/AdvancedFilters/AdvancedFilters.tsx +279 -0
  21. package/src/components/shared/PaginatedContent/PaginatedContent.tsx +1 -0
  22. package/src/components/shared/SearchBar/SearchBar.tsx +15 -5
  23. package/src/components/shared/SearchHeader/SearchHeader.tsx +2 -0
  24. package/src/hooks/useTooltipPosition.ts +73 -0
@@ -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
+ `;
@@ -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
+ };