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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{Textarea-u7ONHI5M.d.cts → Textarea-BNvLsAXP.d.cts} +1 -10
  3. package/dist/{Textarea-u7ONHI5M.d.ts → Textarea-BNvLsAXP.d.ts} +1 -10
  4. package/dist/atoms.cjs +5 -3
  5. package/dist/atoms.cjs.map +1 -1
  6. package/dist/atoms.d.cts +12 -3
  7. package/dist/atoms.d.ts +12 -3
  8. package/dist/atoms.js +5 -3
  9. package/dist/atoms.js.map +1 -1
  10. package/dist/index.cjs +794 -300
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +34 -17
  13. package/dist/index.d.ts +34 -17
  14. package/dist/index.js +790 -296
  15. package/dist/index.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/atoms/Chip/Chip.tsx +29 -0
  18. package/src/atoms/SecondaryInput/SecondaryInput.tsx +5 -3
  19. package/src/molecules/Address/Address.tsx +113 -26
  20. package/src/molecules/CTAContainer/CTAContainer.tsx +28 -2
  21. package/src/molecules/ContentCard/ContentCard.tsx +5 -0
  22. package/src/molecules/ExtendedButton/ExtendedButton.stories.tsx +31 -0
  23. package/src/molecules/ExtendedButton/ExtendedButton.tsx +29 -0
  24. package/src/molecules/ProductInfo/ProductInfo.tsx +18 -0
  25. package/src/molecules/SideNav/SideNav.stories.tsx +161 -11
  26. package/src/molecules/SideNav/SideNav.tsx +219 -44
  27. package/src/organism/ContactForm/ContactForm.tsx +13 -2
  28. package/src/organism/FeatureCarousel/FeatureCarousel.tsx +130 -45
  29. package/src/organism/Footer/Footer.tsx +2 -2
  30. package/src/organism/Header/Header.tsx +70 -0
  31. package/src/templates/AboutUs/AboutUs.tsx +2 -2
  32. package/src/templates/Contact/Contact.tsx +21 -6
  33. package/src/templates/FAQ/FAQ.tsx +0 -1
  34. package/src/templates/Features/Features.tsx +26 -18
  35. package/src/templates/Hero/Hero.tsx +34 -8
  36. package/src/templates/OtherProducts/OtherProducts.tsx +73 -22
  37. 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 StyledNav = styled.nav`
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
- interface NavItem {
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
- items: NavItem[];
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 = 'primary',
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
- <StyledNav>
104
- <Spacer />
105
- <NavList>
106
- {items.map((item) => {
107
- const isActive = activeItem === item.href;
108
- return (
109
- <NavItem key={item.href}>
110
- <NavLink href={item.href} $isActive={isActive}>
111
- {isActive ? <ActiveTriangle triangleColor={triangleColor} /> : <InactiveSpacer />}
112
-
113
- <TextButton
114
- asChild
115
- text={item.label}
116
- bgVariant={isActive ? activeTextBgVariant : inactiveTextBgVariant}
117
- textVariant={isActive ? activeTextVariant : inactiveTextVariant}
118
- uppercase={false}
119
- bold={isActive}
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
- </NavLink>
122
- </NavItem>
123
- );
124
- })}
125
- </NavList>
126
-
127
- <Divider dividerColor={dividerColor} />
128
-
129
- <Spacer />
130
-
131
- <TextButton
132
- text="Back to top"
133
- onClick={onBackToTop}
134
- bgVariant={backToTopBgVariant}
135
- textVariant={backToTopTextVariant}
136
- uppercase={false}
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
- flex: 1;
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: 1000px;
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
- transform: translateX(${props => props.$translateX}px);
29
- transition: transform 0.3s ease-in-out;
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 cardWidth = 284;
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
- const cardsToShow = 3;
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
- setCurrentIndex(prev => Math.max(0, prev - 1));
105
- };
169
+ const handlePrev = () => setCurrentIndex(prev => Math.max(0, prev - 1));
170
+ const handleNext = () => setCurrentIndex(prev => Math.min(maxIndex, prev + 1));
106
171
 
107
- const handleNext = () => {
108
- setCurrentIndex(prev => Math.min(maxIndex, prev + 1));
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 translateX = -currentIndex * (cardWidth + gap);
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
- <NavigationWrapper>
134
- <NavButton
135
- onClick={handlePrev}
136
- $disabled={currentIndex === 0}
137
- navColor={navButtonColor}
138
- >
139
- <HiArrowLeft size={24} />
140
- </NavButton>
141
-
142
- <Indicators>
143
- {Array.from({ length: maxIndex + 1 }).map((_, index) => (
144
- <Indicator
145
- key={index}
146
- $active={index === currentIndex}
147
- indicatorColor={indicatorColor}
148
- />
149
- ))}
150
- </Indicators>
151
-
152
- <NavButton
153
- onClick={handleNext}
154
- $disabled={currentIndex === maxIndex}
155
- navColor={navButtonColor}
156
- >
157
- <HiArrowRight size={24} />
158
- </NavButton>
159
- </NavigationWrapper>
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 LegalLink {
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: LegalLink[];
142
+ legalLinks: LegalLinkData[];
143
143
  copyrightText: string;
144
144
  variant?: Variant;
145
145
  primaryArrowVariant?: 'brand' | 'teal' | 'blue';