@rpg-engine/long-bow 0.8.171 → 0.8.173

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 (54) hide show
  1. package/dist/components/Store/CartView.d.ts +21 -1
  2. package/dist/components/Store/CountdownTimer.d.ts +7 -0
  3. package/dist/components/Store/FeaturedBanner.d.ts +23 -0
  4. package/dist/components/Store/PurchaseSuccess.d.ts +18 -0
  5. package/dist/components/Store/Store.d.ts +50 -2
  6. package/dist/components/Store/StoreBadges.d.ts +13 -0
  7. package/dist/components/Store/StoreCharacterSkinRow.d.ts +1 -0
  8. package/dist/components/Store/StoreItemRow.d.ts +10 -0
  9. package/dist/components/Store/TrustBar.d.ts +9 -0
  10. package/dist/components/Store/sections/StoreItemsSection.d.ts +13 -0
  11. package/dist/components/Store/sections/StorePacksSection.d.ts +11 -0
  12. package/dist/components/shared/CTAButton/CTAButton.d.ts +1 -0
  13. package/dist/components/shared/CustomScrollbar.d.ts +9 -0
  14. package/dist/index.d.ts +6 -1
  15. package/dist/long-bow.cjs.development.js +1279 -303
  16. package/dist/long-bow.cjs.development.js.map +1 -1
  17. package/dist/long-bow.cjs.production.min.js +1 -1
  18. package/dist/long-bow.cjs.production.min.js.map +1 -1
  19. package/dist/long-bow.esm.js +1276 -305
  20. package/dist/long-bow.esm.js.map +1 -1
  21. package/dist/stories/Features/store/FeaturedBanner.stories.d.ts +1 -0
  22. package/dist/stories/Features/store/PurchaseSuccess.stories.d.ts +1 -0
  23. package/dist/stories/Features/store/StoreBadges.stories.d.ts +1 -0
  24. package/dist/stories/Features/store/TrustBar.stories.d.ts +1 -0
  25. package/package.json +2 -2
  26. package/src/components/Marketplace/BuyPanel.tsx +1 -1
  27. package/src/components/RPGUI/RPGUIScrollbar.tsx +2 -2
  28. package/src/components/Store/CartView.tsx +143 -33
  29. package/src/components/Store/CountdownTimer.tsx +86 -0
  30. package/src/components/Store/FeaturedBanner.tsx +270 -0
  31. package/src/components/Store/PurchaseSuccess.tsx +255 -0
  32. package/src/components/Store/Store.tsx +236 -51
  33. package/src/components/Store/StoreBadges.tsx +94 -0
  34. package/src/components/Store/StoreCharacterSkinRow.tsx +113 -22
  35. package/src/components/Store/StoreItemRow.tsx +135 -17
  36. package/src/components/Store/TrustBar.tsx +69 -0
  37. package/src/components/Store/__test__/CountdownTimer.spec.tsx +100 -0
  38. package/src/components/Store/__test__/FeaturedBanner.spec.tsx +207 -0
  39. package/src/components/Store/__test__/PurchaseSuccess.spec.tsx +174 -0
  40. package/src/components/Store/__test__/StoreBadges.spec.tsx +133 -0
  41. package/src/components/Store/__test__/TrustBar.spec.tsx +85 -0
  42. package/src/components/Store/sections/StoreItemsSection.tsx +27 -1
  43. package/src/components/Store/sections/StorePacksSection.tsx +92 -28
  44. package/src/components/shared/CTAButton/CTAButton.tsx +25 -1
  45. package/src/components/shared/CustomScrollbar.ts +41 -0
  46. package/src/components/shared/ItemRowWrapper.tsx +26 -12
  47. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +1 -0
  48. package/src/components/shared/SpriteFromAtlas.tsx +4 -1
  49. package/src/index.tsx +6 -1
  50. package/src/stories/Features/store/FeaturedBanner.stories.tsx +121 -0
  51. package/src/stories/Features/store/PurchaseSuccess.stories.tsx +74 -0
  52. package/src/stories/Features/store/Store.stories.tsx +39 -3
  53. package/src/stories/Features/store/StoreBadges.stories.tsx +83 -0
  54. package/src/stories/Features/store/TrustBar.stories.tsx +51 -0
