mautourco-components 0.2.89 → 0.2.91

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/README.md CHANGED
@@ -17,15 +17,15 @@ npm install mautourco-components
17
17
  #### Option 1 : Import unique (recommandé)
18
18
 
19
19
  ```tsx
20
- // Dans votre index.tsx ou App.tsx
21
- // Importez tous les styles en une seule ligne
20
+ // In your index.tsx or App.tsx
21
+ // Import all styles in a single line
22
22
  import 'mautourco-components/dist/styles/mautourco.css';
23
23
  ```
24
24
 
25
25
  #### Option 2 : Imports individuels (si vous n'avez besoin que de certains composants)
26
26
 
27
27
  ```tsx
28
- // Importez uniquement les styles dont vous avez besoin
28
+ // Import only the styles you need
29
29
  import 'mautourco-components/dist/styles/tokens/tokens.css';
30
30
  import 'mautourco-components/dist/styles/components/forms.css';
31
31
  import 'mautourco-components/dist/styles/components/typography.css';
@@ -86,7 +86,7 @@ const Logo = () => <Icon name="mautourcoLogo" size="md" />;
86
86
 
87
87
  function App() {
88
88
  const handleLinkClick = (link: { label: string; route: string }) => {
89
- // Gérer la navigation (ex: avec React Router)
89
+ // Handle navigation (e.g., with React Router)
90
90
  console.log('Navigation vers:', link.route);
91
91
  };
92
92
 
@@ -102,7 +102,7 @@ function App() {
102
102
  onLogout={() => console.log('Logout')}
103
103
  />
104
104
 
105
- {/* Votre contenu principal */}
105
+ {/* Your main content */}
106
106
  <main>...</main>
107
107
 
108
108
  <Footer
@@ -185,7 +185,7 @@ function App() {
185
185
  ## Mise à jour du package
186
186
 
187
187
  ```bash
188
- # Dans votre projet
188
+ # In your project
189
189
  npm update mautourco-components
190
190
  ```
191
191
 
@@ -196,7 +196,9 @@ npm update mautourco-components
196
196
  ```bash
197
197
  npm run build:package # Build du package pour distribution
198
198
  npm run build-tokens # Génération des design tokens
199
- npm run storybook # Démarre Storybook
199
+ npm run storybook # Démarre Storybook en mode développement
200
+ npm run storybook:dev # Démarre Storybook avec watch des tokens
201
+ npm run build-storybook # Build Storybook pour production
200
202
  npm publish # Publication sur npm (après login)
