property-practice-ui 0.0.1

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 (189) hide show
  1. package/.prettierignore +3 -0
  2. package/.prettierrc +4 -0
  3. package/.storybook/main.ts +30 -0
  4. package/.storybook/preview.ts +32 -0
  5. package/.storybook/vitest.setup.ts +6 -0
  6. package/CHANGELOG.md +7 -0
  7. package/dist/index.js +10148 -0
  8. package/dist/index.mjs +10148 -0
  9. package/eslint.config.mjs +4 -0
  10. package/package.json +63 -0
  11. package/postcss.config.js +6 -0
  12. package/src/atoms/ArrowButton/ArrowButton.stories.tsx +52 -0
  13. package/src/atoms/ArrowButton/ArrowButton.tsx +71 -0
  14. package/src/atoms/Button/Button.stories.tsx +45 -0
  15. package/src/atoms/Button/Button.tsx +91 -0
  16. package/src/atoms/CheckboxItem/CheckboxItem.tsx +49 -0
  17. package/src/atoms/Description/Description.tsx +43 -0
  18. package/src/atoms/ExtendedButton/ExtendedButton.stories.tsx +61 -0
  19. package/src/atoms/ExtendedButton/ExtendedButton.tsx +40 -0
  20. package/src/atoms/FeatureItem/FeatureItem.stories.tsx +25 -0
  21. package/src/atoms/FeatureItem/FeatureItem.tsx +90 -0
  22. package/src/atoms/FormContainer/FormContainer.tsx +34 -0
  23. package/src/atoms/Header/Header.stories.tsx +22 -0
  24. package/src/atoms/Header/Header.tsx +53 -0
  25. package/src/atoms/Input/Input.stories.tsx +23 -0
  26. package/src/atoms/Input/Input.tsx +28 -0
  27. package/src/atoms/Label/Label.stories.tsx +28 -0
  28. package/src/atoms/Label/Label.tsx +53 -0
  29. package/src/atoms/Loader/Loader.tsx +49 -0
  30. package/src/atoms/Pill/Pill.stories.tsx +30 -0
  31. package/src/atoms/Pill/Pill.tsx +82 -0
  32. package/src/atoms/RadioItem/RadioItem.stories.tsx +25 -0
  33. package/src/atoms/RadioItem/RadioItem.tsx +54 -0
  34. package/src/atoms/SecondaryInput/SecondaryInput.stories.tsx +30 -0
  35. package/src/atoms/SecondaryInput/SecondaryInput.tsx +125 -0
  36. package/src/atoms/SocialButton/SocialButton.stories.tsx +79 -0
  37. package/src/atoms/SocialButton/SocialButton.tsx +90 -0
  38. package/src/atoms/TermsCheckbox/TermsCheckbox.stories.tsx +62 -0
  39. package/src/atoms/TermsCheckbox/TermsCheckbox.tsx +150 -0
  40. package/src/atoms/Text/Text.stories.tsx +32 -0
  41. package/src/atoms/Text/Text.tsx +49 -0
  42. package/src/atoms/TextButton/TextButton.stories.tsx +77 -0
  43. package/src/atoms/TextButton/TextButton.tsx +78 -0
  44. package/src/atoms/Textarea/Textarea.tsx +36 -0
  45. package/src/atoms/ToggleButton/ToggleButton.stories.tsx +32 -0
  46. package/src/atoms/ToggleButton/ToggleButton.tsx +106 -0
  47. package/src/atoms/index.ts +20 -0
  48. package/src/components/DynamicInput.tsx +54 -0
  49. package/src/components/FileUpload.tsx +123 -0
  50. package/src/components/Filter/Filter.tsx +33 -0
  51. package/src/components/ModeSwitch.tsx +66 -0
  52. package/src/components/NavMenu.tsx +83 -0
  53. package/src/components/SearchBar/Search.stories.tsx +25 -0
  54. package/src/components/SearchBar/Search.tsx +40 -0
  55. package/src/components/SortBy/SortBy.stories.tsx +27 -0
  56. package/src/components/SortBy/SortBy.tsx +45 -0
  57. package/src/components/Spinner.tsx +30 -0
  58. package/src/components/Table/Table.stories.tsx +56 -0
  59. package/src/components/Table/Table.tsx +25 -0
  60. package/src/components/TableInner/TableInner.tsx +27 -0
  61. package/src/components/TableInner/tableInner.stories.tsx +21 -0
  62. package/src/components/TableList.tsx +195 -0
  63. package/src/components/TableRow/TableRow.stories.tsx +55 -0
  64. package/src/components/TableRow/TableRow.tsx +343 -0
  65. package/src/components/Tabs/Tabs.stories.tsx +42 -0
  66. package/src/components/Tabs/Tabs.tsx +56 -0
  67. package/src/components/Toast.tsx +192 -0
  68. package/src/components/TopMenu.tsx +62 -0
  69. package/src/index.ts +20 -0
  70. package/src/molecules/Accordion/Accordion.stories.tsx +54 -0
  71. package/src/molecules/Accordion/Accordion.tsx +45 -0
  72. package/src/molecules/AccordionContent/AccordionContent.tsx +16 -0
  73. package/src/molecules/AccordionHeader/AccordionHeader.stories.tsx +31 -0
  74. package/src/molecules/AccordionHeader/AccordionHeader.tsx +67 -0
  75. package/src/molecules/Address/Address.stories.tsx +116 -0
  76. package/src/molecules/Address/Address.tsx +136 -0
  77. package/src/molecules/CTAContainer/CTAContainer.stories.tsx +67 -0
  78. package/src/molecules/CTAContainer/CTAContainer.tsx +98 -0
  79. package/src/molecules/Checkbox/Checkbox.tsx +60 -0
  80. package/src/molecules/ContentCard/ContentCard.stories.tsx +31 -0
  81. package/src/molecules/ContentCard/ContentCard.tsx +80 -0
  82. package/src/molecules/DocumentAccordionHeader/DocumentAccordionHeader.tsx +50 -0
  83. package/src/molecules/DocumentAccordionRow/DocumentAccordionRow.tsx +66 -0
  84. package/src/molecules/Dropdown/Dropdown.stories.tsx +39 -0
  85. package/src/molecules/Dropdown/Dropdown.tsx +89 -0
  86. package/src/molecules/EmptyState/EmptyState.stories.tsx +26 -0
  87. package/src/molecules/EmptyState/EmptyState.tsx +49 -0
  88. package/src/molecules/FAQAccordion/FAQAccordion.stories.tsx +63 -0
  89. package/src/molecules/FAQAccordion/FAQAccordion.tsx +98 -0
  90. package/src/molecules/FeatureContainer/FeatureContainer.stories.tsx +40 -0
  91. package/src/molecules/FeatureContainer/FeatureContainer.tsx +59 -0
  92. package/src/molecules/FileButton/FileButton.tsx +54 -0
  93. package/src/molecules/Input/Input.stories.tsx +30 -0
  94. package/src/molecules/Input/Input.tsx +31 -0
  95. package/src/molecules/InputContainer/InputContainer.tsx +60 -0
  96. package/src/molecules/JutaBrand/JutaBrand.stories.tsx +43 -0
  97. package/src/molecules/JutaBrand/JutaBrand.tsx +74 -0
  98. package/src/molecules/Modal/Modal.tsx +71 -0
  99. package/src/molecules/OverviewRowItem/OverviewRowItem.stories.tsx +23 -0
  100. package/src/molecules/OverviewRowItem/OverviewRowItem.tsx +39 -0
  101. package/src/molecules/PDFPreviewer/PDFPreviewer.tsx +42 -0
  102. package/src/molecules/PageLayout/PageLayout.tsx +60 -0
  103. package/src/molecules/ProductInfo/ProductInfo.stories.tsx +50 -0
  104. package/src/molecules/ProductInfo/ProductInfo.tsx +90 -0
  105. package/src/molecules/RadioGroup/RadioGroup.stories.tsx +47 -0
  106. package/src/molecules/RadioGroup/RadioGroup.tsx +55 -0
  107. package/src/molecules/RatesChart/RatesChart.stories.tsx +61 -0
  108. package/src/molecules/RatesChart/RatesChart.tsx +185 -0
  109. package/src/molecules/SideNav/SideNav.stories.tsx +64 -0
  110. package/src/molecules/SideNav/SideNav.tsx +140 -0
  111. package/src/molecules/SidePanel/SidePanel.stories.tsx +87 -0
  112. package/src/molecules/SidePanel/SidePanel.tsx +138 -0
  113. package/src/molecules/StepperHeaderTab/StepperHeaderTab.stories.tsx +23 -0
  114. package/src/molecules/StepperHeaderTab/StepperHeaderTab.tsx +43 -0
  115. package/src/molecules/Textarea/Textarea.tsx +29 -0
  116. package/src/molecules/index.ts +23 -0
  117. package/src/organism/ContactForm/ContactForm.stories.tsx +15 -0
  118. package/src/organism/ContactForm/ContactForm.tsx +54 -0
  119. package/src/organism/DocumentListAccordion/DocumentListAccordion.tsx +96 -0
  120. package/src/organism/FeatureCarousel/FeatureCarousel.stories.tsx +81 -0
  121. package/src/organism/FeatureCarousel/FeatureCarousel.tsx +162 -0
  122. package/src/organism/Footer/Footer.stories.tsx +77 -0
  123. package/src/organism/Footer/Footer.tsx +231 -0
  124. package/src/organism/Header/Header.stories.tsx +51 -0
  125. package/src/organism/Header/Header.tsx +76 -0
  126. package/src/organism/OverviewList/OverviewList.stories.tsx +43 -0
  127. package/src/organism/OverviewList/OverviewList.tsx +56 -0
  128. package/src/organism/ToastProvider/ToastProvider.tsx +40 -0
  129. package/src/organism/VersionLabel/VersionLabel.tsx +9 -0
  130. package/src/organism/index.ts +9 -0
  131. package/src/styles/tailwind.css +8 -0
  132. package/src/templates/AboutUs/AboutUs.stories.tsx +60 -0
  133. package/src/templates/AboutUs/AboutUs.tsx +97 -0
  134. package/src/templates/Contact/Contact.stories.tsx +62 -0
  135. package/src/templates/Contact/Contact.tsx +125 -0
  136. package/src/templates/FAQ/FAQ.stories.tsx +82 -0
  137. package/src/templates/FAQ/FAQ.tsx +91 -0
  138. package/src/templates/Features/Features.stories.tsx +94 -0
  139. package/src/templates/Features/Features.tsx +79 -0
  140. package/src/templates/Hero/Hero.stories.tsx +105 -0
  141. package/src/templates/Hero/Hero.tsx +139 -0
  142. package/src/templates/OtherProducts/OtherProducts.stories.tsx +77 -0
  143. package/src/templates/OtherProducts/OtherProducts.tsx +86 -0
  144. package/src/templates/index.ts +7 -0
  145. package/src/tokens/animations.ts +11 -0
  146. package/src/tokens/breakpoints.ts +7 -0
  147. package/src/tokens/colors.stories.tsx +77 -0
  148. package/src/tokens/colors.ts +59 -0
  149. package/src/tokens/radii.ts +6 -0
  150. package/src/tokens/sizes.ts +8 -0
  151. package/src/tokens/spaces.ts +21 -0
  152. package/src/types.ts +20 -0
  153. package/stories/Button.stories.ts +54 -0
  154. package/stories/Button.tsx +41 -0
  155. package/stories/Configure.mdx +364 -0
  156. package/stories/Header.stories.ts +34 -0
  157. package/stories/Header.tsx +71 -0
  158. package/stories/Page.stories.ts +33 -0
  159. package/stories/Page.tsx +91 -0
  160. package/stories/TopMenu.stories.tsx +51 -0
  161. package/stories/assets/accessibility.png +0 -0
  162. package/stories/assets/accessibility.svg +1 -0
  163. package/stories/assets/addon-library.png +0 -0
  164. package/stories/assets/assets.png +0 -0
  165. package/stories/assets/avif-test-image.avif +0 -0
  166. package/stories/assets/context.png +0 -0
  167. package/stories/assets/discord.svg +1 -0
  168. package/stories/assets/docs.png +0 -0
  169. package/stories/assets/figma-plugin.png +0 -0
  170. package/stories/assets/github.svg +1 -0
  171. package/stories/assets/share.png +0 -0
  172. package/stories/assets/styling.png +0 -0
  173. package/stories/assets/testing.png +0 -0
  174. package/stories/assets/theming.png +0 -0
  175. package/stories/assets/tutorials.svg +1 -0
  176. package/stories/assets/youtube.svg +1 -0
  177. package/stories/button.css +30 -0
  178. package/stories/header.css +32 -0
  179. package/stories/page.css +68 -0
  180. package/tailwind.config.js +34 -0
  181. package/tsconfig.json +8 -0
  182. package/types/index.ts +5 -0
  183. package/types/inputAttributes.ts +16 -0
  184. package/types/menuItem.ts +17 -0
  185. package/types/orderType.ts +2 -0
  186. package/types/tableListItem.ts +7 -0
  187. package/types/toast.ts +1 -0
  188. package/vitest.config.ts +37 -0
  189. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,98 @@
