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 +9 -7
- package/dist/components/molecules/Accordion/Accordion.d.ts +41 -0
- package/dist/components/molecules/Accordion/Accordion.js +56 -0
- package/dist/components/molecules/Accordion/index.d.ts +2 -0
- package/dist/components/molecules/Accordion/index.js +1 -0
- package/dist/components/organisms/SearchBarTransfer/SearchBarTransfer.js +73 -5
- package/dist/styles/components/molecule/accordion.css +193 -0
- package/dist/styles/mautourco.css +1 -0
- package/package.json +1 -1
- package/src/components/molecules/Accordion/Accordion.stories.tsx +139 -0
- package/src/components/molecules/Accordion/Accordion.tsx +142 -0
- package/src/components/molecules/Accordion/index.ts +2 -0
- package/src/components/organisms/CarBookingCard/index.ts +1 -0
- package/src/components/organisms/SearchBarTransfer/SearchBarTransfer.tsx +83 -7
- package/src/styles/components/molecule/accordion.css +146 -0
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
|
-
//
|
|
21
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
{/*
|
|
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
|
-
#
|
|
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 @@
|
|
|
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
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
@@ -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;
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 (
|
|
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
|
+
}
|