@autobusal/routes-order 1.9.0 → 1.9.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,60 @@ All notable changes to this package are documented here. This project follows
6
6
  > Note: 1.8.0 was published without a changelog entry. The gap is left as-is
7
7
  > rather than reconstructed after the fact.
8
8
 
9
+ ## [1.9.3] - 2026-08-01
10
+
11
+ ### Changed
12
+
13
+ - **Trip length reads as a duration, not a clock time.** "08:30" sat
14
+ directly between two real departure times with nothing to say it meant
15
+ eight and a half hours rather than half past eight. It now renders as
16
+ `07h 30m` behind an hourglass, and an exact number of hours drops the
17
+ minutes (`08h`, not `08h 00m`). The unit labels come from the translation
18
+ files - "h"/"m" are a Latin-script convention, so Greek reads `07ω 30λ`,
19
+ Bulgarian and Macedonian `07ч 30м`, Albanian `07o 30m`.
20
+ obtapi's `-` sentinel (no computable duration) still passes straight
21
+ through rather than becoming a confident "0h 0m".
22
+ - **Stop name down to 12px** on the result cards.
23
+
24
+ ## [1.9.2] - 2026-08-01
25
+
26
+ ### Changed
27
+
28
+ - **Stop name stepped down to 14px** on the result cards. At the same 16px
29
+ as body copy it competed with the city name above it; the city should be
30
+ read first and the stop only when it is needed.
31
+ - **Country code renders upper case and smaller.** It arrives from the
32
+ database lower case ("alb", "grc") - an ISO country code is upper case by
33
+ definition, so it is cased at render time rather than left to the data,
34
+ where every consumer would have to remember.
35
+
36
+ ## [1.9.1] - 2026-08-01
37
+
38
+ ### Changed
39
+
40
+ - **Filters became a sidebar** at 1280px and up - always visible, sticky
41
+ while the list scrolls, no click needed to discover them. Below 1280px
42
+ they stay the collapsible panel above the list. The breakpoint is 1280
43
+ rather than 1024 because the result cards switch to their wide row layout
44
+ at 1024px on VIEWPORT width, not container width, so taking a column out
45
+ of a 1024px window would leave the cards using a layout their remaining
46
+ space cannot carry.
47
+ - **Facet counts are a filled pill**, separate from the from-price. They
48
+ used to read as one muted string ("5 · 38.00 EUR") where the two numbers
49
+ ran together and it was not obvious which was which.
50
+ - **Facet labels wrap instead of truncating.** In a sidebar column an
51
+ ellipsis reduced two different stops at the same city to the same
52
+ "Terminali Lindor Inte..." - the one distinction the filter exists to
53
+ make.
54
+ - **The result-count / sort bar sits on the card surface** rather than
55
+ floating on the page background.
56
+
57
+ ### Fixed
58
+
59
+ - Filter panel grid tracks used `1fr`, whose implicit min-width is `auto`,
60
+ so a long stop name pushed the whole panel wider than its column and
61
+ pushed the counts and prices out of view entirely. Now `minmax(0, 1fr)`.
62
+
9
63
  ## [1.9.0] - 2026-08-01
10
64
 
11
65
  ### Added
@@ -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, Slider, Reset } from './styles';
4
+ import { Container, 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 {
@@ -83,7 +83,8 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
83
83
  <ChoiceName title={ item.label }>{ item.label }</ChoiceName>
84
84
 
85
85
  <ChoiceMeta>
86
- { item.count }{ item.fromDisplay ? ` · ${ item.fromDisplay }` : '' }
86
+ <ChoiceCount>{ item.count }</ChoiceCount>
87
+ { item.fromDisplay && <ChoicePrice>{ item.fromDisplay }</ChoicePrice> }
87
88
  </ChoiceMeta>
88
89
  </Choice>
89
90
  )) }
@@ -100,8 +101,10 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
100
101
  { open ? <AiOutlineCaretUp /> : <AiOutlineCaretDown /> }
