mautourco-components 0.2.77 → 0.2.79

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.
@@ -162,11 +162,42 @@
162
162
  opacity: 0.6;
163
163
  }
164
164
 
165
- .chip--clickable:focus {
165
+ /* Focus styles - specific to each color */
166
+ .chip--clickable.chip--brand:focus {
166
167
  outline: 2px solid var(--chip-color-brand-outline-foreground, #ed4c09);
167
168
  outline-offset: 2px;
168
169
  }
169
170
 
171
+ .chip--clickable.chip--accent:focus {
172
+ outline: 2px solid var(--chip-color-accent-outline-foreground, #0f7173);
173
+ outline-offset: 2px;
174
+ }
175
+
176
+ .chip--clickable.chip--blue:focus {
177
+ outline: 2px solid var(--chip-color-blue-outline-foreground, #2e4780);
178
+ outline-offset: 2px;
179
+ }
180
+
181
+ .chip--clickable.chip--green:focus {
182
+ outline: 2px solid var(--chip-color-green-outline-foreground, #4a6045);
183
+ outline-offset: 2px;
184
+ }
185
+
186
+ .chip--clickable.chip--yellow:focus {
187
+ outline: 2px solid var(--chip-color-yellow-outline-foreground, #eab308);
188
+ outline-offset: 2px;
189
+ }
190
+
191
+ .chip--clickable.chip--red:focus {
192
+ outline: 2px solid var(--chip-color-red-outline-foreground, #991b1b);
193
+ outline-offset: 2px;
194
+ }
195
+
196
+ .chip--clickable.chip--neutral:focus {
197
+ outline: 2px solid var(--chip-color-neutral-outline-foreground, #9ca3af);
198
+ outline-offset: 2px;
199
+ }
200
+
170
201
  .chip--outline.chip--black-text {
171
202
  color: var(--chip-color-neutral-outline-text, #262626);
172
203
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Do not edit directly, this file was auto-generated.
3
+ */
4
+
5
+ .vehicle-supplement {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--spacing-gap-gap-6, 24px);
9
+ padding: var(--spacing-padding-px-0, 0px);
10
+ }
11
+
12
+ .vehicle-supplement__content {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--spacing-gap-gap-4, 16px);
16
+ }
17
+
18
+ .vehicle-supplement__transfer-section {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--spacing-gap-gap-4, 16px);
22
+ }
23
+
24
+ .vehicle-supplement__transfer-header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ height: 44px;
29
+ padding: var(--spacing-gap-gap-0, 0px);
30
+ }
31
+
32
+ .vehicle-supplement__transfer-title {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: var(--spacing-gap-gap-2, 8px);
36
+ }
37
+
38
+ .vehicle-supplement__transfer-label {
39
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
40
+ font-weight: var(--font-weight-font-bold, 700);
41
+ font-size: var(--font-size-text-base, 16px);
42
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
43
+ color: var(--checkbox-color-label-default, #303642);
44
+ }
45
+
46
+ .vehicle-supplement__supplements-list {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: var(--spacing-gap-gap-3, 12px);
50
+ }
51
+
52
+ .vehicle-supplement__supplement-row {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ height: 32px;
57
+ padding: var(--spacing-padding-px-0, 0px);
58
+ }
59
+
60
+ .vehicle-supplement__supplement-name {
61
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
62
+ font-weight: var(--font-weight-font-medium, 500);
63
+ font-size: var(--font-size-text-base, 16px);
64
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
65
+ color: var(--checkbox-color-label-default, #303642);
66
+ }
67
+
68
+ .vehicle-supplement__divider {
69
+ width: 100%;
70
+ height: 0;
71
+ border-top: 1px solid var(--color-border-default, #d9d9d9);
72
+ }
73
+
74
+ .vehicle-supplement__footer {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: flex-end;
78
+ gap: var(--spacing-gap-gap-4, 16px);
79
+ height: 36px;
80
+ }
81
+
82
+ .vehicle-supplement__cancel-button,
83
+ .vehicle-supplement__done-button {
84
+ min-width: 114px;
85
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { TransferDocket } from '../../../types/docket/services.types';
3
+ import './VehicleSupplement.css';
4
+ export interface Supplement {
5
+ supplement_name: string;
6
+ max?: number;
7
+ min?: number;
8
+ }
9
+ export interface SupplementValue {
10
+ transferId: number;
11
+ supplementName: string;
12
+ value: number;
13
+ }
14
+ export interface VehicleSupplementProps {
15
+ /** Array of available supplements */
16
+ supplements: Supplement[];
17
+ /** Array of transfers (Arrival, Inter-Hotel, Departure) */
18
+ transfer: TransferDocket[];
19
+ /** Handler called when supplement values change */
20
+ onChange: (values: SupplementValue[]) => void;
21
+ /** Handler called when Done button is clicked */
22
+ onDone: (values: SupplementValue[]) => void;
23
+ /** Handler called when Clear/Cancel button is clicked */
24
+ onClear: () => void;
25
+ /** Initial values for the supplements (optional) */
26
+ initialValues?: SupplementValue[];
27
+ /** Additional CSS classes */
28
+ className?: string;
29
+ }
30
+ declare const VehicleSupplement: React.FC<VehicleSupplementProps>;
31
+ export default VehicleSupplement;
@@ -0,0 +1,101 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useState } from 'react';
14
+ import Button from '../../atoms/Button/Button';
15
+ import Chip from '../../atoms/Chip/Chip';
16
+ import Icon from '../../atoms/Icon/Icon';
17
+ import Stepper from '../Stepper/Stepper';
18
+ import './VehicleSupplement.css';
19
+ var VehicleSupplement = function (_a) {
20
+ var supplements = _a.supplements, transfer = _a.transfer, onChange = _a.onChange, onDone = _a.onDone, onClear = _a.onClear, _b = _a.initialValues, initialValues = _b === void 0 ? [] : _b, _c = _a.className, className = _c === void 0 ? '' : _c;
21
+ // Initialize state for all supplements across all transfers
22
+ var _d = useState(function () {
23
+ // If initial values are provided, use them
24
+ if (initialValues.length > 0) {
25
+ return initialValues;
26
+ }
27
+ // Otherwise, create default values with 0
28
+ var defaultValues = [];
29
+ transfer.forEach(function (t) {
30
+ supplements.forEach(function (s) {
31
+ defaultValues.push({
32
+ transferId: t.IdTransfer,
33
+ supplementName: s.supplement_name,
34
+ value: 0,
35
+ });
36
+ });
37
+ });
38
+ return defaultValues;
39
+ }), supplementValues = _d[0], setSupplementValues = _d[1];
40
+ var getValue = function (transferId, supplementName) {
41
+ var item = supplementValues.find(function (v) { return v.transferId === transferId && v.supplementName === supplementName; });
42
+ return (item === null || item === void 0 ? void 0 : item.value) || 0;
43
+ };
44
+ var handleSupplementChange = function (transferId, supplementName, newValue) {
45
+ var updatedValues = supplementValues.map(function (v) {
46
+ return v.transferId === transferId && v.supplementName === supplementName
47
+ ? __assign(__assign({}, v), { value: newValue }) : v;
48
+ });
49
+ setSupplementValues(updatedValues);
50
+ onChange(updatedValues);
51
+ };
52
+ var getTransferTypeIcon = function (transferType) {
53
+ var type = transferType.toLowerCase();
54
+ if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
55
+ return 'plane-landing-outline';
56
+ }
57
+ if (type.includes('departure') || type.includes('outbound')) {
58
+ return 'plane-takeoff-outline';
59
+ }
60
+ return 'building-2-outline';
61
+ };
62
+ var getTransferTypeLabel = function (transferType) {
63
+ var type = transferType.toLowerCase();
64
+ if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
65
+ return 'Arrival';
66
+ }
67
+ if (type.includes('departure') || type.includes('outbound')) {
68
+ return 'Departure';
69
+ }
70
+ return 'Inter-Hotel';
71
+ };
72
+ var formatDate = function (dateStr) {
73
+ try {
74
+ var date = new Date(dateStr);
75
+ var day = String(date.getDate()).padStart(2, '0');
76
+ var month = String(date.getMonth() + 1).padStart(2, '0');
77
+ var year = date.getFullYear();
78
+ return "".concat(day, "/").concat(month, "/").concat(year);
79
+ }
80
+ catch (_a) {
81
+ return dateStr;
82
+ }
83
+ };
84
+ var handleDone = function () {
85
+ onDone(supplementValues);
86
+ };
87
+ var baseClass = 'vehicle-supplement';
88
+ var classes = [baseClass, className].filter(Boolean).join(' ');
89
+ return (_jsxs("div", { className: classes, children: [_jsx("div", { className: "".concat(baseClass, "__content"), children: transfer.map(function (t, index) {
90
+ var isLast = index === transfer.length - 1;
91
+ var typeLabel = getTransferTypeLabel(t.TransferType);
92
+ var iconName = getTransferTypeIcon(t.TransferType);
93
+ return (_jsxs("div", { className: "".concat(baseClass, "__transfer-section"), children: [_jsxs("div", { className: "".concat(baseClass, "__transfer-header"), children: [_jsxs("div", { className: "".concat(baseClass, "__transfer-title"), children: [_jsx(Icon, { name: iconName, size: "sm" }), _jsx("span", { className: "".concat(baseClass, "__transfer-label"), children: typeLabel })] }), _jsx(Chip, { leadingIcon: "calendar", label: formatDate(t.TransferDate), size: "sm", color: "neutral", isBlackText: true })] }), _jsx("div", { className: "".concat(baseClass, "__supplements-list"), children: supplements.map(function (supplement) {
94
+ var currentValue = getValue(t.IdTransfer, supplement.supplement_name);
95
+ return (_jsxs("div", { className: "".concat(baseClass, "__supplement-row"), children: [_jsx("span", { className: "".concat(baseClass, "__supplement-name"), children: supplement.supplement_name }), _jsx(Stepper, { value: currentValue, min: supplement.min || 0, max: supplement.max || 99, onChange: function (value) {
96
+ return handleSupplementChange(t.IdTransfer, supplement.supplement_name, value);
97
+ } })] }, supplement.supplement_name));
98
+ }) }), !isLast && _jsx("div", { className: "".concat(baseClass, "__divider") })] }, t.IdTransfer));
99
+ }) }), _jsxs("div", { className: "".concat(baseClass, "__footer"), children: [_jsx(Button, { variant: "outline-secondary", size: "sm", onClick: onClear, className: "".concat(baseClass, "__cancel-button"), children: "Cancel" }), _jsx(Button, { variant: "secondary", size: "sm", onClick: handleDone, className: "".concat(baseClass, "__done-button"), children: "Done" })] })] }));
100
+ };
101
+ export default VehicleSupplement;
@@ -0,0 +1,2 @@
1
+ export { default } from './VehicleSupplement';
2
+ export type { VehicleSupplementProps, Supplement, SupplementValue } from './VehicleSupplement';
@@ -0,0 +1 @@
1
+ export { default } from './VehicleSupplement';
@@ -250,4 +250,122 @@
250
250
 
251
251
  .car-booking-card__cta {
252
252
  white-space: nowrap;
253
+ }
254
+
255
+ /* Supplement dropdown styles */
256
+ .car-booking-card__supplement-dropdown {
257
+ position: relative;
258
+ width: 100%;
259
+ }
260
+
261
+ .car-booking-card__supplement-trigger {
262
+ width: 100%;
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: space-between;
266
+ gap: var(--spacing-gap-gap-2, 8px);
267
+ padding: var(--spacing-padding-py-3, 12px) var(--spacing-padding-px-4, 16px);
268
+ background: var(--color-elevation-level-1, #ffffff);
269
+ border: var(--input-border-width-default, 1px) solid var(--color-border-default, #d4d4d4);
270
+ border-radius: var(--border-radius-rounded-xl, 12px);
271
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
272
+ font-size: var(--font-size-text-base, 16px);
273
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
274
+ color: var(--color-text-default, #262626);
275
+ cursor: pointer;
276
+ transition: border-color 0.2s ease, background-color 0.2s ease;
277
+ min-height: 48px;
278
+ }
279
+
280
+ .car-booking-card__supplement-trigger:hover:not(:disabled) {
281
+ border-color: var(--color-border-active-default, #0f7173);
282
+ }
283
+
284
+ .car-booking-card__supplement-trigger:focus {
285
+ outline: var(--border-width-focus, 2px) solid var(--color-border-active-default, #0f7173);
286
+ outline-offset: var(--spacing-base-0-5, 2px);
287
+ }
288
+
289
+ .car-booking-card__supplement-trigger--disabled {
290
+ background: var(--color-elevation-level-2, #f5f5f5);
291
+ cursor: not-allowed;
292
+ opacity: 0.6;
293
+ }
294
+
295
+ .car-booking-card__supplement-placeholder {
296
+ color: var(--color-text-subtle, #737373);
297
+ font-weight: var(--font-weight-font-regular, 400);
298
+ }
299
+
300
+ .car-booking-card__supplement-trigger--has-value .car-booking-card__supplement-placeholder {
301
+ color: var(--color-text-default, #262626);
302
+ }
303
+
304
+ .car-booking-card__supplement-chips {
305
+ display: flex;
306
+ flex-wrap: wrap;
307
+ gap: var(--spacing-gap-gap-2, 8px);
308
+ flex: 1;
309
+ align-items: center;
310
+ }
311
+
312
+ /* Ensure chips are properly interactive within the button */
313
+ .car-booking-card__supplement-chips .chip {
314
+ cursor: pointer;
315
+ transition: opacity 0.15s ease, transform 0.15s ease;
316
+ -webkit-user-select: none;
317
+ user-select: none;
318
+ }
319
+
320
+ /* Hover state for chips - subtle scale and opacity change */
321
+ .car-booking-card__supplement-chips .chip:hover {
322
+ opacity: 0.85;
323
+ transform: translateY(-1px);
324
+ }
325
+
326
+ /* Active/pressed state for chips */
327
+ .car-booking-card__supplement-chips .chip:active {
328
+ opacity: 0.7;
329
+ transform: translateY(0);
330
+ }
331
+
332
+ /* Make close icon more prominent on chip hover */
333
+ .car-booking-card__supplement-chips .chip:hover .chip__icon--trailing {
334
+ opacity: 1;
335
+ transform: scale(1.1);
336
+ }
337
+
338
+ /* Transition for chip icons */
339
+ .car-booking-card__supplement-chips .chip .chip__icon--trailing {
340
+ transition: transform 0.15s ease, opacity 0.15s ease;
341
+ opacity: 0.8;
342
+ }
343
+
344
+ .car-booking-card__supplement-icon {
345
+ transition: transform 0.2s ease;
346
+ flex-shrink: 0;
347
+ }
348
+
349
+ .car-booking-card__supplement-icon--open {
350
+ transform: rotate(180deg);
351
+ }
352
+
353
+ .car-booking-card__supplement-panel {
354
+ position: absolute;
355
+ top: calc(100% + var(--spacing-gap-gap-2, 8px));
356
+ left: 0;
357
+ right: 0;
358
+ z-index: 10;
359
+ background: var(--color-elevation-level-1, #ffffff);
360
+ border-radius: var(--border-radius-rounded-xl, 12px);
361
+ box-shadow:
362
+ var(--spacing-base-0, 0px) var(--spacing-base-1, 4px)
363
+ var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
364
+ rgba(48, 54, 66, 0.1),
365
+ var(--spacing-base-0, 0px) var(--spacing-base-px, 1px)
366
+ var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
367
+ rgba(48, 54, 66, 0.11);
368
+ padding: var(--spacing-padding-py-6, 24px);
369
+ max-height: 600px;
370
+ overflow-y: auto;
253
371
  }
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
+ import { TransferDocket } from '../../../types/docket/services.types';
2
3
  import { ButtonProps } from '../../atoms/Button/Button';
3
4
  import { FeatureRowProps } from '../../molecules/FeatureRow/FeatureRow';
5
+ import { Supplement, SupplementValue } from '../../molecules/VehicleSupplement/VehicleSupplement';
4
6
  import './CarBookingCard.css';
5
7
  export type CarBookingCardSize = 'small' | 'large';
6
8
  export type CarBookingCardState = 'default' | 'selected' | 'hover';
@@ -36,17 +38,19 @@ export interface CarBookingCardProps {
36
38
  supplementMessageState?: 'default' | 'error';
37
39
  supplementLabel?: string;
38
40
  supplementPlaceholder?: string;
39
- supplementValue?: string;
40
- supplementState?: 'default' | 'loading' | 'selected' | 'error' | 'disabled';
41
- supplementOptions?: string[];
42
- onSupplementSelect?: (option: string) => void;
41
+ /** Array of available supplements */
42
+ supplements?: Supplement[];
43
+ /** Array of transfers for supplement selection */
44
+ transfers?: TransferDocket[];
45
+ /** Handler when supplements are selected */
46
+ onSupplementChange?: (values: SupplementValue[]) => void;
43
47
  /** Footer price */
44
48
  totalPrice: string;
45
49
  totalPriceLabel?: string;
46
50
  /** Footer CTA */
47
51
  ctaLabel: string;
48
52
  ctaButtonProps?: Omit<ButtonProps, 'children'>;
49
- onCtaClick?: ButtonProps['onClick'];
53
+ onCtaClick?: (event: React.MouseEvent<HTMLButtonElement>, supplements?: SupplementValue[]) => void;
50
54
  /** Readonly mode - disables interactions and shows values as text */
51
55
  readonly?: boolean;
52
56
  className?: string;
@@ -10,24 +10,99 @@ var __assign = (this && this.__assign) || function () {
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { useState } from 'react';
13
+ import { useEffect, useRef, useState } from 'react';
14
14
  import Button from '../../atoms/Button/Button';
15
+ import Chip from '../../atoms/Chip/Chip';
15
16
  import Divider from '../../atoms/Divider/Divider';
16
- import DropdownInput from '../../atoms/Inputs/DropdownInput/DropdownInput';
17
+ import Icon from '../../atoms/Icon/Icon';
17
18
  import { Heading, Text } from '../../atoms/Typography/Typography';
18
19
  import FeatureRow from '../../molecules/FeatureRow/FeatureRow';
20
+ import VehicleSupplement from '../../molecules/VehicleSupplement/VehicleSupplement';
19
21
  import './CarBookingCard.css';
20
22
  var CarBookingCard = function (_a) {
21
23
  var _b;
22
- var imageSrc = _a.imageSrc, title = _a.title, _c = _a.size, size = _c === void 0 ? 'large' : _c, _d = _a.state, state = _d === void 0 ? 'default' : _d, _e = _a.type, type = _e === void 0 ? 'default' : _e, features = _a.features, infoText = _a.infoText, _f = _a.priceTitle, priceTitle = _f === void 0 ? 'Price breakdown' : _f, _g = _a.priceRows, priceRows = _g === void 0 ? [] : _g, showSupplement = _a.showSupplement, supplementMessage = _a.supplementMessage, _h = _a.supplementMessageState, supplementMessageState = _h === void 0 ? 'error' : _h, _j = _a.supplementLabel, supplementLabel = _j === void 0 ? 'Supplement' : _j, _k = _a.supplementPlaceholder, supplementPlaceholder = _k === void 0 ? 'Select a supplement' : _k, supplementValue = _a.supplementValue, _l = _a.supplementState, supplementState = _l === void 0 ? 'default' : _l, _m = _a.supplementOptions, supplementOptions = _m === void 0 ? [] : _m, onSupplementSelect = _a.onSupplementSelect, totalPrice = _a.totalPrice, _o = _a.totalPriceLabel, totalPriceLabel = _o === void 0 ? 'Total price' : _o, ctaLabel = _a.ctaLabel, ctaButtonProps = _a.ctaButtonProps, onCtaClick = _a.onCtaClick, _p = _a.readonly, readonly = _p === void 0 ? false : _p, _q = _a.className, className = _q === void 0 ? '' : _q;
24
+ var imageSrc = _a.imageSrc, title = _a.title, _c = _a.size, size = _c === void 0 ? 'large' : _c, _d = _a.state, state = _d === void 0 ? 'default' : _d, _e = _a.type, type = _e === void 0 ? 'default' : _e, features = _a.features, infoText = _a.infoText, _f = _a.priceTitle, priceTitle = _f === void 0 ? 'Price breakdown' : _f, _g = _a.priceRows, priceRows = _g === void 0 ? [] : _g, showSupplement = _a.showSupplement, supplementMessage = _a.supplementMessage, _h = _a.supplementMessageState, supplementMessageState = _h === void 0 ? 'error' : _h, _j = _a.supplementLabel, supplementLabel = _j === void 0 ? 'Supplement' : _j, _k = _a.supplementPlaceholder, supplementPlaceholder = _k === void 0 ? 'Select a supplement' : _k, _l = _a.supplements, supplements = _l === void 0 ? [] : _l, _m = _a.transfers, transfers = _m === void 0 ? [] : _m, onSupplementChange = _a.onSupplementChange, totalPrice = _a.totalPrice, _o = _a.totalPriceLabel, totalPriceLabel = _o === void 0 ? 'Total price' : _o, ctaLabel = _a.ctaLabel, ctaButtonProps = _a.ctaButtonProps, onCtaClick = _a.onCtaClick, _p = _a.readonly, readonly = _p === void 0 ? false : _p, _q = _a.className, className = _q === void 0 ? '' : _q;
23
25
  var _r = useState(state === 'selected'), isSelected = _r[0], setIsSelected = _r[1];
24
26
  var _s = useState(false), isHovered = _s[0], setIsHovered = _s[1];
25
- var resolvedShowSupplement = showSupplement !== null && showSupplement !== void 0 ? showSupplement : Boolean(supplementLabel || supplementPlaceholder || supplementOptions.length > 0);
26
- var resolvedSupplementState = supplementMessage ? 'disabled' : supplementState;
27
- // Handle CTA click: toggle between "Add to quote" and "Selected"
27
+ var _t = useState(false), isSupplementOpen = _t[0], setIsSupplementOpen = _t[1];
28
+ var _u = useState([]), selectedSupplements = _u[0], setSelectedSupplements = _u[1];
29
+ var dropdownRef = useRef(null);
30
+ var panelRef = useRef(null);
31
+ // Synchronize internal isSelected state with the state prop when it changes from parent
32
+ useEffect(function () {
33
+ setIsSelected(state === 'selected');
34
+ }, [state]);
35
+ var resolvedShowSupplement = showSupplement !== null && showSupplement !== void 0 ? showSupplement : Boolean(supplements.length > 0 && transfers.length > 0);
36
+ var hasSupplementsSelected = selectedSupplements.some(function (s) { return s.value > 0; });
37
+ // Close dropdown when clicking outside
38
+ useEffect(function () {
39
+ var handleClickOutside = function (event) {
40
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
41
+ setIsSupplementOpen(false);
42
+ }
43
+ };
44
+ if (isSupplementOpen) {
45
+ document.addEventListener('mousedown', handleClickOutside);
46
+ }
47
+ return function () {
48
+ document.removeEventListener('mousedown', handleClickOutside);
49
+ };
50
+ }, [isSupplementOpen]);
51
+ // Auto-scroll to panel when opened
52
+ useEffect(function () {
53
+ if (isSupplementOpen && panelRef.current) {
54
+ // Small delay to ensure the panel is rendered
55
+ setTimeout(function () {
56
+ var _a;
57
+ (_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
58
+ behavior: 'smooth',
59
+ block: 'nearest',
60
+ });
61
+ }, 100);
62
+ }
63
+ }, [isSupplementOpen]);
64
+ var getSupplementSummary = function () {
65
+ var summary = [];
66
+ var grouped = selectedSupplements.reduce(function (acc, item) {
67
+ if (item.value > 0) {
68
+ if (!acc[item.supplementName]) {
69
+ acc[item.supplementName] = 0;
70
+ }
71
+ acc[item.supplementName] += item.value;
72
+ }
73
+ return acc;
74
+ }, {});
75
+ Object.entries(grouped).forEach(function (_a) {
76
+ var name = _a[0], count = _a[1];
77
+ summary.push("".concat(name, " x").concat(count));
78
+ });
79
+ return summary;
80
+ };
81
+ var handleSupplementDone = function (values) {
82
+ setSelectedSupplements(values);
83
+ setIsSupplementOpen(false);
84
+ onSupplementChange === null || onSupplementChange === void 0 ? void 0 : onSupplementChange(values);
85
+ };
86
+ var handleSupplementClear = function () {
87
+ setIsSupplementOpen(false);
88
+ };
89
+ var handleSupplementToggle = function () {
90
+ if (!supplementMessage) {
91
+ setIsSupplementOpen(!isSupplementOpen);
92
+ }
93
+ };
94
+ var handleRemoveSupplement = function (supplementName) {
95
+ var updatedValues = selectedSupplements.map(function (s) {
96
+ return s.supplementName === supplementName ? __assign(__assign({}, s), { value: 0 }) : s;
97
+ });
98
+ setSelectedSupplements(updatedValues);
99
+ onSupplementChange === null || onSupplementChange === void 0 ? void 0 : onSupplementChange(updatedValues);
100
+ };
101
+ // Handle CTA click: toggle between "Add to quote" and "Selected" and pass supplement data
28
102
  var handleCtaClick = function (e) {
29
103
  setIsSelected(!isSelected);
30
- onCtaClick === null || onCtaClick === void 0 ? void 0 : onCtaClick(e);
104
+ // Pass the selected supplements to the parent component
105
+ onCtaClick === null || onCtaClick === void 0 ? void 0 : onCtaClick(e, selectedSupplements.length > 0 ? selectedSupplements : undefined);
31
106
  };
32
107
  // Determine button state based on selection and hover
33
108
  var shouldShowRemove = isSelected && isHovered;
@@ -71,6 +146,12 @@ var CarBookingCard = function (_a) {
71
146
  return (_jsxs("article", { className: classes, children: [_jsx("div", { className: "car-booking-card__image-wrap", children: _jsx("img", { className: "car-booking-card__image", src: imageSrc, alt: "" }) }), _jsx("div", { className: "car-booking-card__active-divider" }), _jsxs("div", { className: "car-booking-card__body", children: [_jsxs("section", { className: "car-booking-card__section car-booking-card__section--content", children: [_jsxs("div", { className: "car-booking-card__title", children: [_jsx("span", { className: "car-booking-card__title-marker", "aria-hidden": "true" }), _jsx(Heading, { level: 4, variant: "bold", color: "accent", className: "car-booking-card__title-text", children: title })] }), _jsx("div", { className: "car-booking-card__features", children: features.map(function (feature, idx) {
72
147
  var _a;
73
148
  return (_jsx(FeatureRow, __assign({}, feature, { className: "car-booking-card__feature-row ".concat((_a = feature.className) !== null && _a !== void 0 ? _a : '').trim() }), "".concat(feature.label, "-").concat(idx)));
74
- }) }), infoText && !readonly && (_jsxs("div", { className: "car-booking-card__info", children: [_jsx("span", { className: "car-booking-card__info-icon", "aria-hidden": "true", children: "i" }), _jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "default", className: "car-booking-card__info-text", children: infoText })] }))] }), resolvedShowSupplement && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--supplement", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__supplement-header", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: supplementLabel }), supplementMessage && (_jsx(Text, { as: "p", size: "sm", leading: "5", variant: "regular", className: "car-booking-card__supplement-message ".concat(supplementMessageState === 'error' ? 'car-booking-card__supplement-message--error' : '').trim(), children: supplementMessage }))] }), readonly ? (_jsx(Text, { size: "sm", leading: "5", variant: "regular", color: supplementValue ? "default" : "subtle", className: "car-booking-card__supplement-value", children: supplementValue || supplementPlaceholder })) : (_jsx(DropdownInput, { placeholder: supplementPlaceholder, value: supplementValue, state: resolvedSupplementState, options: supplementOptions, onSelect: onSupplementSelect }))] })), priceRows.length > 0 && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--price", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__price-content", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: priceTitle }), _jsx("div", { className: "car-booking-card__price-rows", children: priceRows.map(function (row, idx) { return (_jsxs("div", { className: "car-booking-card__price-row", children: [_jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "subtle", className: "car-booking-card__price-label", children: row.label }), _jsx(Text, { size: "sm", leading: "5", variant: "bold", color: "subtle", className: "car-booking-card__price-value", children: row.value })] }, "".concat(row.label, "-").concat(idx))); }) })] }), _jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" })] })), !readonly && _jsxs("footer", { className: "car-booking-card__footer", children: [_jsxs("div", { className: "car-booking-card__total", children: [_jsx(Text, { size: "base", variant: "bold", color: "accent", className: "car-booking-card__total-price", children: totalPrice }), _jsx(Text, { size: "sm", variant: "regular", color: "subtle", className: "car-booking-card__total-label", children: totalPriceLabel })] }), !readonly && (_jsx("div", { onMouseEnter: function () { return setIsHovered(true); }, onMouseLeave: function () { return setIsHovered(false); }, className: "car-booking-card__cta-wrapper", children: _jsx(Button, __assign({}, resolvedCtaButtonProps, { onClick: handleCtaClick, className: "car-booking-card__cta ".concat((_b = resolvedCtaButtonProps === null || resolvedCtaButtonProps === void 0 ? void 0 : resolvedCtaButtonProps.className) !== null && _b !== void 0 ? _b : '').trim(), children: resolvedCtaLabel })) }))] })] })] }));
149
+ }) }), infoText && !readonly && (_jsxs("div", { className: "car-booking-card__info", children: [_jsx("span", { className: "car-booking-card__info-icon", "aria-hidden": "true", children: "i" }), _jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "default", className: "car-booking-card__info-text", children: infoText })] }))] }), resolvedShowSupplement && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--supplement", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__supplement-header", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: supplementLabel }), supplementMessage && (_jsx(Text, { as: "p", size: "sm", leading: "5", variant: "regular", className: "car-booking-card__supplement-message ".concat(supplementMessageState === 'error' ? 'car-booking-card__supplement-message--error' : '').trim(), children: supplementMessage }))] }), _jsxs("div", { className: "car-booking-card__supplement-dropdown", ref: dropdownRef, children: [_jsxs("button", { type: "button", className: "car-booking-card__supplement-trigger ".concat(hasSupplementsSelected ? 'car-booking-card__supplement-trigger--has-value' : '', " ").concat(supplementMessage ? 'car-booking-card__supplement-trigger--disabled' : ''), onClick: handleSupplementToggle, disabled: Boolean(supplementMessage), children: [hasSupplementsSelected ? (_jsx("div", { className: "car-booking-card__supplement-chips", children: getSupplementSummary().map(function (summary, idx) {
150
+ var name = summary.split(' x')[0];
151
+ return (_jsx(Chip, { label: summary, size: "sm", color: "accent", trailingIcon: "close", onClick: function (e) {
152
+ e.stopPropagation();
153
+ handleRemoveSupplement(name);
154
+ } }, idx));
155
+ }) })) : (_jsx("span", { className: "car-booking-card__supplement-placeholder", children: supplementPlaceholder })), _jsx(Icon, { name: "chevron-down", size: "sm", className: "car-booking-card__supplement-icon ".concat(isSupplementOpen ? 'car-booking-card__supplement-icon--open' : '') })] }), isSupplementOpen && (_jsx("div", { className: "car-booking-card__supplement-panel", ref: panelRef, children: _jsx(VehicleSupplement, { supplements: supplements, transfer: transfers, initialValues: selectedSupplements, onChange: function () { }, onDone: handleSupplementDone, onClear: handleSupplementClear }) }))] })] })), priceRows.length > 0 && !readonly && (_jsxs("section", { className: "car-booking-card__section car-booking-card__section--price", children: [_jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" }), _jsxs("div", { className: "car-booking-card__price-content", children: [_jsx(Text, { as: "h3", size: "md", variant: "bold", leading: "none", color: "default", className: "car-booking-card__section-title", children: priceTitle }), _jsx("div", { className: "car-booking-card__price-rows", children: priceRows.map(function (row, idx) { return (_jsxs("div", { className: "car-booking-card__price-row", children: [_jsx(Text, { size: "sm", leading: "5", variant: "regular", color: "subtle", className: "car-booking-card__price-label", children: row.label }), _jsx(Text, { size: "sm", leading: "5", variant: "bold", color: "subtle", className: "car-booking-card__price-value", children: row.value })] }, "".concat(row.label, "-").concat(idx))); }) })] }), _jsx(Divider, { variant: "dashed", className: "car-booking-card__dashed-divider" })] })), !readonly && _jsxs("footer", { className: "car-booking-card__footer", children: [_jsxs("div", { className: "car-booking-card__total", children: [_jsx(Text, { size: "base", variant: "bold", color: "accent", className: "car-booking-card__total-price", children: totalPrice }), _jsx(Text, { size: "sm", variant: "regular", color: "subtle", className: "car-booking-card__total-label", children: totalPriceLabel })] }), !readonly && (_jsx("div", { onMouseEnter: function () { return setIsHovered(true); }, onMouseLeave: function () { return setIsHovered(false); }, className: "car-booking-card__cta-wrapper", children: _jsx(Button, __assign({}, resolvedCtaButtonProps, { onClick: handleCtaClick, className: "car-booking-card__cta ".concat((_b = resolvedCtaButtonProps === null || resolvedCtaButtonProps === void 0 ? void 0 : resolvedCtaButtonProps.className) !== null && _b !== void 0 ? _b : '').trim(), children: resolvedCtaLabel })) }))] })] })] }));
75
156
  };
76
157
  export default CarBookingCard;
package/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ export { ServiceLanguages } from './components/molecules/ServiceLanguages/Servic
39
39
  export { default as ServiceSelector } from './components/molecules/ServiceSelector/ServiceSelector';
40
40
  export { default as Stepper } from './components/molecules/Stepper/Stepper';
41
41
  export { default as TextWithIcon } from './components/molecules/TextWithIcon/TextWithIcon';
42
+ export { default as VehicleSupplement } from './components/molecules/VehicleSupplement/VehicleSupplement';
43
+ export type { VehicleSupplementProps, Supplement, SupplementValue } from './components/molecules/VehicleSupplement';
42
44
  export { default as TimelineItem } from './components/molecules/TimelineItem/TimelineItem';
43
45
  export { default as Toast } from './components/molecules/Toast/Toast';
44
46
  export * from './components/molecules/TooltipDisplay/TooltipDisplay';
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ export { ServiceLanguages } from './components/molecules/ServiceLanguages/Servic
41
41
  export { default as ServiceSelector } from './components/molecules/ServiceSelector/ServiceSelector';
42
42
  export { default as Stepper } from './components/molecules/Stepper/Stepper';
43
43
  export { default as TextWithIcon } from './components/molecules/TextWithIcon/TextWithIcon';
44
+ export { default as VehicleSupplement } from './components/molecules/VehicleSupplement/VehicleSupplement';
44
45
  export { default as TimelineItem } from './components/molecules/TimelineItem/TimelineItem';
45
46
  export { default as Toast } from './components/molecules/Toast/Toast';
46
47
  export * from './components/molecules/TooltipDisplay/TooltipDisplay';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mautourco-components",
3
- "version": "0.2.77",
3
+ "version": "0.2.79",
4
4
  "private": false,
5
5
  "description": "Bibliothèque de composants Mautourco pour le redesign",
6
6
  "main": "dist/index.js",
@@ -156,11 +156,42 @@
156
156
  opacity: 0.6;
157
157
  }
158
158
 
159
- .chip--clickable:focus {
159
+ /* Focus styles - specific to each color */
160
+ .chip--clickable.chip--brand:focus {
160
161
  outline: 2px solid var(--chip-color-brand-outline-foreground, #ed4c09);
161
162
  outline-offset: 2px;
162
163
  }
163
164
 
165
+ .chip--clickable.chip--accent:focus {
166
+ outline: 2px solid var(--chip-color-accent-outline-foreground, #0f7173);
167
+ outline-offset: 2px;
168
+ }
169
+
170
+ .chip--clickable.chip--blue:focus {
171
+ outline: 2px solid var(--chip-color-blue-outline-foreground, #2e4780);
172
+ outline-offset: 2px;
173
+ }
174
+
175
+ .chip--clickable.chip--green:focus {
176
+ outline: 2px solid var(--chip-color-green-outline-foreground, #4a6045);
177
+ outline-offset: 2px;
178
+ }
179
+
180
+ .chip--clickable.chip--yellow:focus {
181
+ outline: 2px solid var(--chip-color-yellow-outline-foreground, #eab308);
182
+ outline-offset: 2px;
183
+ }
184
+
185
+ .chip--clickable.chip--red:focus {
186
+ outline: 2px solid var(--chip-color-red-outline-foreground, #991b1b);
187
+ outline-offset: 2px;
188
+ }
189
+
190
+ .chip--clickable.chip--neutral:focus {
191
+ outline: 2px solid var(--chip-color-neutral-outline-foreground, #9ca3af);
192
+ outline-offset: 2px;
193
+ }
194
+
164
195
  .chip--outline.chip--black-text {
165
196
  color: var(--chip-color-neutral-outline-text, #262626);
166
197
  }
@@ -0,0 +1,81 @@
1
+ .vehicle-supplement {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-gap-gap-6, 24px);
5
+ padding: var(--spacing-padding-px-0, 0px);
6
+ }
7
+
8
+ .vehicle-supplement__content {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--spacing-gap-gap-4, 16px);
12
+ }
13
+
14
+ .vehicle-supplement__transfer-section {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: var(--spacing-gap-gap-4, 16px);
18
+ }
19
+
20
+ .vehicle-supplement__transfer-header {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ height: 44px;
25
+ padding: var(--spacing-gap-gap-0, 0px);
26
+ }
27
+
28
+ .vehicle-supplement__transfer-title {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: var(--spacing-gap-gap-2, 8px);
32
+ }
33
+
34
+ .vehicle-supplement__transfer-label {
35
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
36
+ font-weight: var(--font-weight-font-bold, 700);
37
+ font-size: var(--font-size-text-base, 16px);
38
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
39
+ color: var(--checkbox-color-label-default, #303642);
40
+ }
41
+
42
+ .vehicle-supplement__supplements-list {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--spacing-gap-gap-3, 12px);
46
+ }
47
+
48
+ .vehicle-supplement__supplement-row {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ height: 32px;
53
+ padding: var(--spacing-padding-px-0, 0px);
54
+ }
55
+
56
+ .vehicle-supplement__supplement-name {
57
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
58
+ font-weight: var(--font-weight-font-medium, 500);
59
+ font-size: var(--font-size-text-base, 16px);
60
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
61
+ color: var(--checkbox-color-label-default, #303642);
62
+ }
63
+
64
+ .vehicle-supplement__divider {
65
+ width: 100%;
66
+ height: 0;
67
+ border-top: 1px solid var(--color-border-default, #d9d9d9);
68
+ }
69
+
70
+ .vehicle-supplement__footer {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: flex-end;
74
+ gap: var(--spacing-gap-gap-4, 16px);
75
+ height: 36px;
76
+ }
77
+
78
+ .vehicle-supplement__cancel-button,
79
+ .vehicle-supplement__done-button {
80
+ min-width: 114px;
81
+ }
@@ -0,0 +1,206 @@
1
+ import React, { useState } from 'react';
2
+ import { TransferDocket } from '../../../types/docket/services.types';
3
+ import Button from '../../atoms/Button/Button';
4
+ import Chip from '../../atoms/Chip/Chip';
5
+ import Icon from '../../atoms/Icon/Icon';
6
+ import Stepper from '../Stepper/Stepper';
7
+ import './VehicleSupplement.css';
8
+
9
+ export interface Supplement {
10
+ supplement_name: string;
11
+ max?: number;
12
+ min?: number;
13
+ }
14
+
15
+ export interface SupplementValue {
16
+ transferId: number;
17
+ supplementName: string;
18
+ value: number;
19
+ }
20
+
21
+ export interface VehicleSupplementProps {
22
+ /** Array of available supplements */
23
+ supplements: Supplement[];
24
+ /** Array of transfers (Arrival, Inter-Hotel, Departure) */
25
+ transfer: TransferDocket[];
26
+ /** Handler called when supplement values change */
27
+ onChange: (values: SupplementValue[]) => void;
28
+ /** Handler called when Done button is clicked */
29
+ onDone: (values: SupplementValue[]) => void;
30
+ /** Handler called when Clear/Cancel button is clicked */
31
+ onClear: () => void;
32
+ /** Initial values for the supplements (optional) */
33
+ initialValues?: SupplementValue[];
34
+ /** Additional CSS classes */
35
+ className?: string;
36
+ }
37
+
38
+ const VehicleSupplement: React.FC<VehicleSupplementProps> = ({
39
+ supplements,
40
+ transfer,
41
+ onChange,
42
+ onDone,
43
+ onClear,
44
+ initialValues = [],
45
+ className = '',
46
+ }) => {
47
+ // Initialize state for all supplements across all transfers
48
+ const [supplementValues, setSupplementValues] = useState<SupplementValue[]>(() => {
49
+ // If initial values are provided, use them
50
+ if (initialValues.length > 0) {
51
+ return initialValues;
52
+ }
53
+
54
+ // Otherwise, create default values with 0
55
+ const defaultValues: SupplementValue[] = [];
56
+ transfer.forEach((t) => {
57
+ supplements.forEach((s) => {
58
+ defaultValues.push({
59
+ transferId: t.IdTransfer,
60
+ supplementName: s.supplement_name,
61
+ value: 0,
62
+ });
63
+ });
64
+ });
65
+ return defaultValues;
66
+ });
67
+
68
+ const getValue = (transferId: number, supplementName: string): number => {
69
+ const item = supplementValues.find(
70
+ (v) => v.transferId === transferId && v.supplementName === supplementName
71
+ );
72
+ return item?.value || 0;
73
+ };
74
+
75
+ const handleSupplementChange = (
76
+ transferId: number,
77
+ supplementName: string,
78
+ newValue: number
79
+ ) => {
80
+ const updatedValues = supplementValues.map((v) =>
81
+ v.transferId === transferId && v.supplementName === supplementName
82
+ ? { ...v, value: newValue }
83
+ : v
84
+ );
85
+ setSupplementValues(updatedValues);
86
+ onChange(updatedValues);
87
+ };
88
+
89
+ const getTransferTypeIcon = (transferType: string) => {
90
+ const type = transferType.toLowerCase();
91
+ if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
92
+ return 'plane-landing-outline';
93
+ }
94
+ if (type.includes('departure') || type.includes('outbound')) {
95
+ return 'plane-takeoff-outline';
96
+ }
97
+ return 'building-2-outline';
98
+ };
99
+
100
+ const getTransferTypeLabel = (transferType: string): string => {
101
+ const type = transferType.toLowerCase();
102
+ if (type.includes('arrival') || type.includes('airport') || type.includes('inbound')) {
103
+ return 'Arrival';
104
+ }
105
+ if (type.includes('departure') || type.includes('outbound')) {
106
+ return 'Departure';
107
+ }
108
+ return 'Inter-Hotel';
109
+ };
110
+
111
+ const formatDate = (dateStr: string): string => {
112
+ try {
113
+ const date = new Date(dateStr);
114
+ const day = String(date.getDate()).padStart(2, '0');
115
+ const month = String(date.getMonth() + 1).padStart(2, '0');
116
+ const year = date.getFullYear();
117
+ return `${day}/${month}/${year}`;
118
+ } catch {
119
+ return dateStr;
120
+ }
121
+ };
122
+
123
+ const handleDone = () => {
124
+ onDone(supplementValues);
125
+ };
126
+
127
+ const baseClass = 'vehicle-supplement';
128
+ const classes = [baseClass, className].filter(Boolean).join(' ');
129
+
130
+ return (
131
+ <div className={classes}>
132
+ <div className={`${baseClass}__content`}>
133
+ {transfer.map((t, index) => {
134
+ const isLast = index === transfer.length - 1;
135
+ const typeLabel = getTransferTypeLabel(t.TransferType);
136
+ const iconName = getTransferTypeIcon(t.TransferType);
137
+
138
+ return (
139
+ <div key={t.IdTransfer} className={`${baseClass}__transfer-section`}>
140
+ <div className={`${baseClass}__transfer-header`}>
141
+ <div className={`${baseClass}__transfer-title`}>
142
+ <Icon name={iconName as any} size="sm" />
143
+ <span className={`${baseClass}__transfer-label`}>{typeLabel}</span>
144
+ </div>
145
+ <Chip
146
+ leadingIcon="calendar"
147
+ label={formatDate(t.TransferDate)}
148
+ size="sm"
149
+ color="neutral"
150
+ isBlackText
151
+ />
152
+ </div>
153
+
154
+ <div className={`${baseClass}__supplements-list`}>
155
+ {supplements.map((supplement) => {
156
+ const currentValue = getValue(t.IdTransfer, supplement.supplement_name);
157
+ return (
158
+ <div
159
+ key={supplement.supplement_name}
160
+ className={`${baseClass}__supplement-row`}
161
+ >
162
+ <span className={`${baseClass}__supplement-name`}>
163
+ {supplement.supplement_name}
164
+ </span>
165
+ <Stepper
166
+ value={currentValue}
167
+ min={supplement.min || 0}
168
+ max={supplement.max || 99}
169
+ onChange={(value) =>
170
+ handleSupplementChange(t.IdTransfer, supplement.supplement_name, value)
171
+ }
172
+ />
173
+ </div>
174
+ );
175
+ })}
176
+ </div>
177
+
178
+ {!isLast && <div className={`${baseClass}__divider`} />}
179
+ </div>
180
+ );
181
+ })}
182
+ </div>
183
+
184
+ <div className={`${baseClass}__footer`}>
185
+ <Button
186
+ variant="outline-secondary"
187
+ size="sm"
188
+ onClick={onClear}
189
+ className={`${baseClass}__cancel-button`}
190
+ >
191
+ Cancel
192
+ </Button>
193
+ <Button
194
+ variant="secondary"
195
+ size="sm"
196
+ onClick={handleDone}
197
+ className={`${baseClass}__done-button`}
198
+ >
199
+ Done
200
+ </Button>
201
+ </div>
202
+ </div>
203
+ );
204
+ };
205
+
206
+ export default VehicleSupplement;
@@ -0,0 +1,2 @@
1
+ export { default } from './VehicleSupplement';
2
+ export type { VehicleSupplementProps, Supplement, SupplementValue } from './VehicleSupplement';
@@ -248,4 +248,121 @@
248
248
  white-space: nowrap;
249
249
  }
250
250
 
251
+ /* Supplement dropdown styles */
252
+ .car-booking-card__supplement-dropdown {
253
+ position: relative;
254
+ width: 100%;
255
+ }
256
+
257
+ .car-booking-card__supplement-trigger {
258
+ width: 100%;
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: space-between;
262
+ gap: var(--spacing-gap-gap-2, 8px);
263
+ padding: var(--spacing-padding-py-3, 12px) var(--spacing-padding-px-4, 16px);
264
+ background: var(--color-elevation-level-1, #ffffff);
265
+ border: var(--input-border-width-default, 1px) solid var(--color-border-default, #d4d4d4);
266
+ border-radius: var(--border-radius-rounded-xl, 12px);
267
+ font-family: var(--font-font-family-body, "Satoshi"), "Inter", "Segoe UI", "system-ui", sans-serif;
268
+ font-size: var(--font-size-text-base, 16px);
269
+ line-height: calc(var(--font-leading-leading-md, 24) * 1px);
270
+ color: var(--color-text-default, #262626);
271
+ cursor: pointer;
272
+ transition: border-color 0.2s ease, background-color 0.2s ease;
273
+ min-height: 48px;
274
+ }
275
+
276
+ .car-booking-card__supplement-trigger:hover:not(:disabled) {
277
+ border-color: var(--color-border-active-default, #0f7173);
278
+ }
279
+
280
+ .car-booking-card__supplement-trigger:focus {
281
+ outline: var(--border-width-focus, 2px) solid var(--color-border-active-default, #0f7173);
282
+ outline-offset: var(--spacing-base-0-5, 2px);
283
+ }
284
+
285
+ .car-booking-card__supplement-trigger--disabled {
286
+ background: var(--color-elevation-level-2, #f5f5f5);
287
+ cursor: not-allowed;
288
+ opacity: 0.6;
289
+ }
290
+
291
+ .car-booking-card__supplement-placeholder {
292
+ color: var(--color-text-subtle, #737373);
293
+ font-weight: var(--font-weight-font-regular, 400);
294
+ }
295
+
296
+ .car-booking-card__supplement-trigger--has-value .car-booking-card__supplement-placeholder {
297
+ color: var(--color-text-default, #262626);
298
+ }
299
+
300
+ .car-booking-card__supplement-chips {
301
+ display: flex;
302
+ flex-wrap: wrap;
303
+ gap: var(--spacing-gap-gap-2, 8px);
304
+ flex: 1;
305
+ align-items: center;
306
+ }
307
+
308
+ /* Ensure chips are properly interactive within the button */
309
+ .car-booking-card__supplement-chips .chip {
310
+ cursor: pointer;
311
+ transition: opacity 0.15s ease, transform 0.15s ease;
312
+ user-select: none;
313
+ }
314
+
315
+ /* Hover state for chips - subtle scale and opacity change */
316
+ .car-booking-card__supplement-chips .chip:hover {
317
+ opacity: 0.85;
318
+ transform: translateY(-1px);
319
+ }
320
+
321
+ /* Active/pressed state for chips */
322
+ .car-booking-card__supplement-chips .chip:active {
323
+ opacity: 0.7;
324
+ transform: translateY(0);
325
+ }
326
+
327
+ /* Make close icon more prominent on chip hover */
328
+ .car-booking-card__supplement-chips .chip:hover .chip__icon--trailing {
329
+ opacity: 1;
330
+ transform: scale(1.1);
331
+ }
332
+
333
+ /* Transition for chip icons */
334
+ .car-booking-card__supplement-chips .chip .chip__icon--trailing {
335
+ transition: transform 0.15s ease, opacity 0.15s ease;
336
+ opacity: 0.8;
337
+ }
338
+
339
+ .car-booking-card__supplement-icon {
340
+ transition: transform 0.2s ease;
341
+ flex-shrink: 0;
342
+ }
343
+
344
+ .car-booking-card__supplement-icon--open {
345
+ transform: rotate(180deg);
346
+ }
347
+
348
+ .car-booking-card__supplement-panel {
349
+ position: absolute;
350
+ top: calc(100% + var(--spacing-gap-gap-2, 8px));
351
+ left: 0;
352
+ right: 0;
353
+ z-index: 10;
354
+ background: var(--color-elevation-level-1, #ffffff);
355
+ border-radius: var(--border-radius-rounded-xl, 12px);
356
+ box-shadow:
357
+ var(--spacing-base-0, 0px) var(--spacing-base-1, 4px)
358
+ var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
359
+ rgba(48, 54, 66, 0.1),
360
+ var(--spacing-base-0, 0px) var(--spacing-base-px, 1px)
361
+ var(--backdrop-blur-backdrop-blur, 8px) var(--spacing-base-0, 0px)
362
+ rgba(48, 54, 66, 0.11);
363
+ padding: var(--spacing-padding-py-6, 24px);
364
+ max-height: 600px;
365
+ overflow-y: auto;
366
+ }
367
+
251
368
 
@@ -1,9 +1,12 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { TransferDocket } from '../../../types/docket/services.types';
2
3
  import Button, { ButtonProps } from '../../atoms/Button/Button';
4
+ import Chip from '../../atoms/Chip/Chip';
3
5
  import Divider from '../../atoms/Divider/Divider';
4
- import DropdownInput from '../../atoms/Inputs/DropdownInput/DropdownInput';
6
+ import Icon from '../../atoms/Icon/Icon';
5
7
  import { Heading, Text } from '../../atoms/Typography/Typography';
6
8
  import FeatureRow, { FeatureRowProps } from '../../molecules/FeatureRow/FeatureRow';
9
+ import VehicleSupplement, { Supplement, SupplementValue } from '../../molecules/VehicleSupplement/VehicleSupplement';
7
10
  import './CarBookingCard.css';
8
11
 
9
12
  export type CarBookingCardSize = 'small' | 'large';
@@ -46,10 +49,12 @@ export interface CarBookingCardProps {
46
49
  supplementMessageState?: 'default' | 'error';
47
50
  supplementLabel?: string;
48
51
  supplementPlaceholder?: string;
49
- supplementValue?: string;
50
- supplementState?: 'default' | 'loading' | 'selected' | 'error' | 'disabled';
51
- supplementOptions?: string[];
52
- onSupplementSelect?: (option: string) => void;
52
+ /** Array of available supplements */
53
+ supplements?: Supplement[];
54
+ /** Array of transfers for supplement selection */
55
+ transfers?: TransferDocket[];
56
+ /** Handler when supplements are selected */
57
+ onSupplementChange?: (values: SupplementValue[]) => void;
53
58
 
54
59
  /** Footer price */
55
60
  totalPrice: string;
@@ -58,7 +63,7 @@ export interface CarBookingCardProps {
58
63
  /** Footer CTA */
59
64
  ctaLabel: string;
60
65
  ctaButtonProps?: Omit<ButtonProps, 'children'>;
61
- onCtaClick?: ButtonProps['onClick'];
66
+ onCtaClick?: (event: React.MouseEvent<HTMLButtonElement>, supplements?: SupplementValue[]) => void;
62
67
 
63
68
  /** Readonly mode - disables interactions and shows values as text */
64
69
  readonly?: boolean;
@@ -81,10 +86,9 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
81
86
  supplementMessageState = 'error',
82
87
  supplementLabel = 'Supplement',
83
88
  supplementPlaceholder = 'Select a supplement',
84
- supplementValue,
85
- supplementState = 'default',
86
- supplementOptions = [],
87
- onSupplementSelect,
89
+ supplements = [],
90
+ transfers = [],
91
+ onSupplementChange,
88
92
  totalPrice,
89
93
  totalPriceLabel = 'Total price',
90
94
  ctaLabel,
@@ -95,16 +99,99 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
95
99
  }) => {
96
100
  const [isSelected, setIsSelected] = useState(state === 'selected');
97
101
  const [isHovered, setIsHovered] = useState(false);
102
+ const [isSupplementOpen, setIsSupplementOpen] = useState(false);
103
+ const [selectedSupplements, setSelectedSupplements] = useState<SupplementValue[]>([]);
104
+ const dropdownRef = useRef<HTMLDivElement>(null);
105
+ const panelRef = useRef<HTMLDivElement>(null);
106
+
107
+ // Synchronize internal isSelected state with the state prop when it changes from parent
108
+ useEffect(() => {
109
+ setIsSelected(state === 'selected');
110
+ }, [state]);
98
111
 
99
112
  const resolvedShowSupplement =
100
- showSupplement ?? Boolean(supplementLabel || supplementPlaceholder || supplementOptions.length > 0);
101
- const resolvedSupplementState =
102
- supplementMessage ? 'disabled' : supplementState;
113
+ showSupplement ?? Boolean(supplements.length > 0 && transfers.length > 0);
114
+
115
+ const hasSupplementsSelected = selectedSupplements.some((s) => s.value > 0);
116
+
117
+ // Close dropdown when clicking outside
118
+ useEffect(() => {
119
+ const handleClickOutside = (event: MouseEvent) => {
120
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
121
+ setIsSupplementOpen(false);
122
+ }
123
+ };
124
+
125
+ if (isSupplementOpen) {
126
+ document.addEventListener('mousedown', handleClickOutside);
127
+ }
128
+
129
+ return () => {
130
+ document.removeEventListener('mousedown', handleClickOutside);
131
+ };
132
+ }, [isSupplementOpen]);
133
+
134
+ // Auto-scroll to panel when opened
135
+ useEffect(() => {
136
+ if (isSupplementOpen && panelRef.current) {
137
+ // Small delay to ensure the panel is rendered
138
+ setTimeout(() => {
139
+ panelRef.current?.scrollIntoView({
140
+ behavior: 'smooth',
141
+ block: 'nearest',
142
+ });
143
+ }, 100);
144
+ }
145
+ }, [isSupplementOpen]);
146
+
147
+ const getSupplementSummary = (): string[] => {
148
+ const summary: string[] = [];
149
+ const grouped = selectedSupplements.reduce((acc, item) => {
150
+ if (item.value > 0) {
151
+ if (!acc[item.supplementName]) {
152
+ acc[item.supplementName] = 0;
153
+ }
154
+ acc[item.supplementName] += item.value;
155
+ }
156
+ return acc;
157
+ }, {} as Record<string, number>);
158
+
159
+ Object.entries(grouped).forEach(([name, count]) => {
160
+ summary.push(`${name} x${count}`);
161
+ });
162
+
163
+ return summary;
164
+ };
165
+
166
+ const handleSupplementDone = (values: SupplementValue[]) => {
167
+ setSelectedSupplements(values);
168
+ setIsSupplementOpen(false);
169
+ onSupplementChange?.(values);
170
+ };
103
171
 
104
- // Handle CTA click: toggle between "Add to quote" and "Selected"
172
+ const handleSupplementClear = () => {
173
+ setIsSupplementOpen(false);
174
+ };
175
+
176
+ const handleSupplementToggle = () => {
177
+ if (!supplementMessage) {
178
+ setIsSupplementOpen(!isSupplementOpen);
179
+ }
180
+ };
181
+
182
+ const handleRemoveSupplement = (supplementName: string) => {
183
+ const updatedValues = selectedSupplements.map((s) =>
184
+ s.supplementName === supplementName ? { ...s, value: 0 } : s
185
+ );
186
+ setSelectedSupplements(updatedValues);
187
+ onSupplementChange?.(updatedValues);
188
+ };
189
+
190
+ // Handle CTA click: toggle between "Add to quote" and "Selected" and pass supplement data
105
191
  const handleCtaClick = (e: React.MouseEvent<HTMLButtonElement>) => {
106
192
  setIsSelected(!isSelected);
107
- onCtaClick?.(e);
193
+ // Pass the selected supplements to the parent component
194
+ onCtaClick?.(e, selectedSupplements.length > 0 ? selectedSupplements : undefined);
108
195
  };
109
196
 
110
197
  // Determine button state based on selection and hover
@@ -215,25 +302,58 @@ const CarBookingCard: React.FC<CarBookingCardProps> = ({
215
302
  </Text>
216
303
  )}
217
304
  </div>
218
- {readonly ? (
219
- <Text
220
- size="sm"
221
- leading="5"
222
- variant="regular"
223
- color={supplementValue ? "default" : "subtle"}
224
- className="car-booking-card__supplement-value"
305
+
306
+ <div className="car-booking-card__supplement-dropdown" ref={dropdownRef}>
307
+ <button
308
+ type="button"
309
+ className={`car-booking-card__supplement-trigger ${hasSupplementsSelected ? 'car-booking-card__supplement-trigger--has-value' : ''} ${supplementMessage ? 'car-booking-card__supplement-trigger--disabled' : ''}`}
310
+ onClick={handleSupplementToggle}
311
+ disabled={Boolean(supplementMessage)}
225
312
  >
226
- {supplementValue || supplementPlaceholder}
227
- </Text>
228
- ) : (
229
- <DropdownInput
230
- placeholder={supplementPlaceholder}
231
- value={supplementValue}
232
- state={resolvedSupplementState}
233
- options={supplementOptions}
234
- onSelect={onSupplementSelect}
235
- />
236
- )}
313
+ {hasSupplementsSelected ? (
314
+ <div className="car-booking-card__supplement-chips">
315
+ {getSupplementSummary().map((summary, idx) => {
316
+ const [name] = summary.split(' x');
317
+ return (
318
+ <Chip
319
+ key={idx}
320
+ label={summary}
321
+ size="sm"
322
+ color="accent"
323
+ trailingIcon="close"
324
+ onClick={(e) => {
325
+ e.stopPropagation();
326
+ handleRemoveSupplement(name);
327
+ }}
328
+ />
329
+ );
330
+ })}
331
+ </div>
332
+ ) : (
333
+ <span className="car-booking-card__supplement-placeholder">
334
+ {supplementPlaceholder}
335
+ </span>
336
+ )}
337
+ <Icon
338
+ name="chevron-down"
339
+ size="sm"
340
+ className={`car-booking-card__supplement-icon ${isSupplementOpen ? 'car-booking-card__supplement-icon--open' : ''}`}
341
+ />
342
+ </button>
343
+
344
+ {isSupplementOpen && (
345
+ <div className="car-booking-card__supplement-panel" ref={panelRef}>
346
+ <VehicleSupplement
347
+ supplements={supplements}
348
+ transfer={transfers}
349
+ initialValues={selectedSupplements}
350
+ onChange={() => {}}
351
+ onDone={handleSupplementDone}
352
+ onClear={handleSupplementClear}
353
+ />
354
+ </div>
355
+ )}
356
+ </div>
237
357
  </section>
238
358
  )}
239
359