@autobusal/routes-order 1.9.0 → 1.9.1

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +673 -0
  2. package/Checkout/Checkout.tsx +519 -0
  3. package/Checkout/Sidebar/Sidebar.tsx +124 -0
  4. package/Checkout/Sidebar/styles.ts +52 -0
  5. package/Facts/Facts.tsx +271 -0
  6. package/Facts/questions.ts +250 -0
  7. package/Facts/service.ts +111 -0
  8. package/Facts/services.ts +19 -0
  9. package/Facts/styles.ts +112 -0
  10. package/Facts/types.ts +167 -0
  11. package/Faq/Faq.tsx +52 -0
  12. package/Faq/styles.ts +68 -0
  13. package/Found/Dates/Dates.tsx +59 -5
  14. package/Found/Dates/styles.ts +137 -7
  15. package/Found/Empty/Empty.tsx +136 -0
  16. package/Found/Empty/styles.ts +98 -0
  17. package/Found/Filters/Filters.tsx +10 -7
  18. package/Found/Filters/styles.ts +79 -8
  19. package/Found/Found.tsx +128 -29
  20. package/Found/Group/Group.tsx +3 -1
  21. package/Found/Header/styles.ts +20 -4
  22. package/Found/Orders/Orders.tsx +25 -4
  23. package/Found/Picks/Picks.tsx +10 -3
  24. package/Found/Picks/styles.ts +60 -9
  25. package/Found/Route/Route.tsx +189 -33
  26. package/Found/Route/styles.ts +488 -50
  27. package/Found/SameDay/SameDay.tsx +35 -0
  28. package/Found/SameDay/styles.ts +27 -0
  29. package/Found/Sort/styles.ts +52 -7
  30. package/Found/Summary/Summary.tsx +1 -1
  31. package/Found/Summary/styles.ts +12 -0
  32. package/Found/Watch/Watch.tsx +187 -0
  33. package/Found/Watch/styles.ts +118 -0
  34. package/Found/refine.ts +69 -0
  35. package/Links/Links.tsx +91 -0
  36. package/Links/styles.ts +75 -0
  37. package/RoutesOrder.tsx +239 -31
  38. package/Schedule/Schedule.tsx +123 -0
  39. package/Schedule/styles.ts +71 -0
  40. package/Sections/Schema.tsx +186 -0
  41. package/Sections/Sections.tsx +160 -0
  42. package/Sections/styles.ts +44 -0
  43. package/Step1/prepare.ts +21 -2
  44. package/Step2.tsx +34 -1
  45. package/Step4/Passenger/Passenger.tsx +253 -24
  46. package/Step4/Passenger/styles.ts +21 -1
  47. package/Step4/Passengers/Display.tsx +10 -2
  48. package/Step4/Passengers/Passengers.tsx +53 -4
  49. package/Step4/Saved/Choose.tsx +99 -0
  50. package/Step4/Saved/Remember.tsx +58 -0
  51. package/Step4/Saved/styles.ts +87 -0
  52. package/Step4/checkAge.ts +15 -4
  53. package/Step5/Addons/Addons.tsx +146 -4
  54. package/Step5/Addons/styles.ts +94 -0
  55. package/Step5/Billing/Billing.tsx +138 -70
  56. package/Step5/Coupons/Coupons.tsx +28 -18
  57. package/Step5/Coupons/styles.ts +41 -8
  58. package/Step5/Payments/Payments.tsx +36 -5
  59. package/Step5/Payments/styles.ts +16 -7
  60. package/Step5/Summary/About/About.tsx +99 -27
  61. package/Step5/Summary/About/styles.ts +118 -1
  62. package/Step5/Summary/styles.ts +12 -5
  63. package/Step5/utilities.ts +67 -12
  64. package/Steps/Steps.tsx +3 -1
  65. package/commerce.ts +24 -0
  66. package/package.json +1 -1
  67. package/services.ts +295 -12
  68. package/styles.ts +126 -0
  69. package/types.ts +78 -1
  70. package/Found/Booking/Booking.tsx +0 -61
  71. package/Found/Booking/styles.ts +0 -50
  72. package/Step4/Step4.tsx +0 -114
  73. package/Step5/Step5.tsx +0 -185