@@ -4,7 +4,7 @@ import {
4
4
  UserAccountTypes,
5
5
  ItemType,
6
6
  } from '@rpg-engine/shared';
7
- import React from 'react';
7
+ import React, { useEffect } from 'react';
8
8
  import styled from 'styled-components';
9
9
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
10
10
  import { StoreCharacterSkinRow } from '../StoreCharacterSkinRow';
@@ -12,6 +12,7 @@ import { StoreItemRow } from '../StoreItemRow';
12
12
  import { SegmentedToggle } from '../../shared/SegmentedToggle';
13
13
  import { SearchBar } from '../../shared/SearchBar/SearchBar';
14
14
  import { useStoreFiltering } from '../../../hooks/useStoreFiltering';
15
+ import { IStoreBadge } from '../StoreBadges';
15
16
 
16
17
  interface IStoreItemsSectionProps {
17
18
  items: IProductBlueprint[];
@@ -20,19 +21,29 @@ interface IStoreItemsSectionProps {
20
21
  quantity: number,
21
22
  metadata?: Record<string, any>
22
23
  ) => void;
24
+ onQuickBuy?: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
23
25
  atlasJSON: Record<string, any>;
24
26
  atlasIMG: string;
25
27
  userAccountType?: UserAccountTypes;
26
28
  textInputItemKeys?: string[];
29
+ itemBadges?: Record<string, { badges?: IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
30
+ /** Fires when an item row becomes visible. Passes item and its 0-based position. */
31
+ onItemView?: (item: IProductBlueprint, position: number) => void;
32
+ /** Fires when the category filter changes. Passes new category and item count. */
33
+ onCategoryChange?: (category: string, itemsShown: number) => void;
27
34
  }
28
35
 
29
36
  export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
30
37
  items,
31
38
  onAddToCart,
39
+ onQuickBuy,
32
40
  atlasJSON,
33
41
  atlasIMG,
34
42
  userAccountType,
35
43
  textInputItemKeys = [],
44
+ itemBadges = {},
45
+ onItemView,
46
+ onCategoryChange,
36
47
  }) => {
37
48
  const {
38
49
  searchQuery,
@@ -43,7 +54,14 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
43
54
  filteredItems,
44
55
  } = useStoreFiltering(items);
45
56
 
57
+ // Fire category change event when the filter changes
58
+ useEffect(() => {
59
+ onCategoryChange?.(selectedCategory, filteredItems.length);
60
+ }, [selectedCategory, filteredItems.length]);
61
+
46
62
  const renderStoreItem = (item: IProductBlueprint) => {
63
+ const meta = itemBadges[item.key];
64
+ const position = filteredItems.indexOf(item);
47
65
  // Prefer a specialized character skin row when needed
48
66
  if (item.metadataType === MetadataType.CharacterSkin) {
49
67
  return (
@@ -66,8 +84,12 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
66
84
  atlasJSON={atlasJSON}
67
85
  atlasIMG={atlasIMG}
68
86
  onAddToCart={onAddToCart}
87
+ onQuickBuy={onQuickBuy}
69
88
  userAccountType={userAccountType || UserAccountTypes.Free}
70
89
  showTextInput
90
+ onView={onItemView}
91
+ positionInList={position}
92
+ {...meta}
71
93
  />
72
94
  );
73
95
  }
@@ -79,7 +101,11 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
79
101
  atlasJSON={atlasJSON}
80
102
  atlasIMG={atlasIMG}
81
103
  onAddToCart={onAddToCart}
104
+ onQuickBuy={onQuickBuy}
82
105
  userAccountType={userAccountType || UserAccountTypes.Free}
106
+ onView={onItemView}
107
+ positionInList={position}
108
+ {...meta}
83
109
  />
84
110
  );
85
111
  };
@@ -1,6 +1,6 @@
1
1
  import { IItemPack } from '@rpg-engine/shared';