1
+ import { useState } from 'react';
2
+ import { HiMinus, HiPlus } from 'react-icons/hi2';
3
+ import styled from 'styled-components';
4
+ import { Description, Header } from '../../atoms';
5
+ import { colors } from '../../tokens/colors';
6
+
7
+ type TextColor = keyof typeof colors.text;
8
+
9
+ const AccordionContainer = styled.div`
10
+ border-bottom: 1px solid ${colors.text.light || '#d1d5db'};
11
+ padding: 1.5rem 0;
12
+ `;
13
+
14
+ const AccordionHeader = styled.button`
15
+ width: 100%;
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 1rem;
19
+ background: none;
20
+ border: none;
21
+ cursor: pointer;
22
+ text-align: left;
23
+ padding: 0;
24
+ `;
25
+
26
+ const NumberBadge = styled.span<{ numberColor: TextColor }>`
27
+ font-size: 1rem;
28
+ font-weight: 600;
29
+ color: ${props => colors.text[props.numberColor]};
30
+ flex-shrink: 0;
31
+ `;
32
+
33
+ const HeaderWrapper = styled.div`
34
+ flex-grow: 1;
35
+ `;
36
+
37
+ const IconWrapper = styled.div<{ iconColor: TextColor }>`
38
+ color: ${props => colors.text[props.iconColor]};
39
+ flex-shrink: 0;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ `;
44
+
45
+ const AccordionContent = styled.div<{ $isOpen: boolean }>`
46
+ max-height: ${props => props.$isOpen ? '1000px' : '0'};
47
+ overflow: hidden;
48
+ transition: max-height 0.3s ease-in-out;
49
+ padding-left: 2.5rem;
50
+ margin-top: ${props => props.$isOpen ? '1rem' : '0'};
51
+ `;
52
+
53
+ interface AccordionProps {
54
+ number: string;
55
+ title: string;
56
+ content: string;
57
+ defaultOpen?: boolean;
58
+ headerVariant?: 'h1' | 'h2' | 'h3';
59
+ numberColor?: TextColor;
60
+ titleColor?: TextColor;
61
+ iconColor?: TextColor;
62
+ contentVariant?: 'primary' | 'secondary' | 'subtle' | 'error';
63
+ }
64
+
65
+ export const FAQAccordion = ({
66
+ number,
67
+ title,
68
+ content,
69
+ defaultOpen = false,
70
+ headerVariant = 'h3',
71
+ numberColor = 'brand',
72
+ titleColor = 'brand',
73
+ iconColor = 'brand',
74
+ contentVariant = 'primary',
75
+ }: AccordionProps) => {
76
+ const [isOpen, setIsOpen] = useState(defaultOpen);
77
+
78
+ return (
79
+ <AccordionContainer>
80
+ <AccordionHeader onClick={() => setIsOpen(!isOpen)}>
81
+ <NumberBadge numberColor={numberColor}>{number}</NumberBadge>
82
+ <HeaderWrapper>
83
+ <Header variant={headerVariant} color={titleColor}>
84
+ {title}
85
+ </Header>
86
+ </HeaderWrapper>
87
+ <IconWrapper iconColor={iconColor}>
88
+ {isOpen ? <HiMinus size={24} /> : <HiPlus size={24} />}
89
+ </IconWrapper>
90
+ </AccordionHeader>
91
+ <AccordionContent $isOpen={isOpen}>
92
+ <Description variant={contentVariant}>
93
+ {content}
94
+ </Description>
95
+ </AccordionContent>
96
+ </AccordionContainer>
97
+ );
98
+ };
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ComponentProps } from 'react';
3
+ import { GiMolecule } from "react-icons/gi";
4
+ import { FeatureContainer as Component } from './FeatureContainer';
5
+
6
+ const meta: Meta = {
7
+ title: 'Molecules/FeatureContainer',
8
+ component: Component,
9
+ };
10
+
11
+ export default meta;
12
+
13
+ const featureItems = [
14
+ {
15
+ thumbnail: <GiMolecule size={24} color="white" />,
16
+ title: 'End-to-End Process Issuing',
17
+ description: 'Litigator manages the full issuing process for you.',
18
+ thumbnailBgColor: '#0d9488',
19
+ },
20
+ {
21
+ thumbnail: <GiMolecule size={24} color="white" />,
22
+ title: 'Automated Workflow',
23
+ description: 'Streamline your processes with intelligent automation.',
24
+ thumbnailBgColor: '#0d9488',
25
+ },
26
+ {
27
+ thumbnail: <GiMolecule size={24} color="white" />,
28
+ title: 'Real-time Updates',
29
+ description: 'Stay informed with instant notifications and tracking.',
30
+ thumbnailBgColor: '#0d9488',
31
+ },
32
+ ];
33
+
34
+ export const FeatureContainer: StoryObj<
35
+ Pick<ComponentProps<typeof Component>, 'items'>
36
+ > = {
37
+ args: {
38
+ items: featureItems,
39
+ },
40
+ };
@@ -0,0 +1,59 @@
1
+ import styled from 'styled-components';
2
+ import { FeatureItem } from '../../atoms';
3
+ import { colors } from '../../tokens/colors';
4
+
5
+ type TextColor = keyof typeof colors.text;
6
+ type BackgroundColor = keyof typeof colors.background;
7
+
8
+ const Container = styled.div`
9
+ display: flex;
10
+ flex-direction: column;
11
+ `;
12
+
13
+ const FeatureWrapper = styled.div`
14
+ padding: 1rem 0;
15
+ border-bottom: 1px solid #e5e7eb;
16
+
17
+ &:last-child {
18
+ border-bottom: none;
19
+ }
20
+ `;
21
+
22
+ interface FeatureItemData {
23
+ thumbnail: React.ReactNode;
24
+ title: string;
25
+ description: string;
26
+ thumbnailSize?: string;
27
+ }
28
+
29
+ interface FeatureContainerProps {
30
+ items: FeatureItemData[];
31
+ thumbnailBgColor?: BackgroundColor;
32
+ titleColor?: TextColor;
33
+ descriptionVariant?: 'primary' | 'secondary' | 'subtle' | 'error';
34
+ }
35
+
36
+ export const FeatureContainer = ({
37
+ items,
38
+ thumbnailBgColor = 'brand',
39
+ titleColor = 'primary',
40
+ descriptionVariant = 'subtle',
41
+ }: FeatureContainerProps) => {
42
+ return (
43
+ <Container>
44
+ {items.map((item, index) => (
45
+ <FeatureWrapper key={index}>
46
+ <FeatureItem
47
+ thumbnail={item.thumbnail}
48
+ title={item.title}
49
+ description={item.description}
50
+ thumbnailSize={item.thumbnailSize}
51
+ thumbnailBgColor={thumbnailBgColor}
52
+ titleColor={titleColor}
53
+ descriptionVariant={descriptionVariant}
54
+ />
55
+ </FeatureWrapper>
56
+ ))}
57
+ </Container>
58
+ );
59
+ };
@@ -0,0 +1,54 @@
1
+ import { ComponentProps, useRef } from 'react';
2
+ import { FaPlus } from 'react-icons/fa6';
3
+ import { Button } from '../../atoms';
4
+
5
+ const acceptTypes = ['.docx', '.doc', '.pdf', 'image/*'] as const;
6
+
7
+ type AcceptType = (typeof acceptTypes)[number];
8
+
9
+ interface FileButtonProps
10
+ extends Omit<ComponentProps<typeof Button>, 'onClick' | 'type'> {
11
+ onFileSelect?: (file: File) => void;
12
+ accept?: AcceptType[];
13
+ multiple?: boolean;
14
+ }
15
+
16
+ export const FileButton = ({
17
+ onFileSelect,
18
+ accept = ['.pdf'],
19
+ multiple = false,
20
+ ...rest
21
+ }: FileButtonProps) => {
22
+ const fileInputRef = useRef<HTMLInputElement>(null);
23
+
24
+ const handleClick = () => {
25
+ if (fileInputRef.current) {
26
+ fileInputRef.current.click();
27
+ }
28
+ };
29
+
30
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
31
+ const files = event.target.files;
32
+ if (files && files.length > 0 && onFileSelect) {
33
+ Array.from(files).forEach((file) => onFileSelect(file));
34
+ }
35
+ };
36
+
37
+ return (
38
+ <>
39
+ <Button
40
+ {...rest}
41
+ icon={<FaPlus fontSize="10px" />}
42
+ onClick={handleClick}
43
+ />
44
+ <input
45
+ type="file"
46
+ ref={fileInputRef}
47
+ style={{ display: 'none' }}
48
+ onChange={handleFileChange}
49
+ accept={accept?.map((item) => item).join(',')}
50
+ multiple={multiple}
51
+ />
52
+ </>
53
+ );
54
+ };
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ComponentProps } from 'react';
3
+ import { Input as Component } from './Input';
4
+
5
+ const meta: Meta = {
6
+ title: 'Molecules/Input',
7
+ component: Component,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ export const Input: StoryObj<
13
+ Pick<
14
+ ComponentProps<typeof Component>,
15
+ 'label' | 'name' | 'placeholder' | 'type' | 'error'
16
+ >
17
+ > = {
18
+ args: {
19
+ label: 'First Name',
20
+ placeholder: 'Full Name',
21
+ type: 'text',
22
+ error: 'default',
23
+ },
24
+ argTypes: {
25
+ label: { control: 'text' },
26
+ placeholder: { control: 'text' },
27
+ type: { control: 'select', options: Component.types },
28
+ error: { control: 'text' },
29
+ },
30
+ };
@@ -0,0 +1,31 @@
1
+ import { ComponentProps, useMemo } from 'react';
2
+ import { Input as InputBase } from '../../atoms/Input/Input';
3
+ import { FloatingLabel } from '../../atoms/Label/Label';
4
+ import { InputContainer } from '../InputContainer/InputContainer';
5
+
6
+ interface InputProps extends ComponentProps<typeof InputBase> {
7
+ label?: string;
8
+ error?: string;
9
+ }
10
+
11
+ export const Input = ({ label, error, ...rest }: InputProps) => {
12
+ const isInvalid = useMemo(
13
+ () => typeof error === 'string' && error.length > 0,
14
+ [error],
15
+ );
16
+
17
+ return (
18
+ <InputContainer error={error}>
19
+ {typeof label === 'string' && (
20
+ <FloatingLabel
21
+ color={isInvalid ? 'error' : 'subtle'}
22
+ value={label}
23
+ fontWeight="600"
24
+ />
25
+ )}
26
+ <InputBase {...rest} />
27
+ </InputContainer>
28
+ );
29
+ };
30
+
31
+ Input.types = InputBase.types;
@@ -0,0 +1,60 @@
1
+ import { ReactNode } from 'react';
2
+ import styled from 'styled-components';
3
+ import { Label } from '../../atoms';
4
+ import { colors } from '../../tokens/colors';
5
+ import { radii } from '../../tokens/radii';
6
+ import { sizes } from '../../tokens/sizes';
7
+ import { spaces } from '../../tokens/spaces';
8
+
9
+ const variants = ['outline', 'none'] as const;
10
+ interface InputContainerProps {
11
+ error?: string;
12
+ children: ReactNode;
13
+ variant?: (typeof variants)[number];
14
+ }
15
+
16
+ const Container = styled.div`
17
+ width: 100%;
18
+ `;
19
+
20
+ const InnerContainer = styled.div<
21
+ Pick<InputContainerProps, 'error' | 'variant'>
22
+ >`
23
+ position: relative;
24
+ width: 100%;
25
+ min-height: 58px;
26
+ display: flex;
27
+ align-items: center;
28
+
29
+ border-style: solid;
30
+ border-width: ${(props) => (props?.variant === 'outline' ? sizes[0] : '0px')};
31
+ border-color: ${(props) =>
32
+ props?.error ? colors.border.error : colors.border.active};
33
+ border-radius: ${radii.lg};
34
+
35
+ padding: ${spaces[3]} ${spaces[3]} ${spaces[2]};
36
+
37
+ &:hover {
38
+ border-color: ${colors.border.hover};
39
+ cursor: text;
40
+ }
41
+
42
+ &:focus-within {
43
+ border-color: ${colors.border.focus};
44
+ }
45
+ `;
46
+
47
+ export const InputContainer = ({
48
+ error,
49
+ children,
50
+ variant = 'outline',
51
+ }: InputContainerProps) => {
52
+ return (
53
+ <Container>
54
+ <InnerContainer error={error} variant={variant}>
55
+ {children}
56
+ </InnerContainer>
57
+ {typeof error === 'string' && <Label value={error} color="error" />}
58
+ </Container>
59
+ );
60
+ };
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ComponentProps } from 'react';
3
+ import { JutaBrand as Component } from './JutaBrand';
4
+
5
+ const meta: Meta = {
6
+ title: 'Molecules/JutaBrand',
7
+ component: Component,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ const logos = [
13
+ 'https://via.placeholder.com/150x60/1e40af/ffffff?text=LENDICOR',
14
+ 'https://via.placeholder.com/150x60/1e40af/ffffff?text=Harris+Billings',
15
+ 'https://via.placeholder.com/150x60/1e40af/ffffff?text=BDP',
16
+ 'https://via.placeholder.com/150x60/1e40af/ffffff?text=cinque',
17
+ ];
18
+
19
+ export const JutaBrand: StoryObj<
20
+ Pick<
21
+ ComponentProps<typeof Component>,
22
+ | 'mainHeader'
23
+ | 'subHeader'
24
+ | 'logos'
25
+ | 'mainHeaderColor'
26
+ | 'subHeaderColor'
27
+ >
28
+ > = {
29
+ args: {
30
+ mainHeader: 'Juta, a trusted brand',
31
+ subHeader: 'With over 170 years of quality solutions in education, legal and business our partners include:',
32
+ logos: logos,
33
+ mainHeaderColor: 'primary',
34
+ subHeaderColor: 'primary',
35
+ },
36
+ argTypes: {
37
+ mainHeader: { control: 'text' },
38
+ subHeader: { control: 'text' },
39
+ logos: { control: 'object' },
40
+ mainHeaderColor: { control: 'text' },
41
+ subHeaderColor: { control: 'text' },
42
+ },
43
+ };
@@ -0,0 +1,74 @@
1
+ import styled from 'styled-components';
2
+ import { Header } from '../../atoms';
3
+ import { colors } from '../../tokens/colors';
4
+
5
+ type Color = keyof typeof colors.text;
6
+
7
+ const Container = styled.div`
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 2rem;
11
+ align-items: center;
12
+ text-align: center;
13
+ padding: 3rem 2rem;
14
+ `;
15
+
16
+ const HeaderWrapper = styled.div`
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 1rem;
20
+ `;
21
+
22
+ const LogosContainer = styled.div`
23
+ display: flex;
24
+ gap: 3rem;
25
+ align-items: center;
26
+ justify-content: center;
27
+ flex-wrap: wrap;
28
+ `;
29
+
30
+ const LogoWrapper = styled.div`
31
+ max-width: 150px;
32
+
33
+ img {
34
+ width: 100%;
35
+ height: auto;
36
+ display: block;
37
+ }
38
+ `;
39
+
40
+ interface JutaBrandProps {
41
+ mainHeader: string;
42
+ subHeader: string;
43
+ logos: string[];
44
+ mainHeaderColor?: Color;
45
+ subHeaderColor?: Color;
46
+ }
47
+
48
+ export const JutaBrand = ({
49
+ mainHeader,
50
+ subHeader,
51
+ logos,
52
+ mainHeaderColor = 'brand',
53
+ subHeaderColor = 'subtle',
54
+ }: JutaBrandProps) => {
55
+ return (
56
+ <Container>
57
+ <HeaderWrapper>
58
+ <Header variant="h1" color={mainHeaderColor}>
59
+ {mainHeader}
60
+ </Header>
61
+ <Header variant="h2" color={subHeaderColor}>
62
+ {subHeader}
63
+ </Header>
64
+ </HeaderWrapper>
65
+ <LogosContainer>
66
+ {logos.map((logo, index) => (
67
+ <LogoWrapper key={index}>
68
+ <img src={logo} alt={`Partner logo ${index + 1}`} />
69
+ </LogoWrapper>
70
+ ))}
71
+ </LogosContainer>
72
+ </Container>
73
+ );
74
+ };
@@ -0,0 +1,71 @@
1
+ import { ReactNode } from 'react';
2
+ import styled from 'styled-components';
3
+ import { fadeIn } from '../../tokens/animations';
4
+ import { breakpoints } from '../../tokens/breakpoints';
5
+ import { colors } from '../../tokens/colors';
6
+ import { radii } from '../../tokens/radii';
7
+ import { spaces } from '../../tokens/spaces';
8
+
9
+ const positions = ['top', 'center', 'bottom'] as const;
10
+
11
+ type ContentPosition = (typeof positions)[number];
12
+
13
+ interface ModalProps {
14
+ visible: boolean;
15
+ onClose: () => void;
16
+ dismissable?: boolean;
17
+ contentPosition?: ContentPosition;
18
+ children: ReactNode;
19
+ }
20
+
21
+ const Overlay = styled.div<{ contentPosition: ContentPosition }>`
22
+ height: 100vh;
23
+ width: 100vw;
24
+ background-color: rgba(0, 0, 0, 0.4);
25
+ position: fixed;
26
+ inset: 0;
27
+ z-index: 1000;
28
+ display: flex;
29
+ align-items: ${(props) =>
30
+ props.contentPosition === 'center'
31
+ ? 'center'
32
+ : props.contentPosition === 'bottom'
33
+ ? 'flex-end'
34
+ : 'flex-start'};
35
+ justify-content: center;
36
+ animation: ${fadeIn} 160ms ease-out;
37
+ `;
38
+
39
+ const ContentContainer = styled.div`
40
+ position: relative;
41
+ width: min(${breakpoints.sm}, 92vw);
42
+ max-height: 86vh;
43
+ overflow: auto;
44
+ border-radius: ${radii.xl};
45
+ padding: ${spaces[6]};
46
+ background-color: ${colors.background.primary};
47
+ `;
48
+
49
+ export const Modal = ({
50
+ visible,
51
+ contentPosition = 'center',
52
+ dismissable = true,
53
+ onClose,
54
+ children,
55
+ }: ModalProps) => {
56
+ function onDismiss() {
57
+ if (dismissable) {
58
+ onClose();
59
+ }
60
+ }
61
+
62
+ if (!visible) return <></>;
63
+
64
+ return (
65
+ <Overlay contentPosition={contentPosition} onClick={onDismiss}>
66
+ <ContentContainer onClick={(e) => e.stopPropagation()}>
67
+ {children}
68
+ </ContentContainer>
69
+ </Overlay>
70
+ );
71
+ };
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { ComponentProps } from "react";
3
+ import { OverviewRowItem as Component } from './OverviewRowItem';
4
+
5
+ const meta: Meta = {
6
+ title: 'Molecules/OverviewRowItem',
7
+ component: Component
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const OverviewRowItem: StoryObj<ComponentProps<typeof Component>> = {
13
+ args: {
14
+ isComplete: true,
15
+ isMainHeader: true,
16
+ title: 'Property Information'
17
+ },
18
+ argTypes: {
19
+ isComplete: {control: 'boolean'},
20
+ isMainHeader: {control: 'boolean'},
21
+ title: {control: 'text'}
22
+ }
23
+ }
@@ -0,0 +1,39 @@
1
+ import { FaCircleCheck, FaCircleExclamation } from "react-icons/fa6"
2
+ import styled from "styled-components"
3
+ import { Header } from "../../atoms"
4
+ import { colors } from "../../tokens/colors"
5
+ import { radii } from "../../tokens/radii"
6
+ import { spaces } from "../../tokens/spaces"
7
+
8
+ const Container = styled.div<{isComplete: boolean}>`
9
+ display: flex;
10
+ align-items: center ;
11
+ justify-content: space-between;
12
+ padding: ${spaces[2]} ${spaces[3]};
13
+ background-color: ${props => props.isComplete ? colors.background.blue : colors.background.light};
14
+ border-radius: ${radii.sm};
15
+ `
16
+
17
+ const Row = styled.div`
18
+ display: flex;
19
+ align-items: center;
20
+ gap: ${spaces[2]};
21
+ `
22
+
23
+ interface OverviewRowItemProps {
24
+ isComplete: boolean
25
+ title: string
26
+ isMainHeader: boolean
27
+ }
28
+
29
+ export const OverviewRowItem = ({isComplete, isMainHeader, title}: OverviewRowItemProps) => {
30
+
31
+ return <Container isComplete={isComplete}>
32
+ <Header variant={isMainHeader ? 'h2' : 'h3'} color={isComplete ? 'secondary' : 'primary'}>{title}</Header>
33
+
34
+ <Row>
35
+ <Header variant='h3' color={isComplete ? 'secondary' : 'primary'}>{isComplete ? 'Complete' : 'Information Outstanding'}</Header>
36
+ {isComplete ? <FaCircleCheck color={colors.background.success}/> : <FaCircleExclamation color={colors.background.error}/>}
37
+ </Row>
38
+ </Container>
39
+ }
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import { EmptyState } from '../EmptyState/EmptyState';
4
+
5
+ interface PDFPreviewerProps {
6
+ url: string;
7
+ }
8
+
9
+ const Container = styled.div`
10
+ width: 100%;
11
+ height: 100%;
12
+ `;
13
+
14
+ export const PDFPreviewer = ({ url }: PDFPreviewerProps) => {
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ setError(null);
19
+ try {
20
+ new URL(url);
21
+ } catch {
22
+ setError('Invalid preview URL');
23
+ }
24
+ }, [url]);
25
+
26
+ if (typeof error === 'string')
27
+ return (
28
+ <EmptyState message="Failed to load this document" variant="documents" />
29
+ );
30
+
31
+ return (
32
+ <Container>
33
+ <iframe
34
+ src={`${url}#view=FitH&toolbar=1&navpanes=0`}
35
+ title="Document preview"
36
+ allow="clipboard-read; clipboard-write"
37
+ referrerPolicy="no-referrer"
38
+ style={{ width: '100%', height: '100%', border: '0' }}
39
+ />
40
+ </Container>
41
+ );
42
+ };