@@ -6,8 +6,20 @@ export const Container = styled.div<{ $type: 'favorite' | 'normal' }>`
6
6
  position: relative;
7
7
  display: flex;
8
8
  flex-direction: column;
9
- gap: 15px;
10
- padding: 15px;
9
+
10
+ /**
11
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
12
+ * Tighter on a phone. Every result carried 15px of padding and 15px
13
+ * between each of its bands, which is most of a thumb of screen spent on
14
+ * gaps before anything is read.
15
+ */
16
+ gap: 8px;
17
+ padding: 10px;
18
+
19
+ @media (min-width: 640px) {
20
+ gap: 15px;
21
+ padding: 15px;
22
+ }
11
23
 
12
24
  ${ props => props.$type === 'favorite' && css`
13
25
  border: 1px solid ${ props => props.theme.font.success };
@@ -45,92 +57,363 @@ export const FavoriteStop = styled.div`
45
57
  border-bottom-right-radius: ${ props => props.theme.borderRadius };
46
58
  `;
47
59
 
60
+ /**
61
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
62
+ *
63
+ * VERTICAL on a phone, side by side from 640px up.
64
+ *
65
+ * Side by side was tried first and does not survive the content: each end
66
+ * gets about 96px at 375px, while "Thessaloniki (GRC) 16:30" measures 215px
67
+ * - it painted 50px outside the card, and the page never scrolled sideways
68
+ * so nothing looked broken until it was measured against the box it was
69
+ * supposed to be in. Stacked, each end has the full width, so nothing is
70
+ * truncated and nothing escapes.
71
+ *
72
+ * Compact is bought back a different way: tight gaps, a short arrow turned
73
+ * to point down the column, and the price on one row instead of a centred
74
+ * stack. Same information, roughly a third less height than before.
75
+ */
48
76
  export const Trip = styled.div`
49
77
  display: flex;
50
78
  flex-direction: column;
51
- gap: 15px;
79
+ gap: 6px;
52
80
 
53
81
  @media (min-width: 640px) {
54
82
  flex-direction: row;
55
83
  flex-wrap: wrap;
84
+ align-items: center;
85
+ gap: 15px;
56
86
  }
57
87
 
88
+ /**
89
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
90
+ * A tighter gap once every cell shares one row: four gaps at 15px spent
91
+ * 60px of the widest layout on whitespace, while the arrival column was
92
+ * overflowing by 50px.
93
+ */
58
94
  @media (min-width: 1024px) {
59
95
  flex-wrap: nowrap;
96
+ gap: 10px;
60
97
  }
61
98
  `;
62
99
 
63
100
  export const Company = styled.div`
64
101
  flex-basis: 100%;
65
102
 
103
+ /**
104
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
105
+ * A column now, because the operator's rating sits under the logo. Left
106
+ * as a bare block it would have laid the stars out beside the logo and
107
+ * eaten the 185px this cell gets on desktop.
108
+ */
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 6px;
112
+
113
+ /*
114
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
115
+ * Centred at EVERY width, not just on desktop. LinkCompany carries an
116
+ * auto horizontal margin, so the logo is centred in this cell whatever the
117
+ * screen - leaving the rating and the route code at flex-start put them
118
+ * under the left edge of a box whose logo sat in the middle of it. On a
119
+ * 768px card that was a 300px gap between the two.
120
+ */
121
+ align-items: center;
122
+
123
+ /**
124
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
125
+ *
126
+ * 185px down to 120px. The logo is an identifier, not the product - it
127
+ * only has to be recognisable - and the 65px it gives back goes to the two
128
+ * location columns, which were the cells actually running out of room.
129
+ *
130
+ */
66
131
  @media (min-width: 1024px) {
67
- flex-basis: 185px;
132
+ flex: 0 0 120px;
68
133
  }
69
134
  `;
70
135
 