101
102
  </Toggle>
102
103
 
103
- { open && (
104
- <Panel className="box">
104
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
105
+ Rendered unconditionally and hidden with CSS rather than gated on
106
+ `open`, because at sidebar widths there is no toggle to open it. */ }
107
+ <Panel className="box" $open={ open }>
105
108
  { facets.buckets.length > 1 && (
106
109
  <Group>
107
110
  <GroupTitle>{ t('routes_order.step2.filters.times') }</GroupTitle>
@@ -118,7 +121,8 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
118
121
  <ChoiceName>{ bucket.label }</ChoiceName>
119
122
 
120
123
  <ChoiceMeta>
121
- { bucket.count }{ bucket.fromDisplay ? ` · ${ bucket.fromDisplay }` : '' }
124
+ <ChoiceCount>{ bucket.count }</ChoiceCount>
125
+ { bucket.fromDisplay && <ChoicePrice>{ bucket.fromDisplay }</ChoicePrice> }
122
126
  </ChoiceMeta>
123
127
  </Choice>
124
128
  )) }
@@ -182,8 +186,7 @@ const Filters = ({ open, refinement, facets, range, currency, t, onToggle, onCha
182
186
  </Reset>
183
187
  </Group>
184
188
  ) }
185
- </Panel>
186
- ) }
189
+ </Panel>
187
190
  </Container>
188
191
  );
189
192
  };
@@ -1,7 +1,21 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
+ /**
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
5
+ *
6
+ * Below 1280px the filters stay a collapsible panel above the list; at and
7
+ * above it they become a permanent sidebar. 1280 rather than 1024 on
8
+ * purpose: the result CARDS switch to their wide row layout at 1024px based
9
+ * on the viewport, not on their container, so carving a 280px column out of
10
+ * a 1024px window would leave the cards using a layout their remaining
11
+ * ~700px cannot carry.
12
+ */
3
13
  export const Container = styled.div`
4
14
  margin-bottom: 15px;
15
+
16
+ @media (min-width: 1280px) {
17
+ margin-bottom: 0;
18
+ }
5
19
  `;
6
20
 
7
21
  export const Toggle = styled.button<{ $open: boolean }>`
@@ -24,6 +38,11 @@ export const Toggle = styled.button<{ $open: boolean }>`
24
38
  ${ props => props.$open && css`
25
39
  border-color: ${ props => props.theme.primary.normal };
26
40
  ` }
41
+
42
+ // the sidebar is always open, so there is nothing left to toggle
43
+ @media (min-width: 1280px) {
44
+ display: none;
45
+ }
27
46
  `;
28
47
 
29
48
  export const Count = styled.span`
@@ -35,18 +54,35 @@ export const Count = styled.span`
35
54
  border-radius: 100px;
36
55
  `;
37
56
 
38
- export const Panel = styled.div`
39
- display: grid;
57
+ export const Panel = styled.div<{ $open: boolean }>`
58
+ display: ${ props => props.$open ? 'grid' : 'none' };
40
59
  gap: 20px;
41
60
  margin-top: 12px;
42
61
  padding: 20px;
62
+ // minmax(0, 1fr) rather than 1fr: a grid track's implicit min-width is
63
+ // auto, so a long stop name like "Terminali Lindor Interurban dhe
64
+ // Nderkombetar i Autobusave (TEG)" pushed the whole panel wider than its
65
+ // column and pushed the counts and prices out of sight
66
+ grid-template-columns: minmax(0, 1fr);
43
67
 
44
68
  @media (min-width: 640px) {
45
- grid-template-columns: repeat(2, 1fr);
69
+ grid-template-columns: repeat(2, minmax(0, 1fr));
46
70
  }
47
71
 
