@rpg-engine/long-bow 0.8.7 → 0.8.9

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 (70) hide show
  1. package/dist/components/InformationCenter/sections/bestiary/{BestiarySection.d.ts → InformationCenterBestiarySection.d.ts} +1 -1
  2. package/dist/components/InformationCenter/sections/faq/{FaqSection.d.ts → InformationCenterFaqSection.d.ts} +1 -1
  3. package/dist/components/InformationCenter/sections/items/{ItemsSection.d.ts → InformationCenterItemsSection.d.ts} +1 -1
  4. package/dist/components/InformationCenter/sections/tutorials/{TutorialsSection.d.ts → InformationCenterTutorialsSection.d.ts} +2 -1
  5. package/dist/components/Item/Inventory/ItemPropertyColorSelector.d.ts +10 -0
  6. package/dist/components/Item/Inventory/ItemPropertySimpleHandler.d.ts +10 -0
  7. package/dist/components/Store/CartView.d.ts +15 -0
  8. package/dist/components/Store/StoreItemDetails.d.ts +16 -0
  9. package/dist/components/Store/StoreItemRow.d.ts +1 -2
  10. package/dist/components/Store/StoreTypes.d.ts +33 -4
  11. package/dist/components/Store/hooks/useStoreCart.d.ts +14 -0
  12. package/dist/components/Store/sections/StoreItemsSection.d.ts +12 -0
  13. package/dist/components/Store/sections/StorePacksSection.d.ts +9 -0
  14. package/dist/components/shared/CTAButton/CTAButton.d.ts +13 -0
  15. package/dist/components/shared/Card/Card.d.ts +14 -0
  16. package/dist/components/shared/Ellipsis.d.ts +1 -1
  17. package/dist/components/shared/PaginatedContent/PaginatedContent.d.ts +3 -1
  18. package/dist/components/shared/ScrollableContent/ScrollableContent.d.ts +23 -0
  19. package/dist/components/shared/SearchBar/SearchBar.d.ts +2 -3
  20. package/dist/components/shared/SearchHeader/SearchHeader.d.ts +17 -0
  21. package/dist/components/shared/ShoppingCart/CartCard.d.ts +14 -0
  22. package/dist/components/shared/ShoppingCart/CartCardHorizontal.d.ts +13 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/long-bow.cjs.development.js +105 -39
  25. package/dist/long-bow.cjs.development.js.map +1 -1
  26. package/dist/long-bow.cjs.production.min.js +1 -1
  27. package/dist/long-bow.cjs.production.min.js.map +1 -1
  28. package/dist/long-bow.esm.js +105 -40
  29. package/dist/long-bow.esm.js.map +1 -1
  30. package/dist/stories/UI/buttonsAndInputs/CTAButton.stories.d.ts +18 -0
  31. package/dist/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.d.ts +3 -0
  32. package/package.json +3 -2
  33. package/src/components/InformationCenter/InformationCenter.tsx +8 -8
  34. package/src/components/InformationCenter/InformationCenterTabView.tsx +0 -1
  35. package/src/components/InformationCenter/sections/bestiary/{BestiarySection.tsx → InformationCenterBestiarySection.tsx} +2 -1
  36. package/src/components/InformationCenter/sections/faq/InformationCenterFaqSection.tsx +81 -0
  37. package/src/components/InformationCenter/sections/items/{ItemsSection.tsx → InformationCenterItemsSection.tsx} +2 -10
  38. package/src/components/InformationCenter/sections/tutorials/InformationCenterTutorialsSection.tsx +135 -0
  39. package/src/components/Item/Inventory/ItemPropertyColorSelector.tsx +75 -0
  40. package/src/components/Item/Inventory/ItemPropertySimpleHandler.tsx +26 -0
  41. package/src/components/Item/Inventory/itemContainerHelper.ts +10 -1
  42. package/src/components/Store/CartView.tsx +271 -0
  43. package/src/components/Store/Store.tsx +199 -96
  44. package/src/components/Store/StoreItemDetails.tsx +161 -0
  45. package/src/components/Store/StoreItemRow.tsx +24 -40
  46. package/src/components/Store/StoreTypes.ts +38 -4
  47. package/src/components/Store/hooks/useStoreCart.ts +121 -0
  48. package/src/components/Store/sections/StoreItemsSection.tsx +52 -0
  49. package/src/components/Store/sections/StorePacksSection.tsx +89 -0
  50. package/src/components/Store/sections/images/custom-skin.png +0 -0
  51. package/src/components/shared/CTAButton/CTAButton.tsx +127 -0
  52. package/src/components/shared/Card/Card.tsx +107 -0
  53. package/src/components/shared/Ellipsis.tsx +20 -22
  54. package/src/components/shared/PaginatedContent/PaginatedContent.tsx +48 -79
  55. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +160 -0
  56. package/src/components/shared/SearchBar/SearchBar.tsx +43 -24
  57. package/src/components/shared/SearchHeader/SearchHeader.tsx +80 -0
  58. package/src/components/shared/ShoppingCart/CartCard.tsx +116 -0
  59. package/src/components/shared/ShoppingCart/CartCardHorizontal.tsx +120 -0
  60. package/src/components/shared/SpriteFromAtlas.tsx +2 -0
  61. package/src/index.tsx +1 -0
  62. package/src/stories/Features/store/Store.stories.tsx +54 -4
  63. package/src/stories/UI/buttonsAndInputs/CTAButton.stories.tsx +77 -0
  64. package/src/stories/UI/dropdownsAndSelectors/ItemPropertyColorSelector.stories.tsx +77 -0
  65. package/dist/components/Store/InternalStoreTab.d.ts +0 -15
  66. package/dist/components/Store/StoreTabContent.d.ts +0 -14
  67. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +0 -51
  68. package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +0 -144
  69. package/src/components/Store/InternalStoreTab.tsx +0 -142
  70. package/src/components/Store/StoreTabContent.tsx +0 -46
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import styled, { css } from 'styled-components';
3
+
4
+ interface ICTAButtonProps {
5
+ icon: React.ReactNode;
6
+ label?: React.ReactNode;
7
+ onClick?: (e: React.MouseEvent) => void;
8
+ className?: string;
9
+ fullWidth?: boolean;
10
+ textColor?: string;
11
+ iconColor?: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export const CTAButton: React.FC<ICTAButtonProps> = ({
16
+ icon,
17
+ label,
18
+ onClick,
19
+ className,
20
+ fullWidth = false,
21
+ textColor = '#ffffff',
22
+ iconColor = '#f59e0b',
23
+ disabled = false,
24
+ }) => {
25
+ return (
26
+ <ButtonContainer
27
+ className={className}
28
+ onPointerDown={disabled ? undefined : onClick}
29
+ $fullWidth={fullWidth}
30
+ $disabled={disabled}
31
+ $color={textColor}
32
+ >
33
+ <ButtonContent>
34
+ <IconWrapper $color={iconColor} $disabled={disabled}>
35
+ {icon}
36
+ </IconWrapper>
37
+ {label && (
38
+ <ButtonLabel $color={textColor} $disabled={disabled}>
39
+ {label}
40
+ </ButtonLabel>
41
+ )}
42
+ </ButtonContent>
43
+ </ButtonContainer>
44
+ );
45
+ };
46
+
47
+ const ButtonContainer = styled.div<{
48
+ $fullWidth: boolean;
49
+ $disabled: boolean;
50
+ $color: string;
51
+ }>`
52
+ display: inline-flex;
53
+ align-items: center;
54
+ padding: 0.75rem 1.25rem;
55
+ background: rgba(0, 0, 0, 0.3);
56
+ border: 2px solid #f59e0b;
57
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.3);
58
+ border-radius: 4px;
59
+ cursor: ${props => (props.$disabled ? 'not-allowed' : 'pointer')};
60
+ transition: all 0.2s;
61
+ position: relative;
62
+ opacity: ${props => (props.$disabled ? 0.5 : 1)};
63
+ // set color for disabled and active
64
+ color: ${props => (props.$disabled ? '#6b7280' : props.$color)};
65
+
66
+ ${props =>
67
+ props.$fullWidth &&
68
+ css`
69
+ display: flex;
70
+ justify-content: center;
71
+ `}
72
+
73
+ &:hover {
74
+ background: ${props =>
75
+ props.$disabled ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.4)'};
76
+ box-shadow: ${props =>
77
+ props.$disabled
78
+ ? '0 0 10px rgba(245, 158, 11, 0.3)'
79
+ : '0 0 15px rgba(245, 158, 11, 0.4)'};
80
+ transform: ${props => (props.$disabled ? 'none' : 'translateY(-1px)')};
81
+ }
82
+
83
+ &:active {
84
+ transform: ${props => (props.$disabled ? 'none' : 'translateY(1px)')};
85
+ box-shadow: ${props =>
86
+ props.$disabled
87
+ ? '0 0 10px rgba(245, 158, 11, 0.3)'
88
+ : '0 0 5px rgba(245, 158, 11, 0.2)'};
89
+ }
90
+
91
+ &:before {
92
+ content: '';
93
+ position: absolute;
94
+ inset: -1px;
95
+ border-radius: 5px;
96
+ padding: 1px;
97
+ background: linear-gradient(45deg, #f59e0b, #fbbf24, #f59e0b);
98
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
99
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
100
+ linear-gradient(#fff 0 0);
101
+ mask-composite: exclude;
102
+ -webkit-mask-composite: destination-out;
103
+ }
104
+ `;
105
+
106
+ const ButtonContent = styled.div`
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.75rem;
110
+ `;
111
+
112
+ const IconWrapper = styled.div<{ $color: string; $disabled: boolean }>`
113
+ svg {
114
+ font-size: 1.25rem;
115
+ color: ${props => props.$color};
116
+ filter: drop-shadow(0 0 2px rgba(245, 158, 11, 0.5));
117
+ opacity: ${props => (props.$disabled ? 0.5 : 1)};
118
+ }
119
+ `;
120
+
121
+ const ButtonLabel = styled.span<{ $color: string; $disabled: boolean }>`
122
+ color: ${props => props.$color};
123
+ font-family: 'Press Start 2P', cursive;
124
+ font-size: 0.875rem;
125
+ text-shadow: 0 0 4px rgba(245, 158, 11, 0.5);
126
+ opacity: ${props => (props.$disabled ? 0.5 : 1)};
127
+ `;
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { uiColors } from '../../../constants/uiColors';
4
+
5
+ export interface ICardProps {
6
+ title: string;
7
+ description: string;
8
+ imageUrl?: string | { src: string; default?: string };
9
+ category?: string;
10
+ onClick?: () => void;
11
+ footer?: React.ReactNode;
12
+ className?: string;
13
+ }
14
+
15
+ export const ShoppingCard: React.FC<ICardProps> = ({
16
+ title,
17
+ description,
18
+ imageUrl,
19
+ category,
20
+ onClick,
21
+ footer,
22
+ className,
23
+ }) => {
24
+ const getImageSrc = () => {
25
+ if (!imageUrl) return '/placeholder-thumbnail.png';
26
+ if (typeof imageUrl === 'string') return imageUrl;
27
+ return imageUrl.default || imageUrl.src;
28
+ };
29
+
30
+ return (
31
+ <CardContainer onClick={onClick} className={className}>
32
+ {imageUrl && (
33
+ <CardThumbnail>
34
+ <img src={getImageSrc()} alt={title} />
35
+ </CardThumbnail>
36
+ )}
37
+ <CardContent>
38
+ <CardTitle>{title}</CardTitle>
39
+ <CardDescription>{description}</CardDescription>
40
+ {category && <CardCategory>{category}</CardCategory>}
41
+ {footer && <CardFooter>{footer}</CardFooter>}
42
+ </CardContent>
43
+ </CardContainer>
44
+ );
45
+ };
46
+
47
+ const CardContainer = styled.div<{ onClick?: () => void }>`
48
+ background: rgba(0, 0, 0, 0.3);
49
+ border-radius: 4px;
50
+ overflow: hidden;
51
+ border: 1px solid ${uiColors.darkGray};
52
+ cursor: ${props => (props.onClick ? 'pointer' : 'default')};
53
+ transition: transform 0.2s ease;
54
+
55
+ &:hover {
56
+ transform: ${props => (props.onClick ? 'translateY(-2px)' : 'none')};
57
+ }
58
+ `;
59
+
60
+ const CardThumbnail = styled.div`
61
+ width: 100%;
62
+ height: 168px;
63
+ background: rgba(0, 0, 0, 0.2);
64
+ overflow: hidden;
65
+
66
+ img {
67
+ width: 100%;
68
+ height: 100%;
69
+ object-fit: cover;
70
+ }
71
+ `;
72
+
73
+ const CardContent = styled.div`
74
+ padding: 12px;
75
+ `;
76
+
77
+ const CardTitle = styled.h3`
78
+ margin: 0;
79
+ font-size: 0.6rem;
80
+ color: ${uiColors.yellow};
81
+ font-family: 'Press Start 2P', cursive;
82
+ margin-bottom: 8px;
83
+ `;
84
+
85
+ const CardDescription = styled.p`
86
+ margin: 0;
87
+ font-size: 0.55rem;
88
+ color: ${uiColors.lightGray};
89
+ font-family: 'Press Start 2P', cursive;
90
+ margin-bottom: 8px;
91
+ line-height: 1.4;
92
+ `;
93
+
94
+ const CardCategory = styled.span`
95
+ font-size: 0.5rem;
96
+ color: ${uiColors.yellow};
97
+ font-family: 'Press Start 2P', cursive;
98
+ background: rgba(255, 255, 255, 0.1);
99
+ padding: 4px 8px;
100
+ border-radius: 4px;
101
+ `;
102
+
103
+ const CardFooter = styled.div`
104
+ margin-top: 12px;
105
+ padding-top: 12px;
106
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
107
+ `;
@@ -3,7 +3,7 @@ import styled from 'styled-components';
3
3
 
4
4
  interface IProps {
5
5
  children: React.ReactNode;
6
- maxLines?: 1 | 2 | 3;
6
+ maxLines?: 1 | 2 | 3 | 4 | 5;
7
7
  maxWidth: string;
8
8
  fontSize?: string;
9
9
  center?: boolean;
@@ -40,37 +40,35 @@ const Container = styled.span<IContainerProps>`
40
40
  overflow: hidden;
41
41
  max-width: ${props => props.maxWidth};
42
42
  font-size: ${props => props.fontSize};
43
-
44
43
  ${props => props.center && `margin: 0 auto;`}
45
44
  }
46
45
 
47
- .ellipsis-2-lines {
48
- display: block;
46
+ .ellipsis-2-lines,
47
+ .ellipsis-3-lines,
48
+ .ellipsis-4-lines,
49
+ .ellipsis-5-lines {
49
50
  display: -webkit-box;
50
- max-width: ${props => props.maxWidth}px;
51
-
52
- height: 25px;
53
- margin: 0 auto;
54
- line-height: 1;
55
- -webkit-line-clamp: 2;
56
51
  -webkit-box-orient: vertical;
57
- text-overflow: ellipsis;
58
52
  overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ max-width: ${props => props.maxWidth};
59
55
  font-size: ${props => props.fontSize};
56
+ line-height: 1.4;
60
57
  }
61
58
 
62
- .ellipsis-3-lines {
63
- display: block;
64
- display: -webkit-box;
65
- max-width: ${props => props.maxWidth}px;
59
+ .ellipsis-2-lines {
60
+ -webkit-line-clamp: 2;
61
+ }
66
62
 
67
- height: 43px;
68
- margin: 0 auto;
69
- line-height: 1;
63
+ .ellipsis-3-lines {
70
64
  -webkit-line-clamp: 3;
71
- -webkit-box-orient: vertical;
72
- text-overflow: ellipsis;
73
- overflow: hidden;
74
- font-size: ${props => props.fontSize};
65
+ }
66
+
67
+ .ellipsis-4-lines {
68
+ -webkit-line-clamp: 4;
69
+ }
70
+
71
+ .ellipsis-5-lines {
72
+ -webkit-line-clamp: 5;
75
73
  }
76
74
  `;
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { usePagination } from '../../CraftBook/hooks/usePagination';
4
- import { Dropdown, IOptionsProps } from '../../Dropdown';
4
+ import { IOptionsProps } from '../../Dropdown';
5
5
  import { Pagination } from '../Pagination/Pagination';
6
- import { SearchBar } from '../SearchBar/SearchBar';
6
+ import { SearchHeader } from '../SearchHeader/SearchHeader';
7
7
 
8
8
  interface IPaginatedContentProps<T> {
9
9
  items: T[];
@@ -24,6 +24,8 @@ interface IPaginatedContentProps<T> {
24
24
  dependencies?: any[];
25
25
  tabId?: string;
26
26
  layout?: 'grid' | 'list';
27
+ gridColumns?: number;
28
+ itemHeight?: string;
27
29
  }
28
30
 
29
31
  export const PaginatedContent = <T extends unknown>({
@@ -37,6 +39,8 @@ export const PaginatedContent = <T extends unknown>({
37
39
  dependencies = [],
38
40
  tabId,
39
41
  layout = 'list',
42
+ gridColumns = 4,
43
+ itemHeight = '180px',
40
44
  }: IPaginatedContentProps<T>): React.ReactElement => {
41
45
  const {
42
46
  currentPage,
@@ -49,50 +53,38 @@ export const PaginatedContent = <T extends unknown>({
49
53
  dependencies: [...dependencies, tabId],
50
54
  });
51
55
 
52
- if (items.length === 0) {
53
- return <EmptyMessage>{emptyMessage}</EmptyMessage>;
54
- }
55
-
56
56
  return (
57
57
  <Container className={className}>
58
58
  {(searchOptions || filterOptions) && (
59
- <HeaderContainer>
60
- <HeaderContent>
61
- {searchOptions && (
62
- <SearchContainer>
63
- <StyledSearchBar
64
- value={searchOptions.value}
65
- onChange={searchOptions.onChange}
66
- placeholder={searchOptions.placeholder || 'Search...'}
67
- />
68
- </SearchContainer>
69
- )}
70
- {filterOptions && (
71
- <FilterContainer>
72
- <StyledDropdown
73
- options={filterOptions.options}
74
- onChange={filterOptions.onOptionChange}
75
- width="200px"
76
- />
77
- </FilterContainer>
78
- )}
79
- </HeaderContent>
80
- </HeaderContainer>
81
- )}
82
- <Content className={`PaginatedContent-content ${layout}`}>
83
- {paginatedItems.map((item, index) => (
84
- <div key={index} className="PaginatedContent-item">
85
- {renderItem(item)}
86
- </div>
87
- ))}
88
- </Content>
89
- <PaginationContainer className="PaginatedContent-pagination">
90
- <Pagination
91
- currentPage={currentPage}
92
- totalPages={totalPages}
93
- onPageChange={setCurrentPage}
59
+ <SearchHeader
60
+ searchOptions={searchOptions}
61
+ filterOptions={filterOptions}
94
62
  />
95
- </PaginationContainer>
63
+ )}
64
+ {items.length === 0 ? (
65
+ <EmptyMessage>{emptyMessage}</EmptyMessage>
66
+ ) : (
67
+ <>
68
+ <Content
69
+ className={`PaginatedContent-content ${layout}`}
70
+ $gridColumns={gridColumns}
71
+ $itemHeight={itemHeight}
72
+ >
73
+ {paginatedItems.map((item, index) => (
74
+ <div key={index} className="PaginatedContent-item">
75
+ {renderItem(item)}
76
+ </div>
77
+ ))}
78
+ </Content>
79
+ <PaginationContainer className="PaginatedContent-pagination">
80
+ <Pagination
81
+ currentPage={currentPage}
82
+ totalPages={totalPages}
83
+ onPageChange={setCurrentPage}
84
+ />
85
+ </PaginationContainer>
86
+ </>
87
+ )}
96
88
  </Container>
97
89
  );
98
90
  };
@@ -105,52 +97,33 @@ const Container = styled.div`
105
97
  width: 100%;
106
98
  `;
107
99
 
108
- const HeaderContainer = styled.div`
109
- padding: 0 1rem;
110
- `;
111
-
112
- const HeaderContent = styled.div`
113
- display: flex;
114
- justify-content: space-between;
115
- align-items: center;
116
- gap: 1rem;
117
- background: rgba(0, 0, 0, 0.2);
118
- padding: 1rem;
119
- border-radius: 4px;
120
- `;
121
-
122
- const SearchContainer = styled.div`
123
- flex: 1;
124
- `;
125
-
126
- const FilterContainer = styled.div`
127
- display: flex;
128
- justify-content: flex-end;
129
- `;
130
-
131
- const StyledSearchBar = styled(SearchBar)`
132
- width: 100%;
133
- `;
134
-
135
- const Content = styled.div`
100
+ const Content = styled.div<{
101
+ $gridColumns: number;
102
+ $maxHeight?: string;
103
+ $itemHeight?: string;
104
+ }>`
136
105
  display: flex;
137
106
  flex-direction: column;
138
107
  gap: 0.5rem;
139
108
  flex: 1;
140
109
  padding: 1rem;
141
110
  min-height: 200px;
142
- overflow-y: auto;
111
+ max-height: ${props => props.$maxHeight};
112
+ overflow-y: ${props => (props.$maxHeight ? 'auto' : 'visible')};
143
113
 
144
114
  &.grid {
145
115
  display: grid;
146
- grid-template-columns: repeat(4, minmax(0, 1fr));
116
+ grid-template-columns: repeat(
117
+ ${props => props.$gridColumns},
118
+ minmax(0, 1fr)
119
+ );
147
120
  gap: 1rem;
121
+ align-items: start;
148
122
 
149
123
  .PaginatedContent-item {
150
- aspect-ratio: 1;
151
124
  display: flex;
152
- align-items: center;
153
- justify-content: center;
125
+ align-items: flex-start;
126
+ height: ${props => props.$itemHeight ?? 'auto'};
154
127
  }
155
128
  }
156
129
 
@@ -176,7 +149,3 @@ const EmptyMessage = styled.div`
176
149
  align-items: center;
177
150
  justify-content: center;
178
151
  `;
179
-
180
- const StyledDropdown = styled(Dropdown)`
181
- min-width: 150px;
182
- `;
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { Dropdown, IOptionsProps } from '../../Dropdown';
4
+ import { SearchBar } from '../SearchBar/SearchBar';
5
+
6
+ interface IScrollableContentProps<T> {
7
+ items: T[];
8
+ renderItem: (item: T) => React.ReactNode;
9
+ emptyMessage?: string;
10
+ className?: string;
11
+ filterOptions?: {
12
+ options: IOptionsProps[];
13
+ selectedOption: string;
14
+ onOptionChange: (value: string) => void;
15
+ };
16
+ searchOptions?: {
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ placeholder?: string;
20
+ };
21
+ layout?: 'grid' | 'list';
22
+ gridColumns?: number;
23
+ maxHeight?: string;
24
+ }
25
+
26
+ export const ScrollableContent = <T extends unknown>({
27
+ items,
28
+ renderItem,
29
+ emptyMessage = 'No items found',
30
+ className,
31
+ filterOptions,
32
+ searchOptions,
33
+ layout = 'list',
34
+ gridColumns = 4,
35
+ maxHeight = '500px',
36
+ }: IScrollableContentProps<T>): React.ReactElement => {
37
+ if (items.length === 0) {
38
+ return <EmptyMessage>{emptyMessage}</EmptyMessage>;
39
+ }
40
+
41
+ return (
42
+ <Container className={className}>
43
+ {(searchOptions || filterOptions) && (
44
+ <HeaderContainer>
45
+ <HeaderContent>
46
+ {searchOptions && (
47
+ <SearchContainer>
48
+ <StyledSearchBar
49
+ value={searchOptions.value}
50
+ onChange={searchOptions.onChange}
51
+ placeholder={searchOptions.placeholder || 'Search...'}
52
+ />
53
+ </SearchContainer>
54
+ )}
55
+ {filterOptions && (
56
+ <FilterContainer>
57
+ <StyledDropdown
58
+ options={filterOptions.options}
59
+ onChange={filterOptions.onOptionChange}
60
+ width="200px"
61
+ />
62
+ </FilterContainer>
63
+ )}
64
+ </HeaderContent>
65
+ </HeaderContainer>
66
+ )}
67
+ <Content
68
+ className={`ScrollableContent-content ${layout}`}
69
+ $gridColumns={gridColumns}
70
+ $maxHeight={maxHeight}
71
+ >
72
+ {items.map((item, index) => (
73
+ <div key={index} className="ScrollableContent-item">
74
+ {renderItem(item)}
75
+ </div>
76
+ ))}
77
+ </Content>
78
+ </Container>
79
+ );
80
+ };
81
+
82
+ const Container = styled.div`
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 1rem;
86
+ width: 100%;
87
+ `;
88
+
89
+ const HeaderContainer = styled.div`
90
+ flex-shrink: 0;
91
+ `;
92
+
93
+ const HeaderContent = styled.div`
94
+ display: flex;
95
+ justify-content: space-between;
96
+ align-items: center;
97
+ gap: 1rem;
98
+ background: rgba(0, 0, 0, 0.2);
99
+ padding: 1rem;
100
+ border-radius: 4px;
101
+ `;
102
+
103
+ const SearchContainer = styled.div`
104
+ flex: 1;
105
+ `;
106
+
107
+ const FilterContainer = styled.div`
108
+ display: flex;
109
+ justify-content: flex-end;
110
+ `;
111
+
112
+ const StyledSearchBar = styled(SearchBar)`
113
+ width: 100%;
114
+ `;
115
+
116
+ const StyledDropdown = styled(Dropdown)`
117
+ min-width: 150px;
118
+ `;
119
+
120
+ const Content = styled.div<{ $gridColumns: number; $maxHeight: string }>`
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 0.5rem;
124
+ flex: 1;
125
+ padding: 1rem;
126
+ max-height: ${props => props.$maxHeight};
127
+ overflow-y: auto;
128
+ overflow-x: hidden;
129
+
130
+ &.grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(
133
+ ${props => props.$gridColumns},
134
+ minmax(0, 1fr)
135
+ );
136
+ gap: 1rem;
137
+ align-items: start;
138
+
139
+ .ScrollableContent-item {
140
+ display: flex;
141
+ align-items: flex-start;
142
+ }
143
+ }
144
+
145
+ &.list {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 0.5rem;
149
+ }
150
+ `;
151
+
152
+ const EmptyMessage = styled.div`
153
+ text-align: center;
154
+ color: #9ca3af;
155
+ padding: 2rem;
156
+ flex: 1;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ `;