property-practice-ui 0.1.4 → 0.1.5

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # property-practice-ui
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 68159b7: introduction mobile responsiveness
8
+
3
9
  ## 0.1.4
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "property-practice-ui",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,29 @@
1
+ import { useMemo } from "react";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export type ChipProps = {
5
+ text: string;
6
+ backgroundColor: 'gray' | 'navy' | 'transparent';
7
+ };
8
+
9
+ export const Chip = ({ text, backgroundColor }: ChipProps) => {
10
+ const styleMap = {
11
+ gray: 'bg-gray-500 text-white border-none',
12
+ navy: 'bg-brand-blue text-white border-none',
13
+ transparent: 'bg-transparent text-brand-blue border-gray-600',
14
+ };
15
+
16
+ const className = useMemo(
17
+ () => twMerge(
18
+ 'inline-flex items-center rounded-full px-3 py-1 text-sm font-medium border',
19
+ styleMap[backgroundColor]
20
+ ),
21
+ [backgroundColor]
22
+ );
23
+
24
+ return (
25
+ <span className={className}>
26
+ {text}
27
+ </span>
28
+ );
29
+ };
@@ -36,11 +36,12 @@ const StyledInput = styled.input<{
36
36
  $borderColor?: string;
37
37
  $focusRingColor?: Color;
38
38
  }>`
39
+ box-sizing: border-box;
39
40
  height: 2.75rem;
40
41
  width: 100%;
41
42
  border-radius: 6px;
42
43
  border: 1px solid ${props => props.$borderColor || '#d1d5db'};
43
- background-color: ${props => props.$bgColor ? colors.background[props.$bgColor] : colors.background?.light || '#f9fafb'};
44
+ background-color: ${props => props.$bgColor ? colors.background[props.$bgColor] : colors.background?.secondary || '#f9fafb'};
44
45
  padding: 0 0.75rem;
45
46
  font-size: 15px;
46
47
  color: ${props => props.$textColor ? colors.text[props.$textColor] : colors.text.primary};
@@ -65,10 +66,11 @@ const StyledTextArea = styled.textarea<{
65
66
  $borderColor?: string;
66
67
  $focusRingColor?: Color;
67
68
  }>`
69
+ box-sizing: border-box;
68
70
  width: 100%;
69
71
  border-radius: 6px;
70
72
  border: 1px solid ${props => props.$borderColor || '#d1d5db'};
71
- background-color: ${props => props.$bgColor ? colors.background[props.$bgColor] : colors.background?.light || '#f9fafb'};
73
+ background-color: ${props => props.$bgColor ? colors.background[props.$bgColor] : colors.background?.secondary || '#f9fafb'};
72
74
  padding: 0.75rem;
73
75
  font-size: 15px;
74
76
  color: ${props => props.$textColor ? colors.text[props.$textColor] : colors.text.primary};
