mautourco-components 0.1.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 (163) hide show
  1. package/README.md +187 -0
  2. package/dist/components/atoms/Avatar/Avatar.d.ts +14 -0
  3. package/dist/components/atoms/Avatar/Avatar.js +31 -0
  4. package/dist/components/atoms/Button/Button.d.ts +27 -0
  5. package/dist/components/atoms/Button/Button.js +35 -0
  6. package/dist/components/atoms/Checkbox/Checkbox.d.ts +13 -0
  7. package/dist/components/atoms/Checkbox/Checkbox.js +33 -0
  8. package/dist/components/atoms/Icon/Icon.d.ts +10 -0
  9. package/dist/components/atoms/Icon/Icon.js +102 -0
  10. package/dist/components/atoms/Icon/icons/ArrivalIcon.d.ts +8 -0
  11. package/dist/components/atoms/Icon/icons/ArrivalIcon.js +31 -0
  12. package/dist/components/atoms/Icon/icons/CalendarIcon.d.ts +12 -0
  13. package/dist/components/atoms/Icon/icons/CalendarIcon.js +41 -0
  14. package/dist/components/atoms/Icon/icons/CarIcon.d.ts +8 -0
  15. package/dist/components/atoms/Icon/icons/CarIcon.js +30 -0
  16. package/dist/components/atoms/Icon/icons/Check.d.ts +8 -0
  17. package/dist/components/atoms/Icon/icons/Check.js +30 -0
  18. package/dist/components/atoms/Icon/icons/CheckCircleIcon.d.ts +8 -0
  19. package/dist/components/atoms/Icon/icons/CheckCircleIcon.js +30 -0
  20. package/dist/components/atoms/Icon/icons/Chevron.d.ts +9 -0
  21. package/dist/components/atoms/Icon/icons/Chevron.js +54 -0
  22. package/dist/components/atoms/Icon/icons/ChevronDownIcon.d.ts +8 -0
  23. package/dist/components/atoms/Icon/icons/ChevronDownIcon.js +30 -0
  24. package/dist/components/atoms/Icon/icons/Close.d.ts +8 -0
  25. package/dist/components/atoms/Icon/icons/Close.js +30 -0
  26. package/dist/components/atoms/Icon/icons/DeleteIcon.d.ts +8 -0
  27. package/dist/components/atoms/Icon/icons/DeleteIcon.js +30 -0
  28. package/dist/components/atoms/Icon/icons/DepartureIcon.d.ts +8 -0
  29. package/dist/components/atoms/Icon/icons/DepartureIcon.js +30 -0
  30. package/dist/components/atoms/Icon/icons/EyeIcon.d.ts +8 -0
  31. package/dist/components/atoms/Icon/icons/EyeIcon.js +30 -0
  32. package/dist/components/atoms/Icon/icons/FacebookIcon.d.ts +8 -0
  33. package/dist/components/atoms/Icon/icons/FacebookIcon.js +36 -0
  34. package/dist/components/atoms/Icon/icons/InfoIcon.d.ts +8 -0
  35. package/dist/components/atoms/Icon/icons/InfoIcon.js +30 -0
  36. package/dist/components/atoms/Icon/icons/LinkedInIcon.d.ts +8 -0
  37. package/dist/components/atoms/Icon/icons/LinkedInIcon.js +36 -0
  38. package/dist/components/atoms/Icon/icons/MapPinIcon.d.ts +8 -0
  39. package/dist/components/atoms/Icon/icons/MapPinIcon.js +30 -0
  40. package/dist/components/atoms/Icon/icons/MautoucoLogo.d.ts +8 -0
  41. package/dist/components/atoms/Icon/icons/MautoucoLogo.js +37 -0
  42. package/dist/components/atoms/Icon/icons/MenuIcon.d.ts +8 -0
  43. package/dist/components/atoms/Icon/icons/MenuIcon.js +37 -0
  44. package/dist/components/atoms/Icon/icons/MoreIcon.d.ts +8 -0
  45. package/dist/components/atoms/Icon/icons/MoreIcon.js +30 -0
  46. package/dist/components/atoms/Icon/icons/Search.d.ts +8 -0
  47. package/dist/components/atoms/Icon/icons/Search.js +30 -0
  48. package/dist/components/atoms/Icon/icons/Settings.d.ts +8 -0
  49. package/dist/components/atoms/Icon/icons/Settings.js +30 -0
  50. package/dist/components/atoms/Icon/icons/StrollerIcon.d.ts +8 -0
  51. package/dist/components/atoms/Icon/icons/StrollerIcon.js +30 -0
  52. package/dist/components/atoms/Icon/icons/TwitterIcon.d.ts +8 -0
  53. package/dist/components/atoms/Icon/icons/TwitterIcon.js +36 -0
  54. package/dist/components/atoms/Icon/icons/User.d.ts +8 -0
  55. package/dist/components/atoms/Icon/icons/User.js +30 -0
  56. package/dist/components/atoms/Icon/icons/UserIcon.d.ts +12 -0
  57. package/dist/components/atoms/Icon/icons/UserIcon.js +41 -0
  58. package/dist/components/atoms/Icon/icons/Youtube.d.ts +8 -0
  59. package/dist/components/atoms/Icon/icons/Youtube.js +36 -0
  60. package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.d.ts +12 -0
  61. package/dist/components/atoms/Inputs/DropdownInput/DropdownInput.js +53 -0
  62. package/dist/components/atoms/Inputs/Input/Input.d.ts +15 -0
  63. package/dist/components/atoms/Inputs/Input/Input.js +27 -0
  64. package/dist/components/atoms/Inputs/Textarea/Textarea.d.ts +14 -0
  65. package/dist/components/atoms/Inputs/Textarea/Textarea.js +15 -0
  66. package/dist/components/atoms/Link/Link.d.ts +44 -0
  67. package/dist/components/atoms/Link/Link.js +76 -0
  68. package/dist/components/atoms/SelectedValue/SelectedValue.d.ts +11 -0
  69. package/dist/components/atoms/SelectedValue/SelectedValue.js +29 -0
  70. package/dist/components/atoms/Spinner/Spinner.d.ts +9 -0
  71. package/dist/components/atoms/Spinner/Spinner.js +38 -0
  72. package/dist/components/atoms/Spinner/variants/ButtonSpinner.d.ts +8 -0
  73. package/dist/components/atoms/Spinner/variants/ButtonSpinner.js +19 -0
  74. package/dist/components/atoms/Spinner/variants/LoadingSpinner.d.ts +7 -0
  75. package/dist/components/atoms/Spinner/variants/LoadingSpinner.js +7 -0
  76. package/dist/components/atoms/Tab/Tab.d.ts +22 -0
  77. package/dist/components/atoms/Tab/Tab.js +54 -0
  78. package/dist/components/atoms/Typography/Heading/Heading.d.ts +9 -0
  79. package/dist/components/atoms/Typography/Heading/Heading.js +25 -0
  80. package/dist/components/atoms/Typography/Text/Text.d.ts +10 -0
  81. package/dist/components/atoms/Typography/Text/Text.js +77 -0
  82. package/dist/components/atoms/Typography/Typography.d.ts +24 -0
  83. package/dist/components/atoms/Typography/Typography.js +100 -0
  84. package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.d.ts +29 -0
  85. package/dist/components/molecules/MultiSelectDropdown/MultiSelectDropdown.js +106 -0
  86. package/dist/components/molecules/UserCard/UserCard.d.ts +20 -0
  87. package/dist/components/molecules/UserCard/UserCard.js +57 -0
  88. package/dist/components/organisms/Footer/Footer.d.ts +38 -0
  89. package/dist/components/organisms/Footer/Footer.js +74 -0
  90. package/dist/components/organisms/TopNavigation/DesktopNav.d.ts +33 -0
  91. package/dist/components/organisms/TopNavigation/DesktopNav.js +26 -0
  92. package/dist/components/organisms/TopNavigation/MobileNav.d.ts +32 -0
  93. package/dist/components/organisms/TopNavigation/MobileNav.js +45 -0
  94. package/dist/components/organisms/TopNavigation/TopNavigation.d.ts +33 -0
  95. package/dist/components/organisms/TopNavigation/TopNavigation.js +20 -0
  96. package/dist/hooks/useMobile.d.ts +5 -0
  97. package/dist/hooks/useMobile.js +26 -0
  98. package/dist/index.d.ts +23 -0
  99. package/dist/index.js +28 -0
  100. package/dist/styles/tokens/tokens.d.ts +3108 -0
  101. package/dist/styles/tokens/tokens.js +2652 -0
  102. package/package.json +90 -0
  103. package/src/components/atoms/Avatar/Avatar.tsx +60 -0
  104. package/src/components/atoms/Button/Button.css +200 -0
  105. package/src/components/atoms/Button/Button.tsx +82 -0
  106. package/src/components/atoms/Checkbox/Checkbox.tsx +69 -0
  107. package/src/components/atoms/Icon/Icon.tsx +135 -0
  108. package/src/components/atoms/Icon/icons/ArrivalIcon.tsx +52 -0
  109. package/src/components/atoms/Icon/icons/CalendarIcon.tsx +63 -0
  110. package/src/components/atoms/Icon/icons/CarIcon.tsx +44 -0
  111. package/src/components/atoms/Icon/icons/Check.tsx +36 -0
  112. package/src/components/atoms/Icon/icons/CheckCircleIcon.tsx +48 -0
  113. package/src/components/atoms/Icon/icons/Chevron.tsx +73 -0
  114. package/src/components/atoms/Icon/icons/ChevronDownIcon.tsx +46 -0
  115. package/src/components/atoms/Icon/icons/Close.tsx +40 -0
  116. package/src/components/atoms/Icon/icons/DeleteIcon.tsx +44 -0
  117. package/src/components/atoms/Icon/icons/DepartureIcon.tsx +50 -0
  118. package/src/components/atoms/Icon/icons/EyeIcon.tsx +44 -0
  119. package/src/components/atoms/Icon/icons/FacebookIcon.tsx +50 -0
  120. package/src/components/atoms/Icon/icons/InfoIcon.tsx +44 -0
  121. package/src/components/atoms/Icon/icons/LinkedInIcon.tsx +50 -0
  122. package/src/components/atoms/Icon/icons/MapPinIcon.tsx +44 -0
  123. package/src/components/atoms/Icon/icons/MautoucoLogo.tsx +93 -0
  124. package/src/components/atoms/Icon/icons/MenuIcon.tsx +49 -0
  125. package/src/components/atoms/Icon/icons/MoreIcon.tsx +44 -0
  126. package/src/components/atoms/Icon/icons/Search.tsx +37 -0
  127. package/src/components/atoms/Icon/icons/Settings.tsx +38 -0
  128. package/src/components/atoms/Icon/icons/StrollerIcon.tsx +44 -0
  129. package/src/components/atoms/Icon/icons/TwitterIcon.tsx +50 -0
  130. package/src/components/atoms/Icon/icons/User.tsx +37 -0
  131. package/src/components/atoms/Icon/icons/UserIcon.tsx +63 -0
  132. package/src/components/atoms/Icon/icons/Youtube.tsx +50 -0
  133. package/src/components/atoms/Inputs/DropdownInput/DropdownInput.tsx +96 -0
  134. package/src/components/atoms/Inputs/Input/Input.tsx +66 -0
  135. package/src/components/atoms/Inputs/Textarea/Textarea.tsx +51 -0
  136. package/src/components/atoms/Link/Link.tsx +168 -0
  137. package/src/components/atoms/SelectedValue/SelectedValue.tsx +59 -0
  138. package/src/components/atoms/Spinner/Spinner.tsx +56 -0
  139. package/src/components/atoms/Spinner/variants/ButtonSpinner.tsx +37 -0
  140. package/src/components/atoms/Spinner/variants/LoadingSpinner.tsx +22 -0
  141. package/src/components/atoms/Tab/Tab.css +147 -0
  142. package/src/components/atoms/Tab/Tab.tsx +96 -0
  143. package/src/components/atoms/Typography/Typography.tsx +153 -0
  144. package/src/components/molecules/MultiSelectDropdown/MultiSelectDropdown.tsx +245 -0
  145. package/src/components/molecules/UserCard/UserCard.stories.tsx +36 -0
  146. package/src/components/molecules/UserCard/UserCard.tsx +173 -0
  147. package/src/components/organisms/Footer/Footer.tsx +290 -0
  148. package/src/components/organisms/TopNavigation/DesktopNav.tsx +122 -0
  149. package/src/components/organisms/TopNavigation/MobileNav.tsx +212 -0
  150. package/src/components/organisms/TopNavigation/TopNavigation.tsx +45 -0
  151. package/src/styles/components/avatar.css +58 -0
  152. package/src/styles/components/checkbox.css +132 -0
  153. package/src/styles/components/dropdown.css +214 -0
  154. package/src/styles/components/forms.css +147 -0
  155. package/src/styles/components/multiselect-dropdown.css +231 -0
  156. package/src/styles/components/organism/footer.css +113 -0
  157. package/src/styles/components/organism/topnavigation.css +162 -0
  158. package/src/styles/components/scrollbar.css +63 -0
  159. package/src/styles/components/selected-value.css +80 -0
  160. package/src/styles/components/typography.css +251 -0
  161. package/src/styles/tokens/_tokens.scss +2072 -0
  162. package/src/styles/tokens/tokens.css +2075 -0
  163. package/src/styles/tokens/tokens.js +2653 -0