48
72
  @media (min-width: 1024px) {
49
- grid-template-columns: repeat(3, 1fr);
73
+ grid-template-columns: repeat(3, minmax(0, 1fr));
74
+ }
75
+
76
+ // as a sidebar: always rendered, one column, and it follows the page down
77
+ // so the filters stay reachable while scrolling a long result list
78
+ @media (min-width: 1280px) {
79
+ display: grid;
80
+ grid-template-columns: minmax(0, 1fr);
81
+ margin-top: 0;
82
+ position: sticky;
83
+ top: 15px;
84
+ max-height: calc(100vh - 30px);
85
+ overflow-y: auto;
50
86
  }
51
87
  `;
52
88
 
@@ -54,6 +90,7 @@ export const Group = styled.div`
54
90
  display: flex;
55
91
  flex-direction: column;
56
92
  gap: 8px;
93
+ min-width: 0;
57
94
  `;
58
95
 
59
96
  export const GroupTitle = styled.h6`
@@ -65,8 +102,9 @@ export const GroupTitle = styled.h6`
65
102
 
66
103
  export const Choice = styled.label<{ $disabled: boolean }>`
67
104
  display: flex;
68
- align-items: center;
105
+ align-items: flex-start;
69
106
  gap: 8px;
107
+ line-height: 1.35;
70
108
  font-size: ${ props => props.theme.size.s };
71
109
  cursor: pointer;
72
110
 
@@ -85,19 +123,52 @@ export const Choice = styled.label<{ $disabled: boolean }>`
85
123
  ` }
86
124
  `;
87
125
 
126
+ /**
127
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
128
+ *
129
+ * Wraps rather than truncating. In the sidebar column an ellipsis turned
130
+ * "Terminali Lindor Interurban dhe Nderkombetar i Autobusave (TEG)" into
131
+ * "Terminali Lindor Inte..." - two stops at the same city are then
132
+ * indistinguishable, which is the one thing this filter exists to tell
133
+ * apart. A second line is cheaper than a useless label.
134
+ */
88
135
  export const ChoiceName = styled.span`
89
136
  flex: 1;
90
- overflow: hidden;
91
- text-overflow: ellipsis;
92
- white-space: nowrap;
137
+ min-width: 0;
138
+ overflow-wrap: anywhere;
93
139
  `;
94
140
 
95
141
  export const ChoiceMeta = styled.span`
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 6px;
96
145
  font-size: ${ props => props.theme.size.xs };
97
146
  color: ${ props => props.theme.font.faded };
98
147
  white-space: nowrap;
99
148
  `;
100
149
 
150
+ /**
151
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
152
+ *
153
+ * How many trips a facet value leaves, as a filled pill. It used to sit
154
+ * next to the price as "5 · 38.00 EUR" in one muted string, where the two
155
+ * numbers read as one and you had to work out which was which. A count and
156
+ * a fare are different kinds of thing and now look like it.
157
+ */
158
+ export const ChoiceCount = styled.span`
159
+ min-width: 22px;
160
+ padding: 1px 6px;
161
+ text-align: center;
162
+ font-weight: 700;
163
+ color: ${ props => props.theme.font.normal };
164
+ background: ${ props => props.theme.background.neutral };
165
+ border-radius: 100px;
166
+ `;
167
+
168
+ export const ChoicePrice = styled.span`
169
+ color: ${ props => props.theme.font.faded };
170
+ `;
171
+
101
172
  export const Slider = styled.input`
102
173
  width: 100%;
103
174
  accent-color: ${ props => props.theme.primary.normal };
package/Found/Found.tsx CHANGED
@@ -9,6 +9,7 @@ import Sort from './Sort/Sort';
9
9
  import Picks from './Picks/Picks';
10
10
  import Filters from './Filters/Filters';
11
11
  import Summary from './Summary/Summary';
12
+ import { Results, Sidebar, Listing } from '../styles';
12
13
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
13
14
  import { FoundData } from '@autobusal/providers/types/routes';
14
15
  import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
@@ -88,17 +89,6 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
88
89
  <Sort value={ refinement.sort } t={ t } onChange={ sort => onRefine({ ...refinement, sort }) } />
89
90
  </Summary>
90
91
 
