property-practice-ui 0.1.4 → 0.2.0
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/CHANGELOG.md +12 -0
- package/dist/{Textarea-u7ONHI5M.d.cts → Textarea-BNvLsAXP.d.cts} +1 -10
- package/dist/{Textarea-u7ONHI5M.d.ts → Textarea-BNvLsAXP.d.ts} +1 -10
- package/dist/atoms.cjs +5 -3
- package/dist/atoms.cjs.map +1 -1
- package/dist/atoms.d.cts +12 -3
- package/dist/atoms.d.ts +12 -3
- package/dist/atoms.js +5 -3
- package/dist/atoms.js.map +1 -1
- package/dist/index.cjs +794 -300
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -17
- package/dist/index.d.ts +34 -17
- package/dist/index.js +790 -296
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/atoms/Chip/Chip.tsx +29 -0
- package/src/atoms/SecondaryInput/SecondaryInput.tsx +5 -3
- package/src/molecules/Address/Address.tsx +113 -26
- package/src/molecules/CTAContainer/CTAContainer.tsx +28 -2
- package/src/molecules/ContentCard/ContentCard.tsx +5 -0
- package/src/molecules/ExtendedButton/ExtendedButton.stories.tsx +31 -0
- package/src/molecules/ExtendedButton/ExtendedButton.tsx +29 -0
- package/src/molecules/ProductInfo/ProductInfo.tsx +18 -0
- package/src/molecules/SideNav/SideNav.stories.tsx +161 -11
- package/src/molecules/SideNav/SideNav.tsx +219 -44
- package/src/organism/ContactForm/ContactForm.tsx +13 -2
- package/src/organism/FeatureCarousel/FeatureCarousel.tsx +130 -45
- package/src/organism/Footer/Footer.tsx +2 -2
- package/src/organism/Header/Header.tsx +70 -0
- package/src/templates/AboutUs/AboutUs.tsx +2 -2
- package/src/templates/Contact/Contact.tsx +21 -6
- package/src/templates/FAQ/FAQ.tsx +0 -1
- package/src/templates/Features/Features.tsx +26 -18
- package/src/templates/Hero/Hero.tsx +34 -8
- package/src/templates/OtherProducts/OtherProducts.tsx +73 -22
- package/src/tokens/colors.ts +1 -0
|
@@ -1,18 +1,85 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
|
-
import { TextButton } from "../../atoms";
|
|
2
|
+
import { ExtendedButton, TextButton } from "../../atoms";
|
|
3
3
|
import { colors } from '../../tokens/colors';
|
|
4
4
|
|
|
5
5
|
type BorderColor = keyof typeof colors.border;
|
|
6
|
+
type BgColor = keyof typeof colors.background;
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const Backdrop = styled.div<{ $isOpen: boolean }>`
|
|
9
|
+
display: none;
|
|
10
|
+
|
|
11
|
+
@media (max-width: 767px) {
|
|
12
|
+
display: ${props => props.$isOpen ? 'block' : 'none'};
|
|
13
|
+
position: fixed;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
right: 0;
|
|
17
|
+
bottom: 0;
|
|
18
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
19
|
+
z-index: 9998;
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const BackToTopWrapper = styled.div`
|
|
24
|
+
@media (max-width: 767px) {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const StyledNav = styled.nav<{
|
|
30
|
+
$isOpen: boolean;
|
|
31
|
+
$mobileBgVariant: BgColor;
|
|
32
|
+
$borderVariant: BorderColor;
|
|
33
|
+
}>`
|
|
8
34
|
width: 195px;
|
|
9
|
-
background-color: #ffffff;
|
|
10
|
-
border-right: 1px solid #e5e7eb;
|
|
11
35
|
display: flex;
|
|
12
36
|
flex-direction: column;
|
|
13
37
|
min-height: 500px;
|
|
14
38
|
height: 100vh;
|
|
15
39
|
padding-bottom: 15px;
|
|
40
|
+
|
|
41
|
+
@media (min-width: 768px) {
|
|
42
|
+
position: relative;
|
|
43
|
+
background-color: #ffffff;
|
|
44
|
+
border-right: 1px solid #e5e7eb;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (max-width: 767px) {
|
|
48
|
+
position: fixed;
|
|
49
|
+
top: 0;
|
|
50
|
+
left: 0;
|
|
51
|
+
width: 100%;
|
|
52
|
+
z-index: 9999;
|
|
53
|
+
background-color: ${props => colors.background[props.$mobileBgVariant]};
|
|
54
|
+
border-right: 1px solid ${props => colors.border[props.$borderVariant]};
|
|
55
|
+
transform: translateX(${props => props.$isOpen ? '0' : '-100%'});
|
|
56
|
+
transition: transform 0.3s ease-in-out;
|
|
57
|
+
box-shadow: ${props => props.$isOpen ? '2px 0 8px rgba(0, 0, 0, 0.15)' : 'none'};
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const CloseButton = styled.button<{ $textVariant: BorderColor }>`
|
|
62
|
+
background: none;
|
|
63
|
+
border: none;
|
|
64
|
+
font-size: 1.5rem;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
padding: 0.5rem;
|
|
67
|
+
color: ${props => colors.border[props.$textVariant]};
|
|
68
|
+
|
|
69
|
+
&:hover {
|
|
70
|
+
opacity: 0.7;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const MobileHeader = styled.div`
|
|
75
|
+
display: none;
|
|
76
|
+
|
|
77
|
+
@media (max-width: 767px) {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
align-items: center;
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
}
|
|
16
83
|
`;
|
|
17
84
|
|
|
18
85
|
const NavList = styled.ul`
|
|
@@ -23,6 +90,20 @@ const NavList = styled.ul`
|
|
|
23
90
|
padding: 0;
|
|
24
91
|
`;
|
|
25
92
|
|
|
93
|
+
const DesktopNavList = styled(NavList)`
|
|
94
|
+
@media (max-width: 767px) {
|
|
95
|
+
display: none;
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const MobileNavList = styled(NavList)`
|
|
100
|
+
display: none;
|
|
101
|
+
|
|
102
|
+
@media (max-width: 767px) {
|
|
103
|
+
display: flex;
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
26
107
|
const NavItem = styled.li`
|
|
27
108
|
display: flex;
|
|
28
109
|
`;
|
|
@@ -67,15 +148,51 @@ const Spacer = styled.div`
|
|
|
67
148
|
flex-grow: 1;
|
|
68
149
|
`;
|
|
69
150
|
|
|
70
|
-
|
|
151
|
+
const DesktopOnlySpacer = styled(Spacer)`
|
|
152
|
+
@media (max-width: 767px) {
|
|
153
|
+
display: none;
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const MobileButtonsWrapper = styled.div`
|
|
158
|
+
display: none;
|
|
159
|
+
|
|
160
|
+
@media (max-width: 767px) {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: row;
|
|
163
|
+
gap: 0.5rem;
|
|
164
|
+
padding: 0 1rem;
|
|
165
|
+
margin-top: auto;
|
|
166
|
+
margin-bottom: 20px;
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
interface NavItemData {
|
|
71
171
|
label: string;
|
|
72
172
|
href: string;
|
|
73
173
|
}
|
|
74
174
|
|
|
75
175
|
interface SideNavProps {
|
|
76
|
-
|
|
176
|
+
logo?: React.ReactNode;
|
|
177
|
+
items: NavItemData[];
|
|
77
178
|
activeItem?: string;
|
|
78
179
|
onBackToTop?: () => void;
|
|
180
|
+
isOpen?: boolean;
|
|
181
|
+
onClose?: () => void;
|
|
182
|
+
mobileButtons?: Array<{
|
|
183
|
+
text: string;
|
|
184
|
+
onClick: () => void;
|
|
185
|
+
bgVariant?: 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'blue' | 'brand' | 'light' | 'transparent';
|
|
186
|
+
textVariant?: 'brand' | 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'light' | 'error' | 'blue';
|
|
187
|
+
arrowButton?: 'blue'
|
|
188
|
+
}>;
|
|
189
|
+
mobileBgVariant?: BgColor;
|
|
190
|
+
borderVariant?: BorderColor;
|
|
191
|
+
closeButtonVariant?: BorderColor;
|
|
192
|
+
mobileActiveTextBgVariant?: 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'blue' | 'brand' | 'light' | 'transparent';
|
|
193
|
+
mobileActiveTextVariant?: 'brand' | 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'light' | 'error' | 'blue';
|
|
194
|
+
mobileInactiveTextBgVariant?: 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'blue' | 'brand' | 'light' | 'transparent';
|
|
195
|
+
mobileInactiveTextVariant?: 'brand' | 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'light' | 'error' | 'blue';
|
|
79
196
|
triangleColor?: BorderColor;
|
|
80
197
|
dividerColor?: BorderColor;
|
|
81
198
|
activeTextBgVariant?: 'primary' | 'secondary' | 'tertiary' | 'subtle' | 'blue' | 'brand' | 'light' | 'transparent';
|
|
@@ -87,11 +204,22 @@ interface SideNavProps {
|
|
|
87
204
|
}
|
|
88
205
|
|
|
89
206
|
export const SideNav = ({
|
|
207
|
+
logo,
|
|
90
208
|
items,
|
|
91
|
-
activeItem,
|
|
92
|
-
onBackToTop,
|
|
209
|
+
activeItem,
|
|
210
|
+
onBackToTop,
|
|
211
|
+
isOpen = true,
|
|
212
|
+
onClose,
|
|
213
|
+
mobileButtons = [],
|
|
214
|
+
mobileBgVariant = 'tertiary',
|
|
215
|
+
borderVariant = 'primary',
|
|
216
|
+
closeButtonVariant = 'light',
|
|
217
|
+
mobileActiveTextBgVariant = 'transparent',
|
|
218
|
+
mobileActiveTextVariant = 'light',
|
|
219
|
+
mobileInactiveTextBgVariant = 'transparent',
|
|
220
|
+
mobileInactiveTextVariant= 'light',
|
|
93
221
|
triangleColor = 'primary',
|
|
94
|
-
dividerColor = '
|
|
222
|
+
dividerColor = 'light',
|
|
95
223
|
activeTextBgVariant = 'transparent',
|
|
96
224
|
activeTextVariant = 'brand',
|
|
97
225
|
inactiveTextBgVariant = 'transparent',
|
|
@@ -99,42 +227,89 @@ export const SideNav = ({
|
|
|
99
227
|
backToTopBgVariant = 'transparent',
|
|
100
228
|
backToTopTextVariant = 'subtle',
|
|
101
229
|
}: SideNavProps) => {
|
|
230
|
+
const renderNavItems = (isMobile: boolean) => items.map((item) => {
|
|
231
|
+
const isActive = activeItem === item.href;
|
|
232
|
+
return (
|
|
233
|
+
<NavItem key={item.href}>
|
|
234
|
+
<NavLink href={item.href} $isActive={isActive}>
|
|
235
|
+
{isActive ? <ActiveTriangle triangleColor={triangleColor} /> : <InactiveSpacer />}
|
|
236
|
+
<TextButton
|
|
237
|
+
asChild
|
|
238
|
+
text={item.label}
|
|
239
|
+
bgVariant={isActive
|
|
240
|
+
? (isMobile && mobileActiveTextBgVariant ? mobileActiveTextBgVariant : activeTextBgVariant)
|
|
241
|
+
: (isMobile && mobileInactiveTextBgVariant ? mobileInactiveTextBgVariant : inactiveTextBgVariant)
|
|
242
|
+
}
|
|
243
|
+
textVariant={isActive
|
|
244
|
+
? (isMobile && mobileActiveTextVariant ? mobileActiveTextVariant : activeTextVariant)
|
|
245
|
+
: (isMobile && mobileInactiveTextVariant ? mobileInactiveTextVariant : inactiveTextVariant)
|
|
246
|
+
}
|
|
247
|
+
uppercase={false}
|
|
248
|
+
bold={isActive}
|
|
249
|
+
/>
|
|
250
|
+
</NavLink>
|
|
251
|
+
</NavItem>
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
102
255
|
return (
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
256
|
+
<>
|
|
257
|
+
<Backdrop $isOpen={isOpen} onClick={onClose} />
|
|
258
|
+
<StyledNav
|
|
259
|
+
$isOpen={isOpen}
|
|
260
|
+
$mobileBgVariant={mobileBgVariant}
|
|
261
|
+
$borderVariant={borderVariant}
|
|
262
|
+
>
|
|
263
|
+
<MobileHeader>
|
|
264
|
+
{logo}
|
|
265
|
+
<CloseButton onClick={onClose} aria-label="Close menu" $textVariant={closeButtonVariant}>
|
|
266
|
+
✕
|
|
267
|
+
</CloseButton>
|
|
268
|
+
</MobileHeader>
|
|
269
|
+
|
|
270
|
+
<DesktopOnlySpacer />
|
|
271
|
+
|
|
272
|
+
{/* Desktop nav */}
|
|
273
|
+
<DesktopNavList>
|
|
274
|
+
{renderNavItems(false)}
|
|
275
|
+
</DesktopNavList>
|
|
276
|
+
|
|
277
|
+
{/* Mobile nav */}
|
|
278
|
+
<MobileNavList>
|
|
279
|
+
{renderNavItems(true)}
|
|
280
|
+
</MobileNavList>
|
|
281
|
+
|
|
282
|
+
<Divider dividerColor={dividerColor} />
|
|
283
|
+
<Spacer />
|
|
284
|
+
|
|
285
|
+
{mobileButtons.length > 0 && (
|
|
286
|
+
<>
|
|
287
|
+
<Divider dividerColor={dividerColor} />
|
|
288
|
+
<MobileButtonsWrapper>
|
|
289
|
+
{mobileButtons.map((button, index) => (
|
|
290
|
+
<ExtendedButton
|
|
291
|
+
key={index}
|
|
292
|
+
text={button.text}
|
|
293
|
+
onClick={button.onClick}
|
|
294
|
+
textBgVariant={button.bgVariant || 'transparent'}
|
|
295
|
+
textVariant={button.textVariant || 'subtle'}
|
|
296
|
+
arrowVariant={button.arrowButton || 'blue'}
|
|
120
297
|
/>
|
|
121
|
-
|
|
122
|
-
</
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
/>
|
|
138
|
-
</StyledNav>
|
|
298
|
+
))}
|
|
299
|
+
</MobileButtonsWrapper>
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
<BackToTopWrapper>
|
|
304
|
+
<TextButton
|
|
305
|
+
text="Back to top"
|
|
306
|
+
onClick={onBackToTop}
|
|
307
|
+
bgVariant={backToTopBgVariant}
|
|
308
|
+
textVariant={backToTopTextVariant}
|
|
309
|
+
uppercase={false}
|
|
310
|
+
/>
|
|
311
|
+
</BackToTopWrapper>
|
|
312
|
+
</StyledNav>
|
|
313
|
+
</>
|
|
139
314
|
);
|
|
140
315
|
};
|
|
@@ -9,17 +9,28 @@ const Container = styled.div`
|
|
|
9
9
|
display: flex;
|
|
10
10
|
flex-direction: column;
|
|
11
11
|
gap: ${spaces[4]};
|
|
12
|
+
width: 100%;
|
|
13
|
+
|
|
12
14
|
@media (max-width: ${breakpoints.sm}) {
|
|
13
15
|
gap: ${spaces[2]};
|
|
14
16
|
}
|
|
15
17
|
`;
|
|
18
|
+
|
|
16
19
|
const NameContainer = styled.div`
|
|
17
20
|
display: flex;
|
|
18
21
|
gap: ${spaces[4]};
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
|
|
19
25
|
@media (max-width: ${breakpoints.sm}) {
|
|
26
|
+
flex-direction: column;
|
|
20
27
|
gap: ${spaces[2]};
|
|
21
28
|
}
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
& > * {
|
|
31
|
+
flex: 1;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
23
34
|
`;
|
|
24
35
|
|
|
25
36
|
export const ContactForm = ({}: ContactFormProps) => {
|
|
@@ -51,4 +62,4 @@ export const ContactForm = ({}: ContactFormProps) => {
|
|
|
51
62
|
/>
|
|
52
63
|
</Container>
|
|
53
64
|
);
|
|
54
|
-
};
|
|
65
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { HiArrowLeft, HiArrowRight } from 'react-icons/hi2';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import { ContentCard } from '../../molecules';
|
|
@@ -7,26 +7,56 @@ import { colors } from '../../tokens/colors';
|
|
|
7
7
|
type TextColor = keyof typeof colors.text;
|
|
8
8
|
type BackgroundColor = keyof typeof colors.background;
|
|
9
9
|
|
|
10
|
+
// REMOVE the useRef and handler functions from here - they were at module level
|
|
11
|
+
|
|
10
12
|
const CarouselContainer = styled.div`
|
|
11
|
-
width:
|
|
13
|
+
width: 100%;
|
|
12
14
|
overflow: hidden;
|
|
13
15
|
position: relative;
|
|
14
|
-
margin: 0 auto;
|
|
15
16
|
`;
|
|
16
17
|
|
|
17
18
|
const CardsWrapper = styled.div`
|
|
18
19
|
display: flex;
|
|
19
20
|
gap: 1.5rem;
|
|
20
|
-
overflow-x: hidden;
|
|
21
|
-
scroll-behavior: smooth;
|
|
22
21
|
padding: 1rem 0;
|
|
22
|
+
|
|
23
|
+
/* Desktop: controlled by JS */
|
|
24
|
+
@media (min-width: 768px) {
|
|
25
|
+
overflow-x: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Mobile: native scroll with snap */
|
|
29
|
+
@media (max-width: 767px) {
|
|
30
|
+
overflow-x: auto;
|
|
31
|
+
scroll-snap-type: x mandatory;
|
|
32
|
+
-webkit-overflow-scrolling: touch;
|
|
33
|
+
scrollbar-width: none;
|
|
34
|
+
|
|
35
|
+
&::-webkit-scrollbar {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
23
39
|
`;
|
|
24
40
|
|
|
25
41
|
const CardContainer = styled.div<{ $translateX: number }>`
|
|
26
42
|
display: flex;
|
|
27
43
|
gap: 1.5rem;
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
|
|
45
|
+
/* Desktop: transform animation */
|
|
46
|
+
@media (min-width: 768px) {
|
|
47
|
+
transform: translateX(${props => props.$translateX}px);
|
|
48
|
+
transition: transform 0.3s ease-in-out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Mobile: natural flow */
|
|
52
|
+
@media (max-width: 767px) {
|
|
53
|
+
transform: none;
|
|
54
|
+
|
|
55
|
+
& > * {
|
|
56
|
+
scroll-snap-align: center;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
30
60
|
`;
|
|
31
61
|
|
|
32
62
|
const NavigationWrapper = styled.div`
|
|
@@ -50,6 +80,10 @@ const NavButton = styled.button<{ $disabled?: boolean; navColor: TextColor }>`
|
|
|
50
80
|
justify-content: center;
|
|
51
81
|
transition: all 0.2s;
|
|
52
82
|
|
|
83
|
+
@media (max-width: 767px) {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
&:hover {
|
|
54
88
|
opacity: ${props => props.$disabled ? 1 : 0.8};
|
|
55
89
|
}
|
|
@@ -85,7 +119,7 @@ interface CarouselProps {
|
|
|
85
119
|
indicatorColor?: TextColor;
|
|
86
120
|
}
|
|
87
121
|
|
|
88
|
-
export const FeatureCarousel = ({
|
|
122
|
+
export const FeatureCarousel = ({
|
|
89
123
|
cards,
|
|
90
124
|
cardTitleColor = 'brand',
|
|
91
125
|
cardDescriptionVariant = 'primary',
|
|
@@ -95,25 +129,74 @@ export const FeatureCarousel = ({
|
|
|
95
129
|
indicatorColor = 'brand',
|
|
96
130
|
}: CarouselProps) => {
|
|
97
131
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
98
|
-
const
|
|
132
|
+
const [cardsToShow, setCardsToShow] = useState(3);
|
|
133
|
+
const [cardWidth, setCardWidth] = useState(284);
|
|
134
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
|
|
136
|
+
// ADD touch refs here, inside the component
|
|
137
|
+
const touchStartX = useRef(0);
|
|
138
|
+
const touchEndX = useRef(0);
|
|
139
|
+
|
|
99
140
|
const gap = 24;
|
|
100
|
-
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const observer = new ResizeObserver((entries) => {
|
|
144
|
+
const entry = entries[0];
|
|
145
|
+
if (!entry) return;
|
|
146
|
+
|
|
147
|
+
const containerWidth = entry.contentRect.width;
|
|
148
|
+
const count = Math.floor((containerWidth + gap) / (284 + gap));
|
|
149
|
+
const clamped = Math.max(1, count);
|
|
150
|
+
|
|
151
|
+
// On mobile, card width matches container
|
|
152
|
+
if (clamped === 1) {
|
|
153
|
+
setCardWidth(containerWidth);
|
|
154
|
+
} else {
|
|
155
|
+
setCardWidth(284);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setCardsToShow(clamped);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (containerRef.current) observer.observe(containerRef.current);
|
|
162
|
+
return () => observer.disconnect();
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const needsCarousel = cards.length > cardsToShow;
|
|
101
166
|
const maxIndex = Math.max(0, cards.length - cardsToShow);
|
|
167
|
+
const translateX = -currentIndex * (cardsToShow === 1 ? cardWidth : (cardWidth + gap));
|
|
102
168
|
|
|
103
|
-
const handlePrev = () =>
|
|
104
|
-
|
|
105
|
-
};
|
|
169
|
+
const handlePrev = () => setCurrentIndex(prev => Math.max(0, prev - 1));
|
|
170
|
+
const handleNext = () => setCurrentIndex(prev => Math.min(maxIndex, prev + 1));
|
|
106
171
|
|
|
107
|
-
|
|
108
|
-
|
|
172
|
+
// ADD touch handlers here, after handlePrev/handleNext
|
|
173
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
174
|
+
if (!e.touches[0]) return;
|
|
175
|
+
touchStartX.current = e.touches[0].clientX;
|
|
109
176
|
};
|
|
110
177
|
|
|
111
|
-
const
|
|
178
|
+
const handleTouchEnd = (e: React.TouchEvent) => {
|
|
179
|
+
if (!e.changedTouches[0]) return;
|
|
180
|
+
touchEndX.current = e.changedTouches[0].clientX;
|
|
181
|
+
const diff = touchStartX.current - touchEndX.current;
|
|
182
|
+
|
|
183
|
+
if (Math.abs(diff) < 50) return;
|
|
184
|
+
|
|
185
|
+
if (diff > 0) {
|
|
186
|
+
handleNext();
|
|
187
|
+
} else {
|
|
188
|
+
handlePrev();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
112
191
|
|
|
113
192
|
return (
|
|
114
|
-
<CarouselContainer
|
|
193
|
+
<CarouselContainer
|
|
194
|
+
ref={containerRef}
|
|
195
|
+
onTouchStart={handleTouchStart}
|
|
196
|
+
onTouchEnd={handleTouchEnd}
|
|
197
|
+
>
|
|
115
198
|
<CardsWrapper>
|
|
116
|
-
<CardContainer $translateX={translateX}>
|
|
199
|
+
<CardContainer $translateX={needsCarousel ? translateX : 0}>
|
|
117
200
|
{cards.map((card, index) => (
|
|
118
201
|
<ContentCard
|
|
119
202
|
key={index}
|
|
@@ -130,33 +213,35 @@ export const FeatureCarousel = ({
|
|
|
130
213
|
</CardContainer>
|
|
131
214
|
</CardsWrapper>
|
|
132
215
|
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
216
|
+
{needsCarousel && (
|
|
217
|
+
<NavigationWrapper>
|
|
218
|
+
<NavButton
|
|
219
|
+
onClick={handlePrev}
|
|
220
|
+
$disabled={currentIndex === 0}
|
|
221
|
+
navColor={navButtonColor}
|
|
222
|
+
>
|
|
223
|
+
<HiArrowLeft size={24} />
|
|
224
|
+
</NavButton>
|
|
225
|
+
|
|
226
|
+
<Indicators>
|
|
227
|
+
{Array.from({ length: maxIndex + 1 }).map((_, index) => (
|
|
228
|
+
<Indicator
|
|
229
|
+
key={index}
|
|
230
|
+
$active={index === currentIndex}
|
|
231
|
+
indicatorColor={indicatorColor}
|
|
232
|
+
/>
|
|
233
|
+
))}
|
|
234
|
+
</Indicators>
|
|
235
|
+
|
|
236
|
+
<NavButton
|
|
237
|
+
onClick={handleNext}
|
|
238
|
+
$disabled={currentIndex === maxIndex}
|
|
239
|
+
navColor={navButtonColor}
|
|
240
|
+
>
|
|
241
|
+
<HiArrowRight size={24} />
|
|
242
|
+
</NavButton>
|
|
243
|
+
</NavigationWrapper>
|
|
244
|
+
)}
|
|
160
245
|
</CarouselContainer>
|
|
161
246
|
);
|
|
162
247
|
};
|
|
@@ -126,7 +126,7 @@ interface SocialLink {
|
|
|
126
126
|
ariaLabel: string;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
interface
|
|
129
|
+
interface LegalLinkData {
|
|
130
130
|
text: string;
|
|
131
131
|
url: string;
|
|
132
132
|
}
|
|
@@ -139,7 +139,7 @@ interface FooterProps {
|
|
|
139
139
|
onSecondaryClick?: () => void;
|
|
140
140
|
addresses: AddressData[];
|
|
141
141
|
socialLinks: SocialLink[];
|
|
142
|
-
legalLinks:
|
|
142
|
+
legalLinks: LegalLinkData[];
|
|
143
143
|
copyrightText: string;
|
|
144
144
|
variant?: Variant;
|
|
145
145
|
primaryArrowVariant?: 'brand' | 'teal' | 'blue';
|