@@ -0,0 +1,147 @@
1
+ .tab {
2
+ --tab-width: var(--tab-inline-lg-width, 124rem);
3
+ --tab-height: var(--tab-inline-lg-height, 78rem);
4
+ position: relative;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ box-sizing: border-box;
9
+ width: calc(var(--tab-width) / 16);
10
+ height: calc(var(--tab-height) / 16);
11
+ gap: calc(var(--tab-spacing-container-gap, 0) / 16);
12
+ padding-top: calc(var(--tab-spacing-container-padding-y, 8rem) / 16);
13
+ padding-bottom: calc(var(--tab-spacing-container-padding-y, 8rem) / 16);
14
+ padding-left: calc(var(--tab-spacing-container-padding-x, 8rem) / 16);
15
+ padding-right: calc(var(--tab-spacing-container-padding-x, 8rem) / 16);
16
+ border: none;
17
+ border-radius: calc(var(--tab-border-radius-default, 0) / 16);
18
+ border-bottom-style: solid;
19
+ border-bottom-width: calc(var(--tab-border-width-default, 4rem) / 16);
20
+ border-bottom-color: var(--tab-color-normal-default-border, #e5e5e5);
21
+ background-color: var(--tab-color-normal-default-background, #ffffff);
22
+ color: var(--color-text-state-default, #737373);
23
+ opacity: calc(var(--opacity-opacity-100, 100) / 100);
24
+ cursor: pointer;
25
+ transition:
26
+ background-color 0.2s ease-in-out,
27
+ border-color 0.2s ease-in-out,
28
+ color 0.2s ease-in-out,
29
+ box-shadow 0.2s ease-in-out,
30
+ transform 0.15s ease-in-out;
31
+ }
32
+
33
+ .tab:focus-visible {
34
+ outline: none;
35
+ box-shadow: 0 0 0 calc(2rem / 16)
36
+ var(--tab-color-normal-focused-border, #e5e5e5);
37
+ background-color: var(--tab-color-normal-focused-background, #ffffff);
38
+ }
39
+
40
+ .tab:hover:not(:disabled):not(.tab--active) {
41
+ background-color: var(--tab-color-normal-hover-background, #ededed);
42
+ border-bottom-color: var(--tab-color-normal-hover-border, #e5e5e5);
43
+ color: var(--color-text-state-hover, #404040);
44
+ }
45
+
46
+ .tab:active:not(:disabled):not(.tab--active) {
47
+ background-color: var(--tab-color-normal-pressed-background, #d9d9d9);
48
+ border-bottom-color: var(--tab-color-normal-pressed-border, #e5e5e5);
49
+ transform: translateY(calc(1rem / 16 * 1));
50
+ color: var(--color-text-state-hover, #404040);
51
+ }
52
+
53
+ .tab--active {
54
+ background-color: var(--tab-color-active-default-background, #ffffff);
55
+ border-bottom-color: var(--tab-color-active-default-border, #0f7173);
56
+ color: var(--color-text-state-filled, #262626);
57
+ }
58
+
59
+ .tab--active:hover:not(:disabled) {
60
+ border-bottom-color: var(--tab-color-active-hover-border, #115b5e);
61
+ background-color: var(--tab-color-active-hover-background, #ffffff);
62
+ }
63
+
64
+ .tab--active:active:not(:disabled) {
65
+ border-bottom-color: var(--tab-color-active-pressed-border, #042c2f);
66
+ background-color: var(--tab-color-active-pressed-background, #ffffff);
67
+ }
68
+
69
+ .tab--active:focus-visible {
70
+ box-shadow: 0 0 0 calc(2rem / 16)
71
+ var(--tab-color-active-focused-border, #042c2f);
72
+ background-color: var(--tab-color-active-focused-background, #ffffff);
73
+ }
74
+
75
+ .tab--disabled,
76
+ .tab:disabled {
77
+ cursor: not-allowed;
78
+ color: var(--color-text-state-disabled, #a3a3a3);
79
+ background-color: var(--tab-color-normal-default-background, #ffffff);
80
+ border-bottom-color: var(--tab-color-normal-default-border, #e5e5e5);
81
+ opacity: 1;
82
+ }
83
+
84
+ .tab__label {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ color: inherit;
89
+ }
90
+
91
+ .tab--variant-inline.tab--size-lg {
92
+ --tab-width: var(--tab-inline-lg-width, 124rem);
93
+ --tab-height: var(--tab-inline-lg-height, 78rem);
94
+ }
95
+
96
+ .tab--variant-inline.tab--size-sm {
97
+ --tab-width: var(--tab-inline-sm-width, 115rem);
98
+ --tab-height: var(--tab-inline-sm-height, 56rem);
99
+ }
100
+
101
+ .tab--variant-outline.tab--size-sm {
102
+ --tab-width: var(--tab-outline-sm-width, 119rem);
103
+ --tab-height: var(--tab-outline-sm-height, 56rem);
104
+ border-bottom-color: var(--tab-color-active-default-border, #0f7173);
105
+ }
106
+
107
+ .tab--variant-outline.tab--size-lg {
108
+ --tab-width: var(--tab-outline-lg-width, 129rem);
109
+ --tab-height: var(--tab-outline-lg-height, 78rem);
110
+ border-bottom-color: var(--tab-color-active-default-border, #0f7173);
111
+ }
112
+
113
+ .tab--variant-outline {
114
+ background-color: var(--tab-color-active-default-background, #ffffff);
115
+ }
116
+
117
+ .tab--variant-outline:hover:not(:disabled):not(.tab--active),
118
+ .tab--variant-outline:active:not(:disabled):not(.tab--active) {
119
+ background-color: var(--tab-color-active-hover-background, #ffffff);
120
+ }
121
+
122
+ .tab--variant-outline.tab--size-sm:hover:not(:disabled):not(.tab--active) {
123
+ border-bottom-color: var(--tab-color-active-hover-border, #115b5e);
124
+ }
125
+
126
+ .tab--variant-outline.tab--size-sm:active:not(:disabled):not(.tab--active) {
127
+ border-bottom-color: var(--tab-color-active-pressed-border, #042c2f);
128
+ }
129
+
130
+ .tab--variant-outline.tab--size-lg:hover:not(:disabled):not(.tab--active) {
131
+ border-bottom-color: var(--tab-color-active-hover-border, #115b5e);
132
+ }
133
+
134
+ .tab--variant-outline.tab--size-lg:active:not(:disabled):not(.tab--active) {
135
+ border-bottom-color: var(--tab-color-active-pressed-border, #042c2f);
136
+ }
137
+
138
+ .tab--variant-outline.tab--size-sm:focus-visible {
139
+ outline: none;
140
+ box-shadow: none;
141
+ --tab-width: var(--tab-outline-sm-focus-width, 121rem);
142
+ --tab-height: var(--tab-outline-sm-focus-height, 59rem);
143
+ border-radius: calc(var(--border-radius-rounded-sm, 4rem) / 16);
144
+ border-top: calc(var(--border-border, 1rem) / 16)
145
+ solid var(--color-atoll-green-800, #0f7173);
146
+ }
147
+
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { Text } from '../Typography/Typography';
3
+ import './Tab.css';
4
+
5
+ type TextComponentProps = React.ComponentProps<typeof Text>;
6
+
7
+ export interface TabProps
8
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
9
+ /** Textual content rendered inside the tab */
10
+ children: React.ReactNode;
11
+ /** Whether the tab is currently active */
12
+ isActive?: boolean;
13
+ /** Visual style family for the tab */
14
+ variant?: 'inline' | 'outline';
15
+ /** Size token used by the selected variant */
16
+ size?: 'sm' | 'lg';
17
+ /** Typography variant when the tab is active */
18
+ activeTextVariant?: TextComponentProps['variant'];
19
+ /** Typography variant when the tab is inactive */
20
+ inactiveTextVariant?: TextComponentProps['variant'];
21
+ /** Typography size applied to the tab label */
22
+ textSize?: TextComponentProps['size'];
23
+ }
24
+
25
+ const Tab = React.forwardRef<HTMLButtonElement, TabProps>(
26
+ (
27
+ {
28
+ children,
29
+ isActive = false,
30
+ disabled = false,
31
+ className = '',
32
+ variant = 'inline',
33
+ size = 'lg',
34
+ activeTextVariant,
35
+ inactiveTextVariant,
36
+ textSize,
37
+ ...rest
38
+ },
39
+ ref
40
+ ) => {
41
+ const { role: roleProp, type: typeProp, ...buttonRest } = rest;
42
+
43
+ const tabClasses = [
44
+ 'tab',
45
+ `tab--variant-${variant}`,
46
+ `tab--size-${size}`,
47
+ isActive ? 'tab--active' : '',
48
+ disabled ? 'tab--disabled' : '',
49
+ className,
50
+ ]
51
+ .filter(Boolean)
52
+ .join(' ');
53
+
54
+ const defaultTypographyVariant = variant === 'outline' ? 'bold' : 'regular';
55
+ const resolvedActiveTextVariant = activeTextVariant ?? defaultTypographyVariant;
56
+ const resolvedInactiveTextVariant =
57
+ inactiveTextVariant ?? defaultTypographyVariant;
58
+ const resolvedVariant = isActive
59
+ ? resolvedActiveTextVariant
60
+ : resolvedInactiveTextVariant;
61
+ const resolvedTextSize =
62
+ textSize ??
63
+ (variant === 'outline'
64
+ ? 'base'
65
+ : size === 'lg'
66
+ ? 'lg'
67
+ : 'base');
68
+
69
+ return (
70
+ <button
71
+ {...buttonRest}
72
+ ref={ref}
73
+ type={typeProp ?? 'button'}
74
+ role={roleProp ?? 'tab'}
75
+ aria-selected={isActive}
76
+ aria-disabled={disabled || undefined}
77
+ disabled={disabled}
78
+ className={tabClasses}
79
+ >
80
+ <Text
81
+ as="span"
82
+ size={resolvedTextSize}
83
+ variant={resolvedVariant}
84
+ className="tab__label"
85
+ >
86
+ {children}
87
+ </Text>
88
+ </button>
89
+ );
90
+ }
91
+ );
92
+
93
+ Tab.displayName = 'Tab';
94
+
95
+ export default Tab;
96
+
@@ -0,0 +1,153 @@
1
+ import React from 'react';
2
+
3
+ type BodyTokenSize = 'xs' | 'sm' | 'md' | 'lg';
4
+ type BodySizeInput = BodyTokenSize | 'base' | 'xl';
5
+ type Variant = 'light' | 'regular' | 'medium' | 'bold' | 'black';
6
+ type Leading = 'none' | '4' | '5' | '6' | '7';
7
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
8
+
9
+ interface BaseProps {
10
+ variant?: Variant;
11
+ leading?: Leading;
12
+ className?: string;
13
+ children: React.ReactNode;
14
+ onClick?: React.MouseEventHandler<Element>;
15
+ }
16
+
17
+ interface HeadingProps extends BaseProps {
18
+ level?: HeadingLevel;
19
+ as?: keyof React.JSX.IntrinsicElements;
20
+ }
21
+
22
+ interface TextProps extends BaseProps {
23
+ size?: BodySizeInput;
24
+ as?: keyof React.JSX.IntrinsicElements;
25
+ }
26
+
27
+ const headingTokenClass: Record<HeadingLevel, string> = {
28
+ 1: 'heading-3xl',
29
+ 2: 'heading-2xl',
30
+ 3: 'heading-xl',
31
+ 4: 'heading-lg',
32
+ 5: 'heading-md',
33
+ 6: 'heading-sm',
34
+ };
35
+
36
+ const leadingFallback: Record<Leading, string> = {
37
+ none: 'leading-none',
38
+ '4': 'leading-4',
39
+ '5': 'leading-5',
40
+ '6': 'leading-6',
41
+ '7': 'leading-7',
42
+ };
43
+
44
+ const bodySizeAlias: Record<BodySizeInput, BodyTokenSize> = {
45
+ xs: 'xs',
46
+ sm: 'sm',
47
+ md: 'md',
48
+ lg: 'lg',
49
+ base: 'md',
50
+ xl: 'lg',
51
+ };
52
+
53
+ const defaultBodyLeading: Record<BodyTokenSize, Leading> = {
54
+ xs: '5',
55
+ sm: '6',
56
+ md: '6',
57
+ lg: '7',
58
+ };
59
+
60
+ const tokenBodyLeadings: Record<BodyTokenSize, Array<'leading-none' | 'leading-4' | 'leading-5' | 'leading-6' | 'leading-7'>> = {
61
+ xs: ['leading-5', 'leading-4', 'leading-none'],
62
+ sm: ['leading-6', 'leading-5', 'leading-none'],
63
+ md: ['leading-6', 'leading-7', 'leading-none'],
64
+ lg: ['leading-7', 'leading-6', 'leading-none'],
65
+ };
66
+
67
+ const mapLeadingToToken = (leading: Leading): 'leading-none' | 'leading-4' | 'leading-5' | 'leading-6' | 'leading-7' => {
68
+ if (leading === 'none') return 'leading-none';
69
+ return `leading-${leading}` as 'leading-4' | 'leading-5' | 'leading-6' | 'leading-7';
70
+ };
71
+
72
+ const getBodyClass = (
73
+ size: BodyTokenSize,
74
+ leading?: Leading
75
+ ): { className: string; lineHeightHandled: boolean } => {
76
+ const availableLeadings = tokenBodyLeadings[size];
77
+ const desiredLeading = leading ? mapLeadingToToken(leading) : mapLeadingToToken(defaultBodyLeading[size]);
78
+
79
+ const resolvedLeading = availableLeadings.includes(desiredLeading)
80
+ ? desiredLeading
81
+ : availableLeadings[0];
82
+
83
+ return {
84
+ className: `body-${size}-${resolvedLeading}`,
85
+ lineHeightHandled: desiredLeading === resolvedLeading,
86
+ };
87
+ };
88
+
89
+ const buildVariantClass = (base: string, variant: Variant): string | undefined => {
90
+ if (!base) return undefined;
91
+ return `${base}--${variant}`;
92
+ };
93
+
94
+
95
+ export const Heading: React.FC<HeadingProps> = ({
96
+ level = 2,
97
+ variant = 'bold',
98
+ leading,
99
+ as,
100
+ className = '',
101
+ children,
102
+ onClick,
103
+ }) => {
104
+ const Tag = as || (`h${level}` as keyof React.JSX.IntrinsicElements);
105
+ const baseClass = headingTokenClass[level] ?? headingTokenClass[2];
106
+ const variantClass = buildVariantClass(baseClass, variant);
107
+
108
+ const classes = [
109
+ baseClass,
110
+ variantClass,
111
+ leading ? leadingFallback[leading] : '',
112
+ className,
113
+ ]
114
+ .filter(Boolean)
115
+ .join(' ');
116
+
117
+ return (
118
+ <Tag className={classes} onClick={onClick}>
119
+ {children}
120
+ </Tag>
121
+ );
122
+ };
123
+
124
+
125
+ export const Text: React.FC<TextProps> = ({
126
+ size = 'md',
127
+ variant = 'regular',
128
+ leading,
129
+ as,
130
+ className = '',
131
+ children,
132
+ onClick,
133
+ }) => {
134
+ const Tag = as || 'p';
135
+ const normalizedSize = bodySizeAlias[size];
136
+ const { className: baseClass, lineHeightHandled } = getBodyClass(normalizedSize, leading);
137
+ const variantClass = buildVariantClass(baseClass, variant);
138
+
139
+ const classes = [
140
+ baseClass,
141
+ variantClass,
142
+ leading && !lineHeightHandled ? leadingFallback[leading] : '',
143
+ className,
144
+ ]
145
+ .filter(Boolean)
146
+ .join(' ');
147
+
148
+ return (
149
+ <Tag className={classes} onClick={onClick}>
150
+ {children}
151
+ </Tag>
152
+ );
153
+ };
@@ -0,0 +1,245 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import DropdownInput from '../../atoms/Inputs/DropdownInput/DropdownInput';
3
+ import Input from '../../atoms/Inputs/Input/Input';
4
+ import Checkbox from '../../atoms/Checkbox/Checkbox';
5
+ import SelectedValue from '../../atoms/SelectedValue/SelectedValue';
6
+ import { Text } from '../../atoms/Typography/Typography';
7
+
8
+ export interface MultiSelectOption {
9
+ id: string;
10
+ label: string;
11
+ disabled?: boolean;
12
+ imageUrl?: string;
13
+ }
14
+
15
+ export interface MultiSelectGroup {
16
+ id: string;
17
+ label: string;
18
+ options: MultiSelectOption[];
19
+ imageUrl?: string;
20
+ }
21
+
22
+ export interface MultiSelectDropdownProps {
23
+ options: MultiSelectOption[];
24
+ selectedValues: string[];
25
+ placeholder?: string;
26
+ onSelectionChange: (selectedIds: string[]) => void;
27
+ disabled?: boolean;
28
+ error?: boolean;
29
+ className?: string;
30
+ maxDisplayedChips?: number;
31
+ selectedValueVariant?: 'filled' | 'text';
32
+ searchable?: boolean;
33
+ searchPlaceholder?: string;
34
+ groups?: MultiSelectGroup[];
35
+ }
36
+
37
+ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({
38
+ options,
39
+ selectedValues,
40
+ placeholder = 'Select items',
41
+ onSelectionChange,
42
+ disabled = false,
43
+ error = false,
44
+ className = '',
45
+ maxDisplayedChips = 3,
46
+ selectedValueVariant = 'filled',
47
+ searchable = false,
48
+ searchPlaceholder = 'Search',
49
+ groups
50
+ }) => {
51
+ const [isOpen, setIsOpen] = useState(false);
52
+ const [query, setQuery] = useState('');
53
+ const dropdownRef = useRef<HTMLDivElement>(null);
54
+
55
+ // Close dropdown when clicking outside
56
+ useEffect(() => {
57
+ const handleClickOutside = (event: MouseEvent) => {
58
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
59
+ setIsOpen(false);
60
+ }
61
+ };
62
+
63
+ document.addEventListener('mousedown', handleClickOutside);
64
+ return () => {
65
+ document.removeEventListener('mousedown', handleClickOutside);
66
+ };
67
+ }, []);
68
+
69
+ const handleToggleDropdown = () => {
70
+ if (!disabled) {
71
+ setIsOpen(!isOpen);
72
+ }
73
+ };
74
+
75
+ const handleOptionToggle = (optionId: string) => {
76
+ if (disabled) return;
77
+
78
+ const isSelected = selectedValues.includes(optionId);
79
+ let newSelectedValues: string[];
80
+
81
+ if (isSelected) {
82
+ newSelectedValues = selectedValues.filter(id => id !== optionId);
83
+ } else {
84
+ newSelectedValues = [...selectedValues, optionId];
85
+ }
86
+
87
+
88
+ onSelectionChange(newSelectedValues);
89
+ };
90
+
91
+ const handleRemoveChip = (optionId: string) => {
92
+ if (disabled) return;
93
+
94
+ const newSelectedValues = selectedValues.filter(id => id !== optionId);
95
+ onSelectionChange(newSelectedValues);
96
+ };
97
+
98
+
99
+ const getSelectedOptions = () => {
100
+ return options.filter(option => selectedValues.includes(option.id));
101
+ };
102
+ const getFilteredOptions = () => {
103
+ if (!searchable || query.trim() === '') return options;
104
+ const q = query.toLowerCase();
105
+ return options.filter(o => o.label.toLowerCase().includes(q));
106
+ };
107
+
108
+
109
+ const getDisplayedChips = () => {
110
+ const selectedOptions = getSelectedOptions();
111
+ return selectedOptions.slice(0, maxDisplayedChips);
112
+ };
113
+
114
+ const getHiddenCount = () => {
115
+ return Math.max(0, selectedValues.length - maxDisplayedChips);
116
+ };
117
+
118
+ const getDropdownState = () => {
119
+ if (disabled) return 'disabled';
120
+ if (error) return 'error';
121
+ if (selectedValues.length > 0) return 'selected';
122
+ return 'default';
123
+ };
124
+
125
+ return (
126
+ <div ref={dropdownRef} className={`multiselect-dropdown ${className}`}>
127
+ <div className="multiselect-dropdown__input-container">
128
+ <DropdownInput
129
+ placeholder={selectedValues.length === 0 ? placeholder : ''}
130
+ state={getDropdownState()}
131
+ onClick={handleToggleDropdown}
132
+ className="multiselect-dropdown__input"
133
+ />
134
+
135
+ {/* Display selected chips */}
136
+ {selectedValues.length > 0 && (
137
+ <div className="multiselect-dropdown__chips">
138
+ {getDisplayedChips().map((option) => (
139
+ <SelectedValue
140
+ key={option.id}
141
+ value={option.label}
142
+ onRemove={() => handleRemoveChip(option.id)}
143
+ className="multiselect-dropdown__chip"
144
+ size="xs"
145
+ iconSize="xs"
146
+ variant={selectedValueVariant}
147
+ />
148
+ ))}
149
+ {getHiddenCount() > 0 && (
150
+ <div className="multiselect-dropdown__hidden-count">
151
+ +{getHiddenCount()}
152
+ </div>
153
+ )}
154
+ </div>
155
+ )}
156
+ </div>
157
+
158
+ {isOpen && (
159
+ <div className="multiselect-dropdown__panel">
160
+ <div className="multiselect-dropdown__menu">
161
+ {searchable && (
162
+ <div className="multiselect-dropdown__search">
163
+ <Input
164
+ placeholder={searchPlaceholder}
165
+ value={query}
166
+ onChange={(e) => setQuery(e.target.value)}
167
+ icon="search"
168
+ iconPosition="trailing"
169
+ />
170
+ </div>
171
+ )}
172
+ <div className="multiselect-dropdown__options">
173
+ {groups && groups.length > 0 ? (
174
+ groups.map((group) => (
175
+ <div key={group.id} className="multiselect-dropdown__group">
176
+ <div className="multiselect-dropdown__group-header">
177
+ {group.imageUrl && (
178
+ <span className="multiselect-dropdown__group-icon" aria-hidden="true">
179
+ <img src={group.imageUrl} alt="" />
180
+ </span>
181
+ )}
182
+ <Text size="sm" variant="medium">{group.label}</Text>
183
+ </div>
184
+ <div className="multiselect-dropdown__group-options">
185
+ {getFilteredOptions()
186
+ .filter(o => group.options.some(go => go.id === o.id))
187
+ .map((option) => (
188
+ <div key={option.id} className="multiselect-dropdown__option">
189
+ <Checkbox
190
+ checked={selectedValues.includes(option.id)}
191
+ disabled={option.disabled || disabled}
192
+ label={option.label}
193
+ leadingContent={option.imageUrl ? (
194
+ <span className="multiselect-dropdown__option-icon" aria-hidden="true">
195
+ <img src={option.imageUrl} alt="" />
196
+ </span>
197
+ ) : undefined}
198
+ onChange={(checked) => {
199
+ if (checked !== selectedValues.includes(option.id)) {
200
+ handleOptionToggle(option.id);
201
+ }
202
+ }}
203
+ />
204
+ </div>
205
+ ))}
206
+ </div>
207
+ </div>
208
+ ))
209
+ ) : (
210
+ getFilteredOptions().map((option) => (
211
+ <div key={option.id} className="multiselect-dropdown__option">
212
+ <Checkbox
213
+ checked={selectedValues.includes(option.id)}
214
+ disabled={option.disabled || disabled}
215
+ label={option.label}
216
+ leadingContent={option.imageUrl ? (
217
+ <span className="multiselect-dropdown__option-icon" aria-hidden="true">
218
+ <img src={option.imageUrl} alt="" />
219
+ </span>
220
+ ) : undefined}
221
+ onChange={(checked) => {
222
+ if (checked !== selectedValues.includes(option.id)) {
223
+ handleOptionToggle(option.id);
224
+ }
225
+ }}
226
+ />
227
+ </div>
228
+ ))
229
+ )}
230
+ </div>
231
+ </div>
232
+
233
+ {/* Footer with count (outside the menu container) */}
234
+ <div className="multiselect-dropdown__footer">
235
+ <Text size="sm" variant="regular" leading="5" className="multiselect-dropdown__footer-text">
236
+ {selectedValues.length} item selected
237
+ </Text>
238
+ </div>
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ };
244
+
245
+ export default MultiSelectDropdown;
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+
3
+ import { UserCard } from './UserCard';
4
+
5
+ const meta = {
6
+ title: 'Molecules/UserCard',
7
+ component: UserCard,
8
+ argTypes: {
9
+ onSelectAgency: { action: 'agency selected' },
10
+ }
11
+ } satisfies Meta<typeof UserCard>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args:{
19
+ user: {
20
+ name: "John Doe",
21
+ agency: "Beachcomber SA",
22
+ isAdmin: true,
23
+ },
24
+ agencies: [
25
+ { id: 1, name: "Beachcomber SA", localisation: "Mauritius" },
26
+ { id: 2, name: "Sun Resorts", localisation: "Maldives" },
27
+ { id: 3, name: "Tropical Escapes", localisation: "Seychelles" },
28
+ ],
29
+ selectedAgency: { id: 1, name: "Beachcomber SA", localisation: "Mauritius" },
30
+ },
31
+ render: (args) => (
32
+ <div className='flex justify-center w-full'>
33
+ <UserCard {...args} />
34
+ </div>
35
+ ),
36
+ };