@douglasneuroinformatics/libui 4.1.0 → 4.2.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/dist/{chunk-655XRTXX.js → chunk-ARKHRGTL.js} +15 -9
- package/dist/{chunk-655XRTXX.js.map → chunk-ARKHRGTL.js.map} +1 -1
- package/dist/{chunk-KI6BSSS6.js → chunk-ZIAKQCCQ.js} +2 -2
- package/dist/components.d.ts +16 -9
- package/dist/components.js +170 -86
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +2 -2
- package/dist/i18n.d.ts +2 -2
- package/dist/i18n.js +1 -1
- package/dist/{types-Dm7os_cB.d.ts → types-DHTtLrqP.d.ts} +15 -8
- package/package.json +2 -2
- package/src/components/Form/Form.stories.tsx +1 -1
- package/src/components/Form/Form.test.tsx +9 -9
- package/src/components/Form/Form.tsx +7 -6
- package/src/components/OneTimePasswordInput/OneTimePasswordInput.spec.tsx +19 -0
- package/src/components/OneTimePasswordInput/OneTimePasswordInput.stories.tsx +27 -0
- package/src/components/OneTimePasswordInput/OneTimePasswordInput.tsx +109 -0
- package/src/components/OneTimePasswordInput/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/i18n/translations/libui.json +14 -8
- /package/dist/{chunk-KI6BSSS6.js.map → chunk-ZIAKQCCQ.js.map} +0 -0
package/dist/hooks.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { D as DEFAULT_THEME, a as SYS_DARK_MEDIA_QUERY, S as StorageName, b as T
|
|
|
3
3
|
import { Promisable } from 'type-fest';
|
|
4
4
|
import { RefObject, useEffect, Dispatch, SetStateAction } from 'react';
|
|
5
5
|
import * as zustand from 'zustand';
|
|
6
|
-
import { T as TranslationNamespace, L as Language, a as TranslateFunction } from './types-
|
|
6
|
+
import { T as TranslationNamespace, L as Language, a as TranslateFunction } from './types-DHTtLrqP.js';
|
|
7
7
|
|
|
8
8
|
declare function useChart(): {
|
|
9
9
|
config: ChartConfig;
|
package/dist/hooks.js
CHANGED
package/dist/i18n.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as zustand from 'zustand';
|
|
2
2
|
import { SetOptional } from 'type-fest';
|
|
3
|
-
import { L as Language, b as Translations, a as TranslateFunction } from './types-
|
|
4
|
-
export { E as ExtractTranslationKey, c as LanguageOptions, d as TranslationKey, T as TranslationNamespace, U as UserConfig } from './types-
|
|
3
|
+
import { L as Language, b as Translations, a as TranslateFunction } from './types-DHTtLrqP.js';
|
|
4
|
+
export { E as ExtractTranslationKey, c as LanguageOptions, d as TranslationKey, T as TranslationNamespace, U as UserConfig } from './types-DHTtLrqP.js';
|
|
5
5
|
|
|
6
6
|
type InitOptions = {
|
|
7
7
|
defaultLanguage?: Language;
|
package/dist/i18n.js
CHANGED
|
@@ -132,11 +132,25 @@ var notifications = {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
|
+
var oneTimePasswordInput = {
|
|
136
|
+
invalidCodeFormat: {
|
|
137
|
+
en: "Invalid code format",
|
|
138
|
+
fr: "Format de code invalide"
|
|
139
|
+
}
|
|
140
|
+
};
|
|
135
141
|
var pagination = {
|
|
142
|
+
firstPage: {
|
|
143
|
+
en: "<< First",
|
|
144
|
+
fr: "<< Première"
|
|
145
|
+
},
|
|
136
146
|
info: {
|
|
137
147
|
en: "Showing {{first}} to {{last}} of {{total}} results",
|
|
138
148
|
fr: "Affichage de {{first}} à {{last}} sur {{total}} résultats"
|
|
139
149
|
},
|
|
150
|
+
lastPage: {
|
|
151
|
+
en: "Last >>",
|
|
152
|
+
fr: "Dernière >>"
|
|
153
|
+
},
|
|
140
154
|
next: {
|
|
141
155
|
en: "Next",
|
|
142
156
|
fr: "Suivant"
|
|
@@ -144,14 +158,6 @@ var pagination = {
|
|
|
144
158
|
previous: {
|
|
145
159
|
en: "Previous",
|
|
146
160
|
fr: "Précédent"
|
|
147
|
-
},
|
|
148
|
-
firstPage: {
|
|
149
|
-
en: "<< First",
|
|
150
|
-
fr: "<< Première"
|
|
151
|
-
},
|
|
152
|
-
lastPage: {
|
|
153
|
-
en: "Last >>",
|
|
154
|
-
fr: "Dernière >>"
|
|
155
161
|
}
|
|
156
162
|
};
|
|
157
163
|
var searchBar = {
|
|
@@ -165,6 +171,7 @@ var libuiTranslations = {
|
|
|
165
171
|
form: form,
|
|
166
172
|
months: months,
|
|
167
173
|
notifications: notifications,
|
|
174
|
+
oneTimePasswordInput: oneTimePasswordInput,
|
|
168
175
|
pagination: pagination,
|
|
169
176
|
searchBar: searchBar
|
|
170
177
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@douglasneuroinformatics/libui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.2.0",
|
|
5
5
|
"packageManager": "pnpm@10.7.1",
|
|
6
6
|
"description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
|
|
7
7
|
"author": "Joshua Unrau",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "rm -rf dist && tsup --config tsup.config.mts",
|
|
55
55
|
"format": "prettier --write src",
|
|
56
|
-
"format:translations": "find src/translations -name '*.json' -exec pnpm exec sort-json {} \\;",
|
|
56
|
+
"format:translations": "find src/i18n/translations -name '*.json' -exec pnpm exec sort-json {} \\;",
|
|
57
57
|
"lint": "tsc && eslint --fix src",
|
|
58
58
|
"prepare": "husky",
|
|
59
59
|
"storybook": "storybook dev --no-open -p 6006",
|
|
@@ -116,14 +116,13 @@ describe('Form', () => {
|
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
describe('custom
|
|
120
|
-
let
|
|
119
|
+
describe('custom onBeforeSubmit error', () => {
|
|
120
|
+
let onBeforeSubmit: Mock;
|
|
121
121
|
|
|
122
122
|
beforeEach(() => {
|
|
123
|
-
|
|
123
|
+
onBeforeSubmit = vi.fn();
|
|
124
124
|
render(
|
|
125
125
|
<Form
|
|
126
|
-
beforeSubmit={beforeSubmit}
|
|
127
126
|
content={{
|
|
128
127
|
value: {
|
|
129
128
|
kind: 'number',
|
|
@@ -135,6 +134,7 @@ describe('Form', () => {
|
|
|
135
134
|
validationSchema={z.object({
|
|
136
135
|
value: z.number({ message: 'Please enter a number' })
|
|
137
136
|
})}
|
|
137
|
+
onBeforeSubmit={onBeforeSubmit}
|
|
138
138
|
onError={onError}
|
|
139
139
|
onSubmit={onSubmit}
|
|
140
140
|
/>
|
|
@@ -156,12 +156,12 @@ describe('Form', () => {
|
|
|
156
156
|
'Please enter a number'
|
|
157
157
|
])
|
|
158
158
|
);
|
|
159
|
-
expect(
|
|
159
|
+
expect(onBeforeSubmit).not.toHaveBeenCalled();
|
|
160
160
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
it('should not allow submitting the form with the
|
|
164
|
-
|
|
163
|
+
it('should not allow submitting the form with the onBeforeSubmit error', async () => {
|
|
164
|
+
onBeforeSubmit.mockResolvedValueOnce({ errorMessage: 'Invalid!', success: false });
|
|
165
165
|
const field: HTMLInputElement = screen.getByLabelText('Value');
|
|
166
166
|
await userEvent.type(field, '-1');
|
|
167
167
|
fireEvent.submit(screen.getByTestId(testid));
|
|
@@ -171,8 +171,8 @@ describe('Form', () => {
|
|
|
171
171
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it('should allow submitting the form if
|
|
175
|
-
|
|
174
|
+
it('should allow submitting the form if onBeforeSubmit returns true', async () => {
|
|
175
|
+
onBeforeSubmit.mockResolvedValueOnce({ success: true });
|
|
176
176
|
const field: HTMLInputElement = screen.getByLabelText('Value');
|
|
177
177
|
await userEvent.type(field, '-1');
|
|
178
178
|
fireEvent.submit(screen.getByTestId(testid));
|
|
@@ -29,7 +29,6 @@ type FormProps<TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<T
|
|
|
29
29
|
left?: React.ReactNode;
|
|
30
30
|
right?: React.ReactNode;
|
|
31
31
|
};
|
|
32
|
-
beforeSubmit?: (data: NoInfer<TData>) => Promisable<{ errorMessage: string; success: false } | { success: true }>;
|
|
33
32
|
className?: string;
|
|
34
33
|
content: FormContent<TData>;
|
|
35
34
|
customStyles?: {
|
|
@@ -39,6 +38,7 @@ type FormProps<TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<T
|
|
|
39
38
|
fieldsFooter?: React.ReactNode;
|
|
40
39
|
id?: string;
|
|
41
40
|
initialValues?: PartialNullableFormDataType<NoInfer<TData>>;
|
|
41
|
+
onBeforeSubmit?: (data: NoInfer<TData>) => Promisable<{ errorMessage: string; success: false } | { success: true }>;
|
|
42
42
|
onError?: (error: z.ZodError<NoInfer<TData>>) => void;
|
|
43
43
|
onSubmit: (data: NoInfer<TData>) => Promisable<void>;
|
|
44
44
|
preventResetValuesOnReset?: boolean;
|
|
@@ -52,13 +52,13 @@ type FormProps<TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<T
|
|
|
52
52
|
|
|
53
53
|
const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TSchema> = z.TypeOf<TSchema>>({
|
|
54
54
|
additionalButtons,
|
|
55
|
-
beforeSubmit,
|
|
56
55
|
className,
|
|
57
56
|
content,
|
|
58
57
|
customStyles,
|
|
59
58
|
fieldsFooter,
|
|
60
59
|
id,
|
|
61
60
|
initialValues,
|
|
61
|
+
onBeforeSubmit,
|
|
62
62
|
onError,
|
|
63
63
|
onSubmit,
|
|
64
64
|
preventResetValuesOnReset,
|
|
@@ -116,8 +116,8 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
|
|
|
116
116
|
handleError(result.error);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
if (
|
|
120
|
-
const beforeSubmitResult = await
|
|
119
|
+
if (onBeforeSubmit) {
|
|
120
|
+
const beforeSubmitResult = await onBeforeSubmit(result.data);
|
|
121
121
|
if (!beforeSubmitResult.success) {
|
|
122
122
|
setErrors({});
|
|
123
123
|
setRootErrors([beforeSubmitResult.errorMessage]);
|
|
@@ -142,7 +142,7 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
|
|
|
142
142
|
const isGrouped = Array.isArray(content);
|
|
143
143
|
|
|
144
144
|
const revalidate = () => {
|
|
145
|
-
const hasErrors = Object.keys(errors).length > 0;
|
|
145
|
+
const hasErrors = Object.keys(errors).length > 0 || rootErrors.length;
|
|
146
146
|
if (hasErrors) {
|
|
147
147
|
validationSchema
|
|
148
148
|
.safeParseAsync(values)
|
|
@@ -156,7 +156,8 @@ const Form = <TSchema extends z.ZodType<FormDataType>, TData extends z.TypeOf<TS
|
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
useEffect(() => {
|
|
159
|
-
|
|
159
|
+
setErrors({});
|
|
160
|
+
setRootErrors([]);
|
|
160
161
|
}, [resolvedLanguage]);
|
|
161
162
|
|
|
162
163
|
const isSuspended = Boolean(suspendWhileSubmitting && isSubmitting);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { OneTimePasswordInput } from './OneTimePasswordInput';
|
|
5
|
+
|
|
6
|
+
type Props = React.ComponentPropsWithoutRef<typeof OneTimePasswordInput>;
|
|
7
|
+
|
|
8
|
+
const TEST_ID = 'OneTimePasswordInput';
|
|
9
|
+
|
|
10
|
+
const TestOneTimePasswordInput: React.FC<Partial<Props>> = (props) => {
|
|
11
|
+
return <OneTimePasswordInput data-testid={TEST_ID} {...(props as Props)} />;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('OneTimePasswordInput', () => {
|
|
15
|
+
it('should render', () => {
|
|
16
|
+
render(<TestOneTimePasswordInput />);
|
|
17
|
+
expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { NotificationHub } from '../NotificationHub';
|
|
4
|
+
import { OneTimePasswordInput } from './OneTimePasswordInput';
|
|
5
|
+
|
|
6
|
+
type Story = StoryObj<typeof OneTimePasswordInput>;
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
args: {
|
|
10
|
+
onComplete: (code) => {
|
|
11
|
+
alert(`Code: ${code}`);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
component: OneTimePasswordInput,
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<NotificationHub />
|
|
20
|
+
<Story />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
} as Meta<typeof OneTimePasswordInput>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { ChangeEvent, ClipboardEvent, KeyboardEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { Promisable } from 'type-fest';
|
|
5
|
+
|
|
6
|
+
import { useNotificationsStore, useTranslation } from '@/hooks';
|
|
7
|
+
import { cn } from '@/utils';
|
|
8
|
+
|
|
9
|
+
const CODE_LENGTH = 6;
|
|
10
|
+
|
|
11
|
+
const EMPTY_CODE = Object.freeze(Array<null>(CODE_LENGTH).fill(null));
|
|
12
|
+
|
|
13
|
+
type OneTimePasswordInputProps = {
|
|
14
|
+
[key: `data-${string}`]: unknown;
|
|
15
|
+
className?: string;
|
|
16
|
+
onComplete: (code: number) => Promisable<void>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getUpdatedDigits(digits: (null | number)[], index: number, value: null | number) {
|
|
20
|
+
const updatedDigits = [...digits];
|
|
21
|
+
updatedDigits[index] = value;
|
|
22
|
+
return updatedDigits;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const OneTimePasswordInput = ({ className, onComplete, ...props }: OneTimePasswordInputProps) => {
|
|
26
|
+
const notifications = useNotificationsStore();
|
|
27
|
+
const { t } = useTranslation('libui');
|
|
28
|
+
const [digits, setDigits] = useState<(null | number)[]>([...EMPTY_CODE]);
|
|
29
|
+
const inputRefs = digits.map(() => useRef<HTMLInputElement>(null));
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const isComplete = digits.every((value) => Number.isInteger(value));
|
|
33
|
+
if (isComplete) {
|
|
34
|
+
void onComplete(parseInt(digits.join('')));
|
|
35
|
+
setDigits([...EMPTY_CODE]);
|
|
36
|
+
}
|
|
37
|
+
}, [digits]);
|
|
38
|
+
|
|
39
|
+
const focusNext = (index: number) => inputRefs[index + 1 === digits.length ? 0 : index + 1]?.current?.focus();
|
|
40
|
+
|
|
41
|
+
const focusPrev = (index: number) => inputRefs[index - 1 >= 0 ? index - 1 : digits.length - 1]?.current?.focus();
|
|
42
|
+
|
|
43
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
|
|
44
|
+
let value: null | number;
|
|
45
|
+
if (e.target.value === '') {
|
|
46
|
+
value = null;
|
|
47
|
+
} else if (Number.isInteger(parseInt(e.target.value))) {
|
|
48
|
+
value = parseInt(e.target.value);
|
|
49
|
+
} else {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setDigits((prevDigits) => getUpdatedDigits(prevDigits, index, value));
|
|
53
|
+
focusNext(index);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
|
|
57
|
+
switch (e.key) {
|
|
58
|
+
case 'ArrowLeft':
|
|
59
|
+
focusPrev(index);
|
|
60
|
+
break;
|
|
61
|
+
case 'ArrowRight':
|
|
62
|
+
focusNext(index);
|
|
63
|
+
break;
|
|
64
|
+
case 'Backspace':
|
|
65
|
+
setDigits((prevDigits) => getUpdatedDigits(prevDigits, index - 1, null));
|
|
66
|
+
focusPrev(index);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
const pastedDigits = e.clipboardData
|
|
73
|
+
.getData('text/plain')
|
|
74
|
+
.split('')
|
|
75
|
+
.slice(0, CODE_LENGTH)
|
|
76
|
+
.map((value) => parseInt(value));
|
|
77
|
+
const isValid = pastedDigits.length === CODE_LENGTH && pastedDigits.every((value) => Number.isInteger(value));
|
|
78
|
+
if (isValid) {
|
|
79
|
+
setDigits(pastedDigits);
|
|
80
|
+
} else {
|
|
81
|
+
notifications.addNotification({
|
|
82
|
+
message: t('oneTimePasswordInput.invalidCodeFormat'),
|
|
83
|
+
type: 'warning'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className={cn('flex gap-2', className)} {...props}>
|
|
90
|
+
{digits.map((_, index) => (
|
|
91
|
+
<input
|
|
92
|
+
className="w-1/6 rounded-md border border-slate-300 bg-transparent p-2 shadow-xs hover:border-slate-300 focus:border-sky-800 focus:outline-hidden dark:border-slate-600 dark:hover:border-slate-400 dark:focus:border-sky-500"
|
|
93
|
+
key={index}
|
|
94
|
+
maxLength={1}
|
|
95
|
+
ref={inputRefs[index]}
|
|
96
|
+
type="text"
|
|
97
|
+
value={digits[index] ?? ''}
|
|
98
|
+
onChange={(e) => {
|
|
99
|
+
handleChange(e, index);
|
|
100
|
+
}}
|
|
101
|
+
onKeyDown={(e) => {
|
|
102
|
+
handleKeyDown(e, index);
|
|
103
|
+
}}
|
|
104
|
+
onPaste={handlePaste}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './OneTimePasswordInput';
|
package/src/components/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from './LineGraph';
|
|
|
32
32
|
export * from './ListboxDropdown';
|
|
33
33
|
export * from './MenuBar';
|
|
34
34
|
export * from './NotificationHub';
|
|
35
|
+
export * from './OneTimePasswordInput';
|
|
35
36
|
export * from './Pagination';
|
|
36
37
|
export * from './Popover';
|
|
37
38
|
export * from './Progress';
|
|
@@ -131,11 +131,25 @@
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
|
+
"oneTimePasswordInput": {
|
|
135
|
+
"invalidCodeFormat": {
|
|
136
|
+
"en": "Invalid code format",
|
|
137
|
+
"fr": "Format de code invalide"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
134
140
|
"pagination": {
|
|
141
|
+
"firstPage": {
|
|
142
|
+
"en": "<< First",
|
|
143
|
+
"fr": "<< Première"
|
|
144
|
+
},
|
|
135
145
|
"info": {
|
|
136
146
|
"en": "Showing {{first}} to {{last}} of {{total}} results",
|
|
137
147
|
"fr": "Affichage de {{first}} à {{last}} sur {{total}} résultats"
|
|
138
148
|
},
|
|
149
|
+
"lastPage": {
|
|
150
|
+
"en": "Last >>",
|
|
151
|
+
"fr": "Dernière >>"
|
|
152
|
+
},
|
|
139
153
|
"next": {
|
|
140
154
|
"en": "Next",
|
|
141
155
|
"fr": "Suivant"
|
|
@@ -143,14 +157,6 @@
|
|
|
143
157
|
"previous": {
|
|
144
158
|
"en": "Previous",
|
|
145
159
|
"fr": "Précédent"
|
|
146
|
-
},
|
|
147
|
-
"firstPage": {
|
|
148
|
-
"en": "<< First",
|
|
149
|
-
"fr": "<< Première"
|
|
150
|
-
},
|
|
151
|
-
"lastPage": {
|
|
152
|
-
"en": "Last >>",
|
|
153
|
-
"fr": "Dernière >>"
|
|
154
160
|
}
|
|
155
161
|
},
|
|
156
162
|
"searchBar": {
|
|
File without changes
|