@@ -92,7 +94,7 @@ export const SecondaryInput = ({
92
94
  labelColor = 'brand',
93
95
  inputTextColor = 'primary',
94
96
  placeholderColor = 'subtle',
95
- backgroundColor = 'light',
97
+ backgroundColor = 'secondary',
96
98
  borderColor = '#d1d5db',
97
99
  focusRingColor = 'brand',
98
100
  ...rest
@@ -1,3 +1,5 @@
1
+ import { useState } from 'react';
2
+ import { HiMinus, HiPlus } from 'react-icons/hi2';
1
3
  import styled from 'styled-components';
2
4
  import { Header, TextButton } from '../../atoms';
3
5
  import { colors } from '../../tokens/colors';
@@ -8,9 +10,81 @@ type Variant = (typeof variants)[number];
8
10
  const AddressContainer = styled.div`
9
11
  display: flex;
10
12
  flex-direction: column;
11
- gap: 1rem;
12
13
  background-color: transparent;
13
14
  padding: 2rem;
15
+
16
+ /* Desktop: normal spacing */
17
+ @media (min-width: 768px) {
18
+ gap: 1rem;
19
+ }
20
+
21
+ /* Mobile: no gap, handled by collapse */
22
+ @media (max-width: 767px) {
23
+ padding: 1rem;
24
+ gap: 0;
25
+ }
26
+ `;
27
+
28
+ const HeaderWrapper = styled.div<{ $isClickable: boolean }>`
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+
33
+ /* Desktop: not clickable */
34
+ @media (min-width: 768px) {
35
+ cursor: default;
36
+ }
37
+
38
+ /* Mobile: clickable */
39
+ @media (max-width: 767px) {
40
+ cursor: ${props => props.$isClickable ? 'pointer' : 'default'};
41
+ padding: 0.5rem 0;
42
+ user-select: none;
43
+ }
44
+ `;
45
+
46
+ const ToggleIcon = styled.div<{ variant: Variant }>`
47
+ display: none;
48
+
49
+ /* Only show on mobile */
50
+ @media (max-width: 767px) {
51
+ display: flex;
52
+ align-items: center;
53
+ font-size: 1.5rem;
54
+
55
+ color: ${(props) => {
56
+ switch (props.variant) {
57
+ case 'primary':
58
+ return colors.text.primary;
59
+ case 'inverse':
60
+ return colors.text.secondary;
61
+ case 'subtle':
62
+ return colors.text.subtle;
63
+ default:
64
+ return colors.text.primary;
65
+ }
66
+ }};
67
+ }
68
+ `;
69
+
70
+ const CollapseContent = styled.div<{ $isOpen: boolean }>`
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 1rem;
74
+
75
+ /* Desktop: always visible */
76
+ @media (min-width: 768px) {
77
+ display: flex;
78
+ }
79
+
80
+ /* Mobile: collapsible */
81
+ @media (max-width: 767px) {
82
+ max-height: ${props => props.$isOpen ? '500px' : '0'};
83
+ opacity: ${props => props.$isOpen ? '1' : '0'};
84
+ overflow: hidden;
85
+ transition: max-height 0.3s ease, opacity 0.3s ease;
86
+ padding-top: ${props => props.$isOpen ? '1rem' : '0'};
87
+ }
14
88
  `;
15
89
 
16
90
  const InfoSection = styled.div`
@@ -61,6 +135,8 @@ export const Address = ({
61
135
  onDirectionsClick,
62
136
  variant = 'primary',
63
137
  }: AddressProps) => {
138
+ const [isOpen, setIsOpen] = useState(false);
139
+
64
140
  const handleDirectionsClick = () => {
65
141
  if (onDirectionsClick) {
66
142
  onDirectionsClick();
@@ -104,31 +180,42 @@ export const Address = ({
104
180
 
105
181
  return (
106
182
  <AddressContainer>
107
- <Header variant="h1" color={getHeaderVariant()}>
108
- {title}
109
- </Header>
110
- <InfoSection>
111
- {phone && (
112
- <InfoText variant={variant}>{phone}</InfoText>
113
- )}
114
- {email && (
115
- <InfoText variant={variant} $bold>{email.toUpperCase()}</InfoText>
116
- )}
117
- {addressLines.map((line, index) => (
118
- <InfoText key={index} variant={variant}>
119
- {line}
120
- </InfoText>
121
- ))}
122
- </InfoSection>
123
- <ButtonWrapper>
124
- <TextButton
125
- text={directionsText}
126
- onClick={handleDirectionsClick}
127
- bgVariant={getButtonBgVariant()}
128
- textVariant={getButtonTextVariant()}
129
- uppercase={true}
130
- />
131
- </ButtonWrapper>
183
+ <HeaderWrapper
184
+ $isClickable={true}
185
+ onClick={() => setIsOpen(!isOpen)}
186
+ >
187
+ <Header variant="h1" color={getHeaderVariant()}>
188
+ {title}
189
+ </Header>
190
+ <ToggleIcon variant={variant}>
191
+ {isOpen ? <HiMinus /> : <HiPlus />}
192
+ </ToggleIcon>
193
+ </HeaderWrapper>
194
+
195
+ <CollapseContent $isOpen={isOpen}>
196
+ <InfoSection>
197
+ {phone && (
198
+ <InfoText variant={variant}>{phone}</InfoText>
199
+ )}
200
+ {email && (
201
+ <InfoText variant={variant} $bold>{email.toUpperCase()}</InfoText>
202
+ )}
203
+ {addressLines.map((line, index) => (
204
+ <InfoText key={index} variant={variant}>
205
+ {line}
206
+ </InfoText>
207
+ ))}
208
+ </InfoSection>
209
+ <ButtonWrapper>
210
+ <TextButton
211
+ text={directionsText}
212
+ onClick={handleDirectionsClick}
213
+ bgVariant={getButtonBgVariant()}
214
+ textVariant={getButtonTextVariant()}
215
+ uppercase={true}
216
+ />
217
+ </ButtonWrapper>
218
+ </CollapseContent>
132
219
  </AddressContainer>
133
220
  );
134
221
  };
@@ -12,8 +12,13 @@ const Container = styled.div<{ variant: Variant }>`
12
12
  flex-direction: column;
13
13
  gap: 1.5rem;
14
14
  padding: 3rem 2rem;
15
-
16
15
  max-width: 500px;
16
+
17
+ /* Mobile adjustments */
18
+ @media (max-width: 767px) {
19
+ padding: 2rem 1rem;
20
+ max-width: 100%;
21
+ }
17
22
  `;
18
23
 
19
24
  const Header = styled.h1<{ textColor: TextColor }>`
@@ -22,6 +27,15 @@ const Header = styled.h1<{ textColor: TextColor }>`
22
27
  font-weight: 700;
23
28
  line-height: 1.2;
24
29
  color: ${(props) => colors.text[props.textColor]};
30
+
31
+ /* Responsive font size */
32
+ @media (max-width: 767px) {
33
+ font-size: 2rem;
34
+ }
35
+
36
+ @media (max-width: 480px) {
37
+ font-size: 1.75rem;
38
+ }
25
39
  `;
26
40
 
27
41
  const Description = styled.p<{ textColor: TextColor }>`
@@ -29,19 +43,31 @@ const Description = styled.p<{ textColor: TextColor }>`
29
43
  font-size: 1.125rem;
30
44
  line-height: 1.6;
31
45
  color: ${(props) => colors.text[props.textColor]};
46
+
47
+ /* Responsive font size */
48
+ @media (max-width: 767px) {
49
+ font-size: 1rem;
50
+ }
32
51
  `;
33
52
 
34
53
  const ButtonGroup = styled.div`
35
54
  display: flex;
36
55
  gap: 1rem;
37
56
  align-items: center;
57
+
58
+ /* Stack vertically on mobile */
59
+ @media (max-width: 767px) {
60
+ flex-direction: column;
61
+ align-items: stretch;
62
+ width: 100%;
63
+ }
38
64
  `;
39
65
 
40
66
  interface CTAContainerProps {
41
67
  header: string;
42
68
  description: string;
43
69
  primaryButtonText: string;
44
- secondaryButtonText?: string; // Made optional
70
+ secondaryButtonText?: string;
45
71
  onPrimaryClick?: () => void;
46
72
  onSecondaryClick?: () => void;
47
73
  variant?: Variant;
@@ -15,6 +15,11 @@ const Container = styled.div`
15
15
  padding: 1.5rem;
16
16
  border-radius: 8px;
17
17
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
18
+ flex-shrink: 0;
19
+
20
+ @media (max-width: 767px) {
21
+ width: 70vw;
22
+ }
18
23
  `;
19
24
 
20
25
  const ThumbnailWrapper = styled.div<{ thumbnailBgColor: BackgroundColor }>`
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ComponentProps } from 'react';
3
+ import { ExtendedButton as Component } from './ExtendedButton';
4
+
5
+ const meta: Meta = {
6
+ title: 'Molecules/ExtendedButton',
7
+ component: Component,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ export const ExtendedButton: StoryObj<
13
+ Pick<
14
+ ComponentProps<typeof Component>,
15
+ 'text' | 'onClick' | 'bgColor' | 'textColor'| 'arrowBgColor'
16
+ >
17
+ > = {
18
+ args: {
19
+ text: 'Register For Free',
20
+ bgColor: 'bg-blue-800',
21
+ textColor: 'text-white',
22
+ arrowBgColor: 'bg-blue-400'
23
+ },
24
+ argTypes: {
25
+ text: {control: 'text'},
26
+ onClick: {action: 'clicked'},
27
+ bgColor: { control: 'text'},
28
+ textColor: { control: 'text'},
29
+ arrowBgColor: { control: 'text'}
30
+ },
31
+ };
@@ -0,0 +1,29 @@
1
+ import { ArrowButton } from "../../atoms";
2
+
3
+ interface ExtendedButtonProps {
4
+ text: string;
5
+ onClick?: () => void;
6
+ bgColor?: string;
7
+ arrowBgColor?: string;
8
+ textColor?: string;
9
+ }
10
+
11
+ export const ExtendedButton = ({
12
+ text,
13
+ onClick,
14
+ bgColor = "bg-blue-800",
15
+ arrowBgColor = "bg-blue-400",
16
+ textColor = "text-white"
17
+ }: ExtendedButtonProps) => {
18
+ return (
19
+ <button
20
+ className={`flex items-center h-10 ${bgColor} hover:opacity-90 transition duration-200`}
21
+ onClick={onClick}
22
+ >
23
+ <ArrowButton asChild bgColor={arrowBgColor} textColor={textColor} />
24
+ <span className={`px-4 ${textColor} text-sm font-semibold tracking-wide uppercase`}>
25
+ {text}
26
+ </span>
27
+ </button>
28
+ );
29
+ };
@@ -9,6 +9,13 @@ const Container = styled.div`
9
9
  gap: 1.5rem;
10
10
  align-items: center;
11
11
  max-width: 600px;
12
+
13
+ /* Mobile: stack vertically */
14
+ @media (max-width: 767px) {
15
+ flex-direction: column;
16
+ align-items: flex-start;
17
+ max-width: 100%;
18
+ }
12
19
  `;
13
20
 
14
21
  const ImageWrapper = styled.div`
@@ -23,12 +30,23 @@ const ImageWrapper = styled.div`
23
30
  height: 100%;
24
31
  object-fit: cover;
25
32
  }
33
+
34
+ /* Mobile: full width */
35
+ @media (max-width: 767px) {
36
+ width: 100%;
37
+ height: 200px;
38
+ }
26
39
  `;
27
40
 
28
41
  const ContentWrapper = styled.div`
29
42
  display: flex;
30
43
  flex-direction: column;
31
44
  gap: 1rem;
45
+
46
+ /* Mobile: full width */
47
+ @media (max-width: 767px) {
48
+ width: 100%;
49
+ }
32
50
  `;
33
51
 
34
52
  const LogoWrapper = styled.div`
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { ComponentProps } from 'react';
2
+ import { ComponentProps, useState } from 'react';
3
3
  import { SideNav as Component } from './SideNav';
4
4
 
5
5
  const meta: Meta = {
@@ -9,7 +9,38 @@ const meta: Meta = {
9
9
 
10
10
  export default meta;
11
11
 
12
- export const SideNav: StoryObj<Omit<ComponentProps<typeof Component>, 'onBackToTop'>> = {
12
+ const SideNavWithState = (args: any) => {
13
+ const [isOpen, setIsOpen] = useState(false);
14
+
15
+ return (
16
+ <Component
17
+ {...args}
18
+ isOpen={isOpen}
19
+ onClose={() => setIsOpen((prev) => !prev)}
20
+ onBackToTop={() => console.log('back-to-top clicked')}
21
+ mobileButtons={
22
+ args.showMobileButtons
23
+ ? [
24
+ {
25
+ text: 'Settings',
26
+ onClick: () => console.log('settings clicked'),
27
+ },
28
+ { text: 'Logout', onClick: () => console.log('logout clicked') },
29
+ ]
30
+ : []
31
+ }
32
+ />
33
+ );
34
+ };
35
+
36
+ export const SideNav: StoryObj<
37
+ Omit<
38
+ ComponentProps<typeof Component>,
39
+ 'onBackToTop' | 'onClose' | 'isOpen' | 'mobileButtons'
40
+ > & {
41
+ showMobileButtons: boolean;
42
+ }
43
+ > = {
13
44
  args: {
14
45
  items: [
15
46
  { label: 'Home', href: '#home' },
@@ -18,6 +49,7 @@ export const SideNav: StoryObj<Omit<ComponentProps<typeof Component>, 'onBackToT
18
49
  { label: 'Contact', href: '#contact' },
19
50
  ],
20
51
  activeItem: '#home',
52
+ // Desktop variants
21
53
  triangleColor: 'primary',
22
54
  dividerColor: 'primary',
23
55
  activeTextBgVariant: 'transparent',
@@ -26,39 +58,157 @@ export const SideNav: StoryObj<Omit<ComponentProps<typeof Component>, 'onBackToT
26
58
  inactiveTextVariant: 'subtle',
27
59
  backToTopBgVariant: 'transparent',
28
60
  backToTopTextVariant: 'subtle',
61
+ // Mobile variants
62
+ mobileBgVariant: 'primary',
63
+ borderVariant: 'primary',
64
+ closeButtonVariant: 'secondary',
65
+ showMobileButtons: false,
29
66
  },
30
67
  argTypes: {
68
+ // Desktop
31
69
  triangleColor: {
32
70
  control: 'select',
33
- options: ['primary', 'secondary', 'hover', 'active', 'error', 'focus', 'brand'],
71
+ options: [
72
+ 'primary',
73
+ 'secondary',
74
+ 'hover',
75
+ 'active',
76
+ 'error',
77
+ 'focus',
78
+ 'brand',
79
+ ],
34
80
  },
35
81
  dividerColor: {
36
82
  control: 'select',
37
- options: ['primary', 'secondary', 'hover', 'active', 'error', 'focus', 'brand'],
83
+ options: [
84
+ 'primary',
85
+ 'secondary',
86
+ 'hover',
87
+ 'active',
88
+ 'error',
89
+ 'focus',
90
+ 'brand',
91
+ ],
38
92
  },
39
93
  activeTextBgVariant: {
40
94
  control: 'select',
41
- options: ['primary', 'secondary', 'tertiary', 'subtle', 'blue', 'brand', 'light', 'transparent'],
95
+ options: [
96
+ 'primary',
97
+ 'secondary',
98
+ 'tertiary',
99
+ 'subtle',
100
+ 'blue',
101
+ 'brand',
102
+ 'light',
103
+ 'transparent',
104
+ ],
42
105
  },
43
106
  activeTextVariant: {
44
107
  control: 'select',
45
- options: ['brand', 'primary', 'secondary', 'tertiary', 'subtle', 'light', 'error', 'blue'],
108
+ options: [
109
+ 'brand',
110
+ 'primary',
111
+ 'secondary',
112
+ 'tertiary',
113
+ 'subtle',
114
+ 'light',
115
+ 'error',
116
+ 'blue',
117
+ ],
46
118
  },
47
119
  inactiveTextBgVariant: {
48
120
  control: 'select',
49
- options: ['primary', 'secondary', 'tertiary', 'subtle', 'blue', 'brand', 'light', 'transparent'],
121
+ options: [
122
+ 'primary',
123
+ 'secondary',
124
+ 'tertiary',
125
+ 'subtle',
126
+ 'blue',
127
+ 'brand',
128
+ 'light',
129
+ 'transparent',
130
+ ],
50
131
  },
51
132
  inactiveTextVariant: {
52
133
  control: 'select',
53
- options: ['brand', 'primary', 'secondary', 'tertiary', 'subtle', 'light', 'error', 'blue'],
134
+ options: [
135
+ 'brand',
136
+ 'primary',
137
+ 'secondary',
138
+ 'tertiary',
139
+ 'subtle',
140
+ 'light',
141
+ 'error',
142
+ 'blue',
143
+ ],
54
144
  },
55
145
  backToTopBgVariant: {
56
146
  control: 'select',
57
- options: ['primary', 'secondary', 'tertiary', 'subtle', 'blue', 'brand', 'light', 'transparent'],
147
+ options: [
148
+ 'primary',
149
+ 'secondary',
150
+ 'tertiary',
151
+ 'subtle',
152
+ 'blue',
153
+ 'brand',
154
+ 'light',
155
+ 'transparent',
156
+ ],
58
157
  },
59
158
  backToTopTextVariant: {
60
159
  control: 'select',
61
- options: ['brand', 'primary', 'secondary', 'tertiary', 'subtle', 'light', 'error', 'blue'],
160
+ options: [
161
+ 'brand',
162
+ 'primary',
163
+ 'secondary',
164
+ 'tertiary',
165
+ 'subtle',
166
+ 'light',
167
+ 'error',
168
+ 'blue',
169
+ ],
170
+ },
171
+ // Mobile
172
+ mobileBgVariant: {
173
+ control: 'select',
174
+ options: [
175
+ 'primary',
176
+ 'secondary',
177
+ 'tertiary',
178
+ 'subtle',
179
+ 'blue',
180
+ 'brand',
181
+ 'light',
182
+ 'transparent',
183
+ ],
184
+ },
185
+ borderVariant: {
186
+ control: 'select',
187
+ options: [
188
+ 'primary',
189
+ 'secondary',
190
+ 'hover',
191
+ 'active',
192
+ 'error',
193
+ 'focus',
194
+ 'brand',
195
+ ],
196
+ },
197
+ closeButtonVariant: {
198
+ control: 'select',
199
+ options: [
200
+ 'primary',
201
+ 'secondary',
202
+ 'hover',
203
+ 'active',
204
+ 'error',
205
+ 'focus',
206
+ 'brand',
207
+ ],
208
+ },
209
+ showMobileButtons: {
210
+ control: 'boolean',
62
211
  },
63
212
  },
64
- };
213
+ render: SideNavWithState,
214
+ };