91
- <Filters
92
- open={ open }
93
- refinement={ refinement }
94
- facets={ facets }
95
- range={ bounds }
96
- currency={ currency }
97
- t={ t }
98
- onToggle={ () => setOpen(!open) }
99
- onChange={ onRefine }
100
- />
101
-
102
92
  <Picks
103
93
  items={ refined }
104
94
  sort={ refinement.sort }
@@ -108,21 +98,40 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
108
98
  </>
109
99
  ) }
110
100
 
111
- <Header t={ t } />
112
-
113
- <Orders
114
- loading={ loading }
115
- data={ refined }
116
- empty={ items.length === 0 }
117
- refined={ isRefined(refinement) }
118
- sort={ refinement.sort }
119
- type={ type }
120
- passengers={ passengers }
121
- preferredStop={ Number(preferredStop) }
122
- t={ t }
123
- onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
124
- onSave={ onSave }
125
- />
101
+ <Results>
102
+ { showControls && (
103
+ <Sidebar>
104
+ <Filters
105
+ open={ open }
106
+ refinement={ refinement }
107
+ facets={ facets }
108
+ range={ bounds }
109
+ currency={ currency }
110
+ t={ t }
111
+ onToggle={ () => setOpen(!open) }
112
+ onChange={ onRefine }
113
+ />
114
+ </Sidebar>
115
+ ) }
116
+
117
+ <Listing>
118
+ <Header t={ t } />
119
+
120
+ <Orders
121
+ loading={ loading }
122
+ data={ refined }
123
+ empty={ items.length === 0 }
124
+ refined={ isRefined(refinement) }
125
+ sort={ refinement.sort }
126
+ type={ type }
127
+ passengers={ passengers }
128
+ preferredStop={ Number(preferredStop) }
129
+ t={ t }
130
+ onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
131
+ onSave={ onSave }
132
+ />
133
+ </Listing>
134
+ </Results>
126
135
  </>
127
136
  );
128
137
  };
@@ -1,9 +1,11 @@
1
1
  import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
3
  import { HiOutlineArrowNarrowRight, HiOutlineDocumentText } from 'react-icons/hi';
4
+ import { BiHourglass } from 'react-icons/bi';
4
5
  import { RiBus2Line } from 'react-icons/ri';
6
+ import { formatDuration } from '../refine';
5
7
  import { Button, RouteFeature, Policy } from '@autobusal/common';
6
- import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
8
+ import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
7
9
  import { FoundData } from '@autobusal/providers/types/routes';
8
10
 
9
11
  // neutral placeholder so a missing/broken operator logo doesn't render the
@@ -54,18 +56,33 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
54
56
  </Company>
55
57
 
56
58
  <Location>
57
- <h2>{ data.locations.from.city.name } ({ data.locations.from.city.country?.iso_code }) { data.locations.from.departure }</h2>
58
- <span>{ data.locations.from.stop?.name }</span>
59
+ <h2>
60
+ { data.locations.from.city.name }
61
+ <Iso>({ data.locations.from.city.country?.iso_code })</Iso>
62
+ { data.locations.from.departure }
63
+ </h2>
64
+
65
+ <Stop>{ data.locations.from.stop?.name }</Stop>
59
66
  </Location>
60
67
 
61
68
  <Time>
62
69
  <HiOutlineArrowNarrowRight />
63
- <span>{ data.duration }</span>
70
+
71
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
72
+ The clock is the point: this number sits between two real
73
+ departure times, and "08:30" gave nothing to distinguish a
74
+ trip length from a time of day. */ }
75
+ <span><BiHourglass />{ formatDuration(data.duration, t) }</span>
64
76
  </Time>
65
77
 
66
78
  <Location>
67
- <h2>{ data.locations.to.city.name } ({ data.locations.to.city.country?.iso_code }) { data.locations.to.arrival }</h2>
68
- <span>{ data.locations.to.stop?.name }</span>
79
+ <h2>
80
+ { data.locations.to.city.name }
81
+ <Iso>({ data.locations.to.city.country?.iso_code })</Iso>
82
+ { data.locations.to.arrival }
83
+ </h2>
84
+
85
+ <Stop>{ data.locations.to.stop?.name }</Stop>
69
86
  </Location>