136
+ /**
137
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
138
+ *
139
+ * min-width: 0 is what makes `flex: 1` mean "an equal share". Without it
140
+ * the default min-width:auto stops a column shrinking below its content,
141
+ * so the arrival column (long city plus a long stop name) grew to 215px
142
+ * while the departure column got 155px - the two halves of the same row
143
+ * were different widths, and no fixed header could ever line up with
144
+ * both. It also stops one long stop name stealing width from the other
145
+ * side of the card.
146
+ */
71
147
  export const Location = styled.div`
72
148
  flex: 1;
149
+ min-width: 0;
73
150
  display: flex;
74
151
  flex-direction: column;
75
- align-items: center;
76
- gap: 5px;
77
- text-align: center;
78
152
 
153
+ /**
154
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
155
+ * Left-aligned while the ends are stacked - centred text in a full-width
156
+ * column leaves the two times floating in the middle of the card with
157
+ * nothing to line them up against. Centred again from 640px, where the
158
+ * two ends sit either side of the arrow and the symmetry is the point.
159
+ */
160
+ align-items: flex-start;
161
+ gap: 1px;
162
+ text-align: left;
163
+
164
+ @media (min-width: 640px) {
165
+ align-items: center;
166
+ gap: 3px;
167
+ text-align: center;
168
+ }
169
+
170
+ /**
171
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
172
+ *
173
+ * The heading has to be allowed to WRAP now that the two ends share a
174
+ * row. The min-width:0 above lets this column shrink, but the heading
175
+ * inside kept its intrinsic single-line width and simply painted outside
176
+ * the card - "Thessaloniki (GRC) 16:30" measured 215px inside a 96px
177
+ * column and ran 50px past the card's right edge. The page never scrolled
178
+ * sideways, so nothing looked broken until the text was measured against
179
+ * the box it was supposed to be in.
180
+ *
181
+ * A step down in size as well, because three columns of a 375px screen do
182
+ * not have room for a headline at body-copy scale.
183
+ */
79
184
  & > h2 {
80
185
  margin-bottom: 0;
186
+ max-width: 100%;
187
+ line-height: 1.25;
188
+ overflow-wrap: anywhere;
81
189
  }
82
190
 
83
- & > span {
84
- font-weight: 700;
85
- color: ${ props => props.theme.font.faded };
191
+ /**
192
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
193
+ * break-word, not normal. Real spaces now let the heading wrap between
194
+ * city, country and time, which covers almost every width - but at exactly
195
+ * 1024px, where the card first lays out in one row and the columns are at
196
+ * their narrowest, "Thessaloniki" on its own still exceeds its column and
197
+ * a heading that cannot break simply paints over the price.
198
+ *
199
+ * break-word only splits a word when it has a line to itself and still
200
+ * does not fit, so the ordinary case is untouched. The phone layout's
201
+ * anywhere would break words it did not have to.
202
+ */
203
+ @media (min-width: 640px) {
204
+ & > h2 {
205
+ overflow-wrap: break-word;
206
+ }
86
207
  }
87
208
  `;
88
209
 
