@rpg-engine/long-bow 0.8.145 → 0.8.148

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.145",
3
+ "version": "0.8.148",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "dependencies": {
85
85
  "@capacitor/core": "^6.1.0",
86
86
  "@rollup/plugin-image": "^2.1.1",
87
- "@rpg-engine/shared": "file:../rpg-shared",
87
+ "@rpg-engine/shared": "^0.10.90",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -3,15 +3,16 @@ import {
3
3
  IMarketplaceBlueprintSummary,
4
4
  ItemSubType,
5
5
  } from '@rpg-engine/shared';
6
- import React, { useState } from 'react';
6
+ import { debounce } from 'lodash';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
8
  import { FaTimes } from 'react-icons/fa';
8
- import styled from 'styled-components';
9
+ import styled, { keyframes } from 'styled-components';
9
10
  import { Dropdown } from '../Dropdown';
10
11
  import { IOptionsProps } from '../Dropdown';
11
12
  import { Input } from '../Input';
12
13
  import { Pager } from '../Pager';
13
14
  import ModalPortal from '../Abstractions/ModalPortal';
14
- import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
15
+ import { BlueprintTable } from './BlueprintTable';
15
16
 
16
17
  export interface IBlueprintSearchModalProps {
17
18
  isOpen: boolean;
@@ -24,10 +25,17 @@ export interface IBlueprintSearchModalProps {
24
25
  isLoading: boolean;
25
26
  atlasJSON: any;
26
27
  atlasIMG: any;
28
+ enableHotkeys?: () => void;
29
+ disableHotkeys?: () => void;
27
30
  }
28
31
 
29
32
  const BLUEPRINTS_PER_PAGE = 10;
30
33
 
34
+ const scaleIn = keyframes`
35
+ from { transform: scale(0.85); opacity: 0; }
36
+ to { transform: scale(1); opacity: 1; }
37
+ `;
38
+
31
39
  const typeOptions: IOptionsProps[] = [
32
40
  { id: 1, value: '', option: 'All Types' },
33
41
  ...Object.keys(ItemSubType)
@@ -53,27 +61,49 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
53
61
  isLoading,
54
62
  atlasJSON,
55
63
  atlasIMG,
64
+ enableHotkeys,
65
+ disableHotkeys,
56
66
  }) => {
57
67
  const [searchName, setSearchName] = useState('');
58
68
  const [selectedType, setSelectedType] = useState('');
59
69
  const [selectedSubType, setSelectedSubType] = useState('');
60
70
 
61
- if (!isOpen) return null;
71
+ const searchNameRef = useRef(searchName);
72
+ const selectedTypeRef = useRef(selectedType);
73
+ const selectedSubTypeRef = useRef(selectedSubType);
74
+
75
+ searchNameRef.current = searchName;
76
+ selectedTypeRef.current = selectedType;
77
+ selectedSubTypeRef.current = selectedSubType;
78
+
79
+ const triggerSearch = useCallback(
80
+ (overrides?: Partial<{ name: string; itemType: string; itemSubType: string; page: number }>): void => {
81
+ onSearch({
82
+ npcId: '',
83
+ name: overrides?.name ?? searchNameRef.current,
84
+ itemType: overrides?.itemType ?? selectedTypeRef.current,
85
+ itemSubType: overrides?.itemSubType ?? selectedSubTypeRef.current,
86
+ page: overrides?.page ?? 1,
87
+ limit: BLUEPRINTS_PER_PAGE,
88
+ });
89
+ },
90
+ [onSearch]
91
+ );
62
92
 
63
- const triggerSearch = (overrides?: Partial<{ name: string; itemType: string; itemSubType: string; page: number }>): void => {
64
- onSearch({
65
- npcId: '',
66
- name: overrides?.name ?? searchName,
67
- itemType: overrides?.itemType ?? selectedType,
68
- itemSubType: overrides?.itemSubType ?? selectedSubType,
69
- page: overrides?.page ?? 1,
70
- limit: BLUEPRINTS_PER_PAGE,
71
- });
72
- };
93
+ const debouncedNameSearch = useMemo(
94
+ () => debounce((name: string) => triggerSearch({ name, page: 1 }), 300),
95
+ [triggerSearch]
96
+ );
97
+
98
+ useEffect(() => {
99
+ return () => { debouncedNameSearch.cancel(); };
100
+ }, [debouncedNameSearch]);
101
+
102
+ if (!isOpen) return null;
73
103
 
74
104
  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
75
105
  setSearchName(e.target.value);
76
- triggerSearch({ name: e.target.value, page: 1 });
106
+ debouncedNameSearch(e.target.value);
77
107
  };
78
108
 
79
109
  const handleTypeChange = (value: string): void => {
@@ -121,6 +151,8 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
121
151
  value={searchName}
122
152
  onChange={handleNameChange}
123
153
  placeholder="Search by name..."
154
+ onFocus={disableHotkeys}
155
+ onBlur={enableHotkeys}
124
156
  />
125
157
  </InputWrapper>
126
158
 
@@ -140,47 +172,22 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
140
172
  </FiltersRow>
141
173
 
142
174
  <ResultsWrapper>
143
- {isLoading ? (
144
- <EmptyState>Loading...</EmptyState>
145
- ) : blueprints.length === 0 ? (
146
- <EmptyState>No blueprints found</EmptyState>
175
+ {blueprints.length === 0 && !isLoading ? (
176
+ <EmptyState>No items found</EmptyState>
147
177
  ) : (
148
- <>
149
- <ResultsHeader>
150
- <ColName>Name</ColName>
151
- <ColType>Type</ColType>
152
- <ColTier>Tier</ColTier>
153
- </ResultsHeader>
154
- {blueprints.map((blueprint) => (
155
- <ResultRow
156
- key={blueprint.key}
157
- onPointerDown={() => onSelect(blueprint)}
158
- >
159
- <SpriteWrapper>
160
- <SpriteFromAtlas
161
- atlasJSON={atlasJSON}
162
- atlasIMG={atlasIMG}
163
- spriteKey={blueprint.texturePath || blueprint.key}
164
- width={32}
165
- height={32}
166
- imgScale={2}
167
- centered
168
- />
169
- </SpriteWrapper>
170
- <ColName>
171
- <BlueprintName>{blueprint.name}</BlueprintName>
172
- <BlueprintKey>{blueprint.key}</BlueprintKey>
173
- </ColName>
174
- <ColType>
175
- <TypeText>{blueprint.type}</TypeText>
176
- {blueprint.subType && blueprint.subType !== blueprint.type && (
177
- <SubTypeText>{blueprint.subType}</SubTypeText>
178
- )}
179
- </ColType>
180
- <ColTier>T{blueprint.tier}</ColTier>
181
- </ResultRow>
182
- ))}
183
- </>
178
+ <ResultsContent $dimmed={isLoading}>
179
+ <BlueprintTable
180
+ blueprints={blueprints}
181
+ atlasJSON={atlasJSON}
182
+ atlasIMG={atlasIMG}
183
+ onSelect={onSelect}
184
+ />
185
+ </ResultsContent>
186
+ )}
187
+ {isLoading && (
188
+ <LoadingOverlay>
189
+ <LoadingText>Loading...</LoadingText>
190
+ </LoadingOverlay>
184
191
  )}
185
192
  </ResultsWrapper>
186
193
 
@@ -226,12 +233,7 @@ const ModalContent = styled.div`
226
233
  flex-direction: column;
227
234
  gap: 12px;
228
235
  pointer-events: auto;
229
- animation: scaleIn 0.15s ease-out;
230
-
231
- @keyframes scaleIn {
232
- from { transform: scale(0.85); opacity: 0; }
233
- to { transform: scale(1); opacity: 1; }
234
- }
236
+ animation: ${scaleIn} 0.15s ease-out;
235
237
  `;
236
238
 
237
239
  const Header = styled.div`
@@ -284,7 +286,6 @@ const InputWrapper = styled.div`
284
286
 
285
287
  const StyledInput = styled(Input)`
286
288
  flex: 1;
287
- height: 10px;
288
289
  `;
289
290
 
290
291
  const FiltersRow = styled.div`
@@ -299,110 +300,40 @@ const StyledDropdown = styled(Dropdown)`
299
300
  `;
300
301
 
301
302
  const ResultsWrapper = styled.div`
303
+ position: relative;
302
304
  overflow-y: auto;
303
- max-height: 320px;
305
+ height: 320px;
304
306
  background: rgba(0, 0, 0, 0.2);
305
307
  border: 1px solid rgba(255, 255, 255, 0.05);
306
308
  border-radius: 4px;
307
309
  `;
308
310
 
309
- const tableRowBase = `
310
- display: grid;
311
- grid-template-columns: 32px 1fr 120px 60px;
312
- align-items: center;
313
- gap: 8px;
314
- padding: 6px 12px;
315
- `;
316
-
317
- const ResultsHeader = styled.div`
318
- ${tableRowBase}
319
- background: rgba(0, 0, 0, 0.4);
320
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
321
- position: sticky;
322
- top: 0;
323
- z-index: 1;
324
-
325
- > * {
326
- font-size: 0.45rem;
327
- color: #888;
328
- text-transform: uppercase;
329
- letter-spacing: 1px;
330
- }
311
+ const ResultsContent = styled.div<{ $dimmed?: boolean }>`
312
+ opacity: ${(p) => (p.$dimmed ? 0.4 : 1)};
313
+ transition: opacity 0.15s ease;
331
314
  `;
332
315
 
333
- const ResultRow = styled.div`
334
- ${tableRowBase}
335
- border-bottom: 1px solid rgba(255, 255, 255, 0.04);
336
- cursor: pointer;
337
- transition: background 0.1s;
338
-
339
- &:hover {
340
- background: rgba(245, 158, 11, 0.08);
341
- }
342
-
343
- &:last-child {
344
- border-bottom: none;
345
- }
346
- `;
347
-
348
- const SpriteWrapper = styled.div`
316
+ const LoadingOverlay = styled.div`
317
+ position: absolute;
318
+ inset: 0;
349
319
  display: flex;
350
320
  align-items: center;
351
321
  justify-content: center;
352
- width: 32px;
353
- height: 32px;
354
- `;
355
-
356
- const ColName = styled.div`
357
- display: flex;
358
- flex-direction: column;
359
- gap: 2px;
360
- overflow: hidden;
361
- `;
362
-
363
- const BlueprintName = styled.span`
364
- font-size: 0.5rem;
365
- color: #ddd;
366
- overflow: hidden;
367
- text-overflow: ellipsis;
368
- white-space: nowrap;
369
- `;
370
-
371
- const BlueprintKey = styled.span`
372
- font-size: 0.4rem;
373
- color: #666;
374
- overflow: hidden;
375
- text-overflow: ellipsis;
376
- white-space: nowrap;
377
- `;
378
-
379
- const ColType = styled.div`
380
- display: flex;
381
- flex-direction: column;
382
- gap: 2px;
383
- `;
384
-
385
- const TypeText = styled.span`
386
- font-size: 0.45rem;
387
- color: #aaa;
388
- `;
389
-
390
- const SubTypeText = styled.span`
391
- font-size: 0.4rem;
392
- color: #666;
322
+ pointer-events: none;
393
323
  `;
394
324
 
395
- const ColTier = styled.div`
396
- font-size: 0.5rem;
325
+ const LoadingText = styled.span`
326
+ font-size: 0.55rem;
397
327
  color: #f59e0b;
398
- text-align: center;
328
+ text-transform: uppercase;
329
+ letter-spacing: 1px;
399
330
  `;
400
331
 
401
332
  const EmptyState = styled.div`
402
333
  display: flex;
403
334
  align-items: center;
404
335
  justify-content: center;
405
- height: 100px;
336
+ height: 100%;
406
337
  font-size: 0.55rem;
407
338
  color: #666;
408
339
  text-transform: uppercase;
@@ -0,0 +1,158 @@
1
+ import { IMarketplaceBlueprintSummary } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
5
+
6
+ export interface IBlueprintTableProps {
7
+ blueprints: IMarketplaceBlueprintSummary[];
8
+ atlasJSON: any;
9
+ atlasIMG: any;
10
+ onSelect?: (blueprint: IMarketplaceBlueprintSummary) => void;
11
+ }
12
+
13
+ export const BlueprintTable: React.FC<IBlueprintTableProps> = ({
14
+ blueprints,
15
+ atlasJSON,
16
+ atlasIMG,
17
+ onSelect,
18
+ }) => {
19
+ return (
20
+ <>
21
+ <ResultsHeader>
22
+ <span />
23
+ <ColName>Name</ColName>
24
+ <ColType>Type</ColType>
25
+ <ColTier>Tier</ColTier>
26
+ </ResultsHeader>
27
+ {blueprints.map((blueprint) => (
28
+ <ResultRow
29
+ key={blueprint.key}
30
+ $selectable={!!onSelect}
31
+ onPointerDown={onSelect ? () => onSelect(blueprint) : undefined}
32
+ >
33
+ <SpriteWrapper>
34
+ <SpriteFromAtlas
35
+ atlasJSON={atlasJSON}
36
+ atlasIMG={atlasIMG}
37
+ spriteKey={blueprint.texturePath || blueprint.key}
38
+ width={32}
39
+ height={32}
40
+ imgScale={2}
41
+ centered
42
+ />
43
+ </SpriteWrapper>
44
+ <ColName>
45
+ <BlueprintName>{blueprint.name}</BlueprintName>
46
+ {(blueprint.type || blueprint.subType) && (
47
+ <BlueprintMeta>
48
+ {[blueprint.type, blueprint.subType]
49
+ .filter(Boolean)
50
+ .filter((v, i, arr) => arr.indexOf(v) === i)
51
+ .join(' · ')}
52
+ </BlueprintMeta>
53
+ )}
54
+ </ColName>
55
+ <ColType>
56
+ <TypeText>{blueprint.type}</TypeText>
57
+ {blueprint.subType && blueprint.subType !== blueprint.type && (
58
+ <SubTypeText>{blueprint.subType}</SubTypeText>
59
+ )}
60
+ </ColType>
61
+ <ColTier>T{blueprint.tier}</ColTier>
62
+ </ResultRow>
63
+ ))}
64
+ </>
65
+ );
66
+ };
67
+
68
+ const tableRowBase = `
69
+ display: grid;
70
+ grid-template-columns: 40px 1fr 120px 50px;
71
+ align-items: center;
72
+ gap: 8px;
73
+ padding: 6px 12px;
74
+ `;
75
+
76
+ const ResultsHeader = styled.div`
77
+ ${tableRowBase}
78
+ background: rgba(0, 0, 0, 0.4);
79
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
80
+ position: sticky;
81
+ top: 0;
82
+ z-index: 1;
83
+
84
+ > * {
85
+ font-size: 0.45rem;
86
+ color: #888;
87
+ text-transform: uppercase;
88
+ letter-spacing: 1px;
89
+ }
90
+ `;
91
+
92
+ const ResultRow = styled.div<{ $selectable: boolean }>`
93
+ ${tableRowBase}
94
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
95
+ cursor: ${p => p.$selectable ? 'pointer' : 'default'};
96
+ transition: background 0.1s;
97
+
98
+ &:hover {
99
+ background: ${p => p.$selectable ? 'rgba(245, 158, 11, 0.08)' : 'none'};
100
+ }
101
+
102
+ &:last-child {
103
+ border-bottom: none;
104
+ }
105
+ `;
106
+
107
+ const SpriteWrapper = styled.div`
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ width: 40px;
112
+ height: 32px;
113
+ `;
114
+
115
+ const ColName = styled.div`
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 2px;
119
+ overflow: hidden;
120
+ `;
121
+
122
+ const BlueprintName = styled.span`
123
+ font-size: 0.5rem;
124
+ color: #ddd;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap;
128
+ `;
129
+
130
+ const BlueprintMeta = styled.span`
131
+ font-size: 0.4rem;
132
+ color: #666;
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ white-space: nowrap;
136
+ `;
137
+
138
+ const ColType = styled.div`
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 2px;
142
+ `;
143
+
144
+ const TypeText = styled.span`
145
+ font-size: 0.45rem;
146
+ color: #aaa;
147
+ `;
148
+
149
+ const SubTypeText = styled.span`
150
+ font-size: 0.4rem;
151
+ color: #666;
152
+ `;
153
+
154
+ const ColTier = styled.div`
155
+ font-size: 0.5rem;
156
+ color: #f59e0b;
157
+ text-align: center;
158
+ `;
@@ -4,7 +4,7 @@ import {
4
4
  import { ShoppingCart } from 'pixelarticons/react/ShoppingCart';
5
5
  import React from 'react';
6
6
  import { FaTimes } from 'react-icons/fa';
7
- import styled from 'styled-components';
7
+ import styled, { keyframes } from 'styled-components';
8
8
  import { Dropdown } from '../Dropdown';
9
9
  import { IOptionsProps } from '../Dropdown';
10
10
  import { Input } from '../Input';
@@ -25,8 +25,15 @@ export interface IBuyOrderDetailsModalProps {
25
25
  onConfirm: () => void;
26
26
  atlasJSON: any;
27
27
  atlasIMG: any;
28
+ enableHotkeys?: () => void;
29
+ disableHotkeys?: () => void;
28
30
  }
29
31
 
32
+ const scaleIn = keyframes`
33
+ from { transform: scale(0.85); opacity: 0; }
34
+ to { transform: scale(1); opacity: 1; }
35
+ `;
36
+
30
37
  const rarityOptions: IOptionsProps[] = [
31
38
  { id: 1, value: 'Common', option: 'Common' },
32
39
  { id: 2, value: 'Uncommon', option: 'Uncommon' },
@@ -48,6 +55,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
48
55
  onConfirm,
49
56
  atlasJSON,
50
57
  atlasIMG,
58
+ enableHotkeys,
59
+ disableHotkeys,
51
60
  }) => {
52
61
  if (!isOpen || !blueprint) return null;
53
62
 
@@ -89,7 +98,7 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
89
98
  </SpriteWrapper>
90
99
  <ItemInfo>
91
100
  <ItemName>{blueprint.name}</ItemName>
92
- <ItemKey>{blueprint.type} - {blueprint.subType} - T{blueprint.tier}</ItemKey>
101
+ <ItemMeta>{[blueprint.type, blueprint.subType].filter(Boolean).filter((v, i, arr) => arr.indexOf(v) === i).join(' · ')} · T{blueprint.tier}</ItemMeta>
93
102
  </ItemInfo>
94
103
  </ItemDisplay>
95
104
 
@@ -102,6 +111,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
102
111
  placeholder="Qty"
103
112
  type="number"
104
113
  min={1}
114
+ onFocus={disableHotkeys}
115
+ onBlur={enableHotkeys}
105
116
  />
106
117
  </FieldRow>
107
118
 
@@ -113,6 +124,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
113
124
  placeholder="Max gold"
114
125
  type="number"
115
126
  min={1}
127
+ onFocus={disableHotkeys}
128
+ onBlur={enableHotkeys}
116
129
  />
117
130
  </FieldRow>
118
131
 
@@ -170,12 +183,7 @@ const ModalContent = styled.div`
170
183
  flex-direction: column;
171
184
  gap: 16px;
172
185
  pointer-events: auto;
173
- animation: scaleIn 0.15s ease-out;
174
-
175
- @keyframes scaleIn {
176
- from { transform: scale(0.85); opacity: 0; }
177
- to { transform: scale(1); opacity: 1; }
178
- }
186
+ animation: ${scaleIn} 0.15s ease-out;
179
187
  `;
180
188
 
181
189
  const Header = styled.div`
@@ -238,7 +246,7 @@ const ItemName = styled.span`
238
246
  white-space: nowrap;
239
247
  `;
240
248
 
241
- const ItemKey = styled.span`
249
+ const ItemMeta = styled.span`
242
250
  font-size: 0.4rem;
243
251
  color: #666;
244
252
  overflow: hidden;
@@ -270,7 +278,6 @@ const Label = styled.label`
270
278
  `;
271
279
 
272
280
  const StyledInput = styled(Input)`
273
- height: 10px;
274
281
  width: 100%;
275
282
  `;
276
283