2
- import React, { useCallback } from 'react';
3
- import { FaCartPlus } from 'react-icons/fa';
2
+ import React, { useCallback, useEffect } from 'react';
3
+ import { FaBolt, FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../../shared/CTAButton/CTAButton';
6
6
  import { ItemRowWrapper } from '../../shared/ItemRowWrapper';
@@ -9,29 +9,55 @@ import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableCont
9
9
  import { SelectArrow } from '../../Arrow/SelectArrow';
10
10
  import { usePackFiltering } from '../../../hooks/usePackFiltering';
11
11
  import { useQuantityControl } from '../../../hooks/useQuantityControl';
12
+ import { IStoreBadge, StoreBadges } from '../StoreBadges';
12
13
 
13
14
  interface IStorePacksSectionProps {
14
15
  packs: IItemPack[];
15
16
  onAddToCart: (pack: IItemPack, quantity: number) => void;
17
+ onQuickBuy?: (pack: IItemPack, quantity: number) => void;
16
18
  onSelectPack?: (pack: IItemPack) => void;
17
19
  atlasJSON?: any;
18
20
  atlasIMG?: string;
21
+ packBadges?: Record<string, { badges?: IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
22
+ /** Fires once on mount per pack row — use for pack_viewed analytics. */
23
+ onPackView?: (pack: IItemPack, position: number) => void;
19
24
  }
20
25
 
21
26
  interface IPackRowItemProps {
22
27
  pack: IItemPack;
23
28
  onAddToCart: (pack: IItemPack, quantity: number) => void;
29
+ onQuickBuy?: (pack: IItemPack, quantity: number) => void;
24
30
  renderPackIcon: (pack: IItemPack) => React.ReactNode;
31
+ badges?: IStoreBadge[];
32
+ buyCount?: number;
33
+ viewersCount?: number;
34
+ saleEndsAt?: string;
35
+ originalPrice?: number;
36
+ onPackView?: (pack: IItemPack, position: number) => void;
37
+ positionInList?: number;
25
38
  }
26
39
 
27
- const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPackIcon }) => {
40
+ const PackRowItem: React.FC<IPackRowItemProps> = ({
41
+ pack, onAddToCart, onQuickBuy, renderPackIcon,
42
+ badges, buyCount, viewersCount, saleEndsAt, originalPrice,
43
+ onPackView, positionInList = 0,
44
+ }) => {
28
45
  const { quantity, handleQuantityChange, handleBlur, incrementQuantity, decrementQuantity, resetQuantity } = useQuantityControl();
29
46
 
47
+ useEffect(() => {
48
+ onPackView?.(pack, positionInList);
49
+ }, []);
50
+
30
51
  const handleAdd = () => {
31
52
  onAddToCart(pack, quantity);
32
53
  resetQuantity();
33
54
  };
34
55
 
56
+ const handleQuickBuy = () => {
57
+ onQuickBuy?.(pack, quantity);
58
+ resetQuantity();
59
+ };
60
+
35
61
  return (
36
62
  <PackRow>
37
63
  <LeftSection>
@@ -41,8 +67,14 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
41
67
 
42
68
  <PackDetails>
43
69
  <PackName>{pack.title}</PackName>
44
- <PackPrice>${pack.priceUSD}</PackPrice>
70
+ <PackPriceRow>
71
+ {originalPrice != null && (
72
+ <PackOriginalPrice>${originalPrice.toFixed(2)}</PackOriginalPrice>
73
+ )}
74
+ <PackPrice $onSale={originalPrice != null}>${pack.priceUSD}</PackPrice>
75
+ </PackPriceRow>
45
76
  {pack.description && <PackDescription>{pack.description}</PackDescription>}
77
+ <StoreBadges badges={badges} buyCount={buyCount} viewersCount={viewersCount} saleEndsAt={saleEndsAt} />
46
78
  </PackDetails>
47
79
  </LeftSection>
48
80
 
@@ -60,11 +92,10 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
60
92
  />
61
93
  <SelectArrow direction="right" onPointerDown={incrementQuantity} size={24} />
62
94
  </ArrowsContainer>
63
- <CTAButton
64
- icon={<FaCartPlus />}
65
- label="Add"
66
- onClick={handleAdd}
67
- />
95
+ {onQuickBuy && (
96
+ <CTAButton icon={<FaBolt />} label="Buy" onClick={handleQuickBuy} iconColor="#fff" textColor="#fff" />
97
+ )}
98
+ <CTAButton icon={<FaCartPlus />} label="Add" onClick={handleAdd} pulse />
68
99
  </Controls>
69
100
  </PackRow>
70
101
  );
@@ -73,8 +104,11 @@ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPac
73
104
  export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
74
105
  packs,
75
106
  onAddToCart,
107
+ onQuickBuy,
76
108
  atlasJSON,
77
109
  atlasIMG,
110
+ packBadges = {},
111
+ onPackView,
78
112
  }) => {
79
113
  const { searchQuery, setSearchQuery, filteredPacks } = usePackFiltering(packs);
80
114
 
@@ -95,15 +129,23 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
95
129
  );
