@autobusal/common 1.33.3 → 1.33.5

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.
@@ -9,6 +9,9 @@ import { AutocompleteData } from './types';
9
9
 
10
10
  interface Props {
11
11
  name: 'from' | 'to'
12
+ /* Edited: Claude - 2026-08-15: accessible name for the visible input -
13
+ see the note in RoutesSearch. Optional so existing callers are unchanged. */
14
+ label?: string
12
15
  defaultValue: string
13
16
  data: AutocompleteData[]
14
17
  validation: string
@@ -17,7 +20,7 @@ interface Props {
17
20
  onUpdate: UseFormSetValue<any>
18
21
  }
19
22
 
20
- const Autocomplete = ({ name, defaultValue, data, validation, t, refs, onUpdate }: Props): JSX.Element => {
23
+ const Autocomplete = ({ name, label, defaultValue, data, validation, t, refs, onUpdate }: Props): JSX.Element => {
21
24
  const found = data.filter(item => {
22
25
  return item.value === defaultValue;
23
26
  });
@@ -104,6 +107,7 @@ const Autocomplete = ({ name, defaultValue, data, validation, t, refs, onUpdate
104
107
  <input
105
108
  type="text"
106
109
  name={ `${ name }_slug` }
110
+ aria-label={ label }
107
111
  autoComplete="off"
108
112
  value={ q }
109
113
  onChange={ (event) => onChange(event) }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.33.4
4
+
5
+ ### Added
6
+
7
+ - **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.
8
+
3
9
  ## 1.33.3
4
10
 
5
11
  ### Added
@@ -9,6 +9,12 @@ import { Container } from './styles';
9
9
  interface Props {
10
10
  type: 'picker' | 'dob' | 'date'
11
11
  name: string
12
+ /* Edited: Claude - 2026-08-15: accessible name for the visible input.
13
+ These controls render a bare <input> with no <label>, no id and no
14
+ aria-*, so every one of them reached a screen reader unnamed - 5 of 5
15
+ on the search form, which is the site's primary conversion path.
16
+ Optional, so existing callers are unaffected. */
17
+ label?: string
12
18
  defaultValue?: string
13
19
  t: TFunction<'general'>
14
20
  validation?: string
@@ -30,7 +36,7 @@ interface Props {
30
36
  onChange?: (value: string) => void
31
37
  }
32
38
 
33
- const Calendar = ({ type, name, defaultValue, t, validation, minDate, refs, onUpdate, onChange }: Props): JSX.Element => {
39
+ const Calendar = ({ type, name, label, defaultValue, t, validation, minDate, refs, onUpdate, onChange }: Props): JSX.Element => {
34
40
  const [ show, setShow ] = useState<boolean>(false);
35
41
 
36
42
  /**
@@ -69,7 +75,7 @@ const Calendar = ({ type, name, defaultValue, t, validation, minDate, refs, onUp
69
75
 
70
76
  return (
71
77
  <Container ref={ ref }>
72
- <input type="text" autoComplete="off" readOnly={ true } value={ prepared } onClick={ () => setShow(true) } />
78
+ <input type="text" aria-label={ label } autoComplete="off" readOnly={ true } value={ prepared } onClick={ () => setShow(true) } />
73
79
 
74
80
  { show && (
75
81
  <Picker
@@ -0,0 +1,103 @@
1
+ import { useState } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import { useGetSettings } from '@autobusal/providers/services';
4
+ import { Container, ContainerLocales, LocaleButton, Textarea } from './styles';
5
+
6
+ export type LocalizedValue = Record<string, Record<string, string>>;
7
+
8
+ export interface LocalizedFieldSpec {
9
+ name: string
10
+ label: string
11
+ type?: 'text' | 'textarea'
12
+ maxLength?: number
13
+ rows?: number
14
+ }
15
+
16
+ interface Props {
17
+ fields: LocalizedFieldSpec[]
18
+ /** Keyed by locale, then by field name - `{ en: { name: 'Foo' } }` */
19
+ value: LocalizedValue
20
+ t: TFunction<'normal'>
21
+ onChange: (next: LocalizedValue) => void
22
+ }
23
+
24
+ /**
25
+ * A generic per-language field editor.
26
+ *
27
+ * Ferjolt Ozuni - Date: 2026-08-11
28
+ *
29
+ * The same shape as SeoOverride and Faqs/Translations - one language shown
30
+ * at a time, languages that already carry copy are marked, the parent owns
31
+ * the state and the submit. This is the generalised version of both:
32
+ * SeoOverride is fixed to title/description, Faqs/Translations to question/
33
+ * answer - this one takes an arbitrary field list, so a section with just
34
+ * ONE translatable field (a category name, a menu label) doesn't need its
35
+ * own bespoke copy of the same ~120 lines to get the same editor.
36
+ *
37
+ * English is the tab an empty editor starts on - it's the required floor
38
+ * every one of these fields falls back to publicly.
39
+ */
40
+ const LocalizedFields = ({ fields, value, t, onChange }: Props): JSX.Element => {
41
+ const { data } = useGetSettings();
42
+
43
+ const available = data.preferences.languages.available;
44
+
45
+ const [ locale, setLocale ] = useState<string>(data.preferences.languages.default);
46
+
47
+ const current = value[locale] ?? {};
48
+
49
+ const update = (field: string, next: string): void => {
50
+ onChange({
51
+ ...value,
52
+ [locale]: { ...current, [field]: next }
53
+ });
54
+ };
55
+
56
+ const written = (language: string): boolean => {
57
+ const entry = value[language] ?? {};
58
+
59
+ return fields.some(field => entry[field.name]?.trim());
60
+ };
61
+
62
+ return (
63
+ <Container className="box">
64
+ <ContainerLocales>
65
+ { available.map(language => (
66
+ <LocaleButton
67
+ key={ language }
68
+ type="button"
69
+ $selected={ language === locale }
70
+ $written={ written(language) }
71
+ onClick={ () => setLocale(language) }
72
+ >
73
+ { t(`languages.${ language }`, { ns: 'common' }) }
74
+ </LocaleButton>
75
+ )) }
76
+ </ContainerLocales>
77
+
78
+ { fields.map((field, index) => (
79
+ <div key={ field.name } className={ index === fields.length - 1 ? 'row row-nomargin' : 'row' }>
80
+ { field.label }
81
+
82
+ { field.type === 'textarea' ? (
83
+ <Textarea
84
+ maxLength={ field.maxLength }
85
+ $rows={ field.rows }
86
+ value={ current[field.name] ?? '' }
87
+ onChange={ (event) => update(field.name, event.target.value) }
88
+ />
89
+ ) : (
90
+ <input
91
+ type="text"
92
+ maxLength={ field.maxLength }
93
+ value={ current[field.name] ?? '' }
94
+ onChange={ (event) => update(field.name, event.target.value) }
95
+ />
96
+ ) }
97
+ </div>
98
+ )) }
99
+ </Container>
100
+ );
101
+ };
102
+
103
+ export default LocalizedFields;
@@ -0,0 +1,52 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ /**
4
+ * Same card treatment as SeoOverride's own Container - this editor sits
5
+ * directly above a Viewer form, so it needs to read as part of the form
6
+ * rather than loose furniture above it.
7
+ */
8
+ export const Container = styled.div`
9
+ margin-bottom: 20px;
10
+ `;
11
+
12
+ export const ContainerLocales = styled.div`
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ gap: 8px;
16
+ margin-bottom: 12px;
17
+ `;
18
+
19
+ /**
20
+ * One button per language, `$written` marking which ones actually have
21
+ * copy - the same treatment as SeoOverride/Faqs' Translations, so every
22
+ * per-language editor in the admin reads as the same control.
23
+ */
24
+ export const LocaleButton = styled.button<{ $selected: boolean, $written: boolean }>`
25
+ padding: 4px 12px;
26
+ border: none;
27
+ border-radius: ${ props => props.theme.borderRadius };
28
+ background: ${ props => (props.$selected ? props.theme.background.neutral : 'transparent') };
29
+ color: ${ props => (props.$written ? props.theme.font.normal : props.theme.font.faded) };
30
+ font-size: ${ props => props.theme.size.s };
31
+ font-weight: ${ props => (props.$selected ? 700 : 400) };
32
+ white-space: nowrap;
33
+ cursor: pointer;
34
+
35
+ &:hover {
36
+ background: ${ props => props.theme.background.neutral };
37
+ }
38
+
39
+ ${ props => props.$written && css`
40
+ &::after {
41
+ content: '•';
42
+ margin-left: 6px;
43
+ color: ${ props => props.theme.primary.normal };
44
+ }
45
+ ` }
46
+ `;
47
+
48
+ export const Textarea = styled.textarea<{ $rows?: number }>`
49
+ height: ${ props => (props.$rows ? props.$rows * 30 : 90) }px;
50
+ padding: 8px 10px;
51
+ resize: vertical;
52
+ `;
@@ -7,6 +7,12 @@ import { getText } from './utilities';
7
7
  import { Container } from './styles';
8
8
 
9
9
  interface Props {
10
+ /* Edited: Claude - 2026-08-15: accessible name for the visible input.
11
+ These controls render a bare <input> with no <label>, no id and no
12
+ aria-*, so every one of them reached a screen reader unnamed - 5 of 5
13
+ on the search form, which is the site's primary conversion path.
14
+ Optional, so existing callers are unaffected. */
15
+ label?: string
10
16
  defaultAdults: number
11
17
  defaultChildren: number
12
18
  defaultBabies: number
@@ -15,7 +21,7 @@ interface Props {
15
21
  onUpdate: UseFormSetValue<any>
16
22
  }
17
23
 
18
- const Passengers = ({ defaultAdults, defaultChildren, defaultBabies, t, refs, onUpdate }: Props): JSX.Element => {
24
+ const Passengers = ({ label, defaultAdults, defaultChildren, defaultBabies, t, refs, onUpdate }: Props): JSX.Element => {
19
25
  const [ show, setShow ] = useState<boolean>(false);
20
26
  const [ adults, setAdults ] = useState<number>(defaultAdults);
21
27
  const [ children, setChildren ] = useState<number>(defaultChildren);
@@ -54,7 +60,7 @@ const Passengers = ({ defaultAdults, defaultChildren, defaultBabies, t, refs, on
54
60
 
55
61
  return (
56
62
  <Container ref={ ref }>
57
- <input type="text" value={ display } readOnly={ true } onClick={ () => setShow(!show) } />
63
+ <input type="text" aria-label={ label } value={ display } readOnly={ true } onClick={ () => setShow(!show) } />
58
64
 
59
65
  { show &&
60
66
  <Persons
package/index.ts CHANGED
@@ -19,6 +19,7 @@ import Gender from './Gender';
19
19
  import { General, Inline, Box, BoxMiddle, GeneralFull } from './Loading/Loading';
20
20
  import IpLogs from './IpLogs/IpLogs';
21
21
  import JsonLd from './JsonLd';
22
+ import LocalizedFields from './LocalizedFields/LocalizedFields';
22
23
  import Meta from './Meta';
23
24
  import Modal from './Modal/Modal';
24
25
  import NotificationIcon from './Notifications/Icon';
@@ -71,6 +72,7 @@ export {
71
72
  Inline,
72
73
  IpLogs,
73
74
  JsonLd,
75
+ LocalizedFields,
74
76
  Meta,
75
77
  Modal,
76
78
  NotificationIcon,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.33.3",
3
+ "version": "1.33.5",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"