210
+ /**
211
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
212
+ *
213
+ * The stop name is supporting detail, not a heading. At the same 16px as
214
+ * body copy it competed with the city above it; a step down lets the eye
215
+ * land on the city first and read the stop only when it needs to.
216
+ */
217
+ /**
218
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
219
+ *
220
+ * Clamped to two lines on a phone. Now that the two ends of the journey
221
+ * share a row, each gets about 96px - and a name like "Terminali Lindor
222
+ * Interurban dhe Nderkombetar i Autobusave (TEG)" ran to five lines and
223
+ * stretched the whole row to match, so one terminal with a long name made
224
+ * every card taller than the screen. Two lines and an ellipsis, which is
225
+ * what the reference layouts do with the same problem.
226
+ */
227
+ export const Stop = styled.span`
228
+ display: -webkit-box;
229
+ -webkit-line-clamp: 2;
230
+ -webkit-box-orient: vertical;
231
+ overflow: hidden;
232
+ font-size: ${ props => props.theme.size.xs };
233
+ font-weight: 700;
234
+ line-height: 1.3;
235
+ color: ${ props => props.theme.font.faded };
236
+
237
+ @media (min-width: 1024px) {
238
+ display: block;
239
+ overflow: visible;
240
+ }
241
+ `;
242
+
243
+ /**
244
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
245
+ *
246
+ * The country code comes out of the database lowercase ("alb", "grc").
247
+ * An ISO country code is by definition upper case, so it is cased here at
248
+ * render time rather than left to the data - every consumer would otherwise
249
+ * have to remember to do it. Set smaller and muted so it qualifies the city
250
+ * without competing with it.
251
+ */
252
+ export const Iso = styled.span`
253
+ // JSX drops the literal spaces around this element, so the gap on either
254
+ // side of "(ALB)" is this margin's job, not whitespace's
255
+ margin: 0 5px;
256
+ font-size: ${ props => props.theme.size.xs };
257
+ font-weight: 700;
258
+ text-transform: uppercase;
259
+ letter-spacing: .5px;
260
+ color: ${ props => props.theme.font.faded };
261
+ `;
262
+
89
263
  export const Time = styled.div`
90
264
  display: flex;
91
- flex-direction: column;
92
- gap: 8px;
265
+ flex-direction: row;
266
+ gap: 6px;
93
267
  align-items: center;
94
268
  font-weight: 700;
95
269
 
270
+ @media (min-width: 640px) {
271
+ flex-direction: column;
272
+ gap: 8px;
273
+ }
274
+
275
+ /**
276
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
277
+ * The arrow sat between two stacked blocks at 1.5x the largest type in
278
+ * the theme. Now that the two ends of the journey share a row with it,
279
+ * that size would take the width the city names need - it is a
280
+ * separator, not a headline.
281
+ */
282
+ /**
283
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
284
+ * Turned to point DOWN while the two ends are stacked - an arrow pointing
285
+ * right between two things sitting one above the other says the wrong
286
+ * thing about which way the journey goes.
287
+ */
96
288
  & > svg {
97
- font-size: calc(1.5 * ${ props => props.theme.size.xxl });
289
+ font-size: ${ props => props.theme.size.l };
290
+ transform: rotate(90deg);
291
+ }
292
+
293
+ @media (min-width: 640px) {
294
+ & > svg {
295
+ font-size: calc(1.5 * ${ props => props.theme.size.xxl });
296
+ transform: none;
297
+ }
98
298
  }
99
299
 
100
300
  & > span {
101
- display: block;
102
- padding: 3px 10px;
103
- font-size: ${ props => props.theme.size.l };
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 5px;
304
+ padding: 3px 12px;
305
+ white-space: nowrap;
306
+ font-size: ${ props => props.theme.size.m };
104
307
  background: ${ props => props.theme.background.neutral };
105
308
  border-radius: 100px;
106
309
  }
107
310
 
311
+ // the hourglass is a hint, not a heading - the big arrow above already
312
+ // owns the visual weight in this column
313
+ & > span > svg {
314
+ flex-shrink: 0;
315
+ font-size: ${ props => props.theme.size.m };
316
+ opacity: .7;
317
+ }
318
+
108
319
  @media (min-width: 640px) {
109
- flex-basis: 120px;
320
+ flex: 0 0 120px;
110
321
  }
111
322
  `;
112
323
 
113
324
  export const Price = styled.div`
114
325
  flex-basis: 100%;
115
326
  display: flex;
116
- flex-direction: column;
117
- gap: 10px;
327
+
328
+ /**
329
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
330
+ *
331
+ * On a phone: the fare and the button share one row, and the tax note sits
332
+ * under them.
333
+ *
334
+ * Reordered in CSS rather than in the markup, because the source order -
335
+ * fare, note, button - is the right one to read aloud and the right one
336
+ * for a crawler: the note explains the number it follows. Only the
337
+ * PAINTING needs to differ, and the order property says exactly that
338
+ * without moving the note away from the price it qualifies.
339
+ */
340
+ flex-direction: row;
341
+ flex-wrap: wrap;
342
+ justify-content: space-between;
118
343
  align-items: center;
344
+ gap: 4px 8px;
119
345
 
120
- & > strong {
121
- font-size: ${ props => props.theme.size.xxl };
122
- }
346
+ & > *:nth-child(1) { order: 1; }
347
+
348
+ /* the note drops to its own line under both */
349
+ & > *:nth-child(2) { order: 3; flex-basis: 100%; }
350
+
351
+ & > *:nth-child(3) { order: 2; }
123
352
 
124
353
  @media (min-width: 768px) {
125
- flex-basis: 165px;
354
+ flex: 0 0 165px;
355
+ flex-direction: column;
356
+ justify-content: initial;
357
+ gap: 8px;
358
+
359
+ & > *:nth-child(1),
360
+ & > *:nth-child(2),
361
+ & > *:nth-child(3) {
362
+ order: initial;
363
+ flex-basis: auto;
364
+ }
126
365
  }
127
366
  `;
128
367
 
