mautourco-components 0.2.88 → 0.2.90

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.
@@ -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';
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import '../../../styles/components/molecule/service-selector.css';
3
2
  export type ServiceType = 'transfer' | 'accommodation' | 'excursion';
4
3
  export interface ServiceOption {
@@ -19,7 +18,9 @@ export interface ServiceSelectorProps {
19
18
  className?: string;
20
19
  /** Options to display */
21
20
  options?: ServiceOption[];
21
+ /** Whether to use the default background color */
22
+ useDefaultBackground?: boolean;
22
23
  }
23
24
  export declare const DEFAULT_SERVICE_SELECTOR_OPTIONS: ServiceOption[];
24
- declare const ServiceSelector: React.FC<ServiceSelectorProps>;
25
+ declare function ServiceSelector({ value, onChange, disabled, options, useDefaultBackground, className, }: ServiceSelectorProps): import("react/jsx-runtime").JSX.Element;
25
26
  export default ServiceSelector;
@@ -25,10 +25,10 @@ var DEFAULT_OPTIONS = [
25
25
  },
26
26
  ];
27
27
  export var DEFAULT_SERVICE_SELECTOR_OPTIONS = DEFAULT_OPTIONS;
28
- var ServiceSelector = function (_a) {
29
- var value = _a.value, onChange = _a.onChange, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.options, options = _c === void 0 ? DEFAULT_OPTIONS : _c, _d = _a.className, className = _d === void 0 ? '' : _d;
30
- var _e = useState(value), selectedValue = _e[0], setSelectedValue = _e[1];
31
- var _f = useState(false), isOpen = _f[0], setIsOpen = _f[1];
28
+ function ServiceSelector(_a) {
29
+ var value = _a.value, onChange = _a.onChange, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.options, options = _c === void 0 ? DEFAULT_OPTIONS : _c, _d = _a.useDefaultBackground, useDefaultBackground = _d === void 0 ? false : _d, _e = _a.className, className = _e === void 0 ? '' : _e;
30
+ var _f = useState(value), selectedValue = _f[0], setSelectedValue = _f[1];
31
+ var _g = useState(false), isOpen = _g[0], setIsOpen = _g[1];
32
32
  var dropdownRef = useRef(null);
33
33
  // Close dropdown when clicking outside
34
34
  useEffect(function () {
@@ -58,7 +58,7 @@ var ServiceSelector = function (_a) {
58
58
  if (disabled)
59
59
  return 'disabled';
60
60
  // If a service is selected, always show selected state (even if open)
61
- if (selectedValue)
61
+ if (selectedValue && !useDefaultBackground)
62
62
  return 'selected';
63
63
  if (isOpen)
64
64
  return 'open';
@@ -81,5 +81,5 @@ var ServiceSelector = function (_a) {
81
81
  var isDisabled = option.disabled || disabled;
82
82
  return (_jsxs("button", { type: "button", className: "service-selector__option ".concat(isSelected ? 'service-selector__option--selected' : '', " ").concat(isDisabled ? 'service-selector__option--disabled' : ''), onClick: function () { return handleOptionSelect(option); }, disabled: isDisabled, role: "option", "aria-selected": isSelected, children: [_jsx(Icon, { name: option.icon, size: "md", className: "service-selector__option-icon ".concat(isSelected ? 'service-selector__option-icon--selected' : '') }), _jsx(Text, { size: "base", variant: "bold", color: isSelected ? 'inverted' : 'default', className: "service-selector__option-text", children: option.label }), option.badge && (_jsx("span", { className: "service-selector__option-badge", children: option.badge }))] }, option.value));
83
83
  }) }) }) })] }));
84
- };
84
+ }
85
85
  export default ServiceSelector;
@@ -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
+ }
@@ -133,7 +133,8 @@
133
133
  position: absolute;
134
134
  top: calc(100% + 8px);
135
135
  left: 0;
136
- width: 280px;
136
+ right: 0;
137
+ width: 100%;
137
138
  background: var(--color-elevation-level-1, #ffffff);
138
139
  border: 1px solid var(--color-border-subtle, #e5e5e5);
139
140
  border-radius: 12px;
@@ -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.88",
3
+ "version": "0.2.90",
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';
@@ -1,5 +1,5 @@
1
1
  import { cn } from '@/src/lib/utils';
2
- import React, { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import '../../../styles/components/molecule/service-selector.css';
4
4
  import Icon from '../../atoms/Icon/Icon';
5
5
  import { Text } from '../../atoms/Typography/Typography';
@@ -26,6 +26,8 @@ export interface ServiceSelectorProps {
26
26
  className?: string;
27
27
  /** Options to display */
28
28
  options?: ServiceOption[];
29
+ /** Whether to use the default background color */
30
+ useDefaultBackground?: boolean;
29
31
  }
30
32
 
31
33
  const DEFAULT_OPTIONS: ServiceOption[] = [
@@ -50,13 +52,14 @@ const DEFAULT_OPTIONS: ServiceOption[] = [
50
52
 
51
53
  export const DEFAULT_SERVICE_SELECTOR_OPTIONS: ServiceOption[] = DEFAULT_OPTIONS;
52
54
 
53
- const ServiceSelector: React.FC<ServiceSelectorProps> = ({
55
+ function ServiceSelector({
54
56
  value,
55
57
  onChange,
56
58
  disabled = false,
57
59
  options = DEFAULT_OPTIONS,
60
+ useDefaultBackground = false,
58
61
  className = '',
59
- }) => {
62
+ }: ServiceSelectorProps) {
60
63
  const [selectedValue, setSelectedValue] = useState(value);
61
64
  const [isOpen, setIsOpen] = useState(false);
62
65
  const dropdownRef = useRef<HTMLDivElement>(null);
@@ -91,7 +94,7 @@ const ServiceSelector: React.FC<ServiceSelectorProps> = ({
91
94
  const getDropdownState = () => {
92
95
  if (disabled) return 'disabled';
93
96
  // If a service is selected, always show selected state (even if open)
94
- if (selectedValue) return 'selected';
97
+ if (selectedValue && !useDefaultBackground) return 'selected';
95
98
  if (isOpen) return 'open';
96
99
  return 'default';
97
100
  };
@@ -182,6 +185,6 @@ const ServiceSelector: React.FC<ServiceSelectorProps> = ({
182
185
  </PopoverContent>
183
186
  </Popover>
184
187
  );
185
- };
188
+ }
186
189
 
187
190
  export default ServiceSelector;
@@ -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
+ }
@@ -117,7 +117,8 @@
117
117
  position: absolute;
118
118
  top: calc(100% + 8px);
119
119
  left: 0;
120
- width: 280px;
120
+ right: 0;
121
+ width: 100%;
121
122
  background: var(--color-elevation-level-1, #ffffff);
122
123
  border: 1px solid var(--color-border-subtle, #e5e5e5);
123
124
  border-radius: 12px;