@shortstravelmgmt/component-lib 0.5.0 → 0.5.1-canary.85.gba94682
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/dist/index.d.ts +82 -13
- package/dist/index.esm.js +481 -143
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +481 -143
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -9498,6 +9498,23 @@ const weekdayStyle$2 = {
|
|
|
9498
9498
|
label: 'TravelDateRange-weekdayStyle',
|
|
9499
9499
|
fontWeight: 700,
|
|
9500
9500
|
};
|
|
9501
|
+
/* The trip's length, after the range: a pill in the dates' navy, set apart as a label. */
|
|
9502
|
+
const daysStyle$1 = {
|
|
9503
|
+
label: 'TravelDateRange-daysStyle',
|
|
9504
|
+
display: 'inline-flex',
|
|
9505
|
+
alignItems: 'center',
|
|
9506
|
+
padding: '2px var(--space-3)',
|
|
9507
|
+
borderRadius: 'var(--radius-full)',
|
|
9508
|
+
border: '1px solid var(--stm-tint-200)',
|
|
9509
|
+
background: 'var(--stm-tint-100)',
|
|
9510
|
+
color: 'var(--color-primary, #19345f)',
|
|
9511
|
+
fontFamily: 'var(--font-family-label)',
|
|
9512
|
+
fontSize: 'var(--fs-sm)',
|
|
9513
|
+
fontWeight: 'var(--fw-semibold)',
|
|
9514
|
+
letterSpacing: '0.06em',
|
|
9515
|
+
lineHeight: 'var(--lh-sm)',
|
|
9516
|
+
textTransform: 'uppercase',
|
|
9517
|
+
};
|
|
9501
9518
|
const hiddenStyle$1 = { label: 'TravelDateRange-hiddenStyle', ...srOnly };
|
|
9502
9519
|
/* A dashed rule ending in an arrowhead, drawn so it scales with the text colour. */
|
|
9503
9520
|
const arrowStyle$1 = {
|
|
@@ -9523,20 +9540,23 @@ const ordinalDay = (day) => {
|
|
|
9523
9540
|
return `${day}${suffix}`;
|
|
9524
9541
|
};
|
|
9525
9542
|
const DatePart = ({ date, withYear }) => (jsxs("span", { children: [jsxs("span", { className: sx(weekdayStyle$2), children: [WEEKDAYS$1[date.getDay()], ","] }), ' ', MONTHS$1[date.getMonth()], " ", ordinalDay(date.getDate()), withYear && `, ${date.getFullYear()}`] }));
|
|
9526
|
-
const DashedArrow = () => (jsx("span", { className: sx(arrowStyle$1), "aria-hidden": "true", children: jsxs("svg", { width: "
|
|
9543
|
+
const DashedArrow = () => (jsx("span", { className: sx(arrowStyle$1), "aria-hidden": "true", children: jsxs("svg", { width: "28", height: "10", viewBox: "0 0 28 10", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "1", y1: "5", x2: "22", y2: "5", strokeDasharray: "3 3" }), jsx("polyline", { points: "19 1.5 26 5 19 8.5", fill: "currentColor" })] }) }));
|
|
9544
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9527
9545
|
/**
|
|
9528
9546
|
* A trip's travel dates as `Sun, Jun 7th ⇢ Tue, Jun 9th, 2026`. The year shows
|
|
9529
9547
|
* once at the end, or on both dates when the trip crosses a year. A single-day
|
|
9530
|
-
* trip shows one date.
|
|
9548
|
+
* trip shows one date. With `showDays`, the trip's length follows as a pill.
|
|
9531
9549
|
*/
|
|
9532
|
-
const TravelDateRange = ({ startDate, endDate, className = '' }) => {
|
|
9550
|
+
const TravelDateRange = ({ startDate, endDate, showDays = false, className = '' }) => {
|
|
9533
9551
|
const start = parse(startDate);
|
|
9534
9552
|
const end = parse(endDate);
|
|
9535
9553
|
if (!start || !end)
|
|
9536
9554
|
return null;
|
|
9537
9555
|
const sameDay = start.getTime() === end.getTime();
|
|
9538
9556
|
const crossesYear = start.getFullYear() !== end.getFullYear();
|
|
9539
|
-
|
|
9557
|
+
// Both ends count; rounded, so a daylight-saving change between them can't shift it.
|
|
9558
|
+
const days = Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1;
|
|
9559
|
+
return (jsxs("p", { className: cx(sx(rangeStyle), className), "data-test": "travel-date-range", children: [jsx(DatePart, { date: start, withYear: sameDay || crossesYear }), !sameDay && (jsxs(Fragment, { children: [jsx(DashedArrow, {}), jsx("span", { className: sx(hiddenStyle$1), children: "to" }), jsx(DatePart, { date: end, withYear: true })] })), showDays && days > 0 && (jsxs("span", { className: sx(daysStyle$1), "data-test": "travel-date-range-days", children: [days, " ", days === 1 ? 'day' : 'days'] }))] }));
|
|
9540
9560
|
};
|
|
9541
9561
|
TravelDateRange.displayName = 'TravelDateRange';
|
|
9542
9562
|
|
|
@@ -10757,9 +10777,12 @@ const Table = ({ columns, data, rowKey = 'id', bordered = false, striped = false
|
|
|
10757
10777
|
* A bordered card: a header band with the title and Version control, then the
|
|
10758
10778
|
* invoice table with its total row and legend.
|
|
10759
10779
|
*/
|
|
10780
|
+
/* No `overflow: hidden`: it would clip the Version menu, which hangs below the header over the table
|
|
10781
|
+
and past the card's bottom on a short invoice. The header band paints no background, so its square
|
|
10782
|
+
corners can't show; the table's wrapper (which scrolls, so clips) rounds its own bottom corners, and
|
|
10783
|
+
its top ones too when there is no header (headlessTableStyle). */
|
|
10760
10784
|
const cardStyle$f = {
|
|
10761
10785
|
label: 'AirInvoiceCard-cardStyle',
|
|
10762
|
-
overflow: 'hidden',
|
|
10763
10786
|
backgroundColor: 'var(--bg-surface, #fff)',
|
|
10764
10787
|
border: '1px solid var(--border-color-dark, #d5d5d5)',
|
|
10765
10788
|
borderRadius: 'var(--radius-xl, 12px)',
|
|
@@ -10773,14 +10796,15 @@ const cardStyle$f = {
|
|
|
10773
10796
|
const headerStyle$E = {
|
|
10774
10797
|
label: 'AirInvoiceCard-headerStyle',
|
|
10775
10798
|
boxSizing: 'border-box',
|
|
10776
|
-
/*
|
|
10777
|
-
|
|
10799
|
+
/* 48px with its rule, the same as its partner card's header (BookingDetailsCard / AirInvoiceCard):
|
|
10800
|
+
the 40px Version control and 3px above and below it fit inside, centred. */
|
|
10801
|
+
minHeight: 48,
|
|
10778
10802
|
display: 'flex',
|
|
10779
10803
|
alignItems: 'center',
|
|
10780
10804
|
justifyContent: 'space-between',
|
|
10781
10805
|
flexWrap: 'wrap',
|
|
10782
10806
|
gap: 'var(--space-3, 12px)',
|
|
10783
|
-
padding: '
|
|
10807
|
+
padding: '3px 24px',
|
|
10784
10808
|
borderBottom: '1px solid var(--border-color-dark, #d5d5d5)',
|
|
10785
10809
|
};
|
|
10786
10810
|
const titleStyle$A = {
|
|
@@ -10820,6 +10844,72 @@ const versionButtonStyle = {
|
|
|
10820
10844
|
'&:hover': { backgroundColor: 'var(--color-subtle, #eaf0f6)' },
|
|
10821
10845
|
'&:focus-visible': { outline: '2px solid var(--color-accent, #00bcd0)', outlineOffset: 2 },
|
|
10822
10846
|
};
|
|
10847
|
+
/* The version as a status pill beside the control, on the small line height the segment cards'
|
|
10848
|
+
"Confirmed: …" pill uses (28px tall), so it centres in the 48px header like the 40px button. */
|
|
10849
|
+
const versionBadgeStyle = {
|
|
10850
|
+
label: 'AirInvoiceCard-versionBadgeStyle',
|
|
10851
|
+
whiteSpace: 'nowrap',
|
|
10852
|
+
lineHeight: 'var(--lh-sm)',
|
|
10853
|
+
};
|
|
10854
|
+
/* The menu takes its rows' width, never wrapping a label, from 280px up; the shared menu would
|
|
10855
|
+
otherwise shrink to the trigger's column. */
|
|
10856
|
+
const versionMenuStyle = {
|
|
10857
|
+
label: 'AirInvoiceCard-versionMenuStyle',
|
|
10858
|
+
'& [role="menu"]': {
|
|
10859
|
+
width: 'max-content',
|
|
10860
|
+
minWidth: 280,
|
|
10861
|
+
padding: 'var(--space-3) 10px 10px',
|
|
10862
|
+
},
|
|
10863
|
+
};
|
|
10864
|
+
const versionMenuHeadingStyle = {
|
|
10865
|
+
label: 'AirInvoiceCard-versionMenuHeadingStyle',
|
|
10866
|
+
padding: '0 var(--space-2) 10px',
|
|
10867
|
+
color: 'var(--fg-3, #6b7785)',
|
|
10868
|
+
fontSize: 10,
|
|
10869
|
+
lineHeight: '14px',
|
|
10870
|
+
fontWeight: 700,
|
|
10871
|
+
letterSpacing: '0.1em',
|
|
10872
|
+
textTransform: 'uppercase',
|
|
10873
|
+
};
|
|
10874
|
+
/* Each version a rounded row, highlighted whole, with a hairline between rows inset from their ends.
|
|
10875
|
+
Doubled (0,2,0) over the shared item's own border, padding and hover. */
|
|
10876
|
+
const versionItemStyle = {
|
|
10877
|
+
label: 'AirInvoiceCard-versionItemStyle',
|
|
10878
|
+
'&&': {
|
|
10879
|
+
position: 'relative',
|
|
10880
|
+
padding: '10px',
|
|
10881
|
+
border: 0,
|
|
10882
|
+
borderRadius: 'var(--radius-lg, 8px)',
|
|
10883
|
+
color: 'var(--fg-1, #282828)',
|
|
10884
|
+
fontSize: 13,
|
|
10885
|
+
whiteSpace: 'nowrap',
|
|
10886
|
+
},
|
|
10887
|
+
/* On every row after another, whether or not it carries the selected class too. */
|
|
10888
|
+
'[role="menuitem"] + &&::before': {
|
|
10889
|
+
content: '""',
|
|
10890
|
+
position: 'absolute',
|
|
10891
|
+
top: 0,
|
|
10892
|
+
left: 'var(--space-2)',
|
|
10893
|
+
right: 'var(--space-2)',
|
|
10894
|
+
height: 1,
|
|
10895
|
+
backgroundColor: 'var(--border-color, #e5e5e5)',
|
|
10896
|
+
},
|
|
10897
|
+
'&&:hover, &&:focus-visible': { backgroundColor: 'var(--bg-subtle)' },
|
|
10898
|
+
};
|
|
10899
|
+
/* Tripled (0,3,0) over the row's own hover. */
|
|
10900
|
+
const versionItemSelectedStyle = {
|
|
10901
|
+
label: 'AirInvoiceCard-versionItemSelectedStyle',
|
|
10902
|
+
'&&&, &&&:hover, &&&:focus-visible': {
|
|
10903
|
+
backgroundColor: 'var(--color-primary-light, #E4F1F8)',
|
|
10904
|
+
color: 'var(--color-primary, #19345f)',
|
|
10905
|
+
fontWeight: 'var(--fw-semibold, 600)',
|
|
10906
|
+
},
|
|
10907
|
+
};
|
|
10908
|
+
const versionCheckStyle = {
|
|
10909
|
+
label: 'AirInvoiceCard-versionCheckStyle',
|
|
10910
|
+
display: 'inline-flex',
|
|
10911
|
+
width: 14,
|
|
10912
|
+
};
|
|
10823
10913
|
const versionCountStyle = {
|
|
10824
10914
|
label: 'AirInvoiceCard-versionCountStyle',
|
|
10825
10915
|
minWidth: 22,
|
|
@@ -10840,7 +10930,8 @@ const versionCountStyle = {
|
|
|
10840
10930
|
const tableStyle$9 = {
|
|
10841
10931
|
label: 'AirInvoiceCard-tableStyle',
|
|
10842
10932
|
border: 0,
|
|
10843
|
-
|
|
10933
|
+
/* The card's bottom corners, just inside its 1px border, which no longer clips. */
|
|
10934
|
+
borderRadius: '0 0 calc(var(--radius-xl, 12px) - 1px) calc(var(--radius-xl, 12px) - 1px)',
|
|
10844
10935
|
'& th, & td': { whiteSpace: 'nowrap' },
|
|
10845
10936
|
'& th:first-of-type, & td:first-of-type': { paddingLeft: 24 },
|
|
10846
10937
|
'& th:last-of-type, & td:last-of-type': { paddingRight: 24 },
|
|
@@ -10850,6 +10941,12 @@ const tableStyle$9 = {
|
|
|
10850
10941
|
fontWeight: 700,
|
|
10851
10942
|
},
|
|
10852
10943
|
};
|
|
10944
|
+
/* With no header band, the table's navy header row is the card's top edge: round its top corners too,
|
|
10945
|
+
so the band doesn't square off the card's rounded border. */
|
|
10946
|
+
const headlessTableStyle = {
|
|
10947
|
+
label: 'AirInvoiceCard-headlessTableStyle',
|
|
10948
|
+
borderRadius: 'calc(var(--radius-xl, 12px) - 1px)',
|
|
10949
|
+
};
|
|
10853
10950
|
const travelerCellStyle$1 = {
|
|
10854
10951
|
label: 'AirInvoiceCard-travelerCellStyle',
|
|
10855
10952
|
display: 'inline-flex',
|
|
@@ -10939,8 +11036,16 @@ const AirInvoiceCard = ({ title, lines, paymentStatus, total, versions = [], sel
|
|
|
10939
11036
|
const showLegend = lines.some((line) => line.knownTraveler);
|
|
10940
11037
|
const legend = (jsxs("span", { className: sx(legendStyle$1), "data-test": "air-invoice-legend", children: [jsx("span", { className: sx(knownTravelerStyle), children: jsx(CheckIcon$1, { size: 16 }) }), "= Known Traveler Number Loaded"] }));
|
|
10941
11038
|
const hasHeader = Boolean(title || versionControlShown(versions));
|
|
10942
|
-
const
|
|
10943
|
-
|
|
11039
|
+
const selectedIndex = versions.findIndex((version) => version.id === selectedVersionId);
|
|
11040
|
+
const selected = versions[selectedIndex];
|
|
11041
|
+
// The newest version is the live one; an older one is being viewed. Set as the segment cards' status
|
|
11042
|
+
// pill ("Confirmed: …"): green for the current version, amber for an older one.
|
|
11043
|
+
const state = selectedIndex === 0 ? 'Current' : 'Viewing';
|
|
11044
|
+
const versionBadge = versionControlShown(versions) && selected && (jsx(StatusBadge, { status: selectedIndex === 0 ? 'ticketed' : 'not-ticketed', label: selected.date ? `${state}: ${selected.date}` : state, className: sx(versionBadgeStyle), "data-test": "air-invoice-version-status" }));
|
|
11045
|
+
const versionControl = versionControlShown(versions) && (jsxs(Dropdown, { label: "Version", alignment: "right", className: sx(versionMenuStyle), trigger: ({ isOpen, toggle }) => (jsxs("button", { type: "button", className: sx(versionButtonStyle), onClick: toggle, "aria-expanded": isOpen, "aria-haspopup": "menu", "data-test": "air-invoice-version", children: [jsx(LayersIcon, {}), "Version", jsx("span", { className: sx(versionCountStyle), children: versions.length }), jsx(ChevronDownIcon$1, { size: 16 })] })), children: [jsx("div", { className: sx(versionMenuHeadingStyle), "aria-hidden": "true", children: "Invoice versions" }), versions.map((version) => (jsx(Dropdown.Item, { className: cx(sx(versionItemStyle), version.id === selectedVersionId && sx(versionItemSelectedStyle)),
|
|
11046
|
+
// Every row keeps the check's slot, so the labels line up whichever is picked.
|
|
11047
|
+
icon: jsx("span", { className: sx(versionCheckStyle), children: version.id === selectedVersionId && jsx(CheckIcon$1, { size: 14 }) }), onClick: () => onVersionChange?.(version.id), children: version.label }, version.id)))] }));
|
|
11048
|
+
return (jsxs("section", { className: cx(sx(cardStyle$f), className), "aria-label": "Air invoice", "data-test": "air-invoice-card", children: [hasHeader && (jsxs("div", { className: sx(headerStyle$E), children: [title && jsx("h2", { className: sx(titleStyle$A), children: title }), jsxs("div", { className: sx(headerEndStyle), children: [versionBadge, versionControl] })] })), jsx(Table, { className: cx(sx(tableStyle$9), !hasHeader && sx(headlessTableStyle)), columns: columns, data: lines, hoverable: false, rowClassName: (line) => cx(line.isVoid && sx(voidRowStyle), line.struckThrough && sx(struckRowStyle)) || undefined, footer: total && (jsxs("tr", { children: [jsx("td", { colSpan: 6, children: jsxs("span", { className: sx(totalCellStyle$1), children: ["Total", showLegend && legend] }) }), jsx("td", { className: sx(upperStyle), children: paymentStatus }), jsx("td", { "data-test": "air-invoice-total", children: total })] })) }), showLegend && !total && (jsx("p", { className: sx(legendBlockStyle), children: legend }))] }));
|
|
10944
11049
|
};
|
|
10945
11050
|
AirInvoiceCard.displayName = 'AirInvoiceCard';
|
|
10946
11051
|
|
|
@@ -11513,6 +11618,9 @@ const AppLayout = ({ navItems = [], activeNavKey, logo, collapsedLogo, account,
|
|
|
11513
11618
|
*/
|
|
11514
11619
|
const cardStyle$e = {
|
|
11515
11620
|
label: 'BookingDetailsCard-cardStyle',
|
|
11621
|
+
/* The details go to one column by the card's own width, not the window's. */
|
|
11622
|
+
containerType: 'inline-size',
|
|
11623
|
+
containerName: 'booking-details',
|
|
11516
11624
|
overflow: 'hidden',
|
|
11517
11625
|
backgroundColor: 'var(--bg-surface, #fff)',
|
|
11518
11626
|
border: '1px solid var(--border-color-dark, #d5d5d5)',
|
|
@@ -11526,8 +11634,8 @@ const cardStyle$e = {
|
|
|
11526
11634
|
const headerStyle$D = {
|
|
11527
11635
|
label: 'BookingDetailsCard-headerStyle',
|
|
11528
11636
|
boxSizing: 'border-box',
|
|
11529
|
-
/*
|
|
11530
|
-
minHeight:
|
|
11637
|
+
/* 48px with its rule, the same as its partner card's header (BookingDetailsCard / AirInvoiceCard). */
|
|
11638
|
+
minHeight: 48,
|
|
11531
11639
|
display: 'flex',
|
|
11532
11640
|
alignItems: 'center',
|
|
11533
11641
|
justifyContent: 'space-between',
|
|
@@ -11567,7 +11675,7 @@ const detailsStyle$2 = {
|
|
|
11567
11675
|
rowGap: 8,
|
|
11568
11676
|
margin: 0,
|
|
11569
11677
|
padding: '20px 24px',
|
|
11570
|
-
'@
|
|
11678
|
+
'@container booking-details (max-width: 32rem)': {
|
|
11571
11679
|
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
11572
11680
|
gridAutoFlow: 'row',
|
|
11573
11681
|
gridTemplateRows: 'none !important',
|
|
@@ -14760,12 +14868,22 @@ const HubAppShell = ({ children, navItems, individualTravelItem, agentsItem, sou
|
|
|
14760
14868
|
* Explicit specificity keeps service and cancellation border colors stable
|
|
14761
14869
|
* regardless of style injection order: base < type (`&&`) < cancelled (`&&&`).
|
|
14762
14870
|
*/
|
|
14871
|
+
/* The header's tinted band: edge to edge across the card, so it reads as the segment's title bar. */
|
|
14872
|
+
const HEADER_BAND = 'var(--stm-tint-200, #b8d8e8)';
|
|
14763
14873
|
const cardStyle$a = {
|
|
14764
14874
|
label: 'ItinerarySegmentCard-cardStyle',
|
|
14765
14875
|
display: 'flex',
|
|
14766
14876
|
flexDirection: 'column',
|
|
14767
|
-
|
|
14768
|
-
|
|
14877
|
+
/* 12px between sections, the same as the route's own padding, so each section has even space above and below. */
|
|
14878
|
+
gap: 'var(--space-3)',
|
|
14879
|
+
/* The route stacks by the card's own width, not the window's (ROUTE_STACK below). */
|
|
14880
|
+
containerType: 'inline-size',
|
|
14881
|
+
containerName: 'segment-card',
|
|
14882
|
+
/* The card's padding, as variables the header band's negative margins follow. */
|
|
14883
|
+
'--segment-card-pad-y': 'var(--space-4)',
|
|
14884
|
+
'--segment-card-pad-x': 'var(--space-4)',
|
|
14885
|
+
/* 12px at the bottom, the same as between sections; the top is the header band's. */
|
|
14886
|
+
padding: 'var(--segment-card-pad-y) var(--segment-card-pad-x) var(--space-3)',
|
|
14769
14887
|
backgroundColor: 'var(--bg-surface)',
|
|
14770
14888
|
border: '1px solid var(--border-color-dark)',
|
|
14771
14889
|
borderLeft: '3px solid var(--color-primary)',
|
|
@@ -14806,7 +14924,8 @@ const sectionVariantStyle = {
|
|
|
14806
14924
|
border: 0,
|
|
14807
14925
|
borderRadius: 0,
|
|
14808
14926
|
boxShadow: 'none',
|
|
14809
|
-
|
|
14927
|
+
'--segment-card-pad-y': 'var(--space-5)',
|
|
14928
|
+
'--segment-card-pad-x': 'var(--space-6)',
|
|
14810
14929
|
},
|
|
14811
14930
|
};
|
|
14812
14931
|
const headerStyle$w = {
|
|
@@ -14814,29 +14933,28 @@ const headerStyle$w = {
|
|
|
14814
14933
|
display: 'flex',
|
|
14815
14934
|
alignItems: 'center',
|
|
14816
14935
|
gap: 'var(--space-3)',
|
|
14936
|
+
/* A band across the card's top, edge to edge: out over the card's padding, back in with its own. */
|
|
14937
|
+
margin: 'calc(-1 * var(--segment-card-pad-y)) calc(-1 * var(--segment-card-pad-x)) 0',
|
|
14938
|
+
padding: 'var(--space-3) var(--segment-card-pad-x)',
|
|
14939
|
+
backgroundColor: HEADER_BAND,
|
|
14940
|
+
borderTopLeftRadius: 'inherit',
|
|
14941
|
+
borderTopRightRadius: 'inherit',
|
|
14942
|
+
flexWrap: 'wrap',
|
|
14817
14943
|
};
|
|
14818
14944
|
const iconStyle$2 = {
|
|
14819
14945
|
label: 'ItinerarySegmentCard-iconStyle',
|
|
14820
14946
|
display: 'inline-flex',
|
|
14821
14947
|
flexShrink: 0,
|
|
14822
14948
|
};
|
|
14949
|
+
/* The headings take the room the check-in button and status pill leave, down to 12rem; only
|
|
14950
|
+
below that do those wrap under them, whatever the window width. */
|
|
14823
14951
|
const headingsStyle = {
|
|
14824
14952
|
label: 'ItinerarySegmentCard-headingsStyle',
|
|
14825
14953
|
display: 'flex',
|
|
14826
14954
|
flexDirection: 'column',
|
|
14827
14955
|
gap: 'var(--space-0-5)',
|
|
14828
14956
|
minWidth: 0,
|
|
14829
|
-
flex: 1,
|
|
14830
|
-
};
|
|
14831
|
-
const confirmationHeaderStyle = {
|
|
14832
|
-
label: 'ItinerarySegmentCard-confirmationHeaderStyle',
|
|
14833
|
-
flexWrap: 'wrap',
|
|
14834
|
-
[`& .${sx(headingsStyle)}`]: {
|
|
14835
|
-
flexBasis: '12rem',
|
|
14836
|
-
'@media (max-width: 767px)': {
|
|
14837
|
-
flexBasis: 'calc(100% - 48px - var(--space-3))',
|
|
14838
|
-
},
|
|
14839
|
-
},
|
|
14957
|
+
flex: '1 1 12rem',
|
|
14840
14958
|
};
|
|
14841
14959
|
const eyebrowStyle$8 = {
|
|
14842
14960
|
label: 'ItinerarySegmentCard-eyebrowStyle',
|
|
@@ -14897,6 +15015,17 @@ const endpointStyle = {
|
|
|
14897
15015
|
gap: 'var(--space-0-5)',
|
|
14898
15016
|
minWidth: 0,
|
|
14899
15017
|
};
|
|
15018
|
+
/* Two endpoints side by side in one route column: a black car's pickup time, then its pickup location. */
|
|
15019
|
+
const endpointGroupStyle = {
|
|
15020
|
+
label: 'ItinerarySegmentCard-endpointGroupStyle',
|
|
15021
|
+
display: 'flex',
|
|
15022
|
+
flexWrap: 'wrap',
|
|
15023
|
+
alignItems: 'flex-start',
|
|
15024
|
+
gap: 'var(--space-2) var(--space-8)',
|
|
15025
|
+
minWidth: 0,
|
|
15026
|
+
};
|
|
15027
|
+
/* Narrower than this, a card stacks its route (departs / connector / arrives) in one column. */
|
|
15028
|
+
const ROUTE_STACK = '@container segment-card (max-width: 32rem)';
|
|
14900
15029
|
const routeStyle$2 = {
|
|
14901
15030
|
label: 'ItinerarySegmentCard-routeStyle',
|
|
14902
15031
|
display: 'grid',
|
|
@@ -14906,23 +15035,32 @@ const routeStyle$2 = {
|
|
|
14906
15035
|
padding: 'var(--space-3) 0',
|
|
14907
15036
|
borderTop: '1px solid var(--border-color)',
|
|
14908
15037
|
borderBottom: '1px solid var(--border-color)',
|
|
15038
|
+
/* Straight under the header band (always the card's first child; its class is merged with the
|
|
15039
|
+
header variant's, so it is matched by position): the band is its top edge, the card's 12px gap its top padding. */
|
|
15040
|
+
[`*:first-child + &`]: {
|
|
15041
|
+
borderTop: 0,
|
|
15042
|
+
paddingTop: 0,
|
|
15043
|
+
},
|
|
14909
15044
|
/* `.route .endpoint:last-child` (0,2,1), with the endpoint's own
|
|
14910
15045
|
content-hashed class standing in for `.endpoint`. */
|
|
14911
|
-
[`& .${sx(endpointStyle)}:last-child`]: {
|
|
15046
|
+
[`& > .${sx(endpointStyle)}:last-child`]: {
|
|
14912
15047
|
textAlign: 'right',
|
|
14913
15048
|
alignItems: 'flex-end',
|
|
14914
|
-
|
|
15049
|
+
[ROUTE_STACK]: {
|
|
14915
15050
|
textAlign: 'left',
|
|
14916
15051
|
alignItems: 'flex-start',
|
|
14917
15052
|
},
|
|
14918
15053
|
},
|
|
14919
|
-
[`& .${sx(endpointStyle)}:only-child`]: {
|
|
15054
|
+
[`& > .${sx(endpointStyle)}:only-child`]: {
|
|
14920
15055
|
gridColumn: '1 / -1',
|
|
14921
15056
|
textAlign: 'left',
|
|
14922
15057
|
alignItems: 'flex-start',
|
|
14923
15058
|
},
|
|
14924
|
-
/*
|
|
14925
|
-
|
|
15059
|
+
/* A location-only black car: its pick-up group is the route's only child. */
|
|
15060
|
+
[`& > .${sx(endpointGroupStyle)}:only-child`]: {
|
|
15061
|
+
gridColumn: '1 / -1',
|
|
15062
|
+
},
|
|
15063
|
+
[ROUTE_STACK]: {
|
|
14926
15064
|
gridTemplateColumns: '1fr',
|
|
14927
15065
|
gap: 'var(--space-3)',
|
|
14928
15066
|
},
|
|
@@ -14946,13 +15084,23 @@ const endpointTimeStyle = {
|
|
|
14946
15084
|
fontWeight: 'var(--fw-semibold)',
|
|
14947
15085
|
color: 'var(--fg-1)',
|
|
14948
15086
|
};
|
|
15087
|
+
/* "next / day" stacked beside the arrival time: two 14px lines, together the time's own 28px line. */
|
|
14949
15088
|
const nextDayStyle = {
|
|
14950
15089
|
label: 'ItinerarySegmentCard-nextDayStyle',
|
|
14951
|
-
|
|
14952
|
-
|
|
15090
|
+
display: 'inline-flex',
|
|
15091
|
+
flexDirection: 'column',
|
|
15092
|
+
alignSelf: 'center',
|
|
15093
|
+
fontSize: 11,
|
|
15094
|
+
lineHeight: '14px',
|
|
14953
15095
|
fontWeight: 'var(--fw-semibold)',
|
|
14954
15096
|
color: 'var(--color-warning)',
|
|
14955
15097
|
whiteSpace: 'nowrap',
|
|
15098
|
+
textAlign: 'left',
|
|
15099
|
+
};
|
|
15100
|
+
/* "same / day": an arrival earlier on the clock than the departure, on the same date. Muted, not a warning. */
|
|
15101
|
+
const sameDayStyle = {
|
|
15102
|
+
label: 'ItinerarySegmentCard-sameDayStyle',
|
|
15103
|
+
color: 'var(--fg-3)',
|
|
14956
15104
|
};
|
|
14957
15105
|
const endpointCodeStyle = {
|
|
14958
15106
|
label: 'ItinerarySegmentCard-endpointCodeStyle',
|
|
@@ -14973,14 +15121,14 @@ const connectorStyle = {
|
|
|
14973
15121
|
label: 'ItinerarySegmentCard-connectorStyle',
|
|
14974
15122
|
alignSelf: 'center',
|
|
14975
15123
|
display: 'flex',
|
|
14976
|
-
|
|
15124
|
+
/* One line (duration → miles); it wraps, stacking, only when the column is too narrow for it. */
|
|
15125
|
+
flexDirection: 'row',
|
|
15126
|
+
flexWrap: 'wrap',
|
|
15127
|
+
justifyContent: 'center',
|
|
14977
15128
|
alignItems: 'center',
|
|
14978
|
-
gap: 'var(--space-1)',
|
|
15129
|
+
gap: 'var(--space-1) var(--space-2)',
|
|
14979
15130
|
minWidth: 72,
|
|
14980
|
-
|
|
14981
|
-
flexDirection: 'row',
|
|
14982
|
-
justifyContent: 'center',
|
|
14983
|
-
gap: 'var(--space-2)',
|
|
15131
|
+
[ROUTE_STACK]: {
|
|
14984
15132
|
width: '100%',
|
|
14985
15133
|
},
|
|
14986
15134
|
};
|
|
@@ -15030,11 +15178,6 @@ const detailLabelStyle = {
|
|
|
15030
15178
|
whiteSpace: 'nowrap',
|
|
15031
15179
|
'&::after': { content: '":"' },
|
|
15032
15180
|
};
|
|
15033
|
-
/** The second pair in one cell, e.g. `Estimated total` after `Rate`, spaced like a new field. */
|
|
15034
|
-
const detailBelowLabelStyle = {
|
|
15035
|
-
label: 'ItinerarySegmentCard-detailBelowLabelStyle',
|
|
15036
|
-
marginLeft: 'var(--space-5)',
|
|
15037
|
-
};
|
|
15038
15181
|
/** A value that needs attention, e.g. `Unassigned` seats. */
|
|
15039
15182
|
const unassignedStyle = {
|
|
15040
15183
|
label: 'ItinerarySegmentCard-unassignedStyle',
|
|
@@ -15047,6 +15190,38 @@ const detailValueStyle = {
|
|
|
15047
15190
|
color: 'var(--fg-1)',
|
|
15048
15191
|
overflowWrap: 'anywhere',
|
|
15049
15192
|
};
|
|
15193
|
+
/* `detailsLayout="stacked"`: each detail a small uppercase label over its value, in a wrapping row,
|
|
15194
|
+
as a host's own fact rows set them (Hub's Rate / Estimated Total / Loyalty Number). */
|
|
15195
|
+
const detailsStackedStyle = {
|
|
15196
|
+
label: 'ItinerarySegmentCard-detailsStackedStyle',
|
|
15197
|
+
gap: 'var(--space-3) var(--space-10)',
|
|
15198
|
+
[`& > .${sx(detailStyle$1)}`]: {
|
|
15199
|
+
flexDirection: 'column',
|
|
15200
|
+
alignItems: 'flex-start',
|
|
15201
|
+
gap: 0,
|
|
15202
|
+
},
|
|
15203
|
+
[`& .${sx(detailLabelStyle)}`]: {
|
|
15204
|
+
fontSize: 'var(--fs-xs)',
|
|
15205
|
+
lineHeight: 'var(--lh-xs)',
|
|
15206
|
+
letterSpacing: '0.025em',
|
|
15207
|
+
textTransform: 'uppercase',
|
|
15208
|
+
'&::after': { content: 'none' },
|
|
15209
|
+
},
|
|
15210
|
+
[`& .${sx(detailValueStyle)}`]: {
|
|
15211
|
+
fontWeight: 'var(--fw-regular, 400)',
|
|
15212
|
+
},
|
|
15213
|
+
};
|
|
15214
|
+
/* Straight under the route, a ruled section drops its own rule: the route's bottom rule already
|
|
15215
|
+
divides them, and a second one 12px below it would double it. */
|
|
15216
|
+
const afterRoute = { [`.${sx(routeStyle$2)} + &`]: { borderTop: 0, paddingTop: 0 } };
|
|
15217
|
+
/* A car's or hotel's facts row (Rate, Estimated Total, ...): stacked labels over values, under a rule
|
|
15218
|
+
of its own, below the address and phone. */
|
|
15219
|
+
const factsStyle$5 = {
|
|
15220
|
+
label: 'ItinerarySegmentCard-factsStyle',
|
|
15221
|
+
paddingTop: 'var(--space-3)',
|
|
15222
|
+
borderTop: '1px solid var(--border-color)',
|
|
15223
|
+
...afterRoute,
|
|
15224
|
+
};
|
|
15050
15225
|
/** Plain: `.mono` adds font-family and letter-spacing, neither of which
|
|
15051
15226
|
`.detailValue` sets, so it never needed the source order it happened to have. */
|
|
15052
15227
|
const monoStyle$2 = {
|
|
@@ -15107,6 +15282,9 @@ const footerStyle$7 = {
|
|
|
15107
15282
|
fontSize: 'var(--fs-sm)',
|
|
15108
15283
|
lineHeight: 'var(--lh-sm)',
|
|
15109
15284
|
color: 'var(--fg-3)',
|
|
15285
|
+
...afterRoute,
|
|
15286
|
+
/* A host footer that renders nothing draws no rule over nothing. */
|
|
15287
|
+
'&:empty': { display: 'none' },
|
|
15110
15288
|
};
|
|
15111
15289
|
|
|
15112
15290
|
/* -------------------------------------------------------------------------- */
|
|
@@ -15237,31 +15415,48 @@ const SEGMENT_META = {
|
|
|
15237
15415
|
limo: { code: 'L', label: 'Black Car' },
|
|
15238
15416
|
rail: { code: 'R', label: 'Rail' },
|
|
15239
15417
|
};
|
|
15418
|
+
/* A motor coach is set like rail, under the bus icon and Hub's own label for it. */
|
|
15419
|
+
const BUS_META = { code: 'B', label: 'Motor Coach' };
|
|
15240
15420
|
/** Address lines read on one line, comma-separated; each line keeps its own element. */
|
|
15241
15421
|
const inlineAddress = (lines) => (jsx("span", { children: lines.map((line, index) => jsxs(Fragment$1, { children: [index > 0 && ', ', jsx("span", { children: line })] }, index)) }));
|
|
15242
15422
|
const phoneLink = (phone) => jsx("a", { className: sx(linkStyle), href: `tel:${phone.replace(/[^\d+]/g, '')}`, children: phone });
|
|
15243
15423
|
/** An address with its phone number beside it on the same line; either may stand alone. */
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15424
|
+
/** An address and its phone as two details, each label kept with its value: a long address pushes the
|
|
15425
|
+
phone, whole, onto the next line rather than splitting "Phone:" from its number. */
|
|
15426
|
+
const addressWithPhone = (addressLabel, lines, phoneLabel, phone) => [
|
|
15427
|
+
...(lines?.length ? [{ label: addressLabel, value: inlineAddress(lines) }] : []),
|
|
15428
|
+
...(phone?.trim() ? [{ label: phoneLabel, value: phoneLink(phone) }] : []),
|
|
15429
|
+
];
|
|
15250
15430
|
const normalizeLocation = (value) => (value ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
15251
|
-
/** `Rate
|
|
15252
|
-
const
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15431
|
+
/** `Rate`, then the estimated total as its own fact (with a note after it, e.g. `(SUV)`); either may stand alone. */
|
|
15432
|
+
const rateFacts = (segment, rate, money, totalNote) => [
|
|
15433
|
+
...(rate ? [{ label: 'Rate', value: rate }] : []),
|
|
15434
|
+
...(segment.cost !== undefined ? [{ label: 'Estimated Total', value: `${money(segment.cost, segment.currency)}${totalNote ? ` ${totalNote}` : ''}` }] : []),
|
|
15435
|
+
];
|
|
15436
|
+
/** An amount with its unit (`$187.00 / night`), or alone when the unit is blank. */
|
|
15437
|
+
const perUnit = (amount, unit) => (unit?.trim() ? `${amount} / ${unit.trim()}` : amount);
|
|
15438
|
+
/** A car's rate, qualified by its category (`$66.80 daily`); with no amount the category is the rate's whole value. */
|
|
15439
|
+
const carRate = (segment, money) => {
|
|
15440
|
+
const category = segment.rateCategory?.trim().toLowerCase();
|
|
15441
|
+
if (segment.dailyRate === undefined)
|
|
15442
|
+
return category ? category[0].toUpperCase() + category.slice(1) : undefined;
|
|
15443
|
+
const amount = money(segment.dailyRate, segment.currency);
|
|
15444
|
+
return category ? `${amount} ${category}` : perUnit(amount, segment.rateUnit === undefined ? 'day' : segment.rateUnit);
|
|
15445
|
+
};
|
|
15446
|
+
const loyaltyDetail = (segment) => {
|
|
15447
|
+
const numbers = segment.loyaltyNumbers?.filter((number) => number.trim());
|
|
15448
|
+
return numbers?.length ? [{ label: 'Loyalty Number', value: numbers.join(' · '), mono: true }] : [];
|
|
15257
15449
|
};
|
|
15258
15450
|
const buildDetails = (segment) => {
|
|
15259
15451
|
const details = [];
|
|
15260
15452
|
if (segment.type === 'limo') {
|
|
15261
|
-
if (segment.pickupLocation?.trim())
|
|
15262
|
-
details.push({ label: 'Pickup location', value: segment.pickupLocation });
|
|
15263
15453
|
if (segment.totalDue?.trim())
|
|
15264
15454
|
details.push({ label: 'Estimated Total Due', value: segment.totalDue });
|
|
15455
|
+
// The car service sits with the booking's details, above its notes.
|
|
15456
|
+
if (segment.vendor?.trim())
|
|
15457
|
+
details.push({ label: 'Vendor', value: segment.vendor });
|
|
15458
|
+
if (segment.vendorPhone?.trim())
|
|
15459
|
+
details.push({ label: 'Vendor phone', value: phoneLink(segment.vendorPhone) });
|
|
15265
15460
|
}
|
|
15266
15461
|
if (segment.type === 'rail') {
|
|
15267
15462
|
if (segment.operatedBy?.trim())
|
|
@@ -15272,58 +15467,84 @@ const buildDetails = (segment) => {
|
|
|
15272
15467
|
details.push({ label: 'Class', value: segment.cabin });
|
|
15273
15468
|
}
|
|
15274
15469
|
if (segment.type === 'air') {
|
|
15275
|
-
|
|
15276
|
-
details.push({ label: 'Class', value: segment.cabin });
|
|
15470
|
+
// Seats first, then Class, as on rail.
|
|
15277
15471
|
if (segment.seatsUnassigned)
|
|
15278
15472
|
details.push({ label: 'Seats', value: jsx("span", { className: sx(unassignedStyle), "data-test": `itinerary-seats-unassigned-${segment.id}`, children: "Unassigned" }) });
|
|
15279
15473
|
else if (segment.seats?.length)
|
|
15280
15474
|
details.push({ label: segment.seats.length > 1 ? 'Seats' : 'Seat', value: segment.seats.join(', ') });
|
|
15475
|
+
if (segment.cabin)
|
|
15476
|
+
details.push({ label: 'Class', value: segment.cabin });
|
|
15477
|
+
// A flight's loyalty numbers sit with its seat and class, before CO₂ (other types list them last).
|
|
15478
|
+
details.push(...loyaltyDetail(segment));
|
|
15281
15479
|
if (segment.co2Emissions?.trim())
|
|
15282
15480
|
details.push({ label: 'CO₂ emissions', value: segment.co2Emissions });
|
|
15283
15481
|
}
|
|
15284
15482
|
if (segment.type === 'car') {
|
|
15285
|
-
|
|
15286
|
-
details.push(...
|
|
15287
|
-
|
|
15288
|
-
if (pickup)
|
|
15289
|
-
details.push(pickup);
|
|
15483
|
+
// The rate, total, car type and loyalty number are the facts row's (buildFacts).
|
|
15484
|
+
details.push(...addressWithPhone('Pick-Up Address', segment.pickupAddress?.filter((line) => line.trim()), 'Pick-Up Phone', segment.phone));
|
|
15485
|
+
// A one-way rental's drop-off, set like its pick-up; left out when it's the same counter.
|
|
15290
15486
|
const dropoffLines = segment.dropoffAddress?.filter((line) => line.trim());
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15487
|
+
// Compared by address, or by location where a side has no address, so two blank addresses
|
|
15488
|
+
// don't make different counters the same.
|
|
15489
|
+
const counter = (address, location) => normalizeLocation((address ?? []).join(' ')) || normalizeLocation(location);
|
|
15490
|
+
const sameAsPickup = counter(segment.dropoffAddress, segment.dropoffLocation) === counter(segment.pickupAddress, segment.pickupLocation);
|
|
15491
|
+
if (!sameAsPickup)
|
|
15492
|
+
details.push(...addressWithPhone('Drop-Off Address', dropoffLines, 'Drop-Off Phone', segment.dropoffPhone));
|
|
15294
15493
|
}
|
|
15295
15494
|
if (segment.type === 'hotel') {
|
|
15296
|
-
|
|
15297
|
-
details.push({ label: 'Chain', value: segment.chainCode ? `${segment.chain} (${segment.chainCode})` : segment.chain });
|
|
15298
|
-
if (segment.roomType)
|
|
15299
|
-
details.push({ label: 'Room type', value: segment.roomType });
|
|
15300
|
-
details.push(...rateDetails(segment, segment.nightlyRate, 'night', 'Estimated Total'));
|
|
15495
|
+
// The rate, total, loyalty number, room and chain are the facts row's (buildFacts).
|
|
15301
15496
|
const hotelLines = [...(segment.address ?? []), ...(segment.cityLine ? [segment.cityLine] : [])].filter((line) => line.trim());
|
|
15302
|
-
|
|
15303
|
-
if (hotelAddress)
|
|
15304
|
-
details.push(hotelAddress);
|
|
15497
|
+
details.push(...addressWithPhone('Address', hotelLines, 'Phone', segment.phone));
|
|
15305
15498
|
}
|
|
15306
15499
|
// Cars and hotels carry the reservation name under their vendor name instead; flights leave it out.
|
|
15307
15500
|
if (segment.reservationName && (segment.type === 'rail' || segment.type === 'limo'))
|
|
15308
15501
|
details.push({ label: 'Reserved for', value: segment.reservationName });
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
details.push({ label: 'Loyalty Number', value: loyaltyNumbers.join(' · '), mono: true });
|
|
15502
|
+
if (segment.type === 'rail' || segment.type === 'limo')
|
|
15503
|
+
details.push(...loyaltyDetail(segment));
|
|
15312
15504
|
return details;
|
|
15313
15505
|
};
|
|
15506
|
+
/**
|
|
15507
|
+
* A rental's or a stay's facts — rate, estimated total, and the rest — as one row of small labels over
|
|
15508
|
+
* values under a rule, below the address and phone, as Hub's itinerary sets them.
|
|
15509
|
+
*/
|
|
15510
|
+
const buildFacts = (segment, money) => {
|
|
15511
|
+
if (segment.type === 'car') {
|
|
15512
|
+
return [
|
|
15513
|
+
...rateFacts(segment, carRate(segment, money), money, segment.isSuv ? '(SUV)' : undefined),
|
|
15514
|
+
// A host that shows the car type elsewhere (or has none) passes it blank, and the fact is left out.
|
|
15515
|
+
...(segment.carType?.trim() ? [{ label: 'Car type', value: segment.carTypeCode ? `${segment.carType} (${segment.carTypeCode})` : segment.carType }] : []),
|
|
15516
|
+
...loyaltyDetail(segment),
|
|
15517
|
+
];
|
|
15518
|
+
}
|
|
15519
|
+
if (segment.type === 'hotel') {
|
|
15520
|
+
return [
|
|
15521
|
+
...rateFacts(segment, segment.nightlyRate !== undefined ? perUnit(money(segment.nightlyRate, segment.currency), segment.rateUnit ?? 'night') : undefined, money),
|
|
15522
|
+
...loyaltyDetail(segment),
|
|
15523
|
+
...(segment.roomType ? [{ label: 'Room type', value: segment.roomType }] : []),
|
|
15524
|
+
...(segment.chain ? [{ label: 'Chain', value: segment.chainCode ? `${segment.chain} (${segment.chainCode})` : segment.chain }] : []),
|
|
15525
|
+
];
|
|
15526
|
+
}
|
|
15527
|
+
return [];
|
|
15528
|
+
};
|
|
15314
15529
|
/**
|
|
15315
15530
|
* One flight, rental car, hotel, Black Car or rail booking.
|
|
15316
15531
|
* Uses a shared header, journey board and labelled details grounded in Hub.
|
|
15317
15532
|
*/
|
|
15318
|
-
const ItinerarySegmentCard = ({ segment, headingLevel = 3, onCheckIn, showCheckIn = false, variant = 'card', footer, renderVendorMark, className = '', }) => {
|
|
15319
|
-
const meta = SEGMENT_META[segment.type];
|
|
15533
|
+
const ItinerarySegmentCard = ({ segment, headingLevel = 3, onCheckIn, showCheckIn = false, variant = 'card', footer, detailsLayout = 'inline', renderVendorMark, formatMoney = formatItineraryMoney, className = '', }) => {
|
|
15534
|
+
const meta = segment.type === 'rail' && segment.mode === 'bus' ? BUS_META : SEGMENT_META[segment.type];
|
|
15320
15535
|
const badge = STATUS_BADGE$1[segment.status];
|
|
15321
15536
|
const pillConfirmation = segment.confirmation?.trim();
|
|
15322
15537
|
const details = buildDetails(segment);
|
|
15538
|
+
const facts = buildFacts(segment, formatMoney);
|
|
15323
15539
|
const Heading = `h${headingLevel}`;
|
|
15324
15540
|
const vendorCode = segment.type === 'air' || segment.type === 'rail' ? segment.carrierCode : segment.type === 'car' ? segment.vendorCode : segment.type === 'hotel' ? segment.chainCode : undefined;
|
|
15325
15541
|
// Flights and trains both flag an arrival on a later calendar day.
|
|
15326
15542
|
const dayOffset = segment.type === 'air' || segment.type === 'rail' ? calendarDayOffset(segment.departureDate, segment.arrivalDate) : 0;
|
|
15543
|
+
// Lands on the date it left, at an earlier clock time: eastward over the date line (SYD 10:10 AM ->
|
|
15544
|
+
// LAX 6:00 AM, both Oct 4). Only with an arrival date given, so an unknown one never reads as "same day".
|
|
15545
|
+
const arrivesEarlierSameDay = (segment.type === 'air' || segment.type === 'rail')
|
|
15546
|
+
&& !!segment.arrivalDate && dayOffset === 0
|
|
15547
|
+
&& (parseClockTime(segment.arrivalTime) ?? Infinity) < (parseClockTime(segment.departureTime) ?? -Infinity);
|
|
15327
15548
|
const duration = segment.type === 'air' || segment.type === 'rail'
|
|
15328
15549
|
? segment.elapsed
|
|
15329
15550
|
: segment.type === 'car' || segment.type === 'limo'
|
|
@@ -15353,9 +15574,11 @@ const ItinerarySegmentCard = ({ segment, headingLevel = 3, onCheckIn, showCheckI
|
|
|
15353
15574
|
}
|
|
15354
15575
|
: segment.type === 'limo'
|
|
15355
15576
|
? {
|
|
15356
|
-
|
|
15577
|
+
// The pickup date only on a standalone card: as a timeline section, the day's header above
|
|
15578
|
+
// already carries it. A date-only black car is labelled by its date.
|
|
15579
|
+
startLabel: segment.pickupTime?.trim() || variant === 'section' ? 'Pick-up time' : 'Pick-up date',
|
|
15357
15580
|
startTime: segment.pickupTime?.trim(),
|
|
15358
|
-
startCode: segment.pickupDate ? formatItineraryDate(segment.pickupDate) : '',
|
|
15581
|
+
startCode: variant === 'card' && segment.pickupDate ? formatItineraryDate(segment.pickupDate) : '',
|
|
15359
15582
|
startPlace: '',
|
|
15360
15583
|
endLabel: 'Drop-off location',
|
|
15361
15584
|
endTime: segment.dropoffLocation?.trim(),
|
|
@@ -15372,15 +15595,31 @@ const ItinerarySegmentCard = ({ segment, headingLevel = 3, onCheckIn, showCheckI
|
|
|
15372
15595
|
endCode: '',
|
|
15373
15596
|
endPlace: '',
|
|
15374
15597
|
};
|
|
15375
|
-
|
|
15598
|
+
// An airport or station code rides on its Departs / Arrives label ("DEPARTS ABI"), in the label's type,
|
|
15599
|
+
// above the time; the other types' second line (a car's or black car's date) keeps its own place.
|
|
15600
|
+
const codeOnLabel = segment.type === 'air' || segment.type === 'rail';
|
|
15601
|
+
// A black car's pickup location is its own block beside the pickup time, labelled and set like the
|
|
15602
|
+
// drop-off location opposite it.
|
|
15603
|
+
const pickupLocation = segment.type === 'limo' ? segment.pickupLocation?.trim() ?? '' : '';
|
|
15604
|
+
const hasPickupWhen = !!(endpoints.startTime || endpoints.startCode || endpoints.startPlace);
|
|
15605
|
+
const hasStart = hasPickupWhen || !!pickupLocation;
|
|
15376
15606
|
const hasEnd = !!(endpoints.endTime || endpoints.endCode || endpoints.endPlace);
|
|
15377
|
-
const
|
|
15607
|
+
const checkInUrl = segment.type === 'air' ? segment.checkInUrl?.trim() : undefined;
|
|
15608
|
+
const canCheckIn = !!checkInUrl || !!onCheckIn;
|
|
15609
|
+
const headerCheckIn = segment.type === 'air' && showCheckIn && canCheckIn;
|
|
15378
15610
|
const checkInLabel = segment.type === 'air' ? `Check in for ${segment.carrierCode ?? segment.carrier} ${segment.flightNumber}` : '';
|
|
15611
|
+
/** Check In as a link to the airline's page when the flight has one, else a button calling `onCheckIn`. */
|
|
15612
|
+
const checkInControl = (variant, children, ariaLabel) => checkInUrl ? (jsx("a", { href: checkInUrl, target: "_blank", rel: "noopener noreferrer", className: cx(sx(buttonStyle), sx(variantStyles$7[variant]), sx(sizeStyles$a.sm)), "aria-label": ariaLabel, "data-test": `itinerary-check-in-${segment.id}`, children: jsx("span", { className: sx(contentStyle$d), children: children }) })) : (jsx(Button, { size: "sm", variant: variant, "aria-label": ariaLabel, "data-test": `itinerary-check-in-${segment.id}`, onClick: () => segment.type === 'air' && onCheckIn?.(segment), children: children }));
|
|
15379
15613
|
const noteSections = [
|
|
15380
15614
|
...(segment.type === 'limo' ? [{ label: 'Cancellation policy', lines: segment.cancellationPolicies }] : []),
|
|
15381
15615
|
{ label: 'Segment Notes', lines: segment.type === 'limo' ? [...(segment.comments ?? []), ...(segment.notes ?? [])] : segment.notes },
|
|
15382
15616
|
];
|
|
15383
|
-
return (jsxs("article", { className: cx(sx(cardStyle$a), sx(typeStyles$1[segment.type]), segment.status === 'cancelled' && sx(isCancelledStyle), variant === 'section' && sx(sectionVariantStyle), className), "data-test": `itinerary-segment-${segment.id}`, "aria-label": `${meta.label} — ${getSegmentTitle(segment)}`, children: [jsxs("div", { className:
|
|
15617
|
+
return (jsxs("article", { className: cx(sx(cardStyle$a), sx(typeStyles$1[segment.type]), segment.status === 'cancelled' && sx(isCancelledStyle), variant === 'section' && sx(sectionVariantStyle), className), "data-test": `itinerary-segment-${segment.id}`, "aria-label": `${meta.label} — ${getSegmentTitle(segment)}`, children: [jsxs("div", { className: sx(headerStyle$w), children: [jsx("span", { className: sx(iconStyle$2), "aria-hidden": "true", children: renderVendorMark?.(segment, vendorCode) ?? jsx(TravelServiceIcon, { type: meta.code, vendor: vendorCode, size: 48 }) }), jsxs("span", { className: sx(headingsStyle), children: [jsx("span", { className: sx(eyebrowStyle$8), children: meta.label }), jsx("span", { className: sx(titleRowStyle$2), children: jsx(Heading, { className: sx(titleStyle$u), children: getSegmentTitle(segment) }) }), segment.type === 'air' && segment.operatedBy?.trim() && jsxs("span", { className: sx(operatedByStyle), children: ["Operated by ", segment.operatedBy] }), (segment.type === 'car' || segment.type === 'hotel') && segment.reservationName?.trim() && (jsxs("span", { className: sx(operatedByStyle), "data-test": `itinerary-reserved-for-${segment.id}`, children: ["Reserved for ", segment.reservationName] }))] }), headerCheckIn && checkInControl('primary', 'Check In', checkInLabel), jsx(StatusBadge, { status: badge.variant, label: pillConfirmation ? `${badge.label}: ${pillConfirmation}` : badge.label, className: sx(statusStyle$4), style: pillConfirmation ? { maxWidth: '100%', whiteSpace: 'normal', overflowWrap: 'anywhere', lineHeight: 'var(--lh-sm)' } : undefined })] }), (hasStart || hasEnd) && jsxs("div", { className: sx(routeStyle$2), children: [hasStart && (() => {
|
|
15618
|
+
const when = hasPickupWhen && jsxs("div", { className: sx(endpointStyle), children: [jsxs("span", { className: sx(endpointLabelStyle), children: [endpoints.startLabel, codeOnLabel && endpoints.startCode && ` ${endpoints.startCode}`] }), segment.type === 'car' && jsx("span", { className: sx(endpointTimeStyle), children: endpoints.startCode }), endpoints.startTime && jsx("span", { className: sx(segment.type === 'car' ? endpointCodeStyle : endpointTimeStyle), children: endpoints.startTime }), !codeOnLabel && segment.type !== 'car' && endpoints.startCode && jsx("span", { className: sx(endpointCodeStyle), children: endpoints.startCode }), endpoints.startPlace && jsx("span", { className: sx(endpointPlaceStyle), children: endpoints.startPlace })] });
|
|
15619
|
+
if (!pickupLocation)
|
|
15620
|
+
return when;
|
|
15621
|
+
return (jsxs("div", { className: sx(endpointGroupStyle), children: [when, jsxs("div", { className: sx(endpointStyle), children: [jsx("span", { className: sx(endpointLabelStyle), children: "Pick-up location" }), jsx("span", { className: sx(endpointTimeStyle), children: pickupLocation })] })] }));
|
|
15622
|
+
})(), hasStart && hasEnd && jsxs("div", { className: sx(connectorStyle), children: [duration && jsx("span", { className: sx(connectorLabelStyle), children: duration }), jsx("span", { className: sx(connectorArrowStyle), "aria-hidden": "true", children: jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" }), jsx("polyline", { points: "12 5 19 12 12 19" })] }) }), (segment.type === 'air' || segment.type === 'rail') && !!segment.miles && jsxs("span", { className: sx(connectorLabelStyle), children: [segment.miles.toLocaleString('en-US'), " miles"] }), segment.type === 'car' && !!segment.carCount && jsxs("span", { className: sx(connectorLabelStyle), children: [segment.carCount, " ", segment.carCount === 1 ? 'Vehicle' : 'Vehicles'] }), segment.type === 'hotel' && !!segment.rooms && jsxs("span", { className: sx(connectorLabelStyle), children: [segment.rooms, " ", segment.rooms === 1 ? 'Room' : 'Rooms'] })] }), hasEnd && jsxs("div", { className: sx(endpointStyle), children: [jsxs("span", { className: sx(endpointLabelStyle), children: [endpoints.endLabel, codeOnLabel && endpoints.endCode && ` ${endpoints.endCode}`] }), segment.type === 'car' && jsx("span", { className: sx(endpointTimeStyle), children: endpoints.endCode }), endpoints.endTime && (jsxs("span", { className: sx(segment.type === 'car' ? endpointCodeStyle : endpointTimeStyle), children: [endpoints.endTime, dayOffset > 0 && (jsxs("span", { className: sx(nextDayStyle), role: "img", "aria-label": dayOffset === 1 ? 'next day' : `${dayOffset} days later`, children: [jsx("span", { "aria-hidden": "true", children: dayOffset === 1 ? 'next' : `+${dayOffset}` }), jsx("span", { "aria-hidden": "true", children: dayOffset === 1 ? 'day' : 'days' })] })), arrivesEarlierSameDay && (jsxs("span", { className: cx(sx(nextDayStyle), sx(sameDayStyle)), role: "img", "aria-label": "same day", children: [jsx("span", { "aria-hidden": "true", children: "same" }), jsx("span", { "aria-hidden": "true", children: "day" })] }))] })), !codeOnLabel && segment.type !== 'car' && endpoints.endCode && jsx("span", { className: sx(endpointCodeStyle), children: endpoints.endCode }), endpoints.endPlace && jsx("span", { className: sx(endpointPlaceStyle), children: endpoints.endPlace })] })] }), details.length > 0 && (jsx("dl", { className: cx(sx(detailsStyle$1), detailsLayout === 'stacked' && sx(detailsStackedStyle)), children: details.map((detail) => (jsxs("div", { className: cx(sx(detailStyle$1), detail.fullRow && sx(detailFullRowStyle)), children: [jsx("dt", { className: sx(detailLabelStyle), children: detail.label }), jsx("dd", { className: cx(sx(detailValueStyle), detail.mono && sx(monoStyle$2)), children: detail.value })] }, detail.label))) })), facts.length > 0 && (jsx("dl", { className: cx(sx(detailsStyle$1), sx(detailsStackedStyle), sx(factsStyle$5)), "data-test": `itinerary-facts-${segment.id}`, children: facts.map((fact) => (jsxs("div", { className: sx(detailStyle$1), children: [jsx("dt", { className: sx(detailLabelStyle), children: fact.label }), jsx("dd", { className: cx(sx(detailValueStyle), fact.mono && sx(monoStyle$2)), children: fact.value })] }, fact.label))) })), segment.type === 'air' && segment.flightStatus && (jsx("p", { className: sx(flightStatusStyle), "data-test": `itinerary-flight-status-${segment.id}`, children: segment.flightStatus })), segment.type === 'air' && (segment.checkInAvailable || !!checkInUrl) && canCheckIn && !headerCheckIn && (jsx("div", { className: sx(actionsStyle$6), children: checkInControl('secondary', checkInLabel) })), noteSections.map(({ label, lines }) => {
|
|
15384
15623
|
const content = lines?.filter((line) => line.trim());
|
|
15385
15624
|
return content?.length ? (jsxs("section", { "aria-label": label, className: sx(footerStyle$7), children: [jsx("div", { className: sx(noteLabelStyle), children: label }), content.map((line, index) => jsx("p", { className: sx(noteLineStyle), children: line }, index))] }, label)) : null;
|
|
15386
15625
|
}), footer && jsx("div", { className: sx(footerStyle$7), children: footer })] }));
|
|
@@ -15441,52 +15680,111 @@ const daySegmentsStyle = {
|
|
|
15441
15680
|
display: 'flex',
|
|
15442
15681
|
flexDirection: 'column',
|
|
15443
15682
|
};
|
|
15444
|
-
/*
|
|
15683
|
+
/* A thin rule between a day's segments, on this wrapper, clear of the segment's own border reset. Each
|
|
15684
|
+
card's tinted header band already marks where one service ends and the next begins, so a change of
|
|
15685
|
+
type gets no heavier rule. */
|
|
15445
15686
|
const daySectionStyle = {
|
|
15446
15687
|
label: 'ItineraryTimeline-daySectionStyle',
|
|
15447
15688
|
'& + &': { borderTop: '1px solid var(--border-color-dark)' },
|
|
15448
15689
|
};
|
|
15449
|
-
/*
|
|
15450
|
-
const typeChangeSectionStyle = {
|
|
15451
|
-
label: 'ItineraryTimeline-typeChangeSectionStyle',
|
|
15452
|
-
'&&': { borderTop: '2px solid var(--color-primary, #19345f)' },
|
|
15453
|
-
};
|
|
15454
|
-
/* A connection: the dividing rule carries the layover, so the section drops its own rule. */
|
|
15690
|
+
/* A connection: its layover section draws its own rules, so the wrapper drops the thin one. */
|
|
15455
15691
|
const layoverSectionStyle = {
|
|
15456
15692
|
label: 'ItineraryTimeline-layoverSectionStyle',
|
|
15457
15693
|
'&&': { borderTop: 0 },
|
|
15458
15694
|
};
|
|
15695
|
+
/* The connection's amber, and its pale wash for the section behind the layover. */
|
|
15696
|
+
const AMBER = 'var(--color-warning-accent, #E8A13A)';
|
|
15697
|
+
const AMBER_WASH = `color-mix(in srgb, ${AMBER} 14%, white)`;
|
|
15698
|
+
/* The layover between two flights: a full-width pale amber section, padded like the cards so its
|
|
15699
|
+
rail lines up under their logo column. */
|
|
15459
15700
|
const layoverStyle = {
|
|
15460
15701
|
label: 'ItineraryTimeline-layoverStyle',
|
|
15461
15702
|
display: 'flex',
|
|
15462
|
-
alignItems: '
|
|
15703
|
+
alignItems: 'stretch',
|
|
15463
15704
|
gap: 'var(--space-3)',
|
|
15464
|
-
/* No top padding: the rule sits exactly where the section's own border would. */
|
|
15465
15705
|
padding: '0 var(--space-6)',
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
content: '""',
|
|
15470
|
-
flex: 1,
|
|
15471
|
-
height: 2,
|
|
15472
|
-
backgroundColor: 'var(--color-warning-accent, #E8A13A)',
|
|
15473
|
-
},
|
|
15706
|
+
backgroundColor: AMBER_WASH,
|
|
15707
|
+
borderTop: `1px solid color-mix(in srgb, ${AMBER} 35%, white)`,
|
|
15708
|
+
borderBottom: `1px solid color-mix(in srgb, ${AMBER} 35%, white)`,
|
|
15474
15709
|
};
|
|
15475
|
-
/*
|
|
15476
|
-
const
|
|
15477
|
-
label: 'ItineraryTimeline-
|
|
15710
|
+
/* The 48px logo column, so the dashed line meets both cards' logos. */
|
|
15711
|
+
const layoverRailStyle = {
|
|
15712
|
+
label: 'ItineraryTimeline-layoverRailStyle',
|
|
15713
|
+
position: 'relative',
|
|
15714
|
+
display: 'flex',
|
|
15715
|
+
alignItems: 'center',
|
|
15716
|
+
justifyContent: 'center',
|
|
15717
|
+
flexShrink: 0,
|
|
15718
|
+
width: 48,
|
|
15719
|
+
};
|
|
15720
|
+
const layoverLineStyle = {
|
|
15721
|
+
label: 'ItineraryTimeline-layoverLineStyle',
|
|
15722
|
+
position: 'absolute',
|
|
15723
|
+
top: 0,
|
|
15724
|
+
bottom: 0,
|
|
15725
|
+
left: '50%',
|
|
15726
|
+
transform: 'translateX(-50%)',
|
|
15727
|
+
borderLeft: `2px dashed ${AMBER}`,
|
|
15728
|
+
};
|
|
15729
|
+
/* A dot where the line meets each flight, centred on the section's top and bottom edges. */
|
|
15730
|
+
const layoverDot = {
|
|
15731
|
+
position: 'absolute',
|
|
15732
|
+
left: '50%',
|
|
15733
|
+
width: 8,
|
|
15734
|
+
height: 8,
|
|
15735
|
+
borderRadius: 9999,
|
|
15736
|
+
backgroundColor: AMBER,
|
|
15737
|
+
};
|
|
15738
|
+
const layoverDotStartStyle = {
|
|
15739
|
+
label: 'ItineraryTimeline-layoverDotStartStyle',
|
|
15740
|
+
...layoverDot,
|
|
15741
|
+
top: 0,
|
|
15742
|
+
transform: 'translate(-50%, -50%)',
|
|
15743
|
+
};
|
|
15744
|
+
const layoverDotEndStyle = {
|
|
15745
|
+
label: 'ItineraryTimeline-layoverDotEndStyle',
|
|
15746
|
+
...layoverDot,
|
|
15747
|
+
bottom: 0,
|
|
15748
|
+
transform: 'translate(-50%, 50%)',
|
|
15749
|
+
};
|
|
15750
|
+
/* The clock on the line, in a white disc ringed in the amber. */
|
|
15751
|
+
const layoverClockStyle = {
|
|
15752
|
+
label: 'ItineraryTimeline-layoverClockStyle',
|
|
15753
|
+
position: 'relative',
|
|
15478
15754
|
display: 'inline-flex',
|
|
15479
15755
|
alignItems: 'center',
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15756
|
+
justifyContent: 'center',
|
|
15757
|
+
boxSizing: 'border-box',
|
|
15758
|
+
width: 28,
|
|
15759
|
+
height: 28,
|
|
15760
|
+
margin: 'var(--space-3) 0',
|
|
15761
|
+
border: `2px solid ${AMBER}`,
|
|
15483
15762
|
borderRadius: 9999,
|
|
15484
|
-
backgroundColor: '
|
|
15485
|
-
color: 'var(--
|
|
15763
|
+
backgroundColor: 'white',
|
|
15764
|
+
color: 'var(--color-warning, #B26B00)',
|
|
15765
|
+
};
|
|
15766
|
+
const layoverTextStyle = {
|
|
15767
|
+
label: 'ItineraryTimeline-layoverTextStyle',
|
|
15768
|
+
display: 'flex',
|
|
15769
|
+
flexDirection: 'column',
|
|
15770
|
+
justifyContent: 'center',
|
|
15771
|
+
minWidth: 0,
|
|
15772
|
+
padding: 'var(--space-3) 0',
|
|
15773
|
+
};
|
|
15774
|
+
const layoverLabelStyle = {
|
|
15775
|
+
label: 'ItineraryTimeline-layoverLabelStyle',
|
|
15776
|
+
fontSize: 'var(--fs-xs)',
|
|
15777
|
+
lineHeight: 'var(--lh-xs)',
|
|
15778
|
+
fontWeight: 'var(--fw-semibold)',
|
|
15779
|
+
letterSpacing: '0.025em',
|
|
15780
|
+
textTransform: 'uppercase',
|
|
15781
|
+
color: 'var(--color-warning, #B26B00)',
|
|
15782
|
+
};
|
|
15783
|
+
const layoverDetailStyle = {
|
|
15784
|
+
label: 'ItineraryTimeline-layoverDetailStyle',
|
|
15486
15785
|
fontSize: 'var(--fs-sm)',
|
|
15487
15786
|
lineHeight: 'var(--lh-sm)',
|
|
15488
|
-
|
|
15489
|
-
'& svg': { color: 'var(--color-warning, #B26B00)' },
|
|
15787
|
+
color: 'var(--fg-1)',
|
|
15490
15788
|
'& strong': { fontWeight: 'var(--fw-bold, 700)' },
|
|
15491
15789
|
};
|
|
15492
15790
|
/* ---------- Loading / error / empty ---------- */
|
|
@@ -15535,13 +15833,18 @@ const errorStateStyle$1 = {
|
|
|
15535
15833
|
* and keeps the legs of a connection in the order they are flown.
|
|
15536
15834
|
*
|
|
15537
15835
|
* Day grouping keys off the plain `YYYY-MM-DD` string, so ordering never depends
|
|
15538
|
-
* on the reader's time zone.
|
|
15539
|
-
* order
|
|
15836
|
+
* on the reader's time zone. A segment's `travelDay`, when given, is its day in
|
|
15837
|
+
* place of its own date; within a day, segments order by their own date, then
|
|
15838
|
+
* time. Segments with no stated time sort last, keeping the order they were given.
|
|
15839
|
+
*
|
|
15840
|
+
* `preserveOrder` keeps each day's segments in the order given (days are still
|
|
15841
|
+
* ordered by date), for a host whose order is already right where local clock
|
|
15842
|
+
* times are not — a day that crosses the date line, ordered by UTC.
|
|
15540
15843
|
*/
|
|
15541
|
-
const groupSegmentsByDay = (segments) => {
|
|
15844
|
+
const groupSegmentsByDay = (segments, { preserveOrder = false } = {}) => {
|
|
15542
15845
|
const days = new Map();
|
|
15543
15846
|
segments.forEach((segment) => {
|
|
15544
|
-
const date = getSegmentDate(segment);
|
|
15847
|
+
const date = segment.travelDay?.trim() || getSegmentDate(segment);
|
|
15545
15848
|
const existing = days.get(date);
|
|
15546
15849
|
if (existing)
|
|
15547
15850
|
existing.push(segment);
|
|
@@ -15554,7 +15857,12 @@ const groupSegmentsByDay = (segments) => {
|
|
|
15554
15857
|
.map(([date, daySegments]) => ({
|
|
15555
15858
|
date,
|
|
15556
15859
|
// Array.prototype.sort is stable, so untimed segments keep their given order.
|
|
15557
|
-
segments: [...daySegments].sort((a, b) => {
|
|
15860
|
+
segments: preserveOrder ? daySegments : [...daySegments].sort((a, b) => {
|
|
15861
|
+
// A segment listed under an earlier travel day (a connection after midnight) follows that day's own.
|
|
15862
|
+
const aDate = getSegmentDate(a);
|
|
15863
|
+
const bDate = getSegmentDate(b);
|
|
15864
|
+
if (aDate && bDate && aDate !== bDate)
|
|
15865
|
+
return aDate.localeCompare(bDate);
|
|
15558
15866
|
const aStart = getSegmentStartMinutes(a);
|
|
15559
15867
|
const bStart = getSegmentStartMinutes(b);
|
|
15560
15868
|
if (aStart === null || bStart === null)
|
|
@@ -15563,16 +15871,24 @@ const groupSegmentsByDay = (segments) => {
|
|
|
15563
15871
|
}),
|
|
15564
15872
|
}));
|
|
15565
15873
|
};
|
|
15566
|
-
const
|
|
15874
|
+
const segmentKind = (segment) => (segment.type === 'rail' && segment.mode === 'bus' ? 'bus' : segment.type);
|
|
15875
|
+
const SEGMENT_NOUNS = {
|
|
15876
|
+
air: ['flight', 'flights'],
|
|
15877
|
+
car: ['car rental', 'car rentals'],
|
|
15878
|
+
hotel: ['hotel stay', 'hotel stays'],
|
|
15879
|
+
limo: ['Black Car transfer', 'Black Car transfers'],
|
|
15880
|
+
rail: ['rail journey', 'rail journeys'],
|
|
15881
|
+
bus: ['motor coach', 'motor coaches'],
|
|
15882
|
+
};
|
|
15567
15883
|
/** Plain-language summary of a day, e.g. `2 flights · 1 hotel stay`. */
|
|
15568
15884
|
const summariseDay = (segments) => {
|
|
15569
15885
|
const counts = new Map();
|
|
15570
|
-
segments.forEach((segment) => counts.set(segment
|
|
15571
|
-
return ['air', 'car', 'hotel', 'limo', 'rail']
|
|
15572
|
-
.filter((
|
|
15573
|
-
.map((
|
|
15574
|
-
const count = counts.get(
|
|
15575
|
-
return `${count} ${SEGMENT_NOUNS[
|
|
15886
|
+
segments.forEach((segment) => counts.set(segmentKind(segment), (counts.get(segmentKind(segment)) ?? 0) + 1));
|
|
15887
|
+
return ['air', 'car', 'hotel', 'limo', 'rail', 'bus']
|
|
15888
|
+
.filter((kind) => counts.has(kind))
|
|
15889
|
+
.map((kind) => {
|
|
15890
|
+
const count = counts.get(kind);
|
|
15891
|
+
return `${count} ${SEGMENT_NOUNS[kind][count > 1 ? 1 : 0]}`;
|
|
15576
15892
|
})
|
|
15577
15893
|
.join(' · ');
|
|
15578
15894
|
};
|
|
@@ -15608,10 +15924,17 @@ const formatLayover = (minutes) => {
|
|
|
15608
15924
|
return rest ? `${hours}h ${rest}m` : `${hours}h`;
|
|
15609
15925
|
};
|
|
15610
15926
|
const ClockIcon = () => (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("circle", { cx: "12", cy: "12", r: "10" }), jsx("polyline", { points: "12 6 12 12 16 14" })] }));
|
|
15927
|
+
/**
|
|
15928
|
+
* The time between two connecting flights, as its own full-width section: a pale
|
|
15929
|
+
* amber band with, under the cards' 48px logo column, a dashed line from the
|
|
15930
|
+
* flight above to the flight below (a dot where it meets each) carrying a clock,
|
|
15931
|
+
* and the layover beside it.
|
|
15932
|
+
*/
|
|
15933
|
+
const LayoverRow = ({ minutes, airport }) => (jsxs("div", { className: sx(layoverStyle), "data-test": "itinerary-layover", children: [jsxs("span", { className: sx(layoverRailStyle), "aria-hidden": "true", children: [jsx("span", { className: sx(layoverLineStyle) }), jsx("span", { className: sx(layoverDotStartStyle) }), jsx("span", { className: sx(layoverDotEndStyle) }), jsx("span", { className: sx(layoverClockStyle), children: jsx(ClockIcon, {}) })] }), jsxs("span", { className: sx(layoverTextStyle), children: [jsx("span", { className: sx(layoverLabelStyle), children: "Connection" }), jsxs("span", { className: sx(layoverDetailStyle), children: [jsx("strong", { children: formatLayover(minutes) }), " layover", airport.trim() ? ` in ${airport}` : ''] })] })] }));
|
|
15611
15934
|
const FILTER_TYPES = {
|
|
15612
15935
|
air: ['air'],
|
|
15613
15936
|
hotel: ['hotel'],
|
|
15614
|
-
ground: ['car', 'limo'],
|
|
15937
|
+
ground: ['car', 'limo', 'bus'],
|
|
15615
15938
|
rail: ['rail'],
|
|
15616
15939
|
};
|
|
15617
15940
|
const FILTER_TABS = [
|
|
@@ -15621,12 +15944,12 @@ const FILTER_TABS = [
|
|
|
15621
15944
|
{ value: 'ground', label: 'Ground', icon: jsx(CarIcon$1, { size: 16 }) },
|
|
15622
15945
|
{ value: 'rail', label: 'Rail', icon: jsx(RailIcon, { size: 16 }) },
|
|
15623
15946
|
];
|
|
15624
|
-
const matchesFilter = (segment, filter) => filter === 'all' || FILTER_TYPES[filter].includes(segment
|
|
15947
|
+
const matchesFilter = (segment, filter) => filter === 'all' || FILTER_TYPES[filter].includes(segmentKind(segment));
|
|
15625
15948
|
/**
|
|
15626
15949
|
* Groups all supported bookings under each travel day.
|
|
15627
15950
|
* Also covers loading, error and nothing-booked-yet states.
|
|
15628
15951
|
*/
|
|
15629
|
-
const ItineraryTimeline = ({ segments, loading = false, error, emptyMessage = 'Nothing is booked on this trip yet. Your travel team will add flights, cars and hotels as they are confirmed.', onCheckIn, renderVendorMark, showTypeTabs = false, className = '', }) => {
|
|
15952
|
+
const ItineraryTimeline = ({ segments, loading = false, error, emptyMessage = 'Nothing is booked on this trip yet. Your travel team will add flights, cars and hotels as they are confirmed.', onCheckIn, renderVendorMark, renderSegmentFooter, formatMoney, preserveOrder = false, showTypeTabs = false, className = '', }) => {
|
|
15630
15953
|
const [filter, setFilter] = useState('all');
|
|
15631
15954
|
const panelId = useId();
|
|
15632
15955
|
if (loading) {
|
|
@@ -15635,7 +15958,7 @@ const ItineraryTimeline = ({ segments, loading = false, error, emptyMessage = 'N
|
|
|
15635
15958
|
if (error) {
|
|
15636
15959
|
return (jsx("div", { className: cx(sx(timelineStyle), className), "data-test": "itinerary-timeline-error", children: jsx("div", { className: cx(sx(stateStyle$3), sx(errorStateStyle$1)), role: "alert", children: jsx("p", { className: sx(stateTextStyle$1), children: error }) }) }));
|
|
15637
15960
|
}
|
|
15638
|
-
const allDays = groupSegmentsByDay(segments);
|
|
15961
|
+
const allDays = groupSegmentsByDay(segments, { preserveOrder });
|
|
15639
15962
|
const days = allDays
|
|
15640
15963
|
.map((day) => ({ ...day, segments: day.segments.filter((segment) => matchesFilter(segment, filter)) }))
|
|
15641
15964
|
.filter((day) => day.segments.length > 0);
|
|
@@ -15650,9 +15973,22 @@ const ItineraryTimeline = ({ segments, loading = false, error, emptyMessage = 'N
|
|
|
15650
15973
|
activation: "automatic", "aria-label": "Segment type", controls: panelId })), jsx("div", { className: sx(daysStyle), id: panelId, role: showTypeTabs ? 'tabpanel' : undefined, children: days.map((day) => (jsxs("section", { className: sx(dayStyle), "aria-labelledby": `itinerary-day-${day.date || 'undated'}`, children: [jsxs("div", { className: sx(dayHeaderStyle), children: [jsx("h2", { className: sx(dayTitleStyle), id: `itinerary-day-${day.date || 'undated'}`, children: day.date ? formatItineraryDate(day.date, 'long') : 'Date to be confirmed' }), jsx("span", { className: sx(daySummaryStyle), children: summariseDay(day.segments) })] }), jsx("div", { className: sx(daySegmentsStyle), children: day.segments.map((segment, index) => {
|
|
15651
15974
|
const previous = index > 0 ? day.segments[index - 1] : undefined;
|
|
15652
15975
|
const layover = previous ? getLayoverMinutes(previous, segment) : null;
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15976
|
+
// Check In goes in the header of the first flight, in the day's order, that can draw it — a
|
|
15977
|
+
// checkInUrl, or checkInAvailable with an onCheckIn handler — for each departure date: one
|
|
15978
|
+
// check-in covers that date's connections, as Hub's API gives one link per departure date. A
|
|
15979
|
+
// flight that offers check-in but can't draw it (no URL, no handler) never claims the date, so
|
|
15980
|
+
// a later flight's working link isn't stripped. A day card holds more than one date only when a
|
|
15981
|
+
// travelDay lists an after-midnight connection under the day before.
|
|
15982
|
+
const offersCheckIn = (item) => item.type === 'air' && (!!item.checkInUrl?.trim() || (!!item.checkInAvailable && !!onCheckIn));
|
|
15983
|
+
const checkInFlight = segment.type === 'air'
|
|
15984
|
+
? day.segments.find((item) => offersCheckIn(item) && item.type === 'air' && item.departureDate === segment.departureDate)
|
|
15985
|
+
: undefined;
|
|
15986
|
+
const showCheckIn = segment.id === checkInFlight?.id;
|
|
15987
|
+
// That date's later flights drop their own offer, so the card draws no second one in its body.
|
|
15988
|
+
const card = segment.type === 'air' && offersCheckIn(segment) && !showCheckIn ? { ...segment, checkInAvailable: false, checkInUrl: undefined } : segment;
|
|
15989
|
+
return (jsxs("div", { className: cx(sx(daySectionStyle), layover !== null && sx(layoverSectionStyle)), children: [layover !== null && segment.type === 'air' && jsx(LayoverRow, { minutes: layover, airport: segment.departureAirport }), jsx(ItinerarySegmentCard, { segment: card, headingLevel: 3, variant: "section",
|
|
15990
|
+
// A flight's Seats / Class / Loyalty Number / CO₂ read as small labels over values.
|
|
15991
|
+
detailsLayout: segment.type === 'air' ? 'stacked' : 'inline', onCheckIn: onCheckIn, showCheckIn: showCheckIn, renderVendorMark: renderVendorMark, footer: renderSegmentFooter?.(segment), formatMoney: formatMoney })] }, segment.id));
|
|
15656
15992
|
}) })] }, day.date || 'undated'))) })] }));
|
|
15657
15993
|
};
|
|
15658
15994
|
ItineraryTimeline.displayName = 'ItineraryTimeline';
|
|
@@ -37712,12 +38048,14 @@ const VIEW_OPTIONS = [
|
|
|
37712
38048
|
{ value: 'all', label: 'All' },
|
|
37713
38049
|
{ value: 'itinerary', label: 'Itinerary Only' },
|
|
37714
38050
|
];
|
|
38051
|
+
const costKind = (segment) => (segment.type === 'rail' && segment.mode === 'bus' ? 'bus' : segment.type);
|
|
37715
38052
|
const COST_ROWS = [
|
|
37716
|
-
{
|
|
37717
|
-
{
|
|
37718
|
-
{
|
|
37719
|
-
{
|
|
37720
|
-
{
|
|
38053
|
+
{ kind: 'air', label: 'Air' },
|
|
38054
|
+
{ kind: 'car', label: 'Car rental' },
|
|
38055
|
+
{ kind: 'hotel', label: 'Hotel' },
|
|
38056
|
+
{ kind: 'limo', label: 'Black Car' },
|
|
38057
|
+
{ kind: 'bus', label: 'Motor Coach' },
|
|
38058
|
+
{ kind: 'rail', label: 'Rail' },
|
|
37721
38059
|
];
|
|
37722
38060
|
/**
|
|
37723
38061
|
* The lead traveler's surname plus how many others are on the record locator,
|
|
@@ -37734,13 +38072,13 @@ const itineraryPageName = (travelers) => {
|
|
|
37734
38072
|
* A traveler's bookings in day order, including Black Car and rail.
|
|
37735
38073
|
* Trip data and print, email, calendar and invoice actions arrive as props.
|
|
37736
38074
|
*/
|
|
37737
|
-
const ItineraryPage = ({ tripName, recordLocator, travelers, startDate, endDate, segments, status = 'confirmed', sportCode, totalCost, currency = 'USD', agent, invoice, bookingDetails = [], loading = false, error, notice, defaultView = 'all', onViewChange, alertBanners = [], travelNotes, closingBanners = [], closingNotes, onRetry, sectionName = 'Individual Travel', pageName, onBack, backLabel = 'Back to individual travel', onPrint, onEmail, onAddToCalendar, onDownloadInvoice, onCheckIn, renderVendorMark, className = '', }) => {
|
|
38075
|
+
const ItineraryPage = ({ tripName, recordLocator, travelers, startDate, endDate, segments, status = 'confirmed', sportCode, totalCost, currency = 'USD', agent, invoice, bookingDetails = [], loading = false, error, notice, defaultView = 'all', onViewChange, alertBanners = [], travelNotes, closingBanners = [], closingNotes, onRetry, sectionName = 'Individual Travel', pageName, onBack, backLabel = 'Back to individual travel', onPrint, onEmail, onAddToCalendar, onDownloadInvoice, onCheckIn, renderVendorMark, renderSegmentFooter, formatMoney = formatItineraryMoney, preserveOrder = false, className = '', }) => {
|
|
37738
38076
|
const badge = STATUS_BADGE[status];
|
|
37739
38077
|
const travelerLabel = travelers.length === 1 ? travelers[0] : `${travelers.length} travelers`;
|
|
37740
38078
|
const showStatus = Boolean(tripName) && !loading && !error;
|
|
37741
38079
|
const costByType = COST_ROWS.map((row) => ({
|
|
37742
38080
|
...row,
|
|
37743
|
-
amount: segments.filter((segment) => segment
|
|
38081
|
+
amount: segments.filter((segment) => costKind(segment) === row.kind).reduce((sum, segment) => sum + (segment.cost ?? 0), 0),
|
|
37744
38082
|
})).filter((row) => row.amount > 0);
|
|
37745
38083
|
const showCosts = totalCost !== undefined && !loading && !error;
|
|
37746
38084
|
const showActions = !loading && !error && Boolean(onPrint || onEmail || onAddToCalendar || onDownloadInvoice);
|
|
@@ -37761,13 +38099,13 @@ const ItineraryPage = ({ tripName, recordLocator, travelers, startDate, endDate,
|
|
|
37761
38099
|
const itineraryOnly = view === 'itinerary';
|
|
37762
38100
|
const showInvoice = Boolean(invoice) && !loading && !error;
|
|
37763
38101
|
// The trip cost is the Booking Details card's second section, under a thin rule.
|
|
37764
|
-
const bookingCard = (jsx(BookingDetailsCard, { details: bookingDetails, refId: recordLocator, children: showCosts && (jsx("section", { "aria-label": "Trip cost", children: jsxs("dl", { className: sx(costsStyle), children: [costByType.map((row) => (jsxs("div", { className: sx(costStyle), children: [jsx("dt", { className: costLabelClass, children: row.label }), jsx("dd", { className: costValueClass, children:
|
|
38102
|
+
const bookingCard = (jsx(BookingDetailsCard, { details: bookingDetails, refId: recordLocator, children: showCosts && (jsx("section", { "aria-label": "Trip cost", children: jsxs("dl", { className: sx(costsStyle), children: [costByType.map((row) => (jsxs("div", { className: sx(costStyle), children: [jsx("dt", { className: costLabelClass, children: row.label }), jsx("dd", { className: costValueClass, children: formatMoney(row.amount, currency) })] }, row.kind))), jsxs("div", { className: cx(sx(costStyle), sx(costTotalStyle)), children: [jsx("dt", { className: cx(costLabelClass, costTotalTextClass), children: "Total" }), jsx("dd", { className: cx(costValueClass, costTotalTextClass), children: formatMoney(totalCost, currency) })] })] }) })) }));
|
|
37765
38103
|
const notices = (alertBanners.length > 0 || travelNotes) && (jsxs("div", { className: sx(noticesStyle), children: [alertBanners.length > 0 && jsx(PageBanners, { className: sx(bannersStyle), systemBanners: [], alertBanners: alertBanners }), travelNotes && jsx("div", { className: sx(travelNotesStyle), "data-test": "itinerary-travel-notes", children: travelNotes })] }));
|
|
37766
38104
|
// A full-width row at the top of the cards: who to call and how to reach them.
|
|
37767
38105
|
const agentCard = agent && (jsxs("section", { className: sx(agentCardStyle), "aria-label": "Your travel team", children: [jsx("div", { className: sx(agentHeaderStyle), children: jsx("h2", { className: sx(agentTitleStyle), children: "Your Travel Team" }) }), jsxs("div", { className: sx(agentRowStyle), children: [jsxs("div", { className: sx(agentInfoStyle), children: [jsx("p", { className: sx(agentNameStyle), children: agent.name }), agent.role && jsx("p", { className: sx(agentRoleStyle), children: agent.role })] }), jsxs("ul", { className: sx(agentLinksStyle), children: [agent.email && (jsx(AgentContact, { icon: jsx(MailIcon, { size: 16 }), label: "Email", children: jsx("a", { className: sx(agentLinkStyle), href: `mailto:${agent.email}`, children: agent.email }) })), agent.businessHours && (jsx(AgentContact, { icon: jsx(CalendarIcon$1, { size: 16 }), label: "Business Hours", children: agent.businessHours })), agent.tollFreeNumber && (jsx(AgentContact, { icon: jsx(PhoneIcon, { size: 16 }), label: "Toll Free Number", children: jsx("a", { className: sx(agentLinkStyle), href: telHref(agent.tollFreeNumber), children: agent.tollFreeNumber }) })), agent.afterHoursPhone && (jsxs(AgentContact, { icon: jsx(HeadsetIcon, { size: 16 }), label: "After-Hours/Emergency Service", children: [jsx("a", { className: sx(agentLinkStyle), href: telHref(agent.afterHoursPhone), children: agent.afterHoursPhone }), agent.afterHoursNote && jsxs(Fragment, { children: [" ", agent.afterHoursNote] })] }))] })] })] }));
|
|
37768
38106
|
const titleRow = (jsxs("div", { className: sx(titleRowStyle), children: [jsx("h2", { className: sx(sectionTitleStyle$3), children: "Air Invoice Details" }), showStatus && jsx(StatusBadge, { status: badge.variant, label: badge.label })] }));
|
|
37769
38107
|
const actions = showActions && (jsxs("div", { className: sx(actionsStyle$1), "aria-label": "Itinerary actions", children: [onPrint && jsx(Button, { type: "button", variant: "secondary", size: "sm", leftIcon: jsx(PrinterIcon, { size: 16 }), "data-test": "itinerary-print", onClick: onPrint, children: "Print" }), onEmail && jsx(Button, { type: "button", variant: "secondary", size: "sm", leftIcon: jsx(MailIcon, { size: 16 }), "data-test": "itinerary-email", onClick: onEmail, children: "Email" }), onAddToCalendar && jsx(Button, { type: "button", variant: "secondary", size: "sm", leftIcon: jsx(CalendarIcon$1, { size: 16 }), "data-test": "itinerary-add-to-calendar", onClick: onAddToCalendar, children: "Add to calendar" }), onDownloadInvoice && jsx(Button, { type: "button", variant: "secondary", size: "sm", leftIcon: jsx(DocumentIcon, { size: 16 }), "data-test": "itinerary-download-invoice", onClick: onDownloadInvoice, children: "Invoice PDF" })] }));
|
|
37770
|
-
return (jsxs("div", { className: cx(sx(pageStyle$d), className), "data-test": "itinerary-page", children: [jsx(PageHeaderSubsection, { sectionName: sectionName, pageName: pageName ?? (travelers.length > 0 ? `Itinerary for ${itineraryPageName(travelers)}` : 'Itinerary'), meta: startDate && endDate ? jsx(TravelDateRange, { startDate: startDate, endDate: endDate }) : undefined, onBack: onBack, backLabel: backLabel }), !showInvoice && notices, !showInvoice && agentCard, showInvoice ? (jsxs("section", { className: sx(invoiceSectionStyle), "aria-label": "Booking and invoice", children: [jsxs("div", { className: sx(invoiceActionsStyle), children: [jsx(Segment, { size: "sm", options: VIEW_OPTIONS, value: view, onChange: changeView, "aria-label": "Itinerary view" }), actions] }), !itineraryOnly && notices, !itineraryOnly && agentCard, !itineraryOnly && bookingCard, !itineraryOnly && invoice && jsx(AirInvoiceCard, { ...invoice, title: "Air Invoice Details" })] })) : (jsxs("header", { className: sx(headerStyle$b), children: [jsxs("div", { className: sx(headerMainStyle$1), children: [titleRow, jsxs("dl", { className: sx(factsStyle$1), children: [recordLocator && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Confirmation" }), jsx("dd", { className: cx(sx(factValueStyle), sx(monoStyle$1)), children: recordLocator })] })), startDate && endDate && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Travel dates" }), jsx("dd", { className: sx(factValueStyle), children: formatItineraryDateRange(startDate, endDate) })] })), travelers.length > 0 && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: travelers.length === 1 ? 'Traveler' : 'Travelers' }), jsx("dd", { className: sx(factValueStyle), title: travelers.join(', '), children: travelerLabel })] })), sportCode && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Program" }), jsx("dd", { className: sx(factValueStyle), children: sportCode })] }))] })] }), actions] })), !showInvoice && bookingCard, !itineraryOnly && travelers.length > 1 && (jsxs("section", { className: sx(travelersStyle), "aria-label": "Travelers on this itinerary", children: [jsx("h2", { className: sx(sectionTitleStyle$3), children: "Travelers" }), jsx("ul", { className: sx(travelerListStyle), children: travelers.map((traveler) => jsx("li", { className: sx(travelerChipStyle), children: traveler }, traveler)) })] })), !itineraryOnly && !loading && !error && notice && (jsx("div", { "data-test": "itinerary-notice", children: jsx(Alert, { variant: "warning", title: "Additional booking details", children: notice }) })), jsxs("div", { className: sx(bodyStyle$1), children: [jsx("div", { className: sx(mainStyle$1), children: jsxs("div", { "data-test": loading ? 'itinerary-loading' : error ? 'itinerary-error' : undefined, children: [jsx(ItineraryTimeline, { segments: segments, loading: loading, error: error, onCheckIn: onCheckIn, renderVendorMark: renderVendorMark, showTypeTabs: true }), error && onRetry && (jsx("div", { className: sx(retryRowStyle), children: jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onRetry, "data-test": "itinerary-retry", children: "Try again" }) }))] }) }), !loading && !error && (closingBanners.length > 0 || closingNotes) && (jsxs("div", { className: sx(noticesStyle), "data-test": "itinerary-closing", children: [closingBanners.length > 0 && jsx(PageBanners, { className: sx(bannersStyle), systemBanners: closingBanners, alertBanners: [] }), closingNotes && jsx("div", { className: sx(closingNotesStyle), children: closingNotes })] }))] })] }));
|
|
38108
|
+
return (jsxs("div", { className: cx(sx(pageStyle$d), className), "data-test": "itinerary-page", children: [jsx(PageHeaderSubsection, { sectionName: sectionName, pageName: pageName ?? (travelers.length > 0 ? `Itinerary for ${itineraryPageName(travelers)}` : 'Itinerary'), meta: startDate && endDate ? jsx(TravelDateRange, { startDate: startDate, endDate: endDate, showDays: true }) : undefined, onBack: onBack, backLabel: backLabel }), !showInvoice && notices, !showInvoice && agentCard, showInvoice ? (jsxs("section", { className: sx(invoiceSectionStyle), "aria-label": "Booking and invoice", children: [jsxs("div", { className: sx(invoiceActionsStyle), children: [jsx(Segment, { size: "sm", options: VIEW_OPTIONS, value: view, onChange: changeView, "aria-label": "Itinerary view" }), actions] }), !itineraryOnly && notices, !itineraryOnly && agentCard, !itineraryOnly && bookingCard, !itineraryOnly && invoice && jsx(AirInvoiceCard, { ...invoice, title: "Air Invoice Details" })] })) : (jsxs("header", { className: sx(headerStyle$b), children: [jsxs("div", { className: sx(headerMainStyle$1), children: [titleRow, jsxs("dl", { className: sx(factsStyle$1), children: [recordLocator && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Confirmation" }), jsx("dd", { className: cx(sx(factValueStyle), sx(monoStyle$1)), children: recordLocator })] })), startDate && endDate && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Travel dates" }), jsx("dd", { className: sx(factValueStyle), children: formatItineraryDateRange(startDate, endDate) })] })), travelers.length > 0 && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: travelers.length === 1 ? 'Traveler' : 'Travelers' }), jsx("dd", { className: sx(factValueStyle), title: travelers.join(', '), children: travelerLabel })] })), sportCode && (jsxs("div", { className: sx(factStyle), children: [jsx("dt", { className: sx(factLabelStyle), children: "Program" }), jsx("dd", { className: sx(factValueStyle), children: sportCode })] }))] })] }), actions] })), !showInvoice && bookingCard, !itineraryOnly && travelers.length > 1 && (jsxs("section", { className: sx(travelersStyle), "aria-label": "Travelers on this itinerary", children: [jsx("h2", { className: sx(sectionTitleStyle$3), children: "Travelers" }), jsx("ul", { className: sx(travelerListStyle), children: travelers.map((traveler) => jsx("li", { className: sx(travelerChipStyle), children: traveler }, traveler)) })] })), !itineraryOnly && !loading && !error && notice && (jsx("div", { "data-test": "itinerary-notice", children: jsx(Alert, { variant: "warning", title: "Additional booking details", children: notice }) })), jsxs("div", { className: sx(bodyStyle$1), children: [jsx("div", { className: sx(mainStyle$1), children: jsxs("div", { "data-test": loading ? 'itinerary-loading' : error ? 'itinerary-error' : undefined, children: [jsx(ItineraryTimeline, { segments: segments, loading: loading, error: error, onCheckIn: onCheckIn, renderVendorMark: renderVendorMark, renderSegmentFooter: renderSegmentFooter, formatMoney: formatMoney, preserveOrder: preserveOrder, showTypeTabs: true }), error && onRetry && (jsx("div", { className: sx(retryRowStyle), children: jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onRetry, "data-test": "itinerary-retry", children: "Try again" }) }))] }) }), !loading && !error && (closingBanners.length > 0 || closingNotes) && (jsxs("div", { className: sx(noticesStyle), "data-test": "itinerary-closing", children: [closingBanners.length > 0 && jsx(PageBanners, { className: sx(bannersStyle), systemBanners: closingBanners, alertBanners: [] }), closingNotes && jsx("div", { className: sx(closingNotesStyle), children: closingNotes })] }))] })] }));
|
|
37771
38109
|
};
|
|
37772
38110
|
ItineraryPage.displayName = 'ItineraryPage';
|
|
37773
38111
|
|