368
+ /**
369
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
370
+ *
371
+ * The decimals ride as superscript rather than at full size. On a fare
372
+ * board the cents are almost never what anyone is comparing, and at the
373
+ * headline size they were taking a third of the width of the widest thing
374
+ * on the card. The line-height is pinned to 1 so the shrunk decimals do
375
+ * not leave a gap under the row.
376
+ */
377
+ export const Amount = styled.strong`
378
+ display: flex;
379
+ align-items: flex-start;
380
+ line-height: 1;
381
+ font-size: ${ props => props.theme.size.xxl };
382
+ `;
383
+
384
+ export const Decimals = styled.span`
385
+ margin-left: 1px;
386
+ font-size: ${ props => props.theme.size.s };
387
+ line-height: 1;
388
+ `;
389
+
390
+ export const Currency = styled.span`
391
+ margin-left: 4px;
392
+ align-self: center;
393
+ font-size: ${ props => props.theme.size.m };
394
+ `;
395
+
396
+ /**
397
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
398
+ *
399
+ * Tucked up against the price it qualifies, which reads as one unit and
400
+ * frees the vertical space the Select button needs.
401
+ */
129
402
  export const PriceNotice = styled.span`
130
- margin-top: -6px;
131
- font-size: ${ props => props.theme.size.xs };
403
+ /**
404
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
405
+ * Left-aligned under the fare on a phone, where it is a full-width line
406
+ * beneath a price and a button rather than the middle of a centred stack.
407
+ */
408
+ font-size: ${ props => props.theme.size.xxs };
409
+ line-height: 1.25;
132
410
  color: ${ props => props.theme.font.faded };
133
- text-align: center;
411
+ text-align: left;
412
+
413
+ @media (min-width: 768px) {
414
+ margin-top: -4px;
415
+ text-align: center;
416
+ }
134
417
  `;
135
418
 
136
419
  export const LinkCompany = styled(Link)`
@@ -143,26 +426,125 @@ export const LinkCompany = styled(Link)`
143
426
 
144
427
  border-radius: 8px;
145
428
  }
429
+
430
+ /**
431
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
432
+ * Smaller from the width where the card lays out in one row, since that is
433
+ * the only place the space was contested.
434
+ */
435
+ @media (min-width: 1024px) {
436
+ max-width: 120px;
437
+
438
+ & > img {
439
+ ${ logoBox('100%', '44px') }
440
+ }
441
+ }
146
442
  `;
147
443
 
148
- export const Details = styled.div`
149
- display: flex;
150
- flex-direction: column;
151
- gap: 15px;
444
+ /**
445
+ * The route code, under the logo.
446
+ *
447
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
448
+ * Moved out of the collapsed details and up beside the operator it belongs
449
+ * to. It is an identifier of the same kind as the logo, and putting it here
450
+ * frees a slot in the details row for things that genuinely need explaining.
451
+ */
452
+ export const CodeCompany = styled(Link)`
453
+ display: inline-flex;
454
+ align-items: center;
455
+ gap: 4px;
456
+ max-width: 100%;
457
+ padding: 2px 8px;
458
+ border: 1px solid ${ props => props.theme.background.neutral };
459
+ border-radius: 100px;
460
+ background: ${ props => props.theme.background.neutral };
461
+ color: ${ props => props.theme.font.faded };
462
+ font-size: ${ props => props.theme.size.xxs };
463
+ font-weight: 700;
464
+ line-height: 1.4;
465
+ letter-spacing: .3px;
466
+ white-space: nowrap;
467
+ overflow: hidden;
468
+ text-overflow: ellipsis;
469
+ transition: all .3s ease;
470
+
471
+ /*
472
+ * A route symbol rather than a bus or a ticket. The code identifies which
473
+ * LINE this is - the same job a flight number does - and a bus icon would
474
+ * say the thing the whole card already says. Slightly larger than the text
475
+ * beside it, because at this size the glyph is what carries the meaning
476
+ * and the letters are only the value.
477
+ */
478
+ & > svg {
479
+ flex-shrink: 0;
480
+ font-size: ${ props => props.theme.size.s };
481
+ opacity: .8;
482
+ }
483
+
484
+ &:hover {
485
+ border-color: ${ props => props.theme.primary.normal };
486
+ color: ${ props => props.theme.primary.normal };
487
+ }
488
+ `;
489
+
490
+ /**
491
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
492
+ *
493
+ * Collapsible, and inline at every width including phones.
494
+ *
495
+ * Hidden with CSS rather than unmounted, deliberately. These cards are on a
496
+ * page the prerender crawl snapshots for every route pair in the sitemap, so
497
+ * amenities, the route code, transit countries and the policy link have to
498
+ * be IN the DOM whether or not a reader has opened them - unmounting would
499
+ * take real per-pair content out of exactly the pages Tier 3 exists to
500
+ * fatten.
501
+ *
502
+ * Wrapping instead of stacking on a phone turns four bands of vertical space
503
+ * into one or two, which is the space the results themselves wanted.
504
+ */
505
+ export const Details = styled.div<{ $open: boolean }>`
506
+ display: ${ props => (props.$open ? 'flex' : 'none') };
507
+ flex-wrap: wrap;
508
+ gap: 6px;
152
509
 
153
510
  @media (min-width: 640px) {
154
- flex-direction: row;
155
- flex-wrap: wrap;
511
+ gap: 15px;
156
512
  }
157
513
  `;
