@autobusal/common 1.36.2 → 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/Button/Button.tsx +14 -1
- package/Button/styles.ts +10 -0
- package/Table/Actions/Icon.tsx +20 -1
- package/package.json +5 -2
- package/CHANGELOG.md +0 -777
package/Button/Button.tsx
CHANGED
|
@@ -5,6 +5,13 @@ interface Props {
|
|
|
5
5
|
type?: 'button' | 'submit'
|
|
6
6
|
size?: 'medium' | 'small'
|
|
7
7
|
loading?: boolean
|
|
8
|
+
/*
|
|
9
|
+
* Claude - 2026-08-30: a button that cannot be disabled is a real gap, not
|
|
10
|
+
* a style choice - the screen that hit it (bulk send with nothing selected)
|
|
11
|
+
* had to fall back to a bespoke <button> to express it, which is how that
|
|
12
|
+
* screen ended up not looking like the rest of the dashboard.
|
|
13
|
+
*/
|
|
14
|
+
disabled?: boolean
|
|
8
15
|
active?: boolean
|
|
9
16
|
subtype?: 'secondary' | 'delete'
|
|
10
17
|
text: string | JSX.Element
|
|
@@ -19,7 +26,7 @@ interface Props {
|
|
|
19
26
|
value?: string
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
const Button = ({ type, size, loading, active, subtype, text, noMargin, maxWidth, onClick, name, value }: Props) => (
|
|
29
|
+
const Button = ({ type, size, loading, disabled, active, subtype, text, noMargin, maxWidth, onClick, name, value }: Props) => (
|
|
23
30
|
<ButtonStyled
|
|
24
31
|
$size={ size }
|
|
25
32
|
type={ loading ? 'button' : type }
|
|
@@ -27,6 +34,12 @@ const Button = ({ type, size, loading, active, subtype, text, noMargin, maxWidth
|
|
|
27
34
|
$subtype={ subtype }
|
|
28
35
|
$margin={ noMargin ? false : true }
|
|
29
36
|
$maxWidth={ maxWidth }
|
|
37
|
+
/*
|
|
38
|
+
* A loading button is disabled too: it already refuses to submit (the
|
|
39
|
+
* type swap above), and without this the pointer still says "click me"
|
|
40
|
+
* while a request is in flight.
|
|
41
|
+
*/
|
|
42
|
+
disabled={ disabled || loading }
|
|
30
43
|
onClick={ onClick }
|
|
31
44
|
name={ name }
|
|
32
45
|
value={ value }
|
package/Button/styles.ts
CHANGED
|
@@ -70,4 +70,14 @@ export const ButtonStyled = styled.button<ButtonType>`
|
|
|
70
70
|
&:hover {
|
|
71
71
|
box-shadow: 0 4px 15px ${ props => props.theme.primary.normal };
|
|
72
72
|
}
|
|
73
|
+
/*
|
|
74
|
+
* Claude - 2026-08-30: a disabled button has to LOOK unavailable, or the
|
|
75
|
+
* only feedback is a click that does nothing. Faded rather than restyled,
|
|
76
|
+
* so it is still recognisably the same button in the same place.
|
|
77
|
+
*/
|
|
78
|
+
&:disabled {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
`;
|
package/Table/Actions/Icon.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
2
|
import { AiOutlineCheck, AiOutlineCloudDownload } from 'react-icons/ai';
|
|
3
|
-
import { BiEditAlt, BiTransfer, BiTrash } from 'react-icons/bi';
|
|
3
|
+
import { BiEditAlt, BiEnvelope, BiMessageDetail, BiTransfer, BiTrash } from 'react-icons/bi';
|
|
4
4
|
import { FaCity, FaTrash } from 'react-icons/fa';
|
|
5
5
|
import { GiMoneyStack } from 'react-icons/gi';
|
|
6
6
|
import { IoDocumentLockOutline } from 'react-icons/io5';
|
|
@@ -22,6 +22,25 @@ const Icon = ({ type, t }: Props): (JSX.Element | null) => {
|
|
|
22
22
|
case 'delete':
|
|
23
23
|
return <BiTrash title={ t('table.actions.delete', { ns: 'common' }) } />;
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
* Claude - 2026-08-30: two verbs the operator-invitation list needs.
|
|
27
|
+
*
|
|
28
|
+
* Added to the shared vocabulary rather than drawn as bespoke buttons on
|
|
29
|
+
* that one screen: an action that looks like an action everywhere else is
|
|
30
|
+
* the entire point of this component, and the screen that prompted this
|
|
31
|
+
* had hand-rolled its own row controls precisely because these were
|
|
32
|
+
* missing.
|
|
33
|
+
*
|
|
34
|
+
* 'compose' is writing the text that will be sent; 'send' is sending it.
|
|
35
|
+
* Two steps, two icons - a single 'mail' for both would make the
|
|
36
|
+
* destructive-ish one (it really posts) look like the harmless one.
|
|
37
|
+
*/
|
|
38
|
+
case 'compose':
|
|
39
|
+
return <BiMessageDetail title={ t('table.actions.custom.compose', { ns: 'common' }) } />;
|
|
40
|
+
|
|
41
|
+
case 'send':
|
|
42
|
+
return <BiEnvelope title={ t('table.actions.custom.send', { ns: 'common' }) } />;
|
|
43
|
+
|
|
25
44
|
case 'login_as':
|
|
26
45
|
return <RiLoginCircleLine title={ t('table.actions.custom.login_as', { ns: 'common' }) } />;
|
|
27
46
|
|
package/package.json
CHANGED
package/CHANGELOG.md
DELETED
|
@@ -1,777 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.36.2 - 2026-08-29
|
|
4
|
-
|
|
5
|
-
- Seat map: a taken seat is DISABLED, struck through and labelled "Seat N - taken", instead of relying on colour alone with a click that was silently swallowed (audit A3-08c).
|
|
6
|
-
|
|
7
|
-
- Drive map: the road line is asked through obtapi's /routes/directions instead of calling OpenRouteService from the browser, which CORS blocked every time - and which shipped a metered third-party key in the bundle to pay for requests that could not succeed (audit A7-26). The hook now answers one feature or null, not a GeoJSON envelope.
|
|
8
|
-
|
|
9
|
-
## 1.36.0 (2026-08-29)
|
|
10
|
-
|
|
11
|
-
- Editors offer Save & Close alongside Save & Stay, so correcting a record no longer bounces you back to its list every time (opt-in per page, editing only). The pair is driven from here (Viewer + Actions).
|
|
12
|
-
|
|
13
|
-
## 1.35.0 (2026-08-29)
|
|
14
|
-
|
|
15
|
-
- RouteFeature's compact chip draws a real tooltip on hover AND on focus, so a tap reveals the amenity name; `title` alone was invisible on touch and slow on desktop.
|
|
16
|
-
- Calendar treats an unparseable defaultValue as nothing chosen, and anchors an empty picker on its minDate - so a floored picker (the return leg) opens on the month being travelled rather than on today or on January.
|
|
17
|
-
|
|
18
|
-
## 1.34.1 (2026-08-29)
|
|
19
|
-
|
|
20
|
-
- Meta's stale-tag cleanup can no longer remove a tag React owns: it now identifies leftovers by having existed before React rendered, not by comparing values (Sentry BUSMAGUS-7 - on a prerendered page the leftover and the live tag carry the same value, so the old comparison deleted React's own node and the next navigation crashed on removeChild).
|
|
21
|
-
|
|
22
|
-
## 1.34.0 (2026-08-29)
|
|
23
|
-
|
|
24
|
-
- Calendar renders one month per page - the neighbouring months' days are blank padding now, so a grid no longer shows two different 1sts and 31sts.
|
|
25
|
-
- Calendar takes `maxYears`, raising a single picker's ceiling above what its type allows (booking pickers stay at one year); Viewer forwards it on 'picker' rows.
|
|
26
|
-
- New SearchableSelect: a <select> you can type into - accent-insensitive matching, group headings, inert disabled options, written for the operator's stop picker.
|
|
27
|
-
- BackWithTitle takes `actions`, rendered as pill links at the far end of the title line.
|
|
28
|
-
- Table row actions for 'cities' and 'stations'.
|
|
29
|
-
|
|
30
|
-
## 1.33.14 (2026-08-26)
|
|
31
|
-
|
|
32
|
-
- ChooseSeat: seat-selection add-on mode - `optional` + `fee` props; unpicked state shows the free-auto note, a made choice gets a back-to-automatic control. Default behaviour (required free pick) unchanged.
|
|
33
|
-
|
|
34
|
-
## 1.33.13
|
|
35
|
-
|
|
36
|
-
### Fixed
|
|
37
|
-
|
|
38
|
-
- **`RouteItem` no longer advertises "From 0.00"**: zero-fare rows are unpriced legs search never sells, so they no longer count toward the cheapest-of figure. (Audit RT-11b.)
|
|
39
|
-
|
|
40
|
-
## 1.33.12
|
|
41
|
-
|
|
42
|
-
### Added
|
|
43
|
-
|
|
44
|
-
- **`DefaultLocaleGate`** - the mirror of `LocaleGate` for the UNPREFIXED public branch: a visitor whose stored language is not the brand default is redirected from `/help` to `/{lang}/help`, so the page shown always matches the address (and its canonical). Crawlers carry no localStorage and keep seeing the canonical default-language page. (Audit decision CMS-07c.)
|
|
45
|
-
|
|
46
|
-
## 1.33.11
|
|
47
|
-
|
|
48
|
-
### Fixed
|
|
49
|
-
|
|
50
|
-
- **The "Add Funds" link in the Agents and Visitors lists 404'd.** It pointed at `/admin/funds/manage/:id`, a route that has never existed; the funds form lives at `/account/funds/manage/:id` (admin + financer). (Platform audit ADM-06g.)
|
|
51
|
-
|
|
52
|
-
## 1.33.9
|
|
53
|
-
|
|
54
|
-
### Fixed
|
|
55
|
-
|
|
56
|
-
- **Every field built through `Viewer` was unnamed.** The label was a `<span>` beside an input with no id, so each one announced as a bare "edit text" and clicking a label did nothing - across every admin and account form in the app, not one screen. `Item` now mints a `useId`-prefixed id for single-control rows and the label is a real `<label htmlFor>`. Additive: no caller changes, and the styled rule set was empty either way.
|
|
57
|
-
|
|
58
|
-
### Added
|
|
59
|
-
|
|
60
|
-
- `Table` and `Viewer` take an optional `confirm`, so a destructive action can ask a question specific to what it destroys rather than the generic one. Existing callers keep the generic question.
|
|
61
|
-
|
|
62
|
-
## 1.33.8
|
|
63
|
-
|
|
64
|
-
### Added
|
|
65
|
-
|
|
66
|
-
- **`Table` headers can sort.** A header cell now takes an optional sort key and direction and renders as a control when it does, so a table backed by a paginated, server-sorted endpoint can drive it from the header rather than a separate control. Tables that pass no sort key are untouched and render exactly as before.
|
|
67
|
-
|
|
68
|
-
## 1.33.7
|
|
69
|
-
|
|
70
|
-
### Changed
|
|
71
|
-
|
|
72
|
-
- **`Required` renders a real `<label>`, and takes an optional `htmlFor`.** It was a `styled.span` sitting beside the field it named, which is a label to a sighted reader and nothing at all to a screen reader - every field it introduced announced as a bare "edit text". Nothing in the stylesheets selects `label` and a flex item computes identically either way, so this is semantics only.
|
|
73
|
-
- **`Password` and `Calendar` take an optional `id`.** A caller cannot associate a visible label with a field it has no way to name, so the shared inputs had to be able to accept one before the forms using them could be fixed.
|
|
74
|
-
- `AiAssist/AiReviewModal` runs its three HTML panes through `sanitizeHtml`. Everything the model returns is HTML rendered into an admin's browser, and every other sink in the codebase sanitises; this one did not.
|
|
75
|
-
|
|
76
|
-
## 1.33.6
|
|
77
|
-
|
|
78
|
-
### Added
|
|
79
|
-
|
|
80
|
-
- **New `Map/Tiles` component - the one place the basemap comes from.** The same `TileLayer` line was copy-pasted into `Drive.tsx`, `@autobusal/map`'s `Display.tsx` and magus's own `RouteMap.tsx`, with a fourth copy in the mobile app, all hardcoding `tile.openstreetmap.org`. OSM's Tile Usage Policy does not cover commercial or heavy application use, and those servers throttle or block by referer and volume without notice - so the exposure was never the bill, it was that reacting to a block meant editing three repos, publishing to npm, redeploying both frontends and shipping a mobile store release.
|
|
81
|
-
|
|
82
|
-
The URL now comes from `/settings/get` (`map.tile_url`) rather than the build, so all four clients switch from one server env var. `null` - the state until a provider is chosen - means the built-in OpenStreetMap default, so this changes nothing for anyone on upgrade. Attribution is rendered by the component rather than travelling with the URL, since ODbL requires it whoever serves the tiles.
|
|
83
|
-
|
|
84
|
-
### Changed
|
|
85
|
-
|
|
86
|
-
- `Drive.tsx` renders `<Tiles />` instead of its own `TileLayer`.
|
|
87
|
-
|
|
88
|
-
## 1.33.4
|
|
89
|
-
|
|
90
|
-
### Added
|
|
91
|
-
|
|
92
|
-
- **New `LocalizedFields` component** - a generic per-language field editor generalising SeoOverride (fixed to title/description) and Faqs/Translations (fixed to question/answer) into one taking an arbitrary field list. A section with just one translatable field (a category name, a menu label) no longer needs its own bespoke ~120-line copy of the same editor to support more than English+Albanian.
|
|
93
|
-
|
|
94
|
-
## 1.33.3
|
|
95
|
-
|
|
96
|
-
### Added
|
|
97
|
-
|
|
98
|
-
- **`RouteItem` shows a "from" price** next to the route name/code - the lowest fare across the route's own `prices`, using the row's own pre-formatted `adult_display` (same currency/decimal convention as everywhere else). Both `browse` and `view` modes get it; a route with no prices at all shows nothing, same as it always could.
|
|
99
|
-
|
|
100
|
-
## 1.33.2
|
|
101
|
-
|
|
102
|
-
### Added
|
|
103
|
-
|
|
104
|
-
- **`Meta` now defaults `og:image`/`twitter:image` to the label's own homepage hero photo** (`{settings.url}/home/background-light.jpg`) when a page doesn't pass its own `image`. Most of the ~136 call sites never had a share-preview image at all, so a link pasted into a chat assistant or social post previewed with nothing. A page with something more specific (an operator's logo, a blog post's own photo) still passes its own `image` and wins - this only fills the gap.
|
|
105
|
-
|
|
106
|
-
## 1.33.1
|
|
107
|
-
|
|
108
|
-
### Added
|
|
109
|
-
|
|
110
|
-
- **`Table` accepts an optional `onReorder(draggedId, targetId)` prop.** When set, every row becomes an HTML5 drag source/drop target (dims to 40% opacity while being dragged) and a completed drag reports the dragged row's id and the row it was dropped on - the caller decides what "reorder" means for its own data. Omitted by every existing table, so this is purely additive; first consumer is `@autobusal/admin-menu` 1.1.2.
|
|
111
|
-
|
|
112
|
-
## 1.33.0
|
|
113
|
-
|
|
114
|
-
### Fixed
|
|
115
|
-
|
|
116
|
-
- **`Viewer`'s `type: 'component'` rows never actually rendered, anywhere.** `Data.tsx` returned `null` for any item whose `name` was `undefined` before the switch statement that handled `'component'` ever ran - and a component row never HAS a name, it isn't a registered form field. Every SectionHeading divider, every `AiFieldAssist` Generate/Improve/Translate trigger, and every description paragraph moved inside a form's own box (SMS/Flex, this cycle) had silently never rendered, in any tab that used one, despite compiling clean and sitting right there in the bundle. Moved the `'component'` case to an early return above the name guard instead of weakening the guard's condition, which would have broken TypeScript's narrowing of `item.name` to `string` for every other case below it.
|
|
117
|
-
- **AI trigger buttons and the review modal rendered raw i18n keys** (`ai_assist.trigger.generate`) instead of translated text. `ai_assist.*` lives in the `common` namespace, but the `t` passed into `AiFieldAssist`/`AiReviewModal` defaults to whatever namespace the calling page's own `useTranslation()` uses (`normal`/`whitelabel`) - every call now passes `{ ns: 'common' }`.
|
|
118
|
-
- **`AiReviewModal` showed a generic "Request failed with status code 422"** instead of the real provider error on a failed generate/translate/improve. Removed its local error state/display entirely - `apiClient`'s existing global response interceptor already toasts `error.response.data.message` on any 422, the same mechanism every other form in the app already relies on; a local `onError` reading react-query's plain `Error.message` could only ever see Axios's generic text, never the backend's.
|
|
119
|
-
|
|
120
|
-
## 1.32.0
|
|
121
|
-
|
|
122
|
-
### Added
|
|
123
|
-
|
|
124
|
-
- **Drag & drop on `File`**, the shared upload control used everywhere in the app - drop a file directly onto it instead of always going through the native picker. The control's own footprint (a single 38px-tall inline button) is unchanged everywhere it is already used; only a border/background highlight appears while dragging. A dropped file is handed to the same hidden `<input>` react-hook-form already registered, via a real dispatched `change` event, so every existing caller's validation/onChange keeps working with no changes on their end.
|
|
125
|
-
|
|
126
|
-
### Fixed
|
|
127
|
-
|
|
128
|
-
- **A checkbox row with a long notice no longer stretches sideways.** `.row-checkbox`'s label+input are laid out `flex-direction: row-reverse`, and the row's own notice/error text (a plain div) was sharing that same row - a short notice fit fine, but a longer one (two sentences, easily) stretched the row's WIDTH to fit on one line, visibly pushing the checkbox by however long the text happened to be. Every div child of a checkbox row now gets `flex-basis: 100%`, dropping straight onto its own full-width line; the label and input are unaffected.
|
|
129
|
-
|
|
130
|
-
## 1.31.0
|
|
131
|
-
|
|
132
|
-
### Added
|
|
133
|
-
|
|
134
|
-
- **A `row-component` field spans the full row width**, same as `row-textarea-html` already did - what lets a Viewer form's own section headings (`SectionHeading`, admin-label's Content/styles.ts) sit as a full-width divider instead of sharing a row with whatever field lands beside them.
|
|
135
|
-
|
|
136
|
-
## 1.30.0
|
|
137
|
-
|
|
138
|
-
### Changed
|
|
139
|
-
|
|
140
|
-
- **`Viewer`'s field grid stays at two columns on every screen width.** It
|
|
141
|
-
used to widen to three columns at >=1300px - fine for a form of
|
|
142
|
-
independent, unrelated fields, but a form built for a two-column rhythm
|
|
143
|
-
(one field, a divider row, a related pair, a divider row, ...) reflows
|
|
144
|
-
into three and starts mixing fields from different logical groups onto
|
|
145
|
-
the same row, with nothing to say which group a given row belongs to.
|
|
146
|
-
Affects every Viewer-based form in the app, not just admin-label's.
|
|
147
|
-
|
|
148
|
-
## 1.29.0
|
|
149
|
-
|
|
150
|
-
### Added
|
|
151
|
-
|
|
152
|
-
- **`AiFieldAssist` + the AI review modal.** The one AI-review surface every
|
|
153
|
-
content admin form shares - Generate/Improve/Translate triggers that open
|
|
154
|
-
a modal where a draft is always reviewed (and can be hand-edited or
|
|
155
|
-
revised on a follow-up instruction) before an explicit Insert writes it
|
|
156
|
-
into the calling field. Renders nothing at all when no AI provider is
|
|
157
|
-
enabled for the label - the visibility gate the AI plan calls for, with
|
|
158
|
-
no separate flag to keep in sync.
|
|
159
|
-
- **`Viewer`'s `'component'` field type accepts a function** of the form's
|
|
160
|
-
own `setValue`, not just plain JSX - the only way a component embedded in
|
|
161
|
-
the field list (AiFieldAssist writing a draft into a SIBLING field like
|
|
162
|
-
content_en) can reach state Viewer has always kept internal. Existing
|
|
163
|
-
callers passing plain JSX are unaffected.
|
|
164
|
-
|
|
165
|
-
## 1.28.0
|
|
166
|
-
|
|
167
|
-
### Added
|
|
168
|
-
|
|
169
|
-
- **`Row` takes an optional `actions`**, overriding the table's list for that
|
|
170
|
-
row. The table-level prop is one array for every row, which cannot express
|
|
171
|
-
an action only some rows have earned - a ticket download belongs to a paid
|
|
172
|
-
order and to no other. `Rows` falls back to the table's list, so every
|
|
173
|
-
existing caller is unaffected.
|
|
174
|
-
- **`print` and `invoice` row-action icons.** Distinct from the existing
|
|
175
|
-
generic `download`.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
## 1.27.5
|
|
179
|
-
|
|
180
|
-
### Added
|
|
181
|
-
|
|
182
|
-
- **`placeholder` on a `select`** — a leading empty option, for "nothing
|
|
183
|
-
chosen".
|
|
184
|
-
|
|
185
|
-
A select whose value matches no option silently displays the first one and
|
|
186
|
-
the browser submits it as though it had been picked. That is exactly how
|
|
187
|
-
every profile whose owner never touched the country field was saved as
|
|
188
|
-
**Afghanistan**: it sorts first, and nothing had ever been selected.
|
|
189
|
-
Changing the default from `0` to `''` did not help on its own — neither
|
|
190
|
-
value exists as an option, so the browser fell back either way.
|
|
191
|
-
|
|
192
|
-
Rendered only when placeholder text is supplied, so a select with a real
|
|
193
|
-
default (sex, status, a type) does not grow an empty row it never wanted.
|
|
194
|
-
|
|
195
|
-
## 1.27.4
|
|
196
|
-
|
|
197
|
-
### Fixed
|
|
198
|
-
|
|
199
|
-
- **The calendar allowed a different set of days depending on the visitor's
|
|
200
|
-
timezone.** Day cells are built with `createDate`, which is `Date.UTC(...)`;
|
|
201
|
-
the selectable bounds were built with `new Date(y, m, d)` — *local*
|
|
202
|
-
midnight. Those are not the same instant, so the boundary day fell on
|
|
203
|
-
either side of the limit depending on where the visitor was sitting.
|
|
204
|
-
|
|
205
|
-
The consequence on the money path: **same-day departures were not
|
|
206
|
-
selectable west of Greenwich.** A visitor in New York or Los Angeles could
|
|
207
|
-
not pick today, because local midnight there is *after* the cell's UTC
|
|
208
|
-
midnight, so today's cell tested as below the minimum. Same-day selling was
|
|
209
|
-
deliberately enabled — this had been quietly undoing it for those visitors,
|
|
210
|
-
and it is invisible from Europe. Both sides are UTC now, so the comparison
|
|
211
|
-
is exact and identical everywhere.
|
|
212
|
-
|
|
213
|
-
Found while chasing an apparent off-by-one in the birth-date picker, which
|
|
214
|
-
turned out to be this skew wearing a different hat.
|
|
215
|
-
|
|
216
|
-
## 1.27.3
|
|
217
|
-
|
|
218
|
-
### Fixed
|
|
219
|
-
|
|
220
|
-
- **The birth-date picker offered today, which the API refuses.** obtapi
|
|
221
|
-
validates with `before:today`, so the calendar was letting you choose an
|
|
222
|
-
answer and then calling it wrong. It now stops at yesterday.
|
|
223
|
-
|
|
224
|
-
The year steps back too when yesterday fell in the previous one: the
|
|
225
|
-
maximum is built from `today.getFullYear() + limits.max.year`, so on 1
|
|
226
|
-
January a month/day of 31 December would otherwise have resolved to the
|
|
227
|
-
coming 31 December rather than the one just gone. Checked against 1
|
|
228
|
-
January, 1 March and a leap year.
|
|
229
|
-
|
|
230
|
-
## 1.27.2
|
|
231
|
-
|
|
232
|
-
### Fixed
|
|
233
|
-
|
|
234
|
-
- **A birth date no longer defaults to today.** `Calendar` pre-fills with
|
|
235
|
-
today when given no value, which is right for every other use of it — a
|
|
236
|
-
departure, a report range, a broadcast day — and wrong for exactly one:
|
|
237
|
-
nobody is born today. On the profile-completion form it meant anybody who
|
|
238
|
-
filled in the fields that *looked* empty saved a date of birth of today,
|
|
239
|
-
silently, and that is the field a passenger's age is judged by when they
|
|
240
|
-
book. Reproduced end to end: the row saved `dob = <today>`.
|
|
241
|
-
|
|
242
|
-
`type="dob"` now starts empty so the field reads as unanswered. The picker
|
|
243
|
-
still opens on the current month — `createDate('')` is an Invalid Date and
|
|
244
|
-
the grid built from it renders as nothing — but that anchor is display
|
|
245
|
-
only; what the form submits stays empty until a day is clicked, and
|
|
246
|
-
obtapi's new `required|date_format:d/m/Y|before:today` says so if it is
|
|
247
|
-
left that way.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
## 1.27.1
|
|
251
|
-
|
|
252
|
-
### Removed
|
|
253
|
-
|
|
254
|
-
- **Everything 1.27.0 added for multi-TLD** — `localeHref`, the `LocaleDomain`
|
|
255
|
-
type, `Meta`'s domain-resolved canonical and hreflang, and
|
|
256
|
-
`ChangeLanguage`'s cross-domain navigation. `Meta` is back to resolving both
|
|
257
|
-
from `window.location.origin` plus the `/{locale}/` prefix.
|
|
258
|
-
|
|
259
|
-
The product decision changed after the code landed: ccTLDs will **301 to
|
|
260
|
-
`.com/{locale}/`** rather than be real per-country sites, so none of this
|
|
261
|
-
had a caller and none of it ever would. A ccTLD's geo signal multiplies
|
|
262
|
-
content that already exists, and a new ccTLD starts at zero authority while
|
|
263
|
-
a subdirectory inherits the domain's on day one.
|
|
264
|
-
|
|
265
|
-
**Do not downgrade to 1.26.1 to get this** — 1.27.0 is published, so
|
|
266
|
-
`@latest` would reintroduce it. This version is 1.26.1's behaviour with a
|
|
267
|
-
higher number.
|
|
268
|
-
|
|
269
|
-
## 1.26.1
|
|
270
|
-
|
|
271
|
-
### Fixed
|
|
272
|
-
|
|
273
|
-
- **`Table` drew an empty "Options" column when a table had only
|
|
274
|
-
top-of-table controls.** `create`, `search` and `extra` all live *above* the
|
|
275
|
-
table — `Actions/Get` already returns null for each — but `Header` and
|
|
276
|
-
`Rows` decided whether to draw the options column from `actions.length > 0`,
|
|
277
|
-
which counted them. A table using the component purely for sorting, search
|
|
278
|
-
and paging therefore got an "Options" heading with an empty cell under every
|
|
279
|
-
row.
|
|
280
|
-
|
|
281
|
-
Invisible on the admin screens, where every table has row actions anyway;
|
|
282
|
-
visible the moment the public bus-company directory used it. The row/top
|
|
283
|
-
split is one shared list now (`Table/rowActions.ts`) rather than a condition
|
|
284
|
-
repeated in three files that could drift.
|
|
285
|
-
|
|
286
|
-
## 1.26.0
|
|
287
|
-
|
|
288
|
-
### Added
|
|
289
|
-
|
|
290
|
-
- **`HandoffBanner`** — "You are managing *X* as its administrator", shown on a
|
|
291
|
-
brand's own domain for the length of a brand handoff, with a **Finish**
|
|
292
|
-
button that ends the session. Rendered by both apps' private layouts; renders
|
|
293
|
-
nothing at all unless `/bff/session` reports a handoff, so an ordinary
|
|
294
|
-
session costs one request. See `magus/BRAND_HANDOFF_PLAN.md`.
|
|
295
|
-
|
|
296
|
-
Not the same component as magus's `ImpersonationBanner` and not driven by the
|
|
297
|
-
same signal: that one reads `localStorage['impersonating']`, which page
|
|
298
|
-
JavaScript sets and can therefore be wrong. This reads the BFF session, which
|
|
299
|
-
is the only thing that actually knows. There is also nothing on this origin to
|
|
300
|
-
*revert* to — the admin's own session is alive and untouched on a different
|
|
301
|
-
origin — so the action is an explicit end, not a revert.
|
|
302
|
-
|
|
303
|
-
- **`handoff` action on `Table`** — its own icon and label rather than borrowing
|
|
304
|
-
`login_as`, for the same reason `reply` is not `approve`: `login_as` changes
|
|
305
|
-
who you are *within* a brand, a handoff leaves for another brand's domain in a
|
|
306
|
-
new tab.
|
|
307
|
-
|
|
308
|
-
### Notes
|
|
309
|
-
|
|
310
|
-
- **There is deliberately no `pagehide`/`sendBeacon` revoke.** One was built, to
|
|
311
|
-
shorten the window when somebody closes the tab, and testing removed it:
|
|
312
|
-
`pagehide` with `persisted === false` also fires on a plain **page refresh**,
|
|
313
|
-
so F5 revoked the token and dumped the admin on the logged-out screen.
|
|
314
|
-
Measured — an identical reload driven by curl, with no beacon in play, keeps
|
|
315
|
-
the session. No browser API distinguishes closing from reloading at that
|
|
316
|
-
moment, so what remains is two mechanisms that both work (the Finish button
|
|
317
|
-
and the two-hour token expiry) rather than three where one misfires.
|
|
318
|
-
|
|
319
|
-
- The banner puts **Finish next to the message** rather than at the far edge.
|
|
320
|
-
`space-between` reads better but the whitelabel admin's content column is a
|
|
321
|
-
fixed 1491px that overflows the viewport at ordinary window sizes, which put
|
|
322
|
-
the button off-screen at x=1856 — the one control that reliably ends the
|
|
323
|
-
session cannot be the one you have to scroll sideways to find.
|
|
324
|
-
|
|
325
|
-
## 1.25.1
|
|
326
|
-
|
|
327
|
-
**The SEO override wears the site's own form clothes.**
|
|
328
|
-
|
|
329
|
-
`SeoOverride` declared its own `Field`/`Label`/`Input`/`Textarea` and restated the global field styling with *different* values — transparent instead of `inputs.background`, a primary-coloured border, its own font size — then sat on the bare page rather than a card. On a country or city screen that made the two SEO fields the only ones on screen not matching the form directly beneath them, which is the opposite of what its own comment claimed.
|
|
330
|
-
|
|
331
|
-
It uses `.box` and `.row` now, the site's own card and field classes, with plain inputs so `GlobalStyles` reaches them exactly as it reaches every `Viewer` row. Being controlled is the only thing that genuinely differs from a Viewer row, and that is a state concern, not a visual one. The textarea keeps a single rule, for height: `GlobalStyles` gives every text input 38px, right for one line and useless for a description.
|
|
332
|
-
|
|
333
|
-
Reaches the label SEO tab too, which had the same problem. Checked it does not land inside another card — `admin-label`'s `Options` is the tab strip, not a wrapper around the tab bodies.
|
|
334
|
-
|
|
335
|
-
## 1.25.0
|
|
336
|
-
|
|
337
|
-
**New `setDepartureZone(zone)` — the date picker floors on today WHERE THE COACH LEAVES.**
|
|
338
|
-
|
|
339
|
-
`setServerToday` was already an improvement on the buyer's device clock, but it is still the wrong clock: it is *our* date, in Tirana, and a coach leaving Bari has nothing to do with it. Only the departure city can say whether the 4th is still today for that journey.
|
|
340
|
-
|
|
341
|
-
Falls back to `serverToday`, then to the device, so the bare search form (no journey chosen yet) behaves exactly as before. An unresolvable zone returns null and falls back too — a picker that refuses to open because a timezone lookup failed would be far worse than one flooring a day out.
|
|
342
|
-
|
|
343
|
-
## 1.24.0
|
|
344
|
-
|
|
345
|
-
**`Policy` is now `Information`.**
|
|
346
|
-
|
|
347
|
-
- `Policy` → `Information`, `useGetPolicy` → `useGetInformation`, and the endpoint behind it moves from `/api/routes/policy` to `/api/routes/information`. **Breaking**: update the import.
|
|
348
|
-
- `RouteItem`'s `ReadPolicy` → `ReadInformation`, reading `route_item.information`.
|
|
349
|
-
- Translation keys: `policy.title` → `information.title`, `route_item.policy` → `route_item.information`.
|
|
350
|
-
|
|
351
|
-
The rename follows the Flexible Ticket model, where what a ticket permits comes from the add-on and the operator's own windows — not from this box.
|
|
352
|
-
|
|
353
|
-
All notable changes to `@autobusal/common` are documented here. This project follows
|
|
354
|
-
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
355
|
-
|
|
356
|
-
> Note: 1.10.0 through 1.15.1 were published without changelog entries. The
|
|
357
|
-
> gap is left as-is rather than reconstructed after the fact.
|
|
358
|
-
|
|
359
|
-
## 1.23.0
|
|
360
|
-
|
|
361
|
-
An unbookable date is inert, not merely faded.
|
|
362
|
-
|
|
363
|
-
It was dimmed and otherwise fully interactive: the pointer cursor and the
|
|
364
|
-
hover highlight still invited a click, and the click was answered with a
|
|
365
|
-
browser alert saying the date was invalid. The calendar offered a day, let
|
|
366
|
-
somebody reach for it, then told them off for taking the offer.
|
|
367
|
-
|
|
368
|
-
`pointer-events: none` removes it from the conversation entirely — no cursor
|
|
369
|
-
change, no hover, no click to refuse — alongside the `aria-disabled` and
|
|
370
|
-
`tabIndex=-1` already on the element. Unreachable by mouse, keyboard and
|
|
371
|
-
screen reader alike, and the alert is gone with it.
|
|
372
|
-
|
|
373
|
-
## 1.22.0
|
|
374
|
-
|
|
375
|
-
The booking calendar floors on the SERVER's date, not the browser's.
|
|
376
|
-
|
|
377
|
-
`new Date()` is the buyer's device clock, which says nothing about where they
|
|
378
|
-
are travelling. Somebody in Shanghai at 06:00 on the 5th, looking at an
|
|
379
|
-
Italian coach that does not leave until the evening of the 4th, had that day
|
|
380
|
-
greyed out — while the server would have sold them the seat quite happily. The
|
|
381
|
-
calendar was refusing a sale the API accepted.
|
|
382
|
-
|
|
383
|
-
It also makes the picker agree with the date strip beside it, which has always
|
|
384
|
-
been built server-side; the two used to disagree about which day came first.
|
|
385
|
-
|
|
386
|
-
A date of birth keeps the local clock, because that one genuinely is about the
|
|
387
|
-
person in front of the screen.
|
|
388
|
-
|
|
389
|
-
## 1.21.0
|
|
390
|
-
|
|
391
|
-
`reply` joins the table actions with its own icon and label. The operator
|
|
392
|
-
review page was borrowing `approve`, so the button on a review about them read
|
|
393
|
-
"Approve" — which is the one thing they cannot do to it.
|
|
394
|
-
|
|
395
|
-
## 1.20.0
|
|
396
|
-
|
|
397
|
-
`Rating` — stars, average and count, or nothing at all. Rendering nothing is
|
|
398
|
-
the feature: obtapi withholds a score until there are enough published reviews
|
|
399
|
-
to say something honest, so an absent rating has to read as "not saying yet"
|
|
400
|
-
rather than as a zero. Anything filling the gap with empty stars would turn a
|
|
401
|
-
new operator into a badly-rated one. Half stars are drawn, because rounding 3.5
|
|
402
|
-
down loses the difference between middling and good — precisely the range where
|
|
403
|
-
a reader is still deciding.
|
|
404
|
-
|
|
405
|
-
## 1.19.0
|
|
406
|
-
|
|
407
|
-
`extra` joins `create` and `search` as a top-of-table slot rather than a
|
|
408
|
-
per-row action. Any table opting into the extra slot also rendered one empty,
|
|
409
|
-
iconless, still-clickable button in every row's options cell — Icon has no
|
|
410
|
-
case for `extra`, so it drew nothing while taking the click. The refund queue
|
|
411
|
-
has been doing this all along; the new review queue would have too.
|
|
412
|
-
|
|
413
|
-
## [1.18.0] - 2026-08-02
|
|
414
|
-
|
|
415
|
-
### Added
|
|
416
|
-
|
|
417
|
-
- **`SeoOverride`** - a per-language editor for the SEO title/description
|
|
418
|
-
overrides, shared by the label, country and city forms. One language is
|
|
419
|
-
shown at a time (a brand writes copy for one or two, and thirteen sets of
|
|
420
|
-
empty inputs invite nobody to fill any of them in) and the languages that
|
|
421
|
-
already have copy are marked, so it is obvious which are done. Controlled
|
|
422
|
-
and deliberately form-less: those pages submit every field in one request,
|
|
423
|
-
so an editor with its own save would either post a partial update or leave
|
|
424
|
-
two save buttons meaning different things on one screen.
|
|
425
|
-
|
|
426
|
-
## [1.17.0] - 2026-08-02
|
|
427
|
-
|
|
428
|
-
### Added
|
|
429
|
-
|
|
430
|
-
- **Locale-prefixed URLs.** `localiseRoutes()` mirrors an absolute-path route
|
|
431
|
-
tree under `/:locale`, and `LocaleGate` guards that branch - rejecting a
|
|
432
|
-
prefix that is not one of the brand's languages, redirecting the default
|
|
433
|
-
language back to its unprefixed home so only one URL is canonical, and
|
|
434
|
-
switching i18n to whatever the URL asks for. `splitLocale`/`withLocale`/
|
|
435
|
-
`localeFromPath` are the shared, router-free definition of a localised
|
|
436
|
-
path, so the crawler and the sitemap agree with the components.
|
|
437
|
-
- **hreflang alternates on every page.** Emitted from `Meta`, which already
|
|
438
|
-
sits on ~136 call sites, so every real page gains them at once and a new
|
|
439
|
-
page cannot forget them. `x-default` points at the unprefixed URL.
|
|
440
|
-
Suppressed for noindex pages and whenever a caller passes an explicit
|
|
441
|
-
canonical, which the path-derived alternates would contradict.
|
|
442
|
-
|
|
443
|
-
### Changed
|
|
444
|
-
|
|
445
|
-
- **`ChangeLanguage` navigates** instead of only swapping the rendered text -
|
|
446
|
-
behind an explicit `localised` prop, so the account and admin areas (which
|
|
447
|
-
are deliberately not locale-prefixed) keep switching in place rather than
|
|
448
|
-
navigating to a 404.
|
|
449
|
-
- **The canonical drops a trailing slash.** Apache serves a prerendered page
|
|
450
|
-
from a directory and 301s `/contact` to `/contact/`, so the same page used
|
|
451
|
-
to advertise whichever form the visitor happened to arrive on. Harmless
|
|
452
|
-
when it was the only path-derived tag; not harmless now that the alternates
|
|
453
|
-
come from the same value, because one page would announce two URL sets.
|
|
454
|
-
|
|
455
|
-
## [1.16.0] - 2026-08-01
|
|
456
|
-
|
|
457
|
-
### Added
|
|
458
|
-
|
|
459
|
-
- **The date field can tell its form when the day changes.** `ViewData.onChange`
|
|
460
|
-
was already honoured by `select` and `checkbox`, but not by `date`/`dob`/
|
|
461
|
-
`picker`, so a form whose other fields depend on the chosen day had no way
|
|
462
|
-
to observe it short of not using `Viewer` at all. `Calendar` now takes an
|
|
463
|
-
optional `onChange(DD/MM/YYYY)` and `Viewer` passes `item.onChange` through
|
|
464
|
-
to it. Purely additive - existing callers are unaffected.
|
|
465
|
-
|
|
466
|
-
## [1.15.6] - 2026-08-01
|
|
467
|
-
|
|
468
|
-
### Changed
|
|
469
|
-
|
|
470
|
-
- **An admin sees the real site while maintenance is on.** Deliberately
|
|
471
|
-
admin only - an operator or agent signing in still gets the holding page,
|
|
472
|
-
because the site is not open to them either. Presentation, not
|
|
473
|
-
protection: obtapi still decides what any account may do.
|
|
474
|
-
- `Calendar` takes an optional `minDate`, which can only RAISE the floor
|
|
475
|
-
the type already sets. Used by the return leg so it cannot start before
|
|
476
|
-
the outbound date.
|
|
477
|
-
|
|
478
|
-
### Fixed
|
|
479
|
-
|
|
480
|
-
- `Required` rendered a fragment, so in the column-flex form rows the label
|
|
481
|
-
and its asterisk became two flex items and the asterisk dropped onto its
|
|
482
|
-
own line.
|
|
483
|
-
|
|
484
|
-
## [1.15.5] - 2026-08-01
|
|
485
|
-
|
|
486
|
-
### Added
|
|
487
|
-
|
|
488
|
-
- `Required` - marks a form label as mandatory. The asterisk is
|
|
489
|
-
`aria-hidden`, since a screen reader announcing "star" tells nobody
|
|
490
|
-
anything; the input's own validation carries the semantics.
|
|
491
|
-
|
|
492
|
-
## [1.15.4] - 2026-08-01
|
|
493
|
-
|
|
494
|
-
### Fixed
|
|
495
|
-
|
|
496
|
-
- **The language dropdown opened upward over the navbar.** It positioned
|
|
497
|
-
itself with `bottom: calc((-1 * languages * 27px) - 50px - 8px)`, guessing
|
|
498
|
-
where its own bottom edge should land by multiplying an assumed 27px row
|
|
499
|
-
height by the language count. The rows are not 27px, so the error grew
|
|
500
|
-
with every language a brand enabled - at thirteen it was far enough out to
|
|
501
|
-
open over the page instead of below the trigger. Now `top: 100%`, which
|
|
502
|
-
needs no arithmetic and cannot drift, plus a height cap so a brand with
|
|
503
|
-
twenty languages does not run off the screen.
|
|
504
|
-
- **`RouteItem` showed the wrong arrival country.** The city read
|
|
505
|
-
`locations[lastLocation]` but the country read `locations[1]`. Identical
|
|
506
|
-
on a two-stop route, wrong on anything longer: Tirana-Veria-Thessaloniki
|
|
507
|
-
rendered "Thessaloniki (alb)", because `locations[1]` is Elbasan.
|
|
508
|
-
- Country codes render upper case here too, matching the search results.
|
|
509
|
-
|
|
510
|
-
## [1.15.3] - 2026-08-01
|
|
511
|
-
|
|
512
|
-
### Changed
|
|
513
|
-
|
|
514
|
-
- **Language switcher shows just the language name**, left-aligned, with
|
|
515
|
-
the panel title left-aligned too. The "Change to" prefix on every row
|
|
516
|
-
said nothing the panel's own title and the flag beside it were not
|
|
517
|
-
already saying. One component serves both the public header and the
|
|
518
|
-
panel sidebar, so both are fixed.
|
|
519
|
-
- **The route map fits the trip** instead of sitting at a fixed zoom 6
|
|
520
|
-
centred on the first stop. Every route drew the same view of the Balkans
|
|
521
|
-
and none of them filled it, so following a route meant panning.
|
|
522
|
-
|
|
523
|
-
### Fixed
|
|
524
|
-
|
|
525
|
-
- **`getCoordinates` sent every interior stop twice** - a four-stop route
|
|
526
|
-
went out as A,B,B,C,C,D,D,D, with the last point repeated against
|
|
527
|
-
itself. Directions APIs reject consecutive identical coordinates, so the
|
|
528
|
-
request failed and the map fell back to bare markers with no road line.
|
|
529
|
-
- **`useGetDriveData` cached on a bare `['drive-data']` key**, with the
|
|
530
|
-
coordinates absent - so every route on the site shared one itinerary and
|
|
531
|
-
the second route page you opened drew the first one's road.
|
|
532
|
-
- Stops with no coordinates are dropped rather than plotted at 0,0, which
|
|
533
|
-
put a pin off the west coast of Africa and dragged the fitted view
|
|
534
|
-
across the Atlantic.
|
|
535
|
-
- The query is skipped entirely when no `VITE_OPEN_ROUTE_SERVICE_KEY` is
|
|
536
|
-
configured, instead of a guaranteed 401 on every route page view.
|
|
537
|
-
- A stray `}` inside the `ButtonChange` styled-component template.
|
|
538
|
-
|
|
539
|
-
## [1.15.2] - 2026-08-01
|
|
540
|
-
|
|
541
|
-
### Added
|
|
542
|
-
|
|
543
|
-
- `RouteFeature` takes an optional `compact` prop: icon only, with the name
|
|
544
|
-
as a hover tooltip and a help cursor to advertise that the hover does
|
|
545
|
-
something. Opt-in, because the browse and route-view pages have the room
|
|
546
|
-
and their labels earn it - only the search results card asks for this.
|
|
547
|
-
|
|
548
|
-
### Fixed
|
|
549
|
-
|
|
550
|
-
- `RouteFeature`'s image had **no `alt` at all**. That mattered already; it
|
|
551
|
-
matters more in compact mode, where the image is the only content and a
|
|
552
|
-
screen reader would otherwise announce nothing whatsoever per amenity.
|
|
553
|
-
|
|
554
|
-
## [1.9.0] - 2026-07-31
|
|
555
|
-
|
|
556
|
-
### Added
|
|
557
|
-
|
|
558
|
-
- **`Viewer`/`Actions`/`Button` support a second submit-type action** via an
|
|
559
|
-
optional `onTest` prop on `Viewer` and a new `'test'` entry in its
|
|
560
|
-
`actions` array. A form with two submit buttons can't otherwise tell
|
|
561
|
-
which one triggered submission - `Button` now forwards a native
|
|
562
|
-
`name`/`value` pair, and `Viewer`'s `onSubmit` reads the submit event's
|
|
563
|
-
`submitter` to route to `onTest` instead of `onSave` when the 'test'
|
|
564
|
-
button was clicked, while keeping the exact same current form values.
|
|
565
|
-
First consumer: `@autobusal/admin-label` 1.10.0's "Test" button on the
|
|
566
|
-
SMTP settings tab.
|
|
567
|
-
|
|
568
|
-
## [1.8.3] - 2026-07-31
|
|
569
|
-
|
|
570
|
-
### Fixed
|
|
571
|
-
|
|
572
|
-
- **`Meta.tsx`'s `useSoleMetaOwnership` no longer runs on every render.**
|
|
573
|
-
It fired on every render of every `<Meta>` usage (136 call sites, no
|
|
574
|
-
dependency array) and directly removed `<head>` children via raw DOM
|
|
575
|
-
`.remove()` calls each time. The duplicate-tag problem it exists for -
|
|
576
|
-
React's `createRoot` not reconciling against tags baked into the
|
|
577
|
-
prerendered static HTML - can only exist before React's first render;
|
|
578
|
-
every render after that, `<title>`/`<meta>`/`<link>` in `<head>` are
|
|
579
|
-
exclusively React 19's own hoisting mechanism's doing, and it already
|
|
580
|
-
correctly swaps them out as components mount/unmount with no help
|
|
581
|
-
needed. Re-running this raw removal on every navigation instead raced
|
|
582
|
-
that hoisting: it could delete a node mid-transition that React's own
|
|
583
|
-
reconciler still held a reference to, and the next time React tried to
|
|
584
|
-
remove or replace that now-detached node, `parentNode` was null and it
|
|
585
|
-
threw an uncaught `TypeError: Cannot read properties of null (reading
|
|
586
|
-
'removeChild')` - not render-phase, so no `ErrorBoundary` catches it,
|
|
587
|
-
silently aborting whatever navigation was in flight (URL updates,
|
|
588
|
-
page never re-renders). This is the actual root cause of the frozen-
|
|
589
|
-
navigation bug worked around piecemeal in `@autobusal/providers`
|
|
590
|
-
1.6.6-1.6.8, `@autobusal/hooks` 1.3.1, and `@autobusal/auth`
|
|
591
|
-
1.5.2-1.5.4 - all of which targeted `GoogleReCaptchaProvider` as a
|
|
592
|
-
contributing trigger, but the crash reproduced identically with that
|
|
593
|
-
provider removed entirely, on ~any route rendering `<Meta>`. Now
|
|
594
|
-
scoped to a module-level flag so the cleanup runs exactly once, on the
|
|
595
|
-
app's first `<Meta>` mount - it does its one real job (wiping stale
|
|
596
|
-
prerendered tags before first paint) and never touches the DOM again.
|
|
597
|
-
|
|
598
|
-
## [1.8.2] - 2026-07-30
|
|
599
|
-
|
|
600
|
-
### Fixed
|
|
601
|
-
|
|
602
|
-
- **`Meta.tsx` duplicate description/canonical/OG/Twitter tags.** The
|
|
603
|
-
existing title-only dedup (`useSoleTitleOwnership`, since 1.5.1) is now
|
|
604
|
-
`useSoleMetaOwnership` and covers every tag Meta renders, not just
|
|
605
|
-
`<title>`. Surfaced by the new build-time prerender step: React doesn't
|
|
606
|
-
reconcile against tags already sitting in `<head>` from a source other
|
|
607
|
-
than its own current render (a prerendered snapshot's baked-in
|
|
608
|
-
description, a leftover from a pre-locale-detection render, ...), so
|
|
609
|
-
multiple `<meta name="description">`/`og:description`/etc could coexist
|
|
610
|
-
- crawlers commonly read the FIRST occurrence, which could be the stale
|
|
611
|
-
one, not the page's real content.
|
|
612
|
-
|
|
613
|
-
## [1.8.1] - 2026-07-30
|
|
614
|
-
|
|
615
|
-
### Fixed
|
|
616
|
-
|
|
617
|
-
- **Added `loading="lazy" decoding="async"` to below-the-fold list images**
|
|
618
|
-
(`BlogItem/Image.tsx` preview/short thumbnails, `CompanyItem.tsx`,
|
|
619
|
-
`RouteItem.tsx` operator logos) - none had lazy loading, so a search
|
|
620
|
-
results page or operator/agent listing loaded every logo eagerly
|
|
621
|
-
regardless of position. `BlogItem/Image.tsx`'s `type="view"` (the single
|
|
622
|
-
article's own hero image, likely that page's LCP element) instead gets
|
|
623
|
-
`loading="eager" fetchPriority="high"`.
|
|
624
|
-
|
|
625
|
-
## [1.8.0] - 2026-07-30
|
|
626
|
-
|
|
627
|
-
### Added
|
|
628
|
-
|
|
629
|
-
- **GA4 SPA pageview tracking in `Meta`.** gtag.js only auto-sends a
|
|
630
|
-
page_view on its initial `config` call - every client-side route change
|
|
631
|
-
was invisible to analytics (pairs with `@autobusal/providers` 1.6.2, which
|
|
632
|
-
disables the automatic one). Meta now sends an explicit pageview
|
|
633
|
-
(`location.pathname + location.search`, current title) whenever the path
|
|
634
|
-
changes, since it's already rendered on nearly every real page. Excluded
|
|
635
|
-
from the effect's dependencies on purpose: a title-only change (e.g. a
|
|
636
|
-
language switch) does not fire a new pageview, only an actual navigation
|
|
637
|
-
does.
|
|
638
|
-
|
|
639
|
-
## [1.7.0] - 2026-07-30
|
|
640
|
-
|
|
641
|
-
### Added
|
|
642
|
-
|
|
643
|
-
- **New `JsonLd` component** - renders a schema.org `<script type="application/ld+json">` block from a plain object, with `<` escaped to `\u003c` so a string value containing "</script>" can't break out of the tag. No hoisting needed (unlike Meta's title/meta/link) - Google explicitly supports structured data anywhere in the document, so this renders in place. First consumers: `@autobusal/providers`' `Preload.tsx` (sitewide Organization + WebSite) and `route-view`/magus's `BlogView.tsx` (BusTrip/BreadcrumbList, Article).
|
|
644
|
-
|
|
645
|
-
## [1.6.0] - 2026-07-30
|
|
646
|
-
|
|
647
|
-
### Added
|
|
648
|
-
|
|
649
|
-
- **`Meta` now emits a canonical `<link>` and `og:url` on every page by
|
|
650
|
-
default**, even when the caller passes no `url` prop - previously `url`
|
|
651
|
-
was never passed at any of this component's ~136 call sites across both
|
|
652
|
-
apps, so no page anywhere had a canonical tag. Defaults to the current
|
|
653
|
-
path (`location.pathname`, via `react-router-dom`'s `useLocation`) with
|
|
654
|
-
the origin prepended - query strings are naturally excluded, so a search
|
|
655
|
-
results page with `?sfrom=X&sto=Y` or a paginated `?page=N` correctly
|
|
656
|
-
canonicalizes to its bare path instead of creating unbounded near-duplicate
|
|
657
|
-
URLs. `url` can still be passed explicitly to override.
|
|
658
|
-
|
|
659
|
-
## [1.5.1] - 2026-07-30
|
|
660
|
-
|
|
661
|
-
### Fixed
|
|
662
|
-
|
|
663
|
-
- **`Meta.tsx` and `@autobusal/providers`'s `Preload.tsx` both render a
|
|
664
|
-
`<title>`, and React 19's native hoisting (added in 1.5.0, see above) does
|
|
665
|
-
NOT dedupe `<title>` between separate component instances.** Both ended up
|
|
666
|
-
as real, simultaneous DOM nodes - technically invalid HTML (a document must
|
|
667
|
-
not have more than one title element), even though `document.title` itself
|
|
668
|
-
still resolved correctly by spec ("first title in tree order" happened to
|
|
669
|
-
always be this one). `Meta` now takes exclusive ownership on mount: it
|
|
670
|
-
removes any other `<title>` element in `<head>` the instant its own real
|
|
671
|
-
page title exists, so exactly one survives.
|
|
672
|
-
|
|
673
|
-
## [1.5.0] - 2026-07-30
|
|
674
|
-
|
|
675
|
-
### Fixed
|
|
676
|
-
|
|
677
|
-
- **`Meta.tsx` migrated off `react-helmet` to React 19's native document
|
|
678
|
-
metadata support.** react-helmet 6.1.0 (unmaintained since ~2021) was
|
|
679
|
-
discovered to be completely non-functional under React 19: zero of its DOM
|
|
680
|
-
writes were ever committing on any route, on any fresh page load, in either
|
|
681
|
-
consuming app - confirmed via the absence of `[data-react-helmet]` in the
|
|
682
|
-
DOM and by bisecting back through the app's dependency history to before
|
|
683
|
-
any of today's changes. Every page's `<title>`, `description`, `robots`,
|
|
684
|
-
`og:*`/`twitter:*` tags were silently frozen at whatever the static
|
|
685
|
-
`index.html` shipped, for every real user and JS-executing crawler landing
|
|
686
|
-
directly on a URL. `<title>`/`<meta>`/`<link>` are now rendered as plain
|
|
687
|
-
elements - React 19 auto-hoists and deduplicates them into `<head>` with no
|
|
688
|
-
library needed, and the same "deepest/last-rendered wins" semantics Helmet
|
|
689
|
-
was supposed to provide (a page's own `<Meta>` overriding
|
|
690
|
-
`@autobusal/providers`'s app-shell fallback title) now actually work.
|
|
691
|
-
|
|
692
|
-
## [1.4.7] - 2026-07-30
|
|
693
|
-
|
|
694
|
-
### Fixed
|
|
695
|
-
|
|
696
|
-
- **Blog thumbnails missing `alt` text (`BlogItem/Image.tsx`).** Neither the
|
|
697
|
-
preview/short grid thumbnail nor the full article image had `alt` - added a
|
|
698
|
-
required `title` prop (threaded through from `BlogItem.tsx`'s
|
|
699
|
-
`article.title`) used as the `alt` text on both.
|
|
700
|
-
|
|
701
|
-
## [1.4.6] - 2026-07-29
|
|
702
|
-
|
|
703
|
-
### Fixed
|
|
704
|
-
|
|
705
|
-
- `Autocomplete/Autocomplete.tsx`: the "From"/"To" route-search inputs kept showing a station's display name in the language that was active when it was selected, since the displayed text (`q`) was plain component state that never resynced when `data` was rebuilt with fresh, relocalized names after a language switch (e.g. "(Alle Stationen)" stayed baked in after switching German -> English). Now re-derives the display name from the selected `value` whenever `data` changes.
|
|
706
|
-
|
|
707
|
-
## [1.4.5] - 2026-07-28
|
|
708
|
-
|
|
709
|
-
### Fixed
|
|
710
|
-
|
|
711
|
-
- `File/File.tsx`: a `required` file-input field blocked saving ANY record that already had a file (image, document, etc.) unless the user re-uploaded it, since a file input can never carry over its previous value - the `required` rule now skips when a `value` (existing file URL) is already present. Found via a live sanity pass: this froze several existing records (e.g. a bus with a real image) out of being editable at all.
|
|
712
|
-
|
|
713
|
-
## [1.4.4] - 2026-07-24
|
|
714
|
-
|
|
715
|
-
### Fixed
|
|
716
|
-
|
|
717
|
-
- `BlogItem.tsx` and `Policy.tsx`: article/policy content is now run through `@autobusal/utilities`'s `sanitizeHtml` before being rendered via `dangerouslySetInnerHTML`, closing an XSS gap where an already-compromised admin account could persist JS in blog articles or policy pages.
|
|
718
|
-
|
|
719
|
-
## [1.4.3] - 2026-07-19
|
|
720
|
-
|
|
721
|
-
### Changed
|
|
722
|
-
|
|
723
|
-
- **`Meta` (`Meta.tsx`) is now brand-configurable, eliminating the per-brand SEO fork.**
|
|
724
|
-
Merged magus's rich SEO `Meta` (robots, canonical, Open Graph and Twitter card tags, plus
|
|
725
|
-
optional `image`/`url`/`type`/`noIndex` props) into the shared component, and replaced the
|
|
726
|
-
hard-coded "BusMagus" strings with a `VITE_APP_NAME` build-time env: the `<title>` suffix,
|
|
727
|
-
`og:title`/`twitter:title` and `og:site_name` derive from that brand name, and when it is
|
|
728
|
-
unset the title renders bare (no suffix) with `og:site_name` omitted — so a brand that has
|
|
729
|
-
not configured a name is unaffected. This lets every whitelabel consume the published
|
|
730
|
-
`Meta` and pass its own brand via env, instead of magus vite-aliasing `@autobusal/common`
|
|
731
|
-
to a local `src/modules/Common` copy just to override this one file. `children` is now
|
|
732
|
-
optional; the prop set is a superset of the previous one, so all existing `<Meta title=…>`
|
|
733
|
-
call sites are unaffected.
|
|
734
|
-
|
|
735
|
-
## [1.4.2] - 2026-07-19
|
|
736
|
-
|
|
737
|
-
### Added
|
|
738
|
-
|
|
739
|
-
- **Cross-field validation + masked pre-fill for `Password` (`Password/Password.tsx`).** Added
|
|
740
|
-
an optional `getValues` prop (react-hook-form's `UseFormGetValues`) that is threaded into
|
|
741
|
-
`Validate(...)`, enabling cross-field rules like `matches:password` (e.g. a
|
|
742
|
-
password-confirmation field). Added an optional `defaultValue` prop applied to the input so
|
|
743
|
-
an admin edit form can render a saved secret as masked dots instead of blank, so editing a
|
|
744
|
-
different field no longer wipes the stored password on save.
|
|
745
|
-
- **Keyboard accessibility for the calendar day picker (`Calendar/Days.tsx`,
|
|
746
|
-
`Calendar/styles.ts`).** Day cells now expose `role="button"`, `tabIndex` (`-1` when
|
|
747
|
-
disabled), `aria-disabled`, `aria-pressed`, and an `onKeyDown` handler that activates on
|
|
748
|
-
Enter/Space, plus a `:focus-visible` outline. Days are now selectable via keyboard, not
|
|
749
|
-
only mouse.
|
|
750
|
-
- **Debounced type-to-filter search (`Table/Top/Search.tsx`).** The table search input now
|
|
751
|
-
submits automatically ~500ms after the user stops typing (via a `useEffect`/`setTimeout`
|
|
752
|
-
with an initial-render guard using `useRef`), instead of requiring an Enter press that
|
|
753
|
-
nothing on screen advertised. Explicit Enter submit is preserved.
|
|
754
|
-
|
|
755
|
-
### Changed
|
|
756
|
-
|
|
757
|
-
- **`Password` pre-fill wired through the dynamic viewer (`Viewer/Data.tsx`).** The `password`
|
|
758
|
-
field case now passes `defaultValue={ value.string }` to `Password`, so saved password
|
|
759
|
-
values hydrate as masked input in generated forms.
|
|
760
|
-
- **Confirm-password persistence of language selection (`ChangeLanguage/ChangeLanguage.tsx`).**
|
|
761
|
-
Language changes are now persisted to `localStorage['language']` so the chosen language
|
|
762
|
-
survives reloads rather than resetting to the default.
|
|
763
|
-
- **Explicit `common` namespace on i18n lookups (`Gender.tsx`, `Seats/ChooseSeat.tsx`).**
|
|
764
|
-
Translation calls now pass `{ ns: 'common' }` (gender male/female labels; seat
|
|
765
|
-
chosen/choose labels) so the correct namespace resolves and the raw key is no longer shown
|
|
766
|
-
when the component is used outside the default namespace context.
|
|
767
|
-
- **Styled Calendar browse dropdown (`Calendar/Browse/styles.ts`).** The select now has a
|
|
768
|
-
visible border, border-radius, padding, pointer cursor, and a custom chevron background,
|
|
769
|
-
replacing `border: none` so it reads as an interactive control.
|
|
770
|
-
|
|
771
|
-
### Fixed
|
|
772
|
-
|
|
773
|
-
- **Missing image alt text (`Avatar.tsx`, `CompanyItem/CompanyItem.tsx`, `RouteItem/RouteItem.tsx`).**
|
|
774
|
-
Operator/company logo `<img>` tags now include `alt={ ...company }`, giving screen readers a
|
|
775
|
-
meaningful label and a fallback when the logo fails to load.
|
|
776
|
-
|
|
777
|
-
Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).
|