96
130
 
97
131
  const renderPack = useCallback(
98
- (pack: IItemPack) => (
99
- <PackRowItem
100
- key={pack.key}
101
- pack={pack}
102
- onAddToCart={onAddToCart}
103
- renderPackIcon={renderPackIcon}
104
- />
105
- ),
106
- [onAddToCart, renderPackIcon]
132
+ (pack: IItemPack) => {
133
+ const meta = packBadges[pack.key];
134
+ const position = filteredPacks.indexOf(pack);
135
+ return (
136
+ <PackRowItem
137
+ key={pack.key}
138
+ pack={pack}
139
+ onAddToCart={onAddToCart}
140
+ onQuickBuy={onQuickBuy}
141
+ renderPackIcon={renderPackIcon}
142
+ onPackView={onPackView}
143
+ positionInList={position}
144
+ {...meta}
145
+ />
146
+ );
147
+ },
148
+ [onAddToCart, onQuickBuy, renderPackIcon, packBadges, onPackView, filteredPacks]
107
149
  );
108
150
 
109
151
  return (
@@ -127,18 +169,23 @@ const PackRow = styled(ItemRowWrapper)``;
127
169
  const LeftSection = styled.div`
128
170
  display: flex;
129
171
  align-items: center;
130
- gap: 0.75rem;
172
+ gap: 1.25rem;
131
173
  flex: 1;
132
174
  min-width: 0;
133
175
  `;
134
176
 
135
177
  const PackIconContainer = styled.div`
136
- width: 40px;
137
- height: 40px;
178
+ width: 48px;
179
+ height: 48px;
138
180
  flex-shrink: 0;
139
181
  display: flex;
140
182
  align-items: center;
141
183
  justify-content: center;
184
+ background: rgba(0, 0, 0, 0.6);
185
+ border: 1px solid rgba(255, 255, 255, 0.1);
186
+ border-radius: 8px;
187
+ padding: 4px;
188
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
142
189
 
143
190
  img {
144
191
  width: 100%;
@@ -151,33 +198,50 @@ const PackDetails = styled.div`
151
198
  flex: 1;
152
199
  display: flex;
153
200
  flex-direction: column;
154
- gap: 0.25rem;
201
+ gap: 0.4rem;
155
202
  min-width: 0;
156
203
  `;
157
204
 
158
205
  const PackName = styled.div`
159
206
  font-family: 'Press Start 2P', cursive;
160
- font-size: 0.75rem;
207
+ font-size: 0.875rem;
161
208
  color: #ffffff;
209
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
210
+ letter-spacing: 0.5px;
211
+ `;
212
+
213
+ const PackPriceRow = styled.div`
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 0.5rem;
162
217
  `;
163
218
 
164
- const PackPrice = styled.div`
219
+ const PackOriginalPrice = styled.span`
165
220
  font-family: 'Press Start 2P', cursive;
166
221
  font-size: 0.625rem;
167
- color: #fef08a;
222
+ color: rgba(255, 255, 255, 0.5);
223
+ text-decoration: line-through;
224
+ `;
225
+
226
+ const PackPrice = styled.div<{ $onSale?: boolean }>`
227
+ font-family: 'Press Start 2P', cursive;
228
+ font-size: 0.75rem;
229
+ color: ${p => p.$onSale ? '#4ade80' : '#fbbf24'};
230
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
168
231
  `;
169
232
 
170
233
  const PackDescription = styled.div`
171
234
  font-family: 'Press Start 2P', cursive;
172
235
  font-size: 0.625rem;
173
- color: rgba(255, 255, 255, 0.7);
174
- line-height: 1.4;
236
+ color: rgba(255, 255, 255, 0.85);
237
+ line-height: 1.5;
238
+ margin-top: 2px;
175
239
  `;
176
240
 
177
241
  const Controls = styled.div`
178
242
  display: flex;
179
243
  align-items: center;
180
- gap: 0.5rem;
244
+ gap: 0.75rem;
181
245
  flex-shrink: 0;
182
246
  `;
183
247
 
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import styled, { css } from 'styled-components';
2
+ import styled, { css, keyframes } from 'styled-components';
3
3
 
4
4
  interface ICTAButtonProps {
5
5
  icon: React.ReactNode;
@@ -10,6 +10,7 @@ interface ICTAButtonProps {
10
10
  textColor?: string;
11
11
  iconColor?: string;
12
12
  disabled?: boolean;
13
+ pulse?: boolean;
13
14
  }
14
15
 
15
16
  export const CTAButton: React.FC<ICTAButtonProps> = ({
@@ -21,6 +22,7 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
21
22
  textColor = '#ffffff',
22
23
  iconColor = '#f59e0b',
23
24
  disabled = false,
25
+ pulse = false,
24
26
  }) => {
25
27
  return (
26
28
  <ButtonContainer
@@ -29,6 +31,7 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
29
31
  $fullWidth={fullWidth}
30
32
  $disabled={disabled}
31
33
  $color={textColor}
34
+ $pulse={pulse}
32
35
  >
33
36
  <ButtonContent>
34
37
  <IconWrapper $color={iconColor} $disabled={disabled}>
@@ -44,10 +47,24 @@ export const CTAButton: React.FC<ICTAButtonProps> = ({
44
47
  );
45
48
  };
46
49
 
50
+ const pulseAnimation = keyframes`
51
+ 0% {
52
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
53
+ }
54
+ 50% {
55
+ box-shadow: 0 0 20px rgba(245, 158, 11, 0.8), 0 0 10px rgba(245, 158, 11, 0.5) inset;
56
+ border-color: #fbbf24;
57
+ }
58
+ 100% {
59
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
60
+ }
61
+ `;
62
+
47
63
  const ButtonContainer = styled.div<{
48
64
  $fullWidth: boolean;
49
65
  $disabled: boolean;
50
66
  $color: string;
67
+ $pulse: boolean;
51
68
  }>`
52
69
  display: inline-flex;
53
70
  align-items: center;
@@ -70,6 +87,13 @@ const ButtonContainer = styled.div<{
70
87
  justify-content: center;
71
88
  `}
72
89
 
90
+ ${props =>
91
+ props.$pulse &&
92
+ !props.$disabled &&
93
+ css`
94
+ animation: ${pulseAnimation} 2s infinite ease-in-out;
95
+ `}
96
+
73
97
  &:hover {
74
98
  background: ${props =>
75
99
  props.$disabled ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.4)'};
@@ -0,0 +1,41 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ /**
4
+ * A reusable CSS mixin for a sleek, thin, 4px webkit scrollbar.
5
+ * Drops the thick native OS scrollbar arrows and housing.
6
+ */
7
+ export const customScrollbarCSS = css`
8
+ scrollbar-width: thin;
9
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
10
+
11
+ &::-webkit-scrollbar {
12
+ width: 4px;
13
+ height: 4px;
14
+ }
15
+ &::-webkit-scrollbar-track {
16
+ background: transparent;
17
+ }
18
+ &::-webkit-scrollbar-thumb {
19
+ background: rgba(255, 255, 255, 0.2);
20
+ border-radius: 4px;
21
+
22
+ &:hover {
23
+ background: rgba(255, 255, 255, 0.3);
24
+ }
25
+ }
26
+ &::-webkit-scrollbar-corner {
27
+ background: transparent;
28
+ }
29
+ &::-webkit-scrollbar-button {
30
+ display: none;
31
+ }
32
+ `;
33
+
34
+ /**
35
+ * A basic div wrapper that applies the custom scrollbar style and guarantees overflow-y.
36
+ */
37
+ export const CustomScrollbarContainer = styled.div`
38
+ ${customScrollbarCSS}
39
+ overflow-y: auto;
40
+ overflow-x: hidden;
41
+ `;
@@ -4,19 +4,33 @@ export const ItemRowWrapper = styled.div<{ $isHighlighted?: boolean }>`
4
4
  display: flex;
5
5
  align-items: center;
6
6
  justify-content: space-between;
7
- padding: 0.6rem 1rem;
8
- margin-bottom: 4px;
9
- background: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.08)' : 'rgba(0, 0, 0, 0.25)'};
10
- border: 1px solid rgba(255, 255, 255, 0.05);
11
- border-radius: 6px;
12
- border-left: 4px solid ${p => p.$isHighlighted ? '#ffd700' : 'transparent'};
13
- transition: all 0.2s ease-in-out;
7
+ padding: 0.6rem 0.8rem;
8
+ margin-bottom: 6px;
9
+ background: ${p => p.$isHighlighted ? 'linear-gradient(to right, rgba(255, 215, 0, 0.15), rgba(0, 0, 0, 0.4))' : 'linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.25))'};
10
+ border: 1px solid ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.4)' : 'rgba(255, 255, 255, 0.1)'};
11
+ border-radius: 8px;
12
+ border-left: 4px solid ${p => p.$isHighlighted ? '#fbbf24' : 'rgba(255, 255, 255, 0.2)'};
13
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.2);
14
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
15
+ position: relative;
16
+ overflow: hidden;
17
+
18
+ /* Subtle inner glow for premium feel */
19
+ &::before {
20
+ content: '';
21
+ position: absolute;
22
+ top: 0; left: 0; right: 0; bottom: 0;
23
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
24
+ border-radius: 8px;
25
+ pointer-events: none;
26
+ }
14
27
 
15
28
  &:hover {
16
- background: rgba(245, 158, 11, 0.08);
17
- border-color: rgba(245, 158, 11, 0.2);
18
- border-left-color: #f59e0b;
19
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
20
- transform: translateY(-1px);
29
+ background: ${p => p.$isHighlighted ? 'linear-gradient(to right, rgba(255, 215, 0, 0.2), rgba(0, 0, 0, 0.5))' : 'linear-gradient(to right, rgba(245, 158, 11, 0.15), rgba(0, 0, 0, 0.4))'};
30
+ border-color: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.6)' : 'rgba(245, 158, 11, 0.3)'};
31
+ border-left-color: ${p => p.$isHighlighted ? '#fcd34d' : '#f59e0b'};
32
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 4px 16px rgba(0, 0, 0, 0.4);
33
+ transform: scale(1.01) translateY(-1px);
34
+ z-index: 10;
21
35
  }
22
36
  `;
@@ -127,6 +127,7 @@ const Content = styled.div<{ $gridColumns: number; $maxHeight: string }>`
127
127
  overflow-y: auto;
128
128
  overflow-x: hidden;
129
129
 
130
+
130
131
  &.grid {
131
132
  display: grid;
132
133
  grid-template-columns: repeat(
@@ -68,7 +68,10 @@ export const SpriteFromAtlas: React.FC<IProps> = ({
68
68
  height={height}
69
69
  hasHover={grayScale}
70
70
  onPointerDown={onPointerDown}
71
- style={containerStyle}
71
+ style={{
72
+ ...(centered ? { display: 'flex', justifyContent: 'center', alignItems: 'center' } : {}),
73
+ ...containerStyle
74
+ }}
72
75
  >
73
76
  <ImgSprite
74
77
  className={`sprite-from-atlas-img ${imgClassname || ''}`}
package/src/index.tsx CHANGED
@@ -67,10 +67,15 @@ export * from './components/SocialModal/SocialModal';
67
67
  export * from './components/Spellbook/Spellbook';
68
68
  export * from './components/Stepper';
69
69
  export * from './components/Store/CartView';
70
+ export * from './components/Store/CountdownTimer';
71
+ export * from './components/Store/FeaturedBanner';
70
72
  export * from './components/Store/hooks/useStoreCart';
71
73
  export * from './components/Store/MetadataCollector';
72
- export * from './components/Store/Store';
73
74
  export * from './components/Store/PaymentMethodModal';
75
+ export * from './components/Store/PurchaseSuccess';
76
+ export * from './components/Store/Store';
77
+ export * from './components/Store/StoreBadges';
78
+ export * from './components/Store/TrustBar';
74
79
  export * from './components/Table/Table';
75
80
  export * from './components/TextArea';
76
81
  export * from './components/TimeWidget/TimeWidget';
@@ -0,0 +1,121 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { RPGUIRoot } from '../../../components/RPGUI/RPGUIRoot';
4
+ import { FeaturedBanner } from '../../../components/Store/FeaturedBanner';
5
+ import itemsAtlasJSON from '../../../mocks/atlas/items/items.json';
6
+ import itemsAtlasIMG from '../../../mocks/atlas/items/items.png';
7
+
8
+ const meta = {
9
+ title: 'Features/Store/FeaturedBanner',
10
+ component: FeaturedBanner,
11
+ parameters: { layout: 'centered' },
12
+ decorators: [
13
+ Story => (
14
+ <RPGUIRoot>
15
+ <div style={{ width: 800 }}>
16
+ <Story />
17
+ </div>
18
+ </RPGUIRoot>
19
+ ),
20
+ ],
21
+ } satisfies Meta<typeof FeaturedBanner>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof FeaturedBanner>;
25
+
26
+ const future = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(); // 2 hours from now
27
+ const nearFuture = new Date(Date.now() + 4 * 60 * 1000).toISOString(); // 4 minutes from now
28
+
29
+ export const Default: Story = {
30
+ render: () => (
31
+ <FeaturedBanner
32
+ items={[
33
+ {
34
+ key: 'starter-pack',
35
+ name: 'Starter Pack',
36
+ description: 'Everything you need to begin your adventure',
37
+ price: 4.99,
38
+ originalPrice: 9.99,
39
+ badge: 'SALE',
40
+ endsAt: future,
41
+ },
42
+ {
43
+ key: 'dragon-skin',
44
+ name: 'Dragon Skin',
45
+ description: 'Rare dragon warrior appearance',
46
+ price: 14.99,
47
+ badge: 'LIMITED',
48
+ endsAt: nearFuture,
49
+ },
50
+ {
51
+ key: 'gold-pack',
52
+ name: 'Gold Pack',
53
+ description: 'Premium content bundle',
54
+ price: 49.99,
55
+ badge: 'POPULAR',
56
+ },
57
+ ]}
58
+ atlasJSON={itemsAtlasJSON}
59
+ atlasIMG={itemsAtlasIMG}
60
+ onSelectItem={item => console.log('Selected:', item.key)}
61
+ onQuickBuy={item => console.log('Quick buy:', item.key)}
62
+ />
63
+ ),
64
+ };
65
+
66
+ export const WithAtlasSprites: Story = {
67
+ render: () => (
68
+ <FeaturedBanner
69
+ items={[
70
+ {
71
+ key: 'atlas-item',
72
+ name: 'Atlas Item',
73
+ description: 'Rendered from sprite atlas',
74
+ texturePath: 'items/greater_life_potion.png',
75
+ price: 7.99,
76
+ originalPrice: 12.99,
77
+ badge: 'NEW',
78
+ },
79
+ ]}
80
+ atlasJSON={itemsAtlasJSON}
81
+ atlasIMG={itemsAtlasIMG}
82
+ onSelectItem={item => console.log('Selected:', item.key)}
83
+ />
84
+ ),
85
+ };
86
+
87
+ export const NoQuickBuy: Story = {
88
+ render: () => (
89
+ <FeaturedBanner
90
+ items={[
91
+ { key: 'item-a', name: 'Item A', price: 5.99, badge: 'EVENT' },
92
+ { key: 'item-b', name: 'Item B', price: 9.99, originalPrice: 14.99, badge: 'SALE' },
93
+ ]}
94
+ atlasJSON={itemsAtlasJSON}
95
+ atlasIMG={itemsAtlasIMG}
96
+ onSelectItem={item => console.log('Selected:', item.key)}
97
+ />
98
+ ),
99
+ };
100
+
101
+ export const SingleItem: Story = {
102
+ render: () => (
103
+ <FeaturedBanner
104
+ items={[
105
+ {
106
+ key: 'solo',
107
+ name: 'Ultimate Bundle',
108
+ description: 'The most complete pack available — everything in one',
109
+ price: 99.99,
110
+ originalPrice: 149.99,
111
+ badge: 'LIMITED',
112
+ endsAt: future,
113
+ },
114
+ ]}
115
+ atlasJSON={itemsAtlasJSON}
116
+ atlasIMG={itemsAtlasIMG}
117
+ onSelectItem={item => console.log('Selected:', item.key)}
118
+ onQuickBuy={item => console.log('Quick buy:', item.key)}
119
+ />
120
+ ),
121
+ };