158
514
 
159
- export const Detail = styled.div`
160
- padding: 8px 10px;
515
+ /**
516
+ * The disclosure. Small and quiet - it is a way to get at supporting detail,
517
+ * not an action competing with "select".
518
+ */
519
+ export const ButtonDetails = styled.button`
520
+ display: inline-flex;
521
+ align-items: center;
522
+ gap: 5px;
523
+ align-self: flex-start;
524
+ padding: 4px 0;
161
525
  font-size: ${ props => props.theme.size.s };
526
+ color: ${ props => props.theme.font.faded };
527
+
528
+ &:hover {
529
+ color: ${ props => props.theme.font.normal };
530
+ }
531
+
532
+ svg {
533
+ transition: transform 0.2s ease;
534
+ }
535
+ `;
536
+
537
+ export const Detail = styled.div`
538
+ flex: 1 1 auto;
539
+ min-width: 0;
540
+ padding: 5px 8px;
541
+ font-size: calc(${ props => props.theme.size.s } - 1px);
162
542
  background: ${ props => props.theme.background.neutral };
163
543
  border-radius: ${ props => props.theme.borderRadius };
164
544
 
165
545
  @media (min-width: 480px) {
546
+ padding: 8px 10px;
547
+ font-size: ${ props => props.theme.size.s };
166
548
  flex-basis: calc(50% - 7.5px);
167
549
  }
168
550
  @media (min-width: 768px) {
@@ -178,10 +560,6 @@ export const TitleDetail = styled.h6`
178
560
  font-size: ${ props => props.theme.size.xs };
179
561
  `;
180
562
 
181
- export const LinkRoute = styled(Link)`
182
- font-weight: 700;
183
- `;
184
-
185
563
  export const ButtonPolicy = styled.button`
186
564
  display: flex;
187
565
  align-items: center;
@@ -195,20 +573,80 @@ export const ButtonPolicy = styled.button`
195
573
  }
196
574
  `;
197
575
 
198
- export const Features = styled.div`
576
+ /**
577
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
578
+ *
579
+ * Left-aligned now that this sits inside a Detail column alongside route,
580
+ * transit and policies, rather than centred in a full-width band of its
581
+ * own under the card.
582
+ */
583
+ export const FeaturesItems = styled.div`
199
584
  display: flex;
200
- flex-direction: column;
201
- align-items: center;
585
+ flex-wrap: wrap;
586
+ gap: 6px;
202
587
  `;
203
588
 