201
203
  ```
202
204
 
@@ -0,0 +1,41 @@
1
+ import { ReactNode } from 'react';
2
+ import { IconName } from '../../atoms/Icon/icons/registry';
3
+ export type AccordionVariant = 'normal' | 'brand';
4
+ export interface AccordionProps {
5
+ /** Title text displayed in the accordion header */
6
+ title: string;
7
+ /** Content to be displayed when accordion is expanded */
8
+ children: ReactNode;
9
+ /** Whether the accordion is expanded by default */
10
+ defaultExpanded?: boolean;
11
+ /** Controlled expanded state */
12
+ expanded?: boolean;
13
+ /** Callback when accordion expanded state changes */
14
+ onExpandedChange?: (expanded: boolean) => void;
15
+ /** Leading icon name to display before the title */
16
+ leadingIcon?: IconName;
17
+ /** Whether the accordion is disabled */
18
+ disabled?: boolean;
19
+ /** Visual variant of the accordion */
20
+ variant?: AccordionVariant;
21
+ /** Additional CSS classes */
22
+ className?: string;
23
+ /** ID for accessibility */
24
+ id?: string;
25
+ }
26
+ /**
27
+ * Accordion component - A collapsible content container that expands or collapses
28
+ * when the header is clicked or tapped.
29
+ *
30
+ * @example
31
+ * <Accordion title="Accordion title">
32
+ * Content goes here
33
+ * </Accordion>
34
+ *
35
+ * @example
36
+ * <Accordion title="With leading icon" leadingIcon="info">
37
+ * Content with a leading icon
38
+ * </Accordion>
39
+ */
40
+ declare function Accordion(props: AccordionProps): import("react/jsx-runtime").JSX.Element;
41
+ export default Accordion;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useId, useState } from 'react';
3
+ import Icon from '../../atoms/Icon/Icon';
4
+ /**
5
+ * Accordion component - A collapsible content container that expands or collapses
6
+ * when the header is clicked or tapped.
7
+ *
8
+ * @example
9
+ * <Accordion title="Accordion title">
10
+ * Content goes here
11
+ * </Accordion>
12
+ *
13
+ * @example
14
+ * <Accordion title="With leading icon" leadingIcon="info">
15
+ * Content with a leading icon
16
+ * </Accordion>
17
+ */
18
+ function Accordion(props) {
19
+ var title = props.title, children = props.children, _a = props.defaultExpanded, defaultExpanded = _a === void 0 ? false : _a, controlledExpanded = props.expanded, onExpandedChange = props.onExpandedChange, leadingIcon = props.leadingIcon, _b = props.disabled, disabled = _b === void 0 ? false : _b, _c = props.variant, variant = _c === void 0 ? 'normal' : _c, _d = props.className, className = _d === void 0 ? '' : _d, providedId = props.id;
20
+ var generatedId = useId();
21
+ var id = providedId || generatedId;
22
+ var headerId = "".concat(id, "-header");
23
+ var contentId = "".concat(id, "-content");
24
+ // Handle controlled vs uncontrolled state
25
+ var _e = useState(defaultExpanded), internalExpanded = _e[0], setInternalExpanded = _e[1];
26
+ var isControlled = controlledExpanded !== undefined;
27
+ var isExpanded = isControlled ? controlledExpanded : internalExpanded;
28
+ var handleToggle = useCallback(function () {
29
+ if (disabled)
30
+ return;
31
+ var newExpanded = !isExpanded;
32
+ if (!isControlled) {
33
+ setInternalExpanded(newExpanded);
34
+ }
35
+ onExpandedChange === null || onExpandedChange === void 0 ? void 0 : onExpandedChange(newExpanded);
36
+ }, [disabled, isExpanded, isControlled, onExpandedChange]);
37
+ var handleKeyDown = useCallback(function (event) {
38
+ if (disabled)
39
+ return;
40
+ if (event.key === 'Enter' || event.key === ' ') {
41
+ event.preventDefault();
42
+ handleToggle();
43
+ }
44
+ }, [disabled, handleToggle]);
45
+ var classNames = [
46
+ 'accordion',
47
+ isExpanded && 'accordion--expanded',
48
+ disabled && 'accordion--disabled',
49
+ variant === 'brand' && 'accordion--brand',
50
+ className,
51
+ ]
52
+ .filter(Boolean)
53
+ .join(' ');
54
+ return (_jsxs("div", { className: classNames, id: id, children: [_jsxs("div", { className: "accordion__header", id: headerId, role: "button", tabIndex: disabled ? -1 : 0, "aria-expanded": isExpanded, "aria-controls": contentId, "aria-disabled": disabled, onClick: handleToggle, onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "accordion__title", children: [leadingIcon && (_jsx("div", { className: "accordion__leading-icon", children: _jsx(Icon, { name: leadingIcon, size: "lg" }) })), _jsx("span", { className: "accordion__title-text", children: title })] }), _jsx("div", { className: "accordion__chevron", children: _jsx(Icon, { name: 'chevron-up', size: "lg" }) })] }), isExpanded && (_jsx("div", { className: "accordion__body", id: contentId, role: "region", "aria-labelledby": headerId, children: _jsx("div", { className: "accordion__content", children: children }) }))] }));
55
+ }
56
+ export default Accordion;
@@ -0,0 +1,2 @@
1
+ export { default as Accordion } from './Accordion';
2
+ export type { AccordionProps, AccordionVariant } from './Accordion';
@@ -0,0 +1 @@
1
+ export { default as Accordion } from './Accordion';
@@ -27,6 +27,7 @@ import Icon from "../../atoms/Icon/Icon";
27
27
  import SegmentedButton from "../../atoms/SegmentedButton/SegmentedButton";
28
28
  import { Heading, Text } from "../../atoms/Typography/Typography";
29
29
  import Toast from "../../molecules/Toast/Toast";
30
+ import { DEFAULT_PAX_DATA_WITH_ADULTS } from "../PaxSelector/PaxSelector";
30
31
  import RoundTrip from "../RoundTrip/RoundTrip";
31
32
  import TransferLine from "../TransferLine/TransferLine";
32
33
  var transferIdCounter = 0;
@@ -63,15 +64,40 @@ var SearchBarTransfer = function (_a) {
63
64
  { value: "roundtrip", label: "Round trip" },
64
65
  { value: "custom", label: "Custom transfer" },
65
66
  ];
66
- // Helper function to check if roundtrip data is empty or has data
67
+ // Helper function to check if pax data is at default state (unchanged by user)
68
+ var isDefaultPaxData = function (paxData) {
69
+ if (!paxData)
70
+ return true;
71
+ // Compare with DEFAULT_PAX_DATA_WITH_ADULTS
72
+ return (paxData.adults === DEFAULT_PAX_DATA_WITH_ADULTS.adults &&
73
+ paxData.teens === DEFAULT_PAX_DATA_WITH_ADULTS.teens &&
74
+ paxData.children === DEFAULT_PAX_DATA_WITH_ADULTS.children &&
75
+ paxData.infants === DEFAULT_PAX_DATA_WITH_ADULTS.infants);
76
+ };
77
+ // Helper function to check if roundtrip data is empty or has data (at least one field)
67
78
  var hasRoundtripData = function (data) {
79
+ var _a, _b;
68
80
  if (!data)
69
81
  return false;
70
- // Check if at least one field has data (half or full data)
71
- return !!(data.paxData ||
82
+ // Check if pax data is not default (user actually changed it)
83
+ var hasNonDefaultPax = data.paxData && !isDefaultPaxData(data.paxData);
84
+ var airportDroppOffPoint = (_a = locations.options) === null || _a === void 0 ? void 0 : _a.find(function (option) { return option.type === 'airport' || option.type === 'port'; });
85
+ // Check if pickupDropoffPoint has changed from default
86
+ var hasChangedPickupDropoff = ((_b = data.pickupDropoffPoint) === null || _b === void 0 ? void 0 : _b.id) !== (airportDroppOffPoint === null || airportDroppOffPoint === void 0 ? void 0 : airportDroppOffPoint.id);
87
+ return !!(hasNonDefaultPax ||
72
88
  data.arrivalDate ||
73
89
  data.departureDate ||
74
- data.pickupDropoffPoint ||
90
+ hasChangedPickupDropoff ||
91
+ data.accommodation);
92
+ };
93
+ // Helper function to check if ALL roundtrip data fields are filled
94
+ var isRoundtripDataComplete = function (data) {
95
+ if (!data)
96
+ return false;
97
+ return !!(data.paxData &&
98
+ data.arrivalDate &&
99
+ data.departureDate &&
100
+ data.pickupDropoffPoint &&
75
101
  data.accommodation);
76
102
  };
77
103
  // Helper function to create prefilled transfer lines from roundtrip data
@@ -125,11 +151,26 @@ var SearchBarTransfer = function (_a) {
125
151
  setMode(transferMode);
126
152
  setError(null);
127
153
  };
154
+ // Get the last inputted pax data from existing transfer lines
155
+ var getLastInputtedPaxData = function () {
156
+ // Find the last transfer line that has pax data
157
+ for (var i = transferLines.length - 1; i >= 0; i--) {
158
+ if (transferLines[i].paxData) {
159
+ console.log({ i: i });
160
+ return transferLines[i].paxData;
161
+ }
162
+ }
163
+ return undefined;
164
+ };
128
165
  // Handle adding transfer lines
129
166
  var handleAddTransfer = function (type) {
167
+ // Get the last inputted pax data to prefill the new transfer
168
+ var lastPaxData = getLastInputtedPaxData();
130
169
  var newTransfer = {
131
170
  id: generateTransferId(),
132
171
  type: type,
172
+ // Nb of pax should be the same as the last inputted nb of pax
173
+ paxData: lastPaxData,
133
174
  };
134
175
  if (transferLines.length > 0) {
135
176
  // If adding inter-hotel, set pickup point to the dropoff of latest arrival
@@ -156,7 +197,7 @@ var SearchBarTransfer = function (_a) {
156
197
  // Handle adding transfer from round-trip mode (switches to custom and adds new transfer)
157
198
  var handleAddTransferFromRoundTrip = function (type) {
158
199
  var newTransferLines = [];
159
- if (hasRoundtripData(roundTripData)) {
200
+ if (isRoundtripDataComplete(roundTripData)) {
160
201
  // Half/full Roundtrip data → prefilled arrival & departure + Adding the appropriate transfer type (2h)
161
202
  var prefilledLines = createPrefilledTransferLines(roundTripData);
162
203
  // Add the new transfer of the clicked type with prefilled data
@@ -187,6 +228,33 @@ var SearchBarTransfer = function (_a) {
187
228
  // Combine prefilled lines with the new transfer
188
229
  newTransferLines = __spreadArray(__spreadArray([], prefilledLines, true), [newTransfer], false);
189
230
  }
231
+ else if (hasRoundtripData(roundTripData)) {
232
+ var newTransfer = {
233
+ id: generateTransferId(),
234
+ type: type,
235
+ paxData: roundTripData.paxData,
236
+ // Use the appropriate date based on transfer type
237
+ transferDate: type === "arrival"
238
+ ? roundTripData.arrivalDate
239
+ : type === "departure"
240
+ ? roundTripData.departureDate
241
+ : undefined,
242
+ // Use appropriate pickup/dropoff points based on transfer type
243
+ pickupPoint: type === "arrival"
244
+ ? roundTripData.pickupDropoffPoint
245
+ : type === "departure"
246
+ ? roundTripData.accommodation
247
+ : type === "inter-hotel"
248
+ ? roundTripData.accommodation // Inter-hotel pickup = arrival's dropoff (accommodation)
249
+ : undefined,
250
+ dropoffPoint: type === "arrival"
251
+ ? roundTripData.accommodation
252
+ : type === "departure"
253
+ ? roundTripData.pickupDropoffPoint
254
+ : undefined,
255
+ };
256
+ newTransferLines = [newTransfer];
257
+ }
190
258
  else {
191
259
  // Empty Roundtrip data → Adding the appropriate transfer type (1h)
192
260
  var newTransfer = {
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Do not edit directly, this file was auto-generated.
3
+ */
4
+
5
+ /* Accordion Component Styles */
6
+
7
+ .accordion {
8
+ display: flex;
9
+ width: 100%;
10
+ flex-direction: column;
11
+ }
12
+
13
+ /* Header Base */
14
+
15
+ .accordion__header {
16
+ display: flex;
17
+ width: 100%;
18
+ cursor: pointer;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ border-width: 1px;
22
+ border-style: solid;
23
+ transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color;
24
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
25
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color;
26
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
27
+ transition-duration: 200ms;
28
+ padding: var(--accordion-spacing-body-padding-y, 16px);
29
+ background-color: var(--accordion-color-header-normal-background-default, white);
30
+ border-color: var(--accordion-color-header-normal-border-default, #a3a3a3);
31
+ border-radius: var(--accordion-border-radius-default, 8px);
32
+ }
33
+
34
+ .accordion__header:hover {
35
+ background-color: var(--accordion-color-header-normal-background-hover, #f5f5f5);
36
+ }
37
+
38
+ .accordion__header:active {
39
+ background-color: var(--accordion-color-header-normal-background-pressed, #d9d9d9);
40
+ }
41
+
42
+ .accordion__header:focus {
43
+ outline: 2px solid transparent;
44
+ outline-offset: 2px;
45
+ background-color: var(--accordion-color-header-normal-background-focused, white);
46
+ }
47
+
48
+ .accordion__header:focus-visible {
49
+ box-shadow: 0 0 0 2px var(--color-icon-default, black);
50
+ }
51
+
52
+ /* Header when expanded */
53
+
54
+ .accordion--expanded .accordion__header {
55
+ border-bottom-left-radius: 0;
56
+ border-bottom-right-radius: 0;
57
+ border-bottom-width: 0;
58
+ }
59
+
60
+ /* Disabled state */
61
+
62
+ .accordion--disabled .accordion__header {
63
+ cursor: not-allowed;
64
+ opacity: 0.5;
65
+ }
66
+
67
+ .accordion--disabled .accordion__header:hover,
68
+ .accordion--disabled .accordion__header:active {
69
+ background-color: var(--accordion-color-header-normal-background-default, white);
70
+ }
71
+
72
+ /* Title container */
73
+
74
+ .accordion__title {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: var(--accordion-spacing-title-gap, 8px);
78
+ }
79
+
80
+ /* Leading icon */
81
+
82
+ .accordion__leading-icon {
83
+ display: flex;
84
+ flex-shrink: 0;
85
+ align-items: center;
86
+ justify-content: center;
87
+ width: 24px;
88
+ height: 24px;
89
+ }
90
+
91
+ /* Title text */
92
+
93
+ .accordion__title-text {
94
+ font-weight: 700;
95
+ color: var(--accordion-color-header-normal-foreground-default, #262626);
96
+ font-family: var(--font-font-family-body, 'Satoshi', sans-serif);
97
+ font-size: var(--font-size-text-base, 16px);
98
+ line-height: var(--font-leading-leading-md, 24px);
99
+ }
100
+
101
+ /* Chevron icon */
102
+
103
+ .accordion__chevron {
104
+ display: flex;
105
+ flex-shrink: 0;
106
+ align-items: center;
107
+ justify-content: center;
108
+ transition-property: transform;
109
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
110
+ transition-duration: 200ms;
111
+ width: 24px;
112
+ height: 24px;
113
+ color: var(--accordion-color-header-normal-foreground-default, #262626);
114
+ }
115
+
116
+ .accordion--expanded .accordion__chevron {
117
+ transform: rotate(180deg);
118
+ }
119
+
120
+ /* Body */
121
+
122
+ .accordion__body {
123
+ overflow: hidden;
124
+ border-width: 1px;
125
+ border-top-width: 0px;
126
+ border-style: solid;
127
+ background-color: var(--accordion-color-select-background-default, white);
128
+ border-color: var(--accordion-color-header-normal-border-default, #a3a3a3);
129
+ border-bottom-left-radius: var(--accordion-border-radius-default, 8px);
130
+ border-bottom-right-radius: var(--accordion-border-radius-default, 8px);
131
+ }
132
+
133
+ /* Body content */
134
+
135
+ .accordion__content {
136
+ display: flex;
137
+ flex-direction: column;
138
+ padding-left: var(--accordion-spacing-body-padding-y, 16px);
139
+ padding-right: var(--accordion-spacing-body-padding-y, 16px);
140
+ padding-top: var(--accordion-spacing-body-padding-x, 8px);
141
+ padding-bottom: var(--accordion-spacing-body-padding-y, 16px);
142
+ }
143
+
144
+ /* Body text style */
145
+
146
+ .accordion__content p {
147
+ color: var(--color-text-default, #262626);
148
+ font-family: var(--font-font-family-body, 'Satoshi', sans-serif);
149
+ font-size: var(--font-size-text-base, 16px);
150
+ line-height: var(--font-leading-leading-md, 24px);
151
+ }
152
+
153
+ /* Animation */
154
+
155
+ .accordion__body--entering,
156
+ .accordion__body--exiting {
157
+ transition-property: all;
158
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
159
+ transition-duration: 200ms;
160
+ }
161
+
162
+ .accordion__body--collapsed {
163
+ max-height: 0;
164
+ opacity: 0;
165
+ }
166
+
167
+ .accordion__body--expanded {
168
+ opacity: 1;
169
+ }
170
+
171
+ /* Brand variant */
172
+
173
+ .accordion--brand .accordion__title-text {
174
+ color: var(--accordion-color-header-brand-foreground-default, var(--color-text-accent));
175
+ }
176
+
177
+ .accordion--brand .accordion__chevron {
178
+ color: var(--accordion-color-header-brand-foreground-default, var(--color-text-accent));
179
+ }
180
+
181
+ /* Scroll style variant */
182
+
183
+ .accordion--scroll .accordion__content {
184
+ flex-direction: row;
185
+ align-items: center;
186
+ gap: var(--accordion-spacing-body-gap, 10px);
187
+ }
188
+
189
+ .accordion--scroll .accordion__content-inner {
190
+ flex: 1 1 0%;
191
+ overflow-y: auto;
192
+ max-height: 135px;
193
+ }
@@ -10,6 +10,7 @@
10
10
  @import "./components/forms.css";
11
11
  @import "./components/illustration.css";
12
12
  @import "./components/molecule/accomodation-docket.css";
13
+ @import "./components/molecule/accordion.css";
13
14
  @import "./components/molecule/age-selector.css";
14
15
  @import "./components/molecule/calendarInput.css";
15
16
  @import "./components/molecule/dateTime.css";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mautourco-components",
3
- "version": "0.2.89",
3
+ "version": "0.2.91",
4
4
  "private": false,
5
5
  "description": "Bibliothèque de composants Mautourco pour le redesign",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,139 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useState } from 'react';
3
+ import Accordion from './Accordion';
4
+
5
+ const meta = {
6
+ title: 'Molecules/Accordion',
7
+ component: Accordion,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ argTypes: {
12
+ onExpandedChange: { action: 'expanded changed' },
13
+ variant: {
14
+ control: { type: 'select' },
15
+ options: ['normal', 'brand'],
16
+ },
17
+ leadingIcon: {
18
+ control: { type: 'select' },
19
+ options: [undefined, 'info', 'calendar', 'user', 'settings', 'booking', 'map-pin'],
20
+ },
21
+ },
22
+ } satisfies Meta<typeof Accordion>;
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ title: 'Accordion title',
31
+ children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
32
+ },
33
+ };
34
+
35
+ export const Expanded: Story = {
36
+ args: {
37
+ title: 'Accordion title',
38
+ children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
39
+ defaultExpanded: true,
40
+ },
41
+ };
42
+
43
+ export const WithLeadingIcon: Story = {
44
+ args: {
45
+ title: 'Accordion title',
46
+ children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
47
+ leadingIcon: 'booking',
48
+ },
49
+ };
50
+
51
+ export const WithLeadingIconExpanded: Story = {
52
+ args: {
53
+ title: 'Accordion title',
54
+ children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
55
+ leadingIcon: 'booking',
56
+ defaultExpanded: true,
57
+ },
58
+ };
59
+
60
+ export const Disabled: Story = {
61
+ args: {
62
+ title: 'Accordion title',
63
+ children: 'This content is hidden because the accordion is disabled.',
64
+ disabled: true,
65
+ },
66
+ };
67
+
68
+ export const BrandVariant: Story = {
69
+ args: {
70
+ title: 'Accordion title',
71
+ children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
72
+ variant: 'brand',
73
+ leadingIcon: 'info',
74
+ },
75
+ };
76
+
77
+ export const WithCustomContent: Story = {
78
+ args: {
79
+ title: 'Flight Details',
80
+ leadingIcon: 'plane-takeoff-outline',
81
+ children: (
82
+ <div className="flex flex-col gap-2">
83
+ <div className="flex justify-between">
84
+ <span className="text-neutral-500">Departure</span>
85
+ <span className="font-medium">10:00 AM</span>
86
+ </div>
87
+ <div className="flex justify-between">
88
+ <span className="text-neutral-500">Arrival</span>
89
+ <span className="font-medium">2:30 PM</span>
90
+ </div>
91
+ <div className="flex justify-between">
92
+ <span className="text-neutral-500">Duration</span>
93
+ <span className="font-medium">4h 30m</span>
94
+ </div>
95
+ </div>
96
+ ),
97
+ },
98
+ };
99
+
100
+ export const MultipleAccordions: Story = {
101
+ render: () => (
102
+ <div className="flex flex-col gap-4 w-[400px]">
103
+ <Accordion title="Section 1" leadingIcon="info">
104
+ Content for section 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
105
+ </Accordion>
106
+ <Accordion title="Section 2" leadingIcon="calendar">
107
+ Content for section 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
108
+ </Accordion>
109
+ <Accordion title="Section 3" leadingIcon="settings">
110
+ Content for section 3. Ut enim ad minim veniam, quis nostrud exercitation.
111
+ </Accordion>
112
+ </div>
113
+ ),
114
+ };
115
+
116
+ export const Controlled: Story = {
117
+ render: function ControlledAccordion() {
118
+ const [expanded, setExpanded] = useState(false);
119
+
120
+ return (
121
+ <div className="flex flex-col gap-4 w-[400px]">
122
+ <button
123
+ onClick={() => setExpanded(!expanded)}
124
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
125
+ >
126
+ {expanded ? 'Collapse' : 'Expand'} Accordion
127
+ </button>
128
+ <Accordion
129
+ title="Controlled Accordion"
130
+ expanded={expanded}
131
+ onExpandedChange={setExpanded}
132
+ leadingIcon="info"
133
+ >
134
+ This accordion is controlled externally. Click the button above to toggle.
135
+ </Accordion>
136
+ </div>
137
+ );
138
+ },
139
+ };
@@ -0,0 +1,142 @@
1
+ import React, { ReactNode, useCallback, useId, useState } from 'react';
2
+ import Icon from '../../atoms/Icon/Icon';
3
+ import { IconName } from '../../atoms/Icon/icons/registry';
4
+
5
+ export type AccordionVariant = 'normal' | 'brand';
6
+
7
+ export interface AccordionProps {
8
+ /** Title text displayed in the accordion header */
9
+ title: string;
10
+ /** Content to be displayed when accordion is expanded */
11
+ children: ReactNode;
12
+ /** Whether the accordion is expanded by default */
13
+ defaultExpanded?: boolean;
14
+ /** Controlled expanded state */
15
+ expanded?: boolean;
16
+ /** Callback when accordion expanded state changes */
17
+ onExpandedChange?: (expanded: boolean) => void;
18
+ /** Leading icon name to display before the title */
19
+ leadingIcon?: IconName;
20
+ /** Whether the accordion is disabled */
21
+ disabled?: boolean;
22
+ /** Visual variant of the accordion */
23
+ variant?: AccordionVariant;
24
+ /** Additional CSS classes */
25
+ className?: string;
26
+ /** ID for accessibility */
27
+ id?: string;
28
+ }
29
+
30
+ /**
31
+ * Accordion component - A collapsible content container that expands or collapses
32
+ * when the header is clicked or tapped.
33
+ *
34
+ * @example
35
+ * <Accordion title="Accordion title">
36
+ * Content goes here
37
+ * </Accordion>
38
+ *
39
+ * @example
40
+ * <Accordion title="With leading icon" leadingIcon="info">
41
+ * Content with a leading icon
42
+ * </Accordion>
43
+ */
44
+ function Accordion(props: AccordionProps) {
45
+ const {
46
+ title,
47
+ children,
48
+ defaultExpanded = false,
49
+ expanded: controlledExpanded,
50
+ onExpandedChange,
51
+ leadingIcon,
52
+ disabled = false,
53
+ variant = 'normal',
54
+ className = '',
55
+ id: providedId,
56
+ } = props;
57
+
58
+ const generatedId = useId();
59
+ const id = providedId || generatedId;
60
+ const headerId = `${id}-header`;
61
+ const contentId = `${id}-content`;
62
+
63
+ // Handle controlled vs uncontrolled state
64
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
65
+ const isControlled = controlledExpanded !== undefined;
66
+ const isExpanded = isControlled ? controlledExpanded : internalExpanded;
67
+
68
+ const handleToggle = useCallback(() => {
69
+ if (disabled) return;
70
+
71
+ const newExpanded = !isExpanded;
72
+
73
+ if (!isControlled) {
74
+ setInternalExpanded(newExpanded);
75
+ }
76
+
77
+ onExpandedChange?.(newExpanded);
78
+ }, [disabled, isExpanded, isControlled, onExpandedChange]);
79
+
80
+ const handleKeyDown = useCallback(
81
+ (event: React.KeyboardEvent) => {
82
+ if (disabled) return;
83
+
84
+ if (event.key === 'Enter' || event.key === ' ') {
85
+ event.preventDefault();
86
+ handleToggle();
87
+ }
88
+ },
89
+ [disabled, handleToggle]
90
+ );
91
+
92
+ const classNames = [
93
+ 'accordion',
94
+ isExpanded && 'accordion--expanded',
95
+ disabled && 'accordion--disabled',
96
+ variant === 'brand' && 'accordion--brand',
97
+ className,
98
+ ]
99
+ .filter(Boolean)
100
+ .join(' ');
101
+
102
+ return (
103
+ <div className={classNames} id={id}>
104
+ <div
105
+ className="accordion__header"
106
+ id={headerId}
107
+ role="button"
108
+ tabIndex={disabled ? -1 : 0}
109
+ aria-expanded={isExpanded}
110
+ aria-controls={contentId}
111
+ aria-disabled={disabled}
112
+ onClick={handleToggle}
113
+ onKeyDown={handleKeyDown}
114
+ >
115
+ <div className="accordion__title">
116
+ {leadingIcon && (
117
+ <div className="accordion__leading-icon">
118
+ <Icon name={leadingIcon} size="lg" />
119
+ </div>
120
+ )}
121
+ <span className="accordion__title-text">{title}</span>
122
+ </div>
123
+ <div className="accordion__chevron">
124
+ <Icon name={'chevron-up'} size="lg" />
125
+ </div>
126
+ </div>
127
+
128
+ {isExpanded && (
129
+ <div
130
+ className="accordion__body"
131
+ id={contentId}
132
+ role="region"
133
+ aria-labelledby={headerId}
134
+ >
135
+ <div className="accordion__content">{children}</div>
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ export default Accordion;
@@ -0,0 +1,2 @@
1
+ export { default as Accordion } from './Accordion';
2
+ export type { AccordionProps, AccordionVariant } from './Accordion';
@@ -10,3 +10,4 @@ export type {
10
10
  CarBookingCardSkeletonProps,
11
11
  CarBookingCardSkeletonSize
12
12
  } from './CarBookingCardSkeleton';
13
+
@@ -7,6 +7,7 @@ import SegmentedButton, { SegmentedButtonOption } from "../../atoms/SegmentedBut
7
7
  import { Heading, Text } from "../../atoms/Typography/Typography";
8
8
  import { LocationGroup, LocationOption } from "../../molecules/LocationDropdown/LocationDropdown";
9
9
  import Toast from "../../molecules/Toast/Toast";
10
+ import { DEFAULT_PAX_DATA_WITH_ADULTS } from "../PaxSelector/PaxSelector";
10
11
  import RoundTrip, { RoundTripData } from "../RoundTrip/RoundTrip";
11
12
  import TransferLine, {
12
13
  TransferLineData,
@@ -102,15 +103,45 @@ const SearchBarTransfer: React.FC<SearchBarTransferProps> = ({
102
103
  { value: "custom", label: "Custom transfer" },
103
104
  ];
104
105
 
105
- // Helper function to check if roundtrip data is empty or has data
106
+ // Helper function to check if pax data is at default state (unchanged by user)
107
+ const isDefaultPaxData = (paxData?: RoundTripData['paxData']): boolean => {
108
+ if (!paxData) return true;
109
+ // Compare with DEFAULT_PAX_DATA_WITH_ADULTS
110
+ return (
111
+ paxData.adults === DEFAULT_PAX_DATA_WITH_ADULTS.adults &&
112
+ paxData.teens === DEFAULT_PAX_DATA_WITH_ADULTS.teens &&
113
+ paxData.children === DEFAULT_PAX_DATA_WITH_ADULTS.children &&
114
+ paxData.infants === DEFAULT_PAX_DATA_WITH_ADULTS.infants
115
+ );
116
+ };
117
+
118
+ // Helper function to check if roundtrip data is empty or has data (at least one field)
106
119
  const hasRoundtripData = (data?: RoundTripData): boolean => {
107
120
  if (!data) return false;
108
- // Check if at least one field has data (half or full data)
121
+ // Check if pax data is not default (user actually changed it)
122
+ const hasNonDefaultPax = data.paxData && !isDefaultPaxData(data.paxData);
123
+
124
+ const airportDroppOffPoint = locations.options?.find(option => option.type === 'airport' || option.type === 'port');
125
+ // Check if pickupDropoffPoint has changed from default
126
+ const hasChangedPickupDropoff = data.pickupDropoffPoint?.id !== airportDroppOffPoint?.id;
127
+
109
128
  return !!(
110
- data.paxData ||
111
- data.arrivalDate ||
112
- data.departureDate ||
113
- data.pickupDropoffPoint ||
129
+ hasNonDefaultPax ||
130
+ data.arrivalDate ||
131
+ data.departureDate ||
132
+ hasChangedPickupDropoff ||
133
+ data.accommodation
134
+ );
135
+ };
136
+
137
+ // Helper function to check if ALL roundtrip data fields are filled
138
+ const isRoundtripDataComplete = (data?: RoundTripData): boolean => {
139
+ if (!data) return false;
140
+ return !!(
141
+ data.paxData &&
142
+ data.arrivalDate &&
143
+ data.departureDate &&
144
+ data.pickupDropoffPoint &&
114
145
  data.accommodation
115
146
  );
116
147
  };
@@ -170,11 +201,30 @@ const SearchBarTransfer: React.FC<SearchBarTransferProps> = ({
170
201
  setError(null);
171
202
  };
172
203
 
204
+ // Get the last inputted pax data from existing transfer lines
205
+ const getLastInputtedPaxData = () => {
206
+ // Find the last transfer line that has pax data
207
+
208
+ for (let i = transferLines.length - 1; i >= 0; i--) {
209
+ if (transferLines[i].paxData) {
210
+ console.log({i});
211
+
212
+ return transferLines[i].paxData;
213
+ }
214
+ }
215
+ return undefined;
216
+ };
217
+
173
218
  // Handle adding transfer lines
174
219
  const handleAddTransfer = (type: TransferType) => {
220
+ // Get the last inputted pax data to prefill the new transfer
221
+ const lastPaxData = getLastInputtedPaxData();
222
+
175
223
  const newTransfer: TransferLineData = {
176
224
  id: generateTransferId(),
177
225
  type,
226
+ // Nb of pax should be the same as the last inputted nb of pax
227
+ paxData: lastPaxData,
178
228
  };
179
229
 
180
230
  if (transferLines.length > 0) {
@@ -236,7 +286,7 @@ const SearchBarTransfer: React.FC<SearchBarTransferProps> = ({
236
286
  const handleAddTransferFromRoundTrip = (type: TransferType) => {
237
287
  let newTransferLines: TransferLineData[] = [];
238
288
 
239
- if (hasRoundtripData(roundTripData)) {
289
+ if (isRoundtripDataComplete(roundTripData)) {
240
290
  // Half/full Roundtrip data → prefilled arrival & departure + Adding the appropriate transfer type (2h)
241
291
  const prefilledLines = createPrefilledTransferLines(roundTripData!);
242
292
 
@@ -268,6 +318,32 @@ const SearchBarTransfer: React.FC<SearchBarTransferProps> = ({
268
318
 
269
319
  // Combine prefilled lines with the new transfer
270
320
  newTransferLines = [...prefilledLines, newTransfer];
321
+ } else if (hasRoundtripData(roundTripData)) {
322
+ const newTransfer: TransferLineData = {
323
+ id: generateTransferId(),
324
+ type,
325
+ paxData: roundTripData!.paxData,
326
+ // Use the appropriate date based on transfer type
327
+ transferDate: type === "arrival"
328
+ ? roundTripData!.arrivalDate
329
+ : type === "departure"
330
+ ? roundTripData!.departureDate
331
+ : undefined,
332
+ // Use appropriate pickup/dropoff points based on transfer type
333
+ pickupPoint: type === "arrival"
334
+ ? roundTripData!.pickupDropoffPoint
335
+ : type === "departure"
336
+ ? roundTripData!.accommodation
337
+ : type === "inter-hotel"
338
+ ? roundTripData!.accommodation // Inter-hotel pickup = arrival's dropoff (accommodation)
339
+ : undefined,
340
+ dropoffPoint: type === "arrival"
341
+ ? roundTripData!.accommodation
342
+ : type === "departure"
343
+ ? roundTripData!.pickupDropoffPoint
344
+ : undefined,
345
+ };
346
+ newTransferLines = [newTransfer];
271
347
  } else {
272
348
  // Empty Roundtrip data → Adding the appropriate transfer type (1h)
273
349
  const newTransfer: TransferLineData = {
@@ -0,0 +1,146 @@
1
+ /* Accordion Component Styles */
2
+
3
+ .accordion {
4
+ @apply flex flex-col w-full;
5
+ }
6
+
7
+ /* Header Base */
8
+ .accordion__header {
9
+ @apply flex items-center justify-between w-full cursor-pointer;
10
+ @apply border border-solid;
11
+ @apply transition-colors duration-200;
12
+ padding: var(--accordion-spacing-body-padding-y, 16px);
13
+ background-color: var(--accordion-color-header-normal-background-default, white);
14
+ border-color: var(--accordion-color-header-normal-border-default, #a3a3a3);
15
+ border-radius: var(--accordion-border-radius-default, 8px);
16
+ }
17
+
18
+ .accordion__header:hover {
19
+ background-color: var(--accordion-color-header-normal-background-hover, #f5f5f5);
20
+ }
21
+
22
+ .accordion__header:active {
23
+ background-color: var(--accordion-color-header-normal-background-pressed, #d9d9d9);
24
+ }
25
+
26
+ .accordion__header:focus {
27
+ @apply outline-none;
28
+ background-color: var(--accordion-color-header-normal-background-focused, white);
29
+ }
30
+
31
+ .accordion__header:focus-visible {
32
+ box-shadow: 0 0 0 2px var(--color-icon-default, black);
33
+ }
34
+
35
+ /* Header when expanded */
36
+ .accordion--expanded .accordion__header {
37
+ border-bottom-left-radius: 0;
38
+ border-bottom-right-radius: 0;
39
+ border-bottom-width: 0;
40
+ }
41
+
42
+ /* Disabled state */
43
+ .accordion--disabled .accordion__header {
44
+ @apply cursor-not-allowed opacity-50;
45
+ }
46
+
47
+ .accordion--disabled .accordion__header:hover,
48
+ .accordion--disabled .accordion__header:active {
49
+ background-color: var(--accordion-color-header-normal-background-default, white);
50
+ }
51
+
52
+ /* Title container */
53
+ .accordion__title {
54
+ @apply flex items-center;
55
+ gap: var(--accordion-spacing-title-gap, 8px);
56
+ }
57
+
58
+ /* Leading icon */
59
+ .accordion__leading-icon {
60
+ @apply flex items-center justify-center shrink-0;
61
+ width: 24px;
62
+ height: 24px;
63
+ }
64
+
65
+ /* Title text */
66
+ .accordion__title-text {
67
+ @apply font-bold;
68
+ color: var(--accordion-color-header-normal-foreground-default, #262626);
69
+ font-family: var(--font-font-family-body, 'Satoshi', sans-serif);
70
+ font-size: var(--font-size-text-base, 16px);
71
+ line-height: var(--font-leading-leading-md, 24px);
72
+ }
73
+
74
+ /* Chevron icon */
75
+ .accordion__chevron {
76
+ @apply flex items-center justify-center shrink-0;
77
+ @apply transition-transform duration-200;
78
+ width: 24px;
79
+ height: 24px;
80
+ color: var(--accordion-color-header-normal-foreground-default, #262626);
81
+ }
82
+
83
+ .accordion--expanded .accordion__chevron {
84
+ transform: rotate(180deg);
85
+ }
86
+
87
+ /* Body */
88
+ .accordion__body {
89
+ @apply border border-solid border-t-0 overflow-hidden;
90
+ background-color: var(--accordion-color-select-background-default, white);
91
+ border-color: var(--accordion-color-header-normal-border-default, #a3a3a3);
92
+ border-bottom-left-radius: var(--accordion-border-radius-default, 8px);
93
+ border-bottom-right-radius: var(--accordion-border-radius-default, 8px);
94
+ }
95
+
96
+ /* Body content */
97
+ .accordion__content {
98
+ @apply flex flex-col;
99
+ padding-left: var(--accordion-spacing-body-padding-y, 16px);
100
+ padding-right: var(--accordion-spacing-body-padding-y, 16px);
101
+ padding-top: var(--accordion-spacing-body-padding-x, 8px);
102
+ padding-bottom: var(--accordion-spacing-body-padding-y, 16px);
103
+ }
104
+
105
+ /* Body text style */
106
+ .accordion__content p {
107
+ color: var(--color-text-default, #262626);
108
+ font-family: var(--font-font-family-body, 'Satoshi', sans-serif);
109
+ font-size: var(--font-size-text-base, 16px);
110
+ line-height: var(--font-leading-leading-md, 24px);
111
+ }
112
+
113
+ /* Animation */
114
+ .accordion__body--entering,
115
+ .accordion__body--exiting {
116
+ @apply transition-all duration-200;
117
+ }
118
+
119
+ .accordion__body--collapsed {
120
+ max-height: 0;
121
+ opacity: 0;
122
+ }
123
+
124
+ .accordion__body--expanded {
125
+ opacity: 1;
126
+ }
127
+
128
+ /* Brand variant */
129
+ .accordion--brand .accordion__title-text {
130
+ color: var(--accordion-color-header-brand-foreground-default, var(--color-text-accent));
131
+ }
132
+
133
+ .accordion--brand .accordion__chevron {
134
+ color: var(--accordion-color-header-brand-foreground-default, var(--color-text-accent));
135
+ }
136
+
137
+ /* Scroll style variant */
138
+ .accordion--scroll .accordion__content {
139
+ @apply flex-row items-center;
140
+ gap: var(--accordion-spacing-body-gap, 10px);
141
+ }
142
+
143
+ .accordion--scroll .accordion__content-inner {
144
+ @apply flex-1 overflow-y-auto;
145
+ max-height: 135px;
146
+ }