@autobusal/routes-order 1.36.0 → 1.37.0

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.
@@ -1,7 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FaFilter } from 'react-icons/fa';
3
3
  import { AiOutlineCaretDown, AiOutlineCaretUp } from 'react-icons/ai';
4
- import { Container, Toggle, Count, Panel, Group, GroupTitle, Choice, ChoiceName, ChoiceMeta, ChoiceCount, ChoicePrice, Slider, Reset } from './styles';
4
+ import { Container, Backdrop, Toggle, Count, Panel, Group, GroupTitle, Choice, ChoiceName, ChoiceMeta, ChoiceCount, ChoicePrice, Slider, Reset } from './styles';
5
5
  import { Refinement, Facets, FacetValue, BucketKey, EMPTY, isRefined } from '../refine';
6
6
 
7
7
  interface Props {
@@ -94,6 +94,12 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
94
94
 
95
95
  return (
96
96
  <Container>
97
+ { /* Claude - 2026-08-31: the dimmed layer behind the filter sheet, so
98
+ it reads as sitting OVER the results rather than as part of them -
99
+ and so a tap anywhere off the sheet closes it, which is what
100
+ everybody tries first. */ }
101
+ <Backdrop $open={ open } onClick={ onToggle } />
102
+
97
103
  <Toggle type="button" $open={ open } aria-expanded={ open } onClick={ onToggle }>
98
104
  <FaFilter />
99
105
  { t('routes_order.step2.filters.title') }
@@ -18,28 +18,95 @@ export const Container = styled.div`
18
18
  }
19
19
  `;
20
20
 
21
+ /**
22
+ * Claude - 2026-08-31 (Ferjolt: "the search results should have the filter
23
+ * button at the bottom of the page as a fixed floating button in contrast
24
+ * background color").
25
+ *
26
+ * FIXED TO THE VIEWPORT, not to the top of the list. Filtering is something
27
+ * people reach for AFTER scrolling - three cards down, having seen the
28
+ * prices, deciding they want only the direct ones. A button at the top of
29
+ * the page is a scroll back up at exactly the moment somebody has formed an
30
+ * opinion, and the further they read the further away it gets.
31
+ *
32
+ * In the brand's PRIMARY colour rather than the card surface, because a
33
+ * floating control has no container to belong to - it has to read as the one
34
+ * thing sitting above the page, and a bordered white pill floating over
35
+ * white cards reads as a rendering bug.
36
+ *
37
+ * Centred rather than in a corner: this is the only floating control on the
38
+ * screen, both thumbs reach the middle, and a corner would collide with the
39
+ * browser's own back/share affordances on the platforms that draw them.
40
+ */
21
41
  export const Toggle = styled.button<{ $open: boolean }>`
42
+ position: fixed;
43
+ left: 50%;
44
+ transform: translateX(-50%);
45
+ z-index: 900;
46
+
47
+ /*
48
+ * Clear of the home indicator on a notched phone. env() resolves to 0 on
49
+ * everything that has no inset, so the fallback is simply the base value.
50
+ */
51
+ bottom: calc(20px + env(safe-area-inset-bottom, 0px));
52
+
22
53
  display: flex;
23
54
  align-items: center;
24
- gap: 8px;
25
- padding: 8px 16px;
26
- font-size: ${ props => props.theme.size.s };
55
+ gap: 6px;
56
+ padding: 9px 16px;
57
+ font-size: ${ props => props.theme.size.xs };
27
58
  font-weight: 700;
28
59
  border-radius: 100px;
29
- border: 1px solid ${ props => props.theme.inputs.border };
30
- background: ${ props => props.theme.background.normal };
31
- color: ${ props => props.theme.font.normal };
60
+ border: none;
61
+ cursor: pointer;
62
+
63
+ /*
64
+ * Claude - 2026-08-31 (Ferjolt: "Make the filter button smaller and green
65
+ * color (our green color)").
66
+ *
67
+ * font.success - the brand's green - rather than primary, which is the
68
+ * gold the "Select Route" button already uses. Two prominent controls in
69
+ * the same colour compete: the gold one is the conversion, and a floating
70
+ * filter pill in that colour would be pulling at the same attention from
71
+ * the corner of every screen. Green reads as a tool, gold as the action.
72
+ */
73
+ background: ${ props => props.theme.font.success };
74
+ color: #ffffff;
75
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
32
76
  transition: all 0.2s ease;
33
77
 
34
78
  &:hover {
35
- border-color: ${ props => props.theme.primary.normal };
79
+ opacity: 0.92;
36
80
  }
37
81
 
38
82
  ${ props => props.$open && css`
39
- border-color: ${ props => props.theme.primary.normal };
83
+ /* above its own sheet, so the same button closes it */
84
+ z-index: 1002;
40
85
  ` }
41
86
 
42
87
  // the sidebar is always open, so there is nothing left to toggle
88
+ @media (min-width: 1280px) {
89
+ position: static;
90
+ transform: none;
91
+ display: none;
92
+ }
93
+ `;
94
+
95
+ /**
96
+ * The dimmed backdrop behind the filter sheet.
97
+ *
98
+ * Rendered only while the sheet is open and only below the sidebar
99
+ * breakpoint. It is what makes the sheet read as a layer over the results
100
+ * rather than as part of them, and it gives a tap target for "close" that
101
+ * everybody already knows.
102
+ */
103
+ export const Backdrop = styled.div<{ $open: boolean }>`
104
+ display: ${ props => props.$open ? 'block' : 'none' };
105
+ position: fixed;
106
+ inset: 0;
107
+ z-index: 899;
108
+ background: rgba(0, 0, 0, 0.45);
109
+
43
110
  @media (min-width: 1280px) {
44
111
  display: none;
45
112
  }
@@ -59,6 +126,36 @@ export const Panel = styled.div<{ $open: boolean }>`
59
126
  gap: 20px;
60
127
  margin-top: 12px;
61
128
  padding: 20px;
129
+
130
+ /*
131
+ * Claude - 2026-08-31: a BOTTOM SHEET on a phone, now that the control
132
+ * that opens it floats at the bottom of the viewport.
133
+ *
134
+ * A fixed button opening a panel inline - possibly hundreds of pixels up
135
+ * the page, wherever the list happens to be scrolled - would look like it
136
+ * had done nothing at all. The sheet opens where the thumb already is.
137
+ *
138
+ * Capped at 80vh and scrollable: this panel holds every operator, every
139
+ * stop at both ends and every amenity, which on a busy pair is far more
140
+ * than one screen. Room is left at the bottom for the toggle, which sits
141
+ * above the sheet and doubles as its close button.
142
+ */
143
+ @media (max-width: 1279px) {
144
+ ${ props => props.$open && css`
145
+ position: fixed;
146
+ left: 0;
147
+ right: 0;
148
+ bottom: 0;
149
+ z-index: 1000;
150
+ max-height: 80vh;
151
+ overflow-y: auto;
152
+ margin-top: 0;
153
+ padding-bottom: calc(90px + env(safe-area-inset-bottom, 0px));
154
+ border-radius: 16px 16px 0 0;
155
+ box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.3);
156
+ background: ${ props.theme.background.normal };
157
+ ` }
158
+ }
62
159
  // minmax(0, 1fr) rather than 1fr: a grid track's implicit min-width is
63
160
  // auto, so a long stop name like "Terminali Lindor Interurban dhe
64
161
  // Nderkombetar i Autobusave (TEG)" pushed the whole panel wider than its
@@ -168,7 +168,7 @@ const Route = ({ data, type, date, passengers, t, onSave, children }: Props): JS
168
168
  </CodeCompany>
169
169
  </Company>
170
170
 
171
- <Location>
171
+ <Location $side="from">
172
172
  <h2>
173
173
  { data.locations.from.city.name }
174
174
  { /* Edited: Ferjolt Ozuni - Date: 2026-08-03
@@ -200,7 +200,7 @@ const Route = ({ data, type, date, passengers, t, onSave, children }: Props): JS
200
200
  <span><BiHourglass />{ formatDuration(data.duration, t) }</span>
201
201
  </Time>
202
202
 
203
- <Location>
203
+ <Location $side="to">
204
204
  <h2>
205
205
  { data.locations.to.city.name }
206
206
  { /* Edited: Ferjolt Ozuni - Date: 2026-08-03
@@ -73,12 +73,35 @@ export const FavoriteStop = styled.div`
73
73
  * to point down the column, and the price on one row instead of a centred
74
74
  * stack. Same information, roughly a third less height than before.
75
75
  */
76
+ /*
77
+ * Claude - 2026-08-31 (Ferjolt: "the search results should be displayed in
78
+ * more compact cards; ... departure details and origin details should be in
79
+ * the same row, separated by distance/duration").
80
+ *
81
+ * A GRID on a phone, not a column. The card used to stack six blocks - logo,
82
+ * rating, code, origin, duration, destination - which came to ~390px before
83
+ * the price row, so barely one result fitted on a screen and comparing two
84
+ * meant scrolling between them. Comparison is the entire job of this page.
85
+ *
86
+ * The row that matters is origin | duration | destination, which is how
87
+ * everybody reads a journey and how every timetable ever printed has set it.
88
+ * Named areas rather than source order, because the operator block sits
89
+ * ABOVE that row in the DOM and would otherwise take the first column.
90
+ */
76
91
  export const Trip = styled.div`
77
- display: flex;
78
- flex-direction: column;
79
- gap: 6px;
92
+ display: grid;
93
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
94
+ grid-template-areas:
95
+ 'company company company'
96
+ 'from time to'
97
+ 'price price price';
98
+ align-items: center;
99
+ gap: 4px 8px;
80
100
 
81
101
  @media (min-width: 640px) {
102
+ display: flex;
103
+ grid-template-columns: none;
104
+ grid-template-areas: none;
82
105
  flex-direction: row;
83
106
  flex-wrap: wrap;
84
107
  align-items: center;
@@ -98,6 +121,7 @@ export const Trip = styled.div`
98
121
  `;
99
122
 
100
123
  export const Company = styled.div`
124
+ grid-area: company;
101
125
  flex-basis: 100%;
102
126
 
103
127
  /**
@@ -120,6 +144,21 @@ export const Company = styled.div`
120
144
  */
121
145
  align-items: center;
122
146
 
147
+ /*
148
+ * Claude - 2026-08-31: ONE STRIP on a phone - logo, rating and route code
149
+ * side by side rather than stacked three deep. Stacked, the operator block
150
+ * alone was taller than the journey row it introduces, which is the wrong
151
+ * way round: the journey is what somebody is choosing between.
152
+ */
153
+ @media (max-width: 639px) {
154
+ flex-direction: row;
155
+ align-items: center;
156
+ justify-content: flex-start;
157
+ flex-wrap: wrap;
158
+ gap: 8px;
159
+ margin-bottom: 4px;
160
+ }
161
+
123
162
  /**
124
163
  * Edited: Ferjolt Ozuni - Date: 2026-08-03
125
164
  *
@@ -144,7 +183,15 @@ export const Company = styled.div`
144
183
  * both. It also stops one long stop name stealing width from the other
145
184
  * side of the card.
146
185
  */
147
- export const Location = styled.div`
186
+ /*
187
+ * Claude - 2026-08-31: `$side` places this in the phone grid.
188
+ *
189
+ * The component renders twice - once for each end of the journey - so it
190
+ * cannot carry a fixed grid-area. The prop is only read below 640px; above
191
+ * it the flex row puts them in source order and the areas are unset.
192
+ */
193
+ export const Location = styled.div<{ $side?: 'from' | 'to' }>`
194
+ grid-area: ${ props => props.$side ?? 'auto' };
148
195
  flex: 1;
149
196
  min-width: 0;
150
197
  display: flex;
@@ -188,6 +235,42 @@ export const Location = styled.div`
188
235
  overflow-wrap: anywhere;
189
236
  }
190
237
 
238
+ /*
239
+ * Claude - 2026-08-31: sized to the phone grid's columns.
240
+ *
241
+ * The three-column journey row leaves each end about 135px of a 375px
242
+ * screen, and a heading at desktop scale does not fit in that - so
243
+ * "Thessaloniki" was being broken mid-word into "Thessalo / niki", which
244
+ * overflow-wrap:anywhere permits and which reads as a rendering fault
245
+ * rather than as a wrap.
246
+ *
247
+ * break-word rather than anywhere: it breaks a word only when the word
248
+ * genuinely cannot fit on a line of its own, so the heading wraps between
249
+ * "Thessaloniki (GRC)" and "06:00" as intended and stops slicing city
250
+ * names in half. The size step is what makes that possible - at this
251
+ * scale the longest city on the network fits its column unbroken.
252
+ */
253
+ /*
254
+ * Claude - 2026-08-31 (Ferjolt: "Make city names smaller font, and
255
+ * countries iso even smaller").
256
+ *
257
+ * MEASURED: the destination column was 96px and "Thessaloniki" at 16px
258
+ * wanted exactly 96px, so it broke mid-word into "Thessalonik / i" - which
259
+ * reads as a rendering fault rather than as a wrap. A step down in the
260
+ * heading and a smaller ISO give the longest city on the network room to
261
+ * sit on one line inside its column.
262
+ *
263
+ * break-word rather than anywhere: a word is only ever broken when it
264
+ * genuinely cannot fit a line of its own, so the heading wraps between the
265
+ * city and the time as intended instead of slicing names in half.
266
+ */
267
+ @media (max-width: 639px) {
268
+ & > h2 {
269
+ font-size: ${ props => props.theme.size.s };
270
+ overflow-wrap: break-word;
271
+ }
272
+ }
273
+
191
274
  /**
192
275
  * Edited: Ferjolt Ozuni - Date: 2026-08-03
193
276
  * break-word, not normal. Real spaces now let the heading wrap between
@@ -254,6 +337,17 @@ export const Iso = styled.span`
254
337
  // side of "(ALB)" is this margin's job, not whitespace's
255
338
  margin: 0 5px;
256
339
  font-size: ${ props => props.theme.size.xs };
340
+
341
+ /*
342
+ * Claude - 2026-08-31 (Ferjolt: "countries iso even smaller"). It is a
343
+ * qualifier on the city beside it, not information anybody scans for -
344
+ * every millimetre it gives back goes to the city name, which is what
345
+ * people actually read.
346
+ */
347
+ @media (max-width: 639px) {
348
+ margin: 0 3px;
349
+ font-size: calc(${ props => props.theme.size.xs } - 2px);
350
+ }
257
351
  font-weight: 700;
258
352
  text-transform: uppercase;
259
353
  letter-spacing: .5px;
@@ -261,12 +355,45 @@ export const Iso = styled.span`
261
355
  `;
262
356
 
263
357
  export const Time = styled.div`
358
+ grid-area: time;
264
359
  display: flex;
265
360
  flex-direction: row;
266
361
  gap: 6px;
267
362
  align-items: center;
268
363
  font-weight: 700;
269
364
 
365
+ /*
366
+ * Claude - 2026-08-31: on a phone this is the separator BETWEEN the two
367
+ * ends rather than a row of its own, so the arrow and the duration stack
368
+ * into a narrow column that the grid sizes to its content.
369
+ */
370
+ /*
371
+ * Claude - 2026-08-31 (Ferjolt: "Make also the duration even smaller so it
372
+ * does not occupie to much space and freeup space for origin and
373
+ * destination").
374
+ *
375
+ * MEASURED: the pill was taking ~137px of a 345px row, which left the two
376
+ * ends 96px each - exactly the width "Thessaloniki" needs, so it broke
377
+ * mid-word. Stripped of its pill background and padding here it costs
378
+ * about half that, and both ends gain 20px.
379
+ */
380
+ @media (max-width: 639px) {
381
+ flex-direction: column;
382
+ gap: 0;
383
+ font-size: calc(${ props => props.theme.size.xs } - 1px);
384
+ white-space: nowrap;
385
+
386
+ & > span {
387
+ padding: 0;
388
+ background: none;
389
+ font-size: calc(${ props => props.theme.size.xs } - 1px);
390
+ }
391
+
392
+ & > span > svg {
393
+ display: none;
394
+ }
395
+ }
396
+
270
397
  @media (min-width: 640px) {
271
398
  flex-direction: column;
272
399
  gap: 8px;
@@ -322,6 +449,14 @@ export const Time = styled.div`
322
449
  `;
323
450
 
324
451
  export const Price = styled.div`
452
+ /*
453
+ * Claude - 2026-08-31: spans the phone grid.
454
+ *
455
+ * Trip is a grid below 640px and this is one of its children, so without
456
+ * an area of its own the fare and the button were placed into the first
457
+ * column - which is why they stacked instead of sharing a line.
458
+ */
459
+ grid-area: price;
325
460
  flex-basis: 100%;
326
461
  display: flex;
327
462
 
@@ -451,6 +586,24 @@ export const LinkCompany = styled(Link)`
451
586
  border-radius: 8px;
452
587
  }
453
588
 
589
+ /*
590
+ * Claude - 2026-08-31 (Ferjolt: "operator logo should be smaller").
591
+ *
592
+ * 165x60 was the largest thing on a phone card, and it is an IDENTIFIER -
593
+ * it only has to be recognisable, and it is recognised at a glance by
594
+ * people who already know the operator and not at all by people who do
595
+ * not. The 32px it gives back is a third of the height the journey row
596
+ * needs.
597
+ */
598
+ @media (max-width: 639px) {
599
+ max-width: 96px;
600
+ margin: 0;
601
+
602
+ & > img {
603
+ ${ logoBox('100%', '28px') }
604
+ }
605
+ }
606
+
454
607
  /**
455
608
  * Edited: Ferjolt Ozuni - Date: 2026-08-03
456
609
  * Smaller from the width where the card lays out in one row, since that is
@@ -549,6 +702,20 @@ export const ButtonDetails = styled.button`
549
702
  font-size: ${ props => props.theme.size.s };
550
703
  color: ${ props => props.theme.font.faded };
551
704
 
705
+ /*
706
+ * Claude - 2026-08-31 (Ferjolt: "'Details' should be displayed below
707
+ * 'Select route' button").
708
+ *
709
+ * On a phone the fare sits left and the button right, and this sat under
710
+ * the FARE - the far side of the card from the control it belongs with.
711
+ * Pushed to the right edge it lands directly beneath the button, which is
712
+ * where the thumb already is after reading the price.
713
+ */
714
+ @media (max-width: 639px) {
715
+ align-self: flex-end;
716
+ font-size: ${ props => props.theme.size.xs };
717
+ }
718
+
552
719
  &:hover {
553
720
  color: ${ props => props.theme.font.normal };
554
721
  }
@@ -1,5 +1,5 @@
1
1
  import { TFunction } from 'i18next';
2
- import { Container, Label, Options, Option } from './styles';
2
+ import { Container, Label, Options, Option, Dropdown } from './styles';
3
3
  import { SortKey, SORTS } from '../refine';
4
4
 
5
5
  interface Props {
@@ -21,6 +21,22 @@ const Sort = ({ value, t, onChange }: Props): JSX.Element => (
21
21
  <Container>
22
22
  <Label>{ t('routes_order.step2.sort.title') }</Label>
23
23
 
24
+ { /*
25
+ Claude - 2026-08-31 (Ferjolt: on a small screen "the 'sort by' should
26
+ be with dropdown option and displayed inline with 'trips found'").
27
+
28
+ TWO CONTROLS, ONE STATE, and only ever one of them on screen - which
29
+ is CSS, not a media-query hook, so there is no flash of the wrong one
30
+ while JavaScript works out how wide the window is.
31
+
32
+ The pills stay on a wide screen for the reason they were chosen:
33
+ somebody who never opens a dropdown still sees that "cheapest" is one
34
+ click away. On a phone that argument loses to arithmetic - five pills
35
+ squeezed to 2px of padding, "Recommended" truncated, and a whole row
36
+ of vertical space spent before the first result. A native select
37
+ costs one line, opens the platform's own picker, and is the control
38
+ people already know.
39
+ */ }
24
40
  <Options>
25
41
  { SORTS.map(sort => (
26
42
  <Option
@@ -34,6 +50,18 @@ const Sort = ({ value, t, onChange }: Props): JSX.Element => (
34
50
  </Option>
35
51
  )) }
36
52
  </Options>
53
+
54
+ <Dropdown
55
+ value={ value }
56
+ aria-label={ t('routes_order.step2.sort.title') }
57
+ onChange={ event => onChange(event.target.value as SortKey) }
58
+ >
59
+ { SORTS.map(sort => (
60
+ <option key={ sort } value={ sort }>
61
+ { t(`routes_order.step2.sort.${ sort }`) }
62
+ </option>
63
+ )) }
64
+ </Dropdown>
37
65
  </Container>
38
66
  );
39
67
 
@@ -7,24 +7,59 @@ import styled, { css } from 'styled-components';
7
7
  * instead of losing a third of it to the "Sort by" label and wrapping onto
8
8
  * three lines.
9
9
  */
10
+ /*
11
+ * Claude - 2026-08-31: a ROW on a phone too, because the control is now a
12
+ * single select rather than five pills - it sits beside the trip count
13
+ * instead of under it, which is what buys back the vertical space.
14
+ */
10
15
  export const Container = styled.div`
11
16
  display: flex;
12
- flex-direction: column;
13
- align-items: stretch;
14
- gap: 6px;
17
+ flex-direction: row;
18
+ align-items: center;
19
+ gap: 8px;
20
+ flex-shrink: 0;
15
21
 
16
22
  @media (min-width: 640px) {
17
- flex-direction: row;
18
- align-items: center;
19
- gap: 8px;
20
23
  flex-wrap: wrap;
21
24
  }
22
25
  `;
23
26
 
27
+ /**
28
+ * The "Sort by" caption. Hidden on a phone: the select shows the current
29
+ * choice in its own words ("Cheapest"), which says what it is without
30
+ * spending a third of the row saying so.
31
+ */
24
32
  export const Label = styled.span`
33
+ display: none;
25
34
  font-size: ${ props => props.theme.size.s };
26
35
  font-weight: 700;
27
36
  color: ${ props => props.theme.font.faded };
37
+
38
+ @media (min-width: 640px) {
39
+ display: inline;
40
+ }
41
+ `;
42
+
43
+ /**
44
+ * The phone control.
45
+ *
46
+ * A real <select>, so it opens the platform's own picker - the wheel on iOS,
47
+ * the sheet on Android - which is faster to use one-handed than anything
48
+ * rebuildable in a div, and accessible without any work.
49
+ */
50
+ export const Dropdown = styled.select`
51
+ max-width: 150px;
52
+ padding: 6px 10px;
53
+ font-size: ${ props => props.theme.size.s };
54
+ font-weight: 700;
55
+ border-radius: ${ props => props.theme.borderRadius };
56
+ border: 1px solid ${ props => props.theme.inputs.border };
57
+ background: ${ props => props.theme.background.normal };
58
+ color: ${ props => props.theme.font.normal };
59
+
60
+ @media (min-width: 640px) {
61
+ display: none;
62
+ }
28
63
  `;
29
64
 
30
65
  /**
@@ -32,11 +67,10 @@ export const Label = styled.span`
32
67
  * height than the first result they were meant to reorder.
33
68
  */
34
69
  export const Options = styled.div`
35
- display: flex;
36
- gap: 3px;
37
- flex-wrap: nowrap;
70
+ display: none;
38
71
 
39
72
  @media (min-width: 640px) {
73
+ display: flex;
40
74
  gap: 6px;
41
75
  flex-wrap: wrap;
42
76
  }
@@ -8,21 +8,27 @@ import styled from 'styled-components';
8
8
  * same card background as everything else on the page rather than floating
9
9
  * loose on the page background.
10
10
  */
11
+ /*
12
+ * Claude - 2026-08-31 (Ferjolt: the sort "should be displayed inline with
13
+ * 'trips found: {number} {price}', but in the far right of the row").
14
+ *
15
+ * One row at EVERY width now, not just from 1024px. It used to stack on a
16
+ * phone, which put the count on one line and five sort pills on the next -
17
+ * two rows of chrome above the first actual result. The sort is a single
18
+ * select on small screens (see Sort/styles), so the pair fits on one line
19
+ * with the count left and the control hard right.
20
+ */
11
21
  export const Container = styled.div`
12
22
  display: flex;
13
- flex-direction: column;
14
- gap: 12px;
23
+ flex-direction: row;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ gap: 10px;
15
27
  margin-bottom: 15px;
16
28
 
17
29
  &.box {
18
30
  padding: 12px 15px;
19
31
  }
20
-
21
- @media (min-width: 1024px) {
22
- flex-direction: row;
23
- align-items: center;
24
- justify-content: space-between;
25
- }
26
32
  `;
27
33
 
28
34
  export const Result = styled.div`
@@ -30,10 +36,16 @@ export const Result = styled.div`
30
36
  align-items: baseline;
31
37
  gap: 8px;
32
38
  flex-wrap: wrap;
39
+ min-width: 0;
33
40
  `;
34
41
 
35
42
  export const Total = styled.strong`
36
- font-size: ${ props => props.theme.size.l };
43
+ font-size: ${ props => props.theme.size.m };
44
+ white-space: nowrap;
45
+
46
+ @media (min-width: 640px) {
47
+ font-size: ${ props => props.theme.size.l };
48
+ }
37
49
  `;
38
50
 
39
51
  export const Range = styled.span`
@@ -15,6 +15,81 @@ export const Table = styled.table`
15
15
  border-collapse: collapse;
16
16
  min-width: 620px;
17
17
 
18
+ /*
19
+ * Claude - 2026-08-31 (Ferjolt: "the timetable on the small screen should
20
+ * not display columns for Duration and Stops, so it can fit the small
21
+ * screens without horizontal scroller").
22
+ *
23
+ * Six columns need 620px, and the phone this is read on has 375. The
24
+ * container scrolls sideways, which is the worst of both: the columns that
25
+ * matter most - when it leaves, what it costs - are the ones that fall off
26
+ * the right edge, and a sideways scroll inside a vertically scrolling page
27
+ * is a gesture people fight rather than use.
28
+ *
29
+ * Duration and Stops are the two that go, and they are the right two: both
30
+ * are DERIVED from the departure and arrival already on the row, so a
31
+ * reader loses arithmetic rather than facts. What stays is when it goes,
32
+ * when it lands, who runs it and what it costs - which is the whole of
33
+ * what a timetable is for.
34
+ *
35
+ * By position rather than by a class, because the cells carry no marker of
36
+ * their own and adding one to two <td>s in a six-column table to express
37
+ * "the third and fourth" is more indirection than the rule it replaces.
38
+ * The min-width goes with them, which is what actually removes the
39
+ * scroller.
40
+ */
41
+ @media (max-width: 767px) {
42
+ min-width: 0;
43
+
44
+ /*
45
+ * table-layout: fixed, so the table is the width of its container and
46
+ * the columns divide THAT, rather than the columns deciding a width the
47
+ * container then has to scroll.
48
+ *
49
+ * Measured without it: the four surviving columns came to 379px inside a
50
+ * 335px card, and the operator column alone claimed 144px for a name
51
+ * that needs seventy - auto layout sizes to the widest cell in the whole
52
+ * body, so one long operator name set the width for every row and no
53
+ * amount of wrapping on that cell brought it back.
54
+ */
55
+ table-layout: fixed;
56
+ width: 100%;
57
+
58
+ th:nth-child(3), td:nth-child(3),
59
+ th:nth-child(4), td:nth-child(4) {
60
+ display: none;
61
+ }
62
+
63
+ th, td {
64
+ padding: 8px 4px;
65
+ font-size: ${ props => props.theme.size.xs };
66
+ }
67
+
68
+ /*
69
+ * The operator is the one cell whose content has no natural width limit,
70
+ * so it is the one allowed to wrap. Measured: with the two derived
71
+ * columns gone the table still came to 379px against a 375px viewport -
72
+ * four pixels, entirely from nowrap on a long operator name, and four
73
+ * pixels is enough to leave the sideways scroll in place and undo the
74
+ * point of the exercise.
75
+ */
76
+ /* the two times and the fare need only what they show; the operator
77
+ takes the remainder and wraps inside it */
78
+ th:nth-child(1), td:nth-child(1),
79
+ th:nth-child(2), td:nth-child(2) {
80
+ width: 22%;
81
+ }
82
+
83
+ th:nth-child(6), td:nth-child(6) {
84
+ width: 26%;
85
+ }
86
+
87
+ th:nth-child(5), td:nth-child(5) {
88
+ white-space: normal;
89
+ word-break: break-word;
90
+ }
91
+ }
92
+
18
93
  th, td {
19
94
  padding: 10px 12px;
20
95
  text-align: left;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts",