@ecohouse/ui 0.1.2 → 0.1.4
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/index.cjs +1734 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +287 -13
- package/dist/index.d.ts +287 -13
- package/dist/index.js +1720 -159
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.stories.tsx +133 -0
- package/src/components/Avatar/Avatar.tsx +389 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Button/Button.tsx +106 -88
- package/src/components/Checkbox/Checkbox.stories.tsx +112 -0
- package/src/components/Checkbox/Checkbox.tsx +150 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Input/Input.stories.tsx +11 -40
- package/src/components/Input/Input.tsx +58 -54
- package/src/components/Input/index.ts +1 -1
- package/src/components/Select/Select.stories.tsx +196 -0
- package/src/components/Select/Select.tsx +851 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Textarea/Textarea.stories.tsx +71 -0
- package/src/components/Textarea/Textarea.tsx +241 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toggle/Toggle.stories.tsx +100 -0
- package/src/components/Toggle/Toggle.tsx +176 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/components/index.ts +16 -1
- package/src/icons/Calendar/CalendarIcon.tsx +21 -0
- package/src/icons/Calendar/CalendarIcon.web.tsx +27 -0
- package/src/icons/Calendar/calendarPath.ts +3 -0
- package/src/icons/Calendar/index.ts +1 -0
- package/src/icons/Check/CheckIcon.tsx +26 -0
- package/src/icons/Check/CheckIcon.web.tsx +32 -0
- package/src/icons/Check/checkPath.ts +2 -0
- package/src/icons/Check/index.ts +1 -0
- package/src/icons/ChevronDown/ChevronDownIcon.tsx +30 -0
- package/src/icons/ChevronDown/ChevronDownIcon.web.tsx +36 -0
- package/src/icons/ChevronDown/chevronDownPath.ts +2 -0
- package/src/icons/ChevronDown/index.ts +1 -0
- package/src/icons/Heart/HeartIcon.tsx +23 -0
- package/src/icons/Heart/HeartIcon.web.tsx +29 -0
- package/src/icons/Heart/heartPath.ts +3 -0
- package/src/icons/Heart/index.ts +1 -0
- package/src/icons/HelpCircle/HelpCircleIcon.tsx +25 -0
- package/src/icons/HelpCircle/HelpCircleIcon.web.tsx +31 -0
- package/src/icons/HelpCircle/helpCirclePath.ts +3 -0
- package/src/icons/HelpCircle/index.ts +1 -0
- package/src/icons/LayoutGrid/LayoutGridIcon.tsx +25 -0
- package/src/icons/LayoutGrid/LayoutGridIcon.web.tsx +31 -0
- package/src/icons/LayoutGrid/index.ts +1 -0
- package/src/icons/LayoutGrid/layoutGridPath.ts +3 -0
- package/src/icons/LogOut/LogOutIcon.tsx +21 -0
- package/src/icons/LogOut/LogOutIcon.web.tsx +27 -0
- package/src/icons/LogOut/index.ts +1 -0
- package/src/icons/LogOut/logOutPath.ts +3 -0
- package/src/icons/Search/SearchIcon.tsx +37 -0
- package/src/icons/Search/SearchIcon.web.tsx +43 -0
- package/src/icons/Search/index.ts +1 -0
- package/src/icons/Search/searchPath.ts +5 -0
- package/src/icons/Settings/SettingsIcon.tsx +21 -0
- package/src/icons/Settings/SettingsIcon.web.tsx +27 -0
- package/src/icons/Settings/index.ts +1 -0
- package/src/icons/Settings/settingsPath.ts +3 -0
- package/src/icons/Video/VideoIcon.tsx +24 -0
- package/src/icons/Video/VideoIcon.web.tsx +30 -0
- package/src/icons/Video/index.ts +1 -0
- package/src/icons/Video/videoPath.ts +5 -0
- package/src/icons/index.ts +10 -0
- package/src/icons/registry.ts +24 -3
- package/src/index.ts +31 -3
- package/src/theme/colors.ts +1 -0
- package/src/theme/index.ts +8 -1
- package/src/theme/typography.ts +77 -2
- package/src/utils/mergeRefs.ts +20 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState, type ComponentRef, type ReactNode } from "react";
|
|
1
|
+
import { forwardRef, useMemo, useRef, useState, type ComponentRef, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
4
|
StyleSheet,
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
import { ShowIcon } from "../../icons";
|
|
15
15
|
import { UserIcon } from "../../icons";
|
|
16
16
|
import { colors, inputTypography } from "../../theme";
|
|
17
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
17
18
|
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
18
19
|
|
|
19
|
-
export type InputState = "default" | "filled" | "active" | "error" | "disabled";
|
|
20
20
|
export type InputSize = "lg" | "sm";
|
|
21
21
|
|
|
22
22
|
export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
|
|
@@ -24,15 +24,18 @@ export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
|
|
|
24
24
|
showLabel?: boolean;
|
|
25
25
|
hint?: string;
|
|
26
26
|
showHint?: boolean;
|
|
27
|
-
state?: InputState;
|
|
28
27
|
size?: InputSize;
|
|
29
|
-
|
|
28
|
+
/** External — validation / form error. */
|
|
30
29
|
error?: boolean;
|
|
30
|
+
/** External — non-interactive. */
|
|
31
|
+
disabled?: boolean;
|
|
31
32
|
leftIcon?: ReactNode;
|
|
32
33
|
showLeftIcon?: boolean;
|
|
33
34
|
rightIcon?: ReactNode;
|
|
34
35
|
showRightIcon?: boolean;
|
|
35
36
|
onRightIconPress?: () => void;
|
|
37
|
+
/** Accessible name for the right icon button, when `onRightIconPress` is set. */
|
|
38
|
+
rightIconAccessibilityLabel?: string;
|
|
36
39
|
style?: StyleProp<ViewStyle>;
|
|
37
40
|
fieldStyle?: StyleProp<ViewStyle>;
|
|
38
41
|
inputStyle?: StyleProp<TextStyle>;
|
|
@@ -40,6 +43,8 @@ export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
|
|
|
40
43
|
inputClassName?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
type VisualState = "default" | "filled" | "active" | "error" | "disabled";
|
|
47
|
+
|
|
43
48
|
type FieldChrome = {
|
|
44
49
|
borderColor: string;
|
|
45
50
|
backgroundColor: string;
|
|
@@ -74,20 +79,18 @@ const sizeConfig = {
|
|
|
74
79
|
},
|
|
75
80
|
} as const satisfies Record<InputSize, Record<string, unknown>>;
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
/** default / filled / active come from value + focus; error / disabled from props. */
|
|
83
|
+
function resolveVisualState({
|
|
79
84
|
disabled,
|
|
80
85
|
error,
|
|
81
86
|
focused,
|
|
82
87
|
hasValue,
|
|
83
88
|
}: {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
error?: boolean;
|
|
89
|
+
disabled: boolean;
|
|
90
|
+
error: boolean;
|
|
87
91
|
focused: boolean;
|
|
88
92
|
hasValue: boolean;
|
|
89
|
-
}):
|
|
90
|
-
if (state) return state;
|
|
93
|
+
}): VisualState {
|
|
91
94
|
if (disabled) return "disabled";
|
|
92
95
|
if (error) return "error";
|
|
93
96
|
if (focused) return "active";
|
|
@@ -95,7 +98,7 @@ function resolveState({
|
|
|
95
98
|
return "default";
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function chromeFor(state:
|
|
101
|
+
function chromeFor(state: VisualState): FieldChrome {
|
|
99
102
|
const base: FieldChrome = {
|
|
100
103
|
borderColor: colors.grey700,
|
|
101
104
|
backgroundColor: colors.grey700,
|
|
@@ -142,35 +145,40 @@ function chromeFor(state: InputState): FieldChrome {
|
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
export function Input(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
148
|
+
export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(function Input(
|
|
149
|
+
{
|
|
150
|
+
label = "Label",
|
|
151
|
+
showLabel = true,
|
|
152
|
+
hint = "Hint",
|
|
153
|
+
showHint = true,
|
|
154
|
+
placeholder = "placeholder",
|
|
155
|
+
size = "lg",
|
|
156
|
+
disabled = false,
|
|
157
|
+
error = false,
|
|
158
|
+
value,
|
|
159
|
+
defaultValue,
|
|
160
|
+
onChangeText,
|
|
161
|
+
onFocus,
|
|
162
|
+
onBlur,
|
|
163
|
+
leftIcon,
|
|
164
|
+
showLeftIcon = false,
|
|
165
|
+
rightIcon,
|
|
166
|
+
showRightIcon = false,
|
|
167
|
+
onRightIconPress,
|
|
168
|
+
rightIconAccessibilityLabel,
|
|
169
|
+
style,
|
|
170
|
+
fieldStyle,
|
|
171
|
+
inputStyle,
|
|
172
|
+
className,
|
|
173
|
+
inputClassName,
|
|
174
|
+
accessibilityLabel,
|
|
175
|
+
...textInputProps
|
|
176
|
+
},
|
|
177
|
+
forwardedRef,
|
|
178
|
+
) {
|
|
172
179
|
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
173
180
|
const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
|
|
181
|
+
const setInputRef = useMemo(() => mergeRefs(inputRef, forwardedRef), [forwardedRef]);
|
|
174
182
|
const [focused, setFocused] = useState(false);
|
|
175
183
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
176
184
|
const metrics = sizeConfig[size];
|
|
@@ -179,15 +187,9 @@ export function Input({
|
|
|
179
187
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
180
188
|
const hasValue = currentValue.length > 0;
|
|
181
189
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
error,
|
|
186
|
-
focused,
|
|
187
|
-
hasValue,
|
|
188
|
-
});
|
|
189
|
-
const chrome = chromeFor(resolvedState);
|
|
190
|
-
const isDisabled = resolvedState === "disabled" || disabled;
|
|
190
|
+
const visualState = resolveVisualState({ disabled, error, focused, hasValue });
|
|
191
|
+
const chrome = chromeFor(visualState);
|
|
192
|
+
const resolvedAccessibilityLabel = accessibilityLabel ?? (showLabel ? undefined : label);
|
|
191
193
|
|
|
192
194
|
const hasLeftIcon = showLeftIcon || leftIcon != null;
|
|
193
195
|
const hasRightIcon = showRightIcon || rightIcon != null;
|
|
@@ -203,7 +205,7 @@ export function Input({
|
|
|
203
205
|
useApplyWebClassName(inputRef, inputClassName);
|
|
204
206
|
|
|
205
207
|
return (
|
|
206
|
-
<View ref={rootRef} style={[styles.root,
|
|
208
|
+
<View ref={rootRef} style={[styles.root, disabled && styles.disabled, style]}>
|
|
207
209
|
{showLabel && label ? (
|
|
208
210
|
<Text style={[styles.label, { color: chrome.labelColor }]}>{label}</Text>
|
|
209
211
|
) : null}
|
|
@@ -232,11 +234,11 @@ export function Input({
|
|
|
232
234
|
) : null}
|
|
233
235
|
|
|
234
236
|
<TextInput
|
|
235
|
-
ref={
|
|
237
|
+
ref={setInputRef}
|
|
236
238
|
{...textInputProps}
|
|
237
239
|
value={isControlled ? value : undefined}
|
|
238
240
|
defaultValue={isControlled ? undefined : defaultValue}
|
|
239
|
-
editable={!
|
|
241
|
+
editable={!disabled}
|
|
240
242
|
placeholder={placeholder}
|
|
241
243
|
placeholderTextColor={chrome.placeholderColor}
|
|
242
244
|
onChangeText={(text) => {
|
|
@@ -259,16 +261,18 @@ export function Input({
|
|
|
259
261
|
Platform.OS === "android" ? styles.inputAndroid : null,
|
|
260
262
|
inputStyle,
|
|
261
263
|
]}
|
|
262
|
-
|
|
264
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
265
|
+
accessibilityState={{ disabled }}
|
|
263
266
|
/>
|
|
264
267
|
|
|
265
268
|
{hasRightIcon ? (
|
|
266
269
|
onRightIconPress ? (
|
|
267
270
|
<TouchableOpacity
|
|
268
271
|
onPress={onRightIconPress}
|
|
269
|
-
disabled={
|
|
272
|
+
disabled={disabled}
|
|
270
273
|
style={[styles.iconSlot, { width: metrics.iconSize, height: metrics.iconSize }]}
|
|
271
274
|
accessibilityRole="button"
|
|
275
|
+
accessibilityLabel={rightIconAccessibilityLabel}
|
|
272
276
|
hitSlop={8}
|
|
273
277
|
>
|
|
274
278
|
{rightIconNode}
|
|
@@ -286,7 +290,7 @@ export function Input({
|
|
|
286
290
|
) : null}
|
|
287
291
|
</View>
|
|
288
292
|
);
|
|
289
|
-
}
|
|
293
|
+
});
|
|
290
294
|
|
|
291
295
|
const styles = StyleSheet.create({
|
|
292
296
|
root: {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Input } from "./Input";
|
|
2
|
-
export type { InputProps, InputSize
|
|
2
|
+
export type { InputProps, InputSize } from "./Input";
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { fn } from "storybook/test";
|
|
4
|
+
import { View, StyleSheet, Text } from "react-native";
|
|
5
|
+
import { Select, type SelectOption } from "./Select";
|
|
6
|
+
import { colors } from "../../theme";
|
|
7
|
+
|
|
8
|
+
const defaultOptions: SelectOption[] = [
|
|
9
|
+
{ label: "Select Item", value: "1" },
|
|
10
|
+
{ label: "Select Item", value: "2" },
|
|
11
|
+
{ label: "Select Item", value: "3" },
|
|
12
|
+
{ label: "Select Item", value: "4" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const namedOptions: SelectOption[] = [
|
|
16
|
+
{ label: "Yerevan", value: "yerevan" },
|
|
17
|
+
{ label: "Gyumri", value: "gyumri" },
|
|
18
|
+
{ label: "Vanadzor", value: "vanadzor" },
|
|
19
|
+
{ label: "Dilijan", value: "dilijan" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const meta = {
|
|
23
|
+
title: "Components/Select",
|
|
24
|
+
component: Select,
|
|
25
|
+
args: {
|
|
26
|
+
label: "Label",
|
|
27
|
+
showLabel: true,
|
|
28
|
+
placeholder: "placeholder",
|
|
29
|
+
options: defaultOptions,
|
|
30
|
+
showLeftIcon: true,
|
|
31
|
+
showSearch: true,
|
|
32
|
+
defaultOpen: false,
|
|
33
|
+
disabled: false,
|
|
34
|
+
multiple: false,
|
|
35
|
+
onValueChange: fn(),
|
|
36
|
+
onOpenChange: fn(),
|
|
37
|
+
},
|
|
38
|
+
argTypes: {
|
|
39
|
+
multiple: { control: "boolean" },
|
|
40
|
+
onValueChange: { action: "valueChange" },
|
|
41
|
+
onOpenChange: { action: "openChange" },
|
|
42
|
+
},
|
|
43
|
+
parameters: {
|
|
44
|
+
backgrounds: { default: "black" },
|
|
45
|
+
},
|
|
46
|
+
} satisfies Meta<typeof Select>;
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof meta>;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {};
|
|
52
|
+
|
|
53
|
+
export const Open: Story = {
|
|
54
|
+
args: { defaultOpen: true },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithValue: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
options: namedOptions,
|
|
60
|
+
defaultValue: "gyumri",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Multiple: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
multiple: true,
|
|
67
|
+
options: namedOptions,
|
|
68
|
+
defaultValue: ["yerevan", "dilijan"],
|
|
69
|
+
defaultOpen: true,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithoutSearch: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
showSearch: false,
|
|
76
|
+
defaultOpen: true,
|
|
77
|
+
options: namedOptions,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const WithoutLeftIcon: Story = {
|
|
82
|
+
args: { showLeftIcon: false },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Error: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
error: true,
|
|
88
|
+
hint: "Hint",
|
|
89
|
+
showHint: true,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Disabled: Story = {
|
|
94
|
+
args: { disabled: true, defaultValue: "1" },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Verifies that a disabled Select stays non-interactive even when forced open. */
|
|
98
|
+
export const DisabledOpen: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
disabled: true,
|
|
101
|
+
defaultOpen: true,
|
|
102
|
+
options: namedOptions,
|
|
103
|
+
defaultValue: "yerevan",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function ControlledSelect() {
|
|
108
|
+
const [value, setValue] = useState<string | undefined>();
|
|
109
|
+
const [open, setOpen] = useState(false);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={storyStyles.column}>
|
|
113
|
+
<Select
|
|
114
|
+
label="Label"
|
|
115
|
+
placeholder="placeholder"
|
|
116
|
+
options={namedOptions}
|
|
117
|
+
value={value}
|
|
118
|
+
onValueChange={setValue}
|
|
119
|
+
open={open}
|
|
120
|
+
onOpenChange={setOpen}
|
|
121
|
+
/>
|
|
122
|
+
<Text style={storyStyles.meta}>
|
|
123
|
+
value: {value ?? "—"} · open: {String(open)}
|
|
124
|
+
</Text>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ControlledMultipleSelect() {
|
|
130
|
+
const [value, setValue] = useState<string[]>(["gyumri"]);
|
|
131
|
+
const [open, setOpen] = useState(true);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<View style={storyStyles.column}>
|
|
135
|
+
<Select
|
|
136
|
+
multiple
|
|
137
|
+
label="Cities"
|
|
138
|
+
placeholder="Select cities"
|
|
139
|
+
options={namedOptions}
|
|
140
|
+
value={value}
|
|
141
|
+
onValueChange={setValue}
|
|
142
|
+
open={open}
|
|
143
|
+
onOpenChange={setOpen}
|
|
144
|
+
/>
|
|
145
|
+
<Text style={storyStyles.meta}>{value.join(", ") || "—"}</Text>
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const Controlled: Story = {
|
|
151
|
+
render: () => <ControlledSelect />,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const ControlledMultiple: Story = {
|
|
155
|
+
render: () => <ControlledMultipleSelect />,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const AllStates: Story = {
|
|
159
|
+
render: () => (
|
|
160
|
+
<View style={storyStyles.row}>
|
|
161
|
+
<Select label="Single" placeholder="placeholder" options={namedOptions} />
|
|
162
|
+
<Select
|
|
163
|
+
label="Single open"
|
|
164
|
+
placeholder="placeholder"
|
|
165
|
+
options={namedOptions}
|
|
166
|
+
defaultOpen
|
|
167
|
+
defaultValue="yerevan"
|
|
168
|
+
/>
|
|
169
|
+
<Select
|
|
170
|
+
multiple
|
|
171
|
+
label="Multiple"
|
|
172
|
+
placeholder="Select cities"
|
|
173
|
+
options={namedOptions}
|
|
174
|
+
defaultValue={["yerevan", "gyumri"]}
|
|
175
|
+
defaultOpen
|
|
176
|
+
/>
|
|
177
|
+
</View>
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const storyStyles = StyleSheet.create({
|
|
182
|
+
column: {
|
|
183
|
+
gap: 12,
|
|
184
|
+
alignItems: "flex-start",
|
|
185
|
+
},
|
|
186
|
+
row: {
|
|
187
|
+
flexDirection: "row",
|
|
188
|
+
flexWrap: "wrap",
|
|
189
|
+
gap: 24,
|
|
190
|
+
alignItems: "flex-start",
|
|
191
|
+
},
|
|
192
|
+
meta: {
|
|
193
|
+
color: colors.grey100,
|
|
194
|
+
fontSize: 12,
|
|
195
|
+
},
|
|
196
|
+
});
|