204
- export const TitleFeatures = styled.h6`
205
- font-size: ${ props => props.theme.size.s };
206
- margin-bottom: 8px;
589
+ /**
590
+ * "Leaves in 40 minutes", on today's departures only.
591
+ *
592
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
593
+ *
594
+ * Same-day seats are sellable now, and a time alone does not tell somebody
595
+ * whether they can make it - "14:00" reads very differently at nine in the
596
+ * morning and at half past one. Under an hour it turns urgent, because that
597
+ * is the point where the answer stops being "yes" and starts being "only if
598
+ * you leave now".
599
+ */
600
+ export const Leaves = styled.div<{ $soon: boolean }>`
601
+ align-self: flex-start;
602
+ padding: 3px 10px;
603
+ border-radius: 100px;
604
+ font-size: ${ props => props.theme.size.xs };
605
+ font-weight: 700;
606
+ white-space: nowrap;
607
+
608
+ color: ${ props => (props.$soon ? '#FFF' : props.theme.font.normal) };
609
+ background: ${ props => (props.$soon ? props.theme.font.error : props.theme.primary.neutral) };
207
610
  `;
208
611
 
209
- export const FeaturesItems = styled.div`
210
- display: flex;
211
- flex-wrap: wrap;
212
- gap: 8px;
213
- justify-content: center;
214
- `;
612
+ /**
613
+ * The +1 on an overnight arrival.
614
+ *
615
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
616
+ *
617
+ * Sits INSIDE the h2, right against the clock time it qualifies, because
618
+ * that is the number it changes the meaning of. Raised and shrunk rather
619
+ * than boxed: it reads as an annotation on the time, the way a footnote
620
+ * marker does, and a second pill on a card that already carries an offer
621
+ * badge and a countdown would compete with both.
622
+ *
623
+ * The title attribute carries the sentence - "arrives the next day" - so
624
+ * the meaning is available to anyone for whom "+1" is not self-evident,
625
+ * which on a first visit is most people.
626
+ */
627
+ export const NextDay = styled.sup`
628
+ margin-left: 4px;
629
+ padding: 1px 4px;
630
+ border-radius: 4px;
631
+ font-size: ${ props => props.theme.size.xs };
632
+ font-weight: 700;
633
+ white-space: nowrap;
634
+ vertical-align: super;
635
+ cursor: help;
636
+ color: ${ props => props.theme.font.normal };
637
+ background: ${ props => props.theme.primary.neutral };
638
+ `;
639
+
640
+ /**
641
+ * The calendar day under one end of an overnight trip.
642
+ *
643
+ * Rendered on BOTH ends or neither: a date shown only over the arrival
644
+ * leaves the reader working out what day the departure was, which is the
645
+ * question the whole thing exists to answer.
646
+ */
647
+ export const Day = styled.span`
648
+ font-size: ${ props => props.theme.size.xs };
649
+ font-weight: 700;
650
+ white-space: nowrap;
651
+ color: ${ props => props.theme.primary.normal };
652
+ `;
@@ -0,0 +1,35 @@
1
+ import { TFunction } from 'i18next';
2
+ import { HiOutlineExclamationCircle } from 'react-icons/hi';
3
+ import { Container } from './styles';
4
+
5
+ interface Props {
6
+ t: TFunction<'common'>
7
+ }
8
+
9
+ /**
10
+ * The caution that comes with selling today's seats.
11
+ *
12
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
13
+ *
14
+ * Same-day departures are sellable now, and two things about them are worth
15
+ * saying plainly before somebody pays.
16
+ *
17
+ * A coach that has already left is filtered out server-side
18
+ * (Search\Departed), and since countries carry a timezone that filter is now
19
+ * judged against the DEPARTURE STOP's own clock rather than ours - so the
20
+ * warning is no longer about doubt on our side.
21
+ *
22
+ * What remains is the honest thing to say when the margin is minutes: every
23
+ * time on the page is local to its stop, the seat is real, the coach is
24
+ * about to go, and whether somebody can reach the terminal in time is not
25
+ * something we can know for them.
26
+ */
27
+ const SameDay = ({ t }: Props): JSX.Element => (
28
+ <Container>
29
+ <HiOutlineExclamationCircle />
30
+
31
+ <span>{ t('routes_order.step2.same_day') }</span>
32
+ </Container>
33
+ );
34
+
35
+ export default SameDay;
@@ -0,0 +1,27 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-03
5
+ *
6
+ * Shown only on a same-day search. A caution, not an alarm - the whole point
7
+ * of selling today is that these seats are worth selling, and a red banner
8
+ * over every one of them would say the opposite.
9
+ */
10
+ export const Container = styled.div`
11
+ display: flex;
12
+ gap: 8px;
13
+ align-items: flex-start;
14
+ margin-bottom: 15px;
15
+ padding: 10px 12px;
16
+ border-radius: ${ props => props.theme.borderRadius };
17
+ background: ${ props => props.theme.primary.neutral };
18
+ color: ${ props => props.theme.font.normal };
19
+ font-size: ${ props => props.theme.size.s };
20
+ line-height: 1.4;
21
+
22
+ & > svg {
23
+ flex: 0 0 auto;
24
+ margin-top: 2px;
25
+ font-size: ${ props => props.theme.size.l };
26
+ }
27
+ `;