70
87
 
71
88
  <Price>
@@ -73,17 +73,46 @@ export const Location = styled.div`
73
73
  display: flex;
74
74
  flex-direction: column;
75
75
  align-items: center;
76
- gap: 5px;
76
+ gap: 3px;
77
77
  text-align: center;
78
78
 
79
79
  & > h2 {
80
80
  margin-bottom: 0;
81
81
  }
82
+ `;
82
83
 
83
- & > span {
84
- font-weight: 700;
85
- color: ${ props => props.theme.font.faded };
86
- }
84
+ /**
85
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
86
+ *
87
+ * The stop name is supporting detail, not a heading. At the same 16px as
88
+ * body copy it competed with the city above it; a step down lets the eye
89
+ * land on the city first and read the stop only when it needs to.
90
+ */
91
+ export const Stop = styled.span`
92
+ font-size: ${ props => props.theme.size.xs };
93
+ font-weight: 700;
94
+ line-height: 1.3;
95
+ color: ${ props => props.theme.font.faded };
96
+ `;
97
+
98
+ /**
99
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
100
+ *
101
+ * The country code comes out of the database lowercase ("alb", "grc").
102
+ * An ISO country code is by definition upper case, so it is cased here at
103
+ * render time rather than left to the data - every consumer would otherwise
104
+ * have to remember to do it. Set smaller and muted so it qualifies the city
105
+ * without competing with it.
106
+ */
107
+ export const Iso = styled.span`
108
+ // JSX drops the literal spaces around this element, so the gap on either
109
+ // side of "(ALB)" is this margin's job, not whitespace's
110
+ margin: 0 5px;
111
+ font-size: ${ props => props.theme.size.xs };
112
+ font-weight: 700;
113
+ text-transform: uppercase;
114
+ letter-spacing: .5px;
115
+ color: ${ props => props.theme.font.faded };
87
116
  `;
88
117
 
89
118
  export const Time = styled.div`
@@ -98,13 +127,24 @@ export const Time = styled.div`
98
127
  }
99
128
 
100
129
  & > span {
101
- display: block;
102
- padding: 3px 10px;
103
- font-size: ${ props => props.theme.size.l };
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 5px;
133
+ padding: 3px 12px;
134
+ white-space: nowrap;
135
+ font-size: ${ props => props.theme.size.m };
104
136
  background: ${ props => props.theme.background.neutral };
105
137
  border-radius: 100px;
106
138
  }
107
139
 
140
+ // the hourglass is a hint, not a heading - the big arrow above already
141
+ // owns the visual weight in this column
142
+ & > span > svg {
143
+ flex-shrink: 0;
144
+ font-size: ${ props => props.theme.size.m };
145
+ opacity: .7;
146
+ }
147
+
108
148
  @media (min-width: 640px) {
109
149
  flex-basis: 120px;
110
150
  }
