@autobusal/common 1.4.0 → 1.4.2

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/Avatar.tsx CHANGED
@@ -8,7 +8,7 @@ interface Props {
8
8
 
9
9
  const Avatar = ({ display }: Props): JSX.Element => (
10
10
  <Container>
11
- { display?.logo_url !== '' && <img src={ display?.logo_url } />}
11
+ { display?.logo_url !== '' && <img src={ display?.logo_url } alt={ display?.company } />}
12
12
  { display?.company }
13
13
  </Container>
14
14
  );
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@autobusal/common` are documented here. This project follows
4
+ [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [1.4.2] - 2026-07-19
7
+
8
+ ### Added
9
+
10
+ - **Cross-field validation + masked pre-fill for `Password` (`Password/Password.tsx`).** Added
11
+ an optional `getValues` prop (react-hook-form's `UseFormGetValues`) that is threaded into
12
+ `Validate(...)`, enabling cross-field rules like `matches:password` (e.g. a
13
+ password-confirmation field). Added an optional `defaultValue` prop applied to the input so
14
+ an admin edit form can render a saved secret as masked dots instead of blank, so editing a
15
+ different field no longer wipes the stored password on save.
16
+ - **Keyboard accessibility for the calendar day picker (`Calendar/Days.tsx`,
17
+ `Calendar/styles.ts`).** Day cells now expose `role="button"`, `tabIndex` (`-1` when
18
+ disabled), `aria-disabled`, `aria-pressed`, and an `onKeyDown` handler that activates on
19
+ Enter/Space, plus a `:focus-visible` outline. Days are now selectable via keyboard, not
20
+ only mouse.
21
+ - **Debounced type-to-filter search (`Table/Top/Search.tsx`).** The table search input now
22
+ submits automatically ~500ms after the user stops typing (via a `useEffect`/`setTimeout`
23
+ with an initial-render guard using `useRef`), instead of requiring an Enter press that
24
+ nothing on screen advertised. Explicit Enter submit is preserved.
25
+
26
+ ### Changed
27
+
28
+ - **`Password` pre-fill wired through the dynamic viewer (`Viewer/Data.tsx`).** The `password`
29
+ field case now passes `defaultValue={ value.string }` to `Password`, so saved password
30
+ values hydrate as masked input in generated forms.
31
+ - **Confirm-password persistence of language selection (`ChangeLanguage/ChangeLanguage.tsx`).**
32
+ Language changes are now persisted to `localStorage['language']` so the chosen language
33
+ survives reloads rather than resetting to the default.
34
+ - **Explicit `common` namespace on i18n lookups (`Gender.tsx`, `Seats/ChooseSeat.tsx`).**
35
+ Translation calls now pass `{ ns: 'common' }` (gender male/female labels; seat
36
+ chosen/choose labels) so the correct namespace resolves and the raw key is no longer shown
37
+ when the component is used outside the default namespace context.
38
+ - **Styled Calendar browse dropdown (`Calendar/Browse/styles.ts`).** The select now has a
39
+ visible border, border-radius, padding, pointer cursor, and a custom chevron background,
40
+ replacing `border: none` so it reads as an interactive control.
41
+
42
+ ### Fixed
43
+
44
+ - **Missing image alt text (`Avatar.tsx`, `CompanyItem/CompanyItem.tsx`, `RouteItem/RouteItem.tsx`).**
45
+ Operator/company logo `<img>` tags now include `alt={ ...company }`, giving screen readers a
46
+ meaningful label and a fallback when the logo fails to load.
47
+
48
+ Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel patch sets into canonical @autobusal source (eliminates per-repo patch-package divergence).
@@ -30,5 +30,11 @@ export const Select = styled.select<{$type?: 'year'}>`
30
30
  `}
31
31
  text-align: center;
32
32
  background-color: ${ props => props.theme.background.neutral };
33
- border: none;
33
+ border: 1px solid ${ props => props.theme.inputs.border };
34
+ border-radius: ${ props => props.theme.borderRadius };
35
+ padding: 0 22px 0 8px;
36
+ cursor: pointer;
37
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888' stroke-width='1.5'/%3E%3C/svg%3E");
38
+ background-repeat: no-repeat;
39
+ background-position: right 6px center;
34
40
  `;
package/Calendar/Days.tsx CHANGED
@@ -81,6 +81,16 @@ const Days = ({ type, value, selected, onSelected }: Props): JSX.Element => {
81
81
  $disabled={ disabled }
82
82
  $selected={ selected === currentValue }
83
83
  onClick={ () => onSelected(currentValue, disabled) }
84
+ role="button"
85
+ tabIndex={ disabled ? -1 : 0 }
86
+ aria-disabled={ disabled }
87
+ aria-pressed={ selected === currentValue }
88
+ onKeyDown={ event => {
89
+ if (event.key === 'Enter' || event.key === ' ') {
90
+ event.preventDefault();
91
+ onSelected(currentValue, disabled);
92
+ }
93
+ } }
84
94
  >
85
95
  <span>{ currentDay }</span>
86
96
  </Day>
@@ -66,4 +66,9 @@ export const Day = styled.div<{
66
66
  &:hover > span {
67
67
  background-color: ${ props => props.theme.background.neutral };
68
68
  }
69
+
70
+ &:focus-visible {
71
+ outline: 2px solid ${ props => props.theme.primary.normal };
72
+ outline-offset: 2px;
73
+ }
69
74
  `;
@@ -21,6 +21,7 @@ const ChangeLanguage = ({ type, t, i18n, onClose }: Props): JSX.Element => {
21
21
 
22
22
  const onChange = (language: string): void => {
23
23
  i18n.changeLanguage(language);
24
+ localStorage.setItem('language', language);
24
25
  onClose();
25
26
  };
26
27
 
@@ -16,12 +16,12 @@ const CompanyItem = ({ item, type }: Props): JSX.Element => {
16
16
  { type === 'operator'
17
17
  ? (
18
18
  <LinkLogo to={ item.link }>
19
- <img src={ item.logo_url } />
19
+ <img src={ item.logo_url } alt={ item.company } />
20
20
  </LinkLogo>
21
21
  )
22
22
  : (
23
23
  <ContainerLogo>
24
- <img src={ item.logo_url } />
24
+ <img src={ item.logo_url } alt={ item.company } />
25
25
  </ContainerLogo>
26
26
  ) }
27
27
 
@@ -8,7 +8,7 @@ interface Props {
8
8
  }
9
9
 
10
10
  const CookieNotification = ({ t }: Props): (JSX.Element | null) => {
11
- const status = document.cookie.indexOf('CookieNotification=viewed') == -1;
11
+ const status = document.cookie.indexOf('OBTCookieNotification=viewed') == -1;
12
12
 
13
13
  const [ show, setShow ] = useState<boolean>(status);
14
14
 
@@ -18,7 +18,7 @@ const CookieNotification = ({ t }: Props): (JSX.Element | null) => {
18
18
  const CookieDate = new Date();
19
19
  CookieDate.setFullYear(CookieDate.getFullYear() + 1);
20
20
 
21
- document.cookie = 'OBTCookieNotification=viewed; expires=' + CookieDate.toUTCString() + '; path=/; SameSite=None; Secure';
21
+ document.cookie = 'OBTCookieNotification=viewed; expires=' + CookieDate.toUTCString() + '; path=/';
22
22
  };
23
23
 
24
24
  if (! show) {
package/Gender.tsx CHANGED
@@ -19,7 +19,7 @@ const Gender = ({ name, defaultValue, t, refs }: Props) => (
19
19
  { ...refs(name) }
20
20
  />
21
21
 
22
- { t('data.gender.male') }
22
+ { t('data.gender.male', { ns: 'common' }) }
23
23
  </label>
24
24
 
25
25
  <label>
@@ -30,7 +30,7 @@ const Gender = ({ name, defaultValue, t, refs }: Props) => (
30
30
  { ...refs(name) }
31
31
  />
32
32
 
33
- { t('data.gender.female') }
33
+ { t('data.gender.female', { ns: 'common' }) }
34
34
  </label>
35
35
  </div>
36
36
  </div>
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
- import { UseFormRegister } from 'react-hook-form';
3
+ import { UseFormRegister, UseFormGetValues } from 'react-hook-form';
4
4
  import { AiFillEye, AiFillEyeInvisible } from 'react-icons/ai';
5
5
  import { Validate } from '@autobusal/utilities';
6
6
  import { Container } from './styles';
@@ -9,20 +9,26 @@ interface Props {
9
9
  name: string
10
10
  tabIndex?: number
11
11
  validation?: string
12
+ // Pre-fill value (masked). Lets an admin form show a saved secret as dots
13
+ // instead of blank, so editing another field doesn't wipe it on save.
14
+ defaultValue?: string
12
15
  t: TFunction<'common'>
13
16
  refs: UseFormRegister<any>
17
+ // Pass react-hook-form's getValues to enable cross-field rules like
18
+ // `matches:password` (e.g. a password-confirmation field).
19
+ getValues?: UseFormGetValues<any>
14
20
  }
15
21
 
16
- const Password = ({ name, tabIndex, validation, t, refs }: Props): JSX.Element => {
22
+ const Password = ({ name, tabIndex, validation, defaultValue, t, refs, getValues }: Props): JSX.Element => {
17
23
  const [ show, setShow ] = useState<boolean>(false);
18
24
 
19
25
  const toggle = (): void => {
20
26
  setShow(!show)
21
27
  };
22
-
28
+
23
29
  return (
24
30
  <Container>
25
- <input type={ show ? 'text' : 'password' } tabIndex={ tabIndex } { ...refs(name, Validate(validation ?? '', t)) } />
31
+ <input type={ show ? 'text' : 'password' } tabIndex={ tabIndex } defaultValue={ defaultValue } { ...refs(name, Validate(validation ?? '', t, getValues)) } />
26
32
 
27
33
  { show
28
34
  ? <AiFillEye onClick={ toggle } />
@@ -32,4 +38,4 @@ const Password = ({ name, tabIndex, validation, t, refs }: Props): JSX.Element =
32
38
  );
33
39
  };
34
40
 
35
- export default Password;
41
+ export default Password;
@@ -21,7 +21,7 @@ const RouteItem = ({ type, route, t }: Props): JSX.Element => {
21
21
  <About>
22
22
  <Image>
23
23
  <Logo to={ route.operator.link }>
24
- <img src={ route.operator.logo_url } />
24
+ <img src={ route.operator.logo_url } alt={ route.operator.company } />
25
25
  </Logo>
26
26
  </Image>
27
27
 
@@ -45,8 +45,8 @@ const ChooseSeat = ({ name, type, defaultValue, bus, picked, validation, t, refs
45
45
  <div>
46
46
  <ButtonSeat type="button" $selected={ selected > 0 } onClick={ () => setShow(true) }>
47
47
  { selected > 0
48
- ? <><FaCheck /> { t('seats.chosen', { number: selected }) }</>
49
- : <><MdEventSeat /> { t('seats.choose') }</> }
48
+ ? <><FaCheck /> { t('seats.chosen', { ns: 'common', number: selected }) }</>
49
+ : <><MdEventSeat /> { t('seats.choose', { ns: 'common' }) }</> }
50
50
  </ButtonSeat>
51
51
 
52
52
  { (show && bus.bus !== undefined) && (
@@ -1,4 +1,4 @@
1
- import { FormEvent, useState } from 'react';
1
+ import { FormEvent, useEffect, useRef, useState } from 'react';
2
2
  import { NavigateFunction } from 'react-router-dom';
3
3
  import { TFunction } from 'i18next';
4
4
  import { parseUrl } from '../browseUrl';
@@ -13,12 +13,33 @@ interface Props {
13
13
  const Search = ({ value, url, t, navigate }: Props): JSX.Element => {
14
14
  const [ q, setQ ] = useState<string>(String(value ?? ''));
15
15
 
16
- const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
17
- event.preventDefault();
16
+ const initial = useRef<boolean>(true);
18
17
 
18
+ const submit = (): void => {
19
19
  const search = parseUrl({ page: 1, q });
20
20
 
21
21
  navigate(`${ url }${ search }`);
22
+ };
23
+
24
+ // typing filters after a short pause - submitting was previously only
25
+ // possible via Enter, which nothing on screen suggested
26
+ useEffect(() => {
27
+ if (initial.current) {
28
+ initial.current = false;
29
+
30
+ return;
31
+ }
32
+
33
+ const timer = setTimeout(submit, 500);
34
+
35
+ return () => clearTimeout(timer);
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [q]);
38
+
39
+ const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
40
+ event.preventDefault();
41
+
42
+ submit();
22
43
  }
23
44
 
24
45
  return (
@@ -33,4 +54,4 @@ const Search = ({ value, url, t, navigate }: Props): JSX.Element => {
33
54
  );
34
55
  };
35
56
 
36
- export default Search;
57
+ export default Search;
package/Viewer/Data.tsx CHANGED
@@ -97,7 +97,7 @@ const Data = ({ id, item, refs, t, onUpdate }: Props): (JSX.Element | null) => {
97
97
  return <input type="number" defaultValue={ value.number } { ...refs(item.name, Validate(String(item.rules), t)) } />
98
98
 
99
99
  case 'password':
100
- return <Password name={ item.name } validation={ item.rules } t={ t } refs={ refs } />;
100
+ return <Password name={ item.name } validation={ item.rules } defaultValue={ value.string } t={ t } refs={ refs } />;
101
101
 
102
102
  case 'select':
103
103
  return <Dropdown item={ item } refs={ refs } t={ t } />;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
+ "author": "Ferjolt Ozuni",
4
5
  "type": "module",
5
6
  "main": "index.ts"
6
7
  }