@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.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: "56", height: "10", viewBox: "0 0 56 10", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "1", y1: "5", x2: "50", y2: "5", strokeDasharray: "3 3" }), jsx("polyline", { points: "47 1.5 54 5 47 8.5", fill: "currentColor" })] }) }));
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
- 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 })] }))] }));
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
- /* Fixed to the height of the Version control's row so the two cards' headers match. */
10777
- minHeight: 65,
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: '12px 24px',
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
- borderRadius: 0,
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 versionControl = versionControlShown(versions) && (jsx(Dropdown, { label: "Version", alignment: "right", 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: versions.map((version) => (jsx(Dropdown.Item, { icon: version.id === selectedVersionId ? jsx(CheckIcon$1, { size: 14 }) : undefined, onClick: () => onVersionChange?.(version.id), children: version.label }, version.id))) }));
10943
- 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 }), jsx("div", { className: sx(headerEndStyle), children: versionControl })] })), jsx(Table, { className: sx(tableStyle$9), 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 }))] }));
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
- /* Fixed to the height of the Version control's row so the two cards' headers match. */
11530
- minHeight: 65,
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
- '@media (max-width: 767px)': {
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
- gap: 'var(--space-4)',
14768
- padding: 'var(--space-4)',
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
- padding: 'var(--space-5) var(--space-6)',
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
- '@media (max-width: 767px)': {
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
- /* Responsive (tablet breakpoint, --bp-md) */
14925
- '@media (max-width: 767px)': {
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
- fontSize: 'var(--fs-xs)',
14952
- lineHeight: 'var(--lh-xs)',
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
- flexDirection: 'column',
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
- '@media (max-width: 767px)': {
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
- const addressWithPhone = (addressLabel, lines, phoneLabel, phone) => {
15245
- const phoneItem = phone?.trim() ? { label: phoneLabel, value: phoneLink(phone) } : undefined;
15246
- if (lines?.length)
15247
- return { label: addressLabel, value: inlineAddress(lines), below: phoneItem, fullRow: true };
15248
- return phoneItem;
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` with the estimated total stacked beneath it, or the total alone when no rate was quoted. */
15252
- const rateDetails = (segment, rate, unit, totalLabel) => {
15253
- const total = segment.cost !== undefined ? { label: totalLabel, value: formatItineraryMoney(segment.cost, segment.currency) } : undefined;
15254
- if (rate !== undefined)
15255
- return [{ label: 'Rate', value: `${formatItineraryMoney(rate, segment.currency)} / ${unit}`, below: total }];
15256
- return total ? [total] : [];
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
- if (segment.cabin)
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
- details.push({ label: 'Car type', value: segment.carTypeCode ? `${segment.carType} (${segment.carTypeCode})` : segment.carType });
15286
- details.push(...rateDetails(segment, segment.dailyRate, 'day', 'Estimated total'));
15287
- const pickup = addressWithPhone('Pick-Up Address', segment.pickupAddress?.filter((line) => line.trim()), 'Pick-Up Phone', segment.phone);
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
- const sameAsPickup = normalizeLocation((segment.dropoffAddress ?? []).join(' ')) === normalizeLocation((segment.pickupAddress ?? []).join(' '));
15292
- if (dropoffLines?.length && !sameAsPickup)
15293
- details.push({ label: 'Drop-Off Address', value: inlineAddress(dropoffLines), fullRow: true });
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
- if (segment.chain)
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
- const hotelAddress = addressWithPhone('Address', hotelLines, 'Phone', segment.phone);
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
- const loyaltyNumbers = segment.loyaltyNumbers?.filter((number) => number.trim());
15310
- if (loyaltyNumbers?.length)
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
- startLabel: segment.pickupTime?.trim() ? 'Pickup time' : 'Pickup date',
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
- const hasStart = !!(endpoints.startTime || endpoints.startCode || endpoints.startPlace);
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 headerCheckIn = segment.type === 'air' && showCheckIn && !!onCheckIn;
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: cx(sx(headerStyle$w), pillConfirmation && sx(confirmationHeaderStyle)), 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 && (jsx(Button, { size: "sm", variant: "primary", "aria-label": checkInLabel, "data-test": `itinerary-check-in-${segment.id}`, onClick: () => onCheckIn?.(segment), children: "Check In" })), 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 && jsxs("div", { className: sx(endpointStyle), children: [jsx("span", { className: sx(endpointLabelStyle), children: endpoints.startLabel }), 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 }), segment.type !== 'car' && endpoints.startCode && jsx("span", { className: sx(endpointCodeStyle), children: endpoints.startCode }), endpoints.startPlace && jsx("span", { className: sx(endpointPlaceStyle), children: endpoints.startPlace })] }), 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: [jsx("span", { className: sx(endpointLabelStyle), children: endpoints.endLabel }), 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), children: ["+", dayOffset, " day", dayOffset === 1 ? '' : 's'] })] })), 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: sx(detailsStyle$1), 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.below && jsxs(Fragment, { children: [jsx("dt", { className: cx(sx(detailLabelStyle), sx(detailBelowLabelStyle)), children: detail.below.label }), jsx("dd", { className: sx(detailValueStyle), children: detail.below.value })] })] }, detail.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 && onCheckIn && !headerCheckIn && (jsx("div", { className: sx(actionsStyle$6), children: jsxs(Button, { size: "sm", variant: "secondary", "data-test": `itinerary-check-in-${segment.id}`, onClick: () => onCheckIn?.(segment), children: ["Check in for ", segment.carrierCode ?? segment.carrier, " ", segment.flightNumber] }) })), noteSections.map(({ label, lines }) => {
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
- /* The rule between a day's segments sits on this wrapper, clear of the segment's own border reset. */
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
- /* Where the segment type changes — air to car, car to hotel — a slightly heavier navy rule. */
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: 'center',
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
- /* Centre the pill on the rule: half its height (line + padding + border) above the section edge. */
15467
- marginTop: 'calc((var(--lh-sm, 20px) + 10px) / -2)',
15468
- '&::before, &::after': {
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
- /* A pill in the warning amber, so a tight connection is hard to miss. */
15476
- const layoverLabelStyle = {
15477
- label: 'ItineraryTimeline-layoverLabelStyle',
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
- gap: 6,
15481
- padding: '4px 12px',
15482
- border: '1px solid var(--color-warning-accent, #E8A13A)',
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: 'color-mix(in srgb, var(--color-warning-accent, #E8A13A) 16%, var(--bg-surface, #fff))',
15485
- color: 'var(--fg-1)',
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
- whiteSpace: 'nowrap',
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. Segments with no stated time sort last, keeping the
15539
- * order they were given.
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 SEGMENT_NOUNS = { air: 'flight', car: 'car rental', hotel: 'hotel stay', limo: 'Black Car transfer', rail: 'rail journey' };
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.type, (counts.get(segment.type) ?? 0) + 1));
15571
- return ['air', 'car', 'hotel', 'limo', 'rail']
15572
- .filter((type) => counts.has(type))
15573
- .map((type) => {
15574
- const count = counts.get(type);
15575
- return `${count} ${SEGMENT_NOUNS[type]}${count > 1 ? 's' : ''}`;
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.type);
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
- const newType = !!previous && previous.type !== segment.type;
15654
- const firstFlight = segment.type === 'air' && day.segments.findIndex((item) => item.type === 'air') === index;
15655
- return (jsxs("div", { className: cx(sx(daySectionStyle), layover !== null && sx(layoverSectionStyle), newType && sx(typeChangeSectionStyle)), "data-type-change": newType || undefined, children: [layover !== null && segment.type === 'air' && (jsx("div", { className: sx(layoverStyle), "data-test": "itinerary-layover", children: jsxs("span", { className: sx(layoverLabelStyle), children: [jsx(ClockIcon, {}), jsxs("span", { children: [jsx("strong", { children: formatLayover(layover) }), " layover in ", segment.departureAirport] })] }) })), jsx(ItinerarySegmentCard, { segment: segment, headingLevel: 3, variant: "section", onCheckIn: onCheckIn, showCheckIn: firstFlight, renderVendorMark: renderVendorMark })] }, segment.id));
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
- { type: 'air', label: 'Air' },
37717
- { type: 'car', label: 'Car rental' },
37718
- { type: 'hotel', label: 'Hotel' },
37719
- { type: 'limo', label: 'Black Car' },
37720
- { type: 'rail', label: 'Rail' },
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.type === row.type).reduce((sum, segment) => sum + (segment.cost ?? 0), 0),
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: formatItineraryMoney(row.amount, currency) })] }, row.type))), jsxs("div", { className: cx(sx(costStyle), sx(costTotalStyle)), children: [jsx("dt", { className: cx(costLabelClass, costTotalTextClass), children: "Total" }), jsx("dd", { className: cx(costValueClass, costTotalTextClass), children: formatItineraryMoney(totalCost, currency) })] })] }) })) }));
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