@@ -20,7 +20,7 @@ interface Props {
20
20
  * results, which is otherwise easy to forget after scrolling.
21
21
  */
22
22
  const Summary = ({ shown, total, range, t, children }: Props): JSX.Element => (
23
- <Container>
23
+ <Container className="box">
24
24
  <Result>
25
25
  { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
26
26
  `total`, not `count` - i18next treats a `count` variable as a
@@ -1,11 +1,23 @@
1
1
  import styled from 'styled-components';
2
2
 
3
+ /**
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
5
+ *
6
+ * Carries the `box` surface (padding is overridden to something tighter -
7
+ * this is a bar, not a panel) so the count and the sort control sit on the
8
+ * same card background as everything else on the page rather than floating
9
+ * loose on the page background.
10
+ */
3
11
  export const Container = styled.div`
4
12
  display: flex;
5
13
  flex-direction: column;
6
14
  gap: 12px;
7
15
  margin-bottom: 15px;
8
16
 
17
+ &.box {
18
+ padding: 12px 15px;
19
+ }
20
+
9
21
  @media (min-width: 1024px) {
10
22
  flex-direction: row;
11
23
  align-items: center;
package/Found/refine.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { TFunction } from 'i18next';
1
2
  import { FoundData } from '@autobusal/providers/types/routes';
2
3
 
3
4
  /**
@@ -97,6 +98,48 @@ export const durationOf = (item: FoundData): number | null => {
97
98
  return minutesOf(item.duration);
98
99
  };
99
100
 
101
+ /**
102
+ * Turn obtapi's "HH:MM" trip length into something that reads as a
103
+ * duration rather than a clock time.
104
+ *
105
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
106
+ *
107
+ * "08:30" sitting between two departure times is genuinely ambiguous -
108
+ * nothing about it says "eight and a half hours" rather than "half past
109
+ * eight", and on this card it sits directly between 09:00 and 16:30. The
110
+ * unit labels come from the translation files because "h"/"m" are not
111
+ * universal; Greek and the Cyrillic locales carry their own.
112
+ *
113
+ * The "-" sentinel (obtapi could not compute a duration) passes straight
114
+ * through - it must not become "0h 0m", which would read as a real answer.
115
+ */
116
+ export const formatDuration = (duration: string, t: TFunction<'common'>): string => {
117
+ if (!duration || duration === '-') {
118
+ return duration;
119
+ }
120
+
121
+ const total = minutesOf(duration);
122
+
123
+ const hours = Math.floor(total / 60);
124
+ const minutes = total % 60;
125
+
126
+ const pad = (value: number): string => (value <= 9 ? `0${ value }` : String(value));
127
+
128
+ const parts: string[] = [];
129
+
130
+ if (hours > 0) {
131
+ parts.push(t('routes_order.step2.route.duration.hours', { value: pad(hours) }));
132
+ }
133
+
134
+ // a trip under an hour still needs a number, and so does an exact
135
+ // number of hours - but "08h 00m" is noise, so zero minutes are dropped
136
+ if (minutes > 0 || hours === 0) {
137
+ parts.push(t('routes_order.step2.route.duration.minutes', { value: pad(minutes) }));
138
+ }
139
+
140
+ return parts.join(' ');
141
+ };
142
+
100
143
  export const bucketOf = (item: FoundData): BucketKey => {
101
144
  const hour = Math.floor(departureOf(item) / 60);
102
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.9.0",
3
+ "version": "1.9.3",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/styles.ts CHANGED
@@ -21,4 +21,40 @@ export const Actions = styled.div`
21
21
  justify-content: center;
22
22
  gap: 20px;
23
23
  margin-top: 25px;
24
+ `;
25
+
26
+ /**
27
+ * Results layout: filters beside the list, not above it.
28
+ *
29
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
30
+ *
31
+ * Below 1280px this is a plain column and the filters stay a collapsible
32
+ * panel. At and above it the filters take a fixed column and the results
33
+ * take the rest. See Found/Filters/styles.ts for why the breakpoint is
34
+ * 1280 rather than 1024.
35
+ */
36
+ export const Results = styled.div`
37
+ display: flex;
38
+ flex-direction: column;
39
+
40
+ @media (min-width: 1280px) {
41
+ flex-direction: row;
42
+ align-items: flex-start;
43
+ gap: 20px;
44
+ }
45
+ `;
46
+
47
+ export const Sidebar = styled.aside`
48
+ min-width: 0;
49
+
50
+ @media (min-width: 1280px) {
51
+ flex: 0 0 290px;
52
+ }
53
+ `;
54
+
55
+ export const Listing = styled.div`
56
+ // min-width:0 stops a long stop name inside a card from forcing this
57
+ // column wider than its share of the row
58
+ flex: 1;
59
+ min-width: 0;
24
60
  `;