@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.
- package/Found/Filters/Filters.tsx +7 -1
- package/Found/Filters/styles.ts +105 -8
- package/Found/Route/Route.tsx +2 -2
- package/Found/Route/styles.ts +171 -4
- package/Found/Sort/Sort.tsx +29 -1
- package/Found/Sort/styles.ts +43 -9
- package/Found/Summary/styles.ts +21 -9
- package/Schedule/styles.ts +75 -0
- package/package.json +1 -1
|
@@ -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') }
|
package/Found/Filters/styles.ts
CHANGED
|
@@ -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:
|
|
25
|
-
padding:
|
|
26
|
-
font-size: ${ props => props.theme.size.
|
|
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:
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
79
|
+
opacity: 0.92;
|
|
36
80
|
}
|
|
37
81
|
|
|
38
82
|
${ props => props.$open && css`
|
|
39
|
-
|
|
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
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -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
|
package/Found/Route/styles.ts
CHANGED
|
@@ -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:
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
}
|
package/Found/Sort/Sort.tsx
CHANGED
|
@@ -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
|
|
package/Found/Sort/styles.ts
CHANGED
|
@@ -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:
|
|
13
|
-
align-items:
|
|
14
|
-
gap:
|
|
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:
|
|
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
|
}
|
package/Found/Summary/styles.ts
CHANGED
|
@@ -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:
|
|
14
|
-
|
|
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.
|
|
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`
|
package/Schedule/styles.ts
CHANGED
|
@@ -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;
|