aplance-ui 1.4.0 → 1.5.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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/icon/icon.tsx +4 -6
  3. package/registry/components/primitives/modal/modal.module.css +5 -0
  4. package/registry/components/primitives/modal/modal.tsx +32 -34
  5. package/registry/components/primitives/popover/popover.module.css +4 -2
  6. package/registry/components/primitives/popover/popover.tsx +16 -17
  7. package/registry/components/ui/accordion/accordion.module.css +52 -0
  8. package/registry/components/ui/accordion/accordion.tsx +97 -25
  9. package/registry/components/ui/avatar/avatar.tsx +2 -2
  10. package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
  11. package/registry/components/ui/button/button.module.css +43 -5
  12. package/registry/components/ui/button/button.tsx +2 -1
  13. package/registry/components/ui/copy-button/copy-button.module.css +2 -0
  14. package/registry/components/ui/data-table/data-table.module.css +68 -25
  15. package/registry/components/ui/data-table/data-table.tsx +1238 -1229
  16. package/registry/components/ui/data-table/filters.tsx +21 -0
  17. package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
  18. package/registry/components/ui/data-table/types.ts +11 -1
  19. package/registry/components/ui/data-table/useDataTable.tsx +1 -2
  20. package/registry/components/ui/data-table/utils.ts +4 -3
  21. package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
  22. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
  23. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +115 -84
  24. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +63 -11
  25. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +51 -55
  26. package/registry/components/ui/input-fields/input/input.tsx +348 -348
  27. package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
  28. package/registry/components/ui/input-fields/media-input/types.ts +8 -2
  29. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
  30. package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
  31. package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
  32. package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
  33. package/registry/components/ui/tabs/tabs.module.css +8 -12
  34. package/registry/components/ui/tabs/tabs.tsx +75 -44
  35. package/registry/components/ui/tooltip/tooltip.module.css +8 -3
  36. package/registry/components/ui/tooltip/tooltip.tsx +16 -3
  37. package/registry/lib/string.ts +3 -3
  38. package/registry/registry.json +33 -3
@@ -1,348 +1,348 @@
1
- import {
2
- type ComponentProps,
3
- useCallback,
4
- useEffect,
5
- useId,
6
- useRef,
7
- useState,
8
- } from "react";
9
- import clsx from "clsx";
10
- import {
11
- IconKey,
12
- IconKeyOff,
13
- IconSearch,
14
- IconX,
15
- IconCalendar,
16
- IconClock,
17
- } from "@tabler/icons-react";
18
- import sCommon from "../common/input-field-common.module.css";
19
- import sButton from "../../button/button.module.css";
20
- import s from "../common/input-field-native.module.css";
21
- import { FieldMessage } from "../common/field-message/field-message";
22
- import { Icon } from "../../../primitives/icon/icon";
23
- import type { BaseInputFieldProps } from "../common/types";
24
-
25
- export type InputType =
26
- | "text"
27
- | "email"
28
- | "tel"
29
- | "url"
30
- | "password"
31
- | "search"
32
- | "number"
33
- | "file"
34
- | "radio"
35
- | "date"
36
- | "month"
37
- | "week"
38
- | "time"
39
- | "datetime-local"
40
- | "checkbox"
41
- | "color"
42
- | "range";
43
-
44
- const styledInputTypes: readonly InputType[] = [
45
- "text",
46
- "email",
47
- "tel",
48
- "url",
49
- "password",
50
- "search",
51
- "number",
52
- "file",
53
- "date",
54
- "month",
55
- "week",
56
- "time",
57
- "datetime-local",
58
- ] as const;
59
-
60
- const clearableInputTypes: readonly InputType[] = [
61
- "text",
62
- "email",
63
- "tel",
64
- "url",
65
- "password",
66
- "search",
67
- "number",
68
- "file",
69
- ] as const;
70
-
71
- const fitWidthInputTypes: readonly InputType[] = [
72
- "checkbox",
73
- "radio",
74
- "color",
75
- ] as const;
76
- const dateInputTypes: readonly InputType[] = [
77
- "date",
78
- "month",
79
- "week",
80
- "datetime-local",
81
- ] as const;
82
-
83
- type BaseProps = BaseInputFieldProps & {
84
- type?: InputType;
85
- onClear?: () => void;
86
- showClear?: boolean;
87
- };
88
-
89
- export type InputProps = Omit<ComponentProps<"input">, "type"> & BaseProps;
90
-
91
- export const Input = ({
92
- type = "text",
93
- label,
94
- errors,
95
- info,
96
- className,
97
- style,
98
- value,
99
- layout = "default",
100
- layoutDirection = "default",
101
- ref,
102
- onClear,
103
- showClear = false,
104
- onChange,
105
- buttonStyle = "none",
106
- iconStart,
107
- iconEnd,
108
- ...props
109
- }: InputProps) => {
110
- const [eyeVisible, setEyeVisible] = useState(false);
111
-
112
- // Derive initial hasValue from controlled value or defaultValue.
113
- // IMPORTANT: do NOT call setHasValue inside a ref callback - it triggers
114
- // a re-render which creates a new ref callback, which React treats as a
115
- // changed ref, repeating the cycle indefinitely (infinite loop).
116
- const [hasValue, setHasValue] = useState(() => {
117
- if (value !== undefined && value !== null && value !== "") return true;
118
- const dv = props.defaultValue;
119
- if (dv !== undefined && dv !== null && dv !== "") return true;
120
- return false;
121
- });
122
-
123
- // Sync hasValue when controlled value changes externally (e.g. form reset)
124
- useEffect(() => {
125
- if (value !== undefined) {
126
- setHasValue(String(value).length > 0);
127
- }
128
- }, [value]);
129
-
130
- const inputRef = useRef<HTMLInputElement | null>(null);
131
-
132
- const isPassword = type === "password";
133
- const isSearch = type === "search";
134
-
135
- const inputType = isPassword && eyeVisible ? "text" : type;
136
- // useId() guarantees a stable, unique id per Input instance. Falling back
137
- // to `name` previously broke radio/checkbox siblings (they share a name,
138
- // so every label's htmlFor pointed to the first input in the DOM).
139
- const generatedId = useId();
140
- const inputId = props.id ?? generatedId;
141
-
142
- const showPasswordToggle = isPassword && hasValue;
143
- const showClearButton =
144
- showClear && hasValue && clearableInputTypes.includes(type);
145
-
146
- const stylesToApplyFields = styledInputTypes.includes(type);
147
- const stylesFitWidth = fitWidthInputTypes.includes(type);
148
- const hasButtonStyle = buttonStyle !== "none";
149
- const buttonToneClass = hasButtonStyle
150
- ? sButton[`tone_${buttonStyle}`]
151
- : undefined;
152
-
153
- const isDateFamily = dateInputTypes.includes(type);
154
- const isTimeFamily = type === "time";
155
-
156
- const showDateCalendarIcon = isDateFamily && !iconEnd;
157
- const showTimeClockIcon = isTimeFamily && !iconEnd;
158
-
159
- const startActionCount = iconStart ? 1 : isSearch ? 1 : 0;
160
- const endActionCount = [
161
- showClearButton,
162
- showPasswordToggle,
163
- showDateCalendarIcon,
164
- showTimeClockIcon,
165
- iconEnd,
166
- ].filter(Boolean).length;
167
-
168
- const dynamicInputStyle = {
169
- ...style,
170
- };
171
-
172
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
173
- setHasValue(e.target.value.length > 0);
174
- onChange?.(e);
175
- };
176
-
177
- // A programmatic value set (e.g. react-hook-form populating a field on reset)
178
- // sets the DOM value via the ref WITHOUT firing onChange, so `hasValue` can lag
179
- // the real value and the reveal/clear actions stay hidden on a pre-filled
180
- // field. Re-sync from the actual value on focus so they appear as soon as the
181
- // field is interacted with.
182
- const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
183
- setHasValue(e.target.value.length > 0);
184
- props.onFocus?.(e);
185
- };
186
-
187
- const handleClear = () => {
188
- const element = inputRef.current;
189
- if (!element) return;
190
-
191
- if (onClear) {
192
- onClear();
193
- return;
194
- }
195
-
196
- if (onChange) {
197
- element.value = "";
198
- setHasValue(false);
199
- const event = {
200
- target: element,
201
- currentTarget: element,
202
- } as React.ChangeEvent<HTMLInputElement>;
203
- onChange(event);
204
- element.focus();
205
- return;
206
- }
207
-
208
- element.value = "";
209
- setHasValue(false);
210
- element.focus();
211
- };
212
-
213
- // Stable ref callback - memoized so React doesn't treat it as a new ref
214
- // on every render. Does NOT call setState (would cause infinite loop).
215
- const refCallback = useCallback(
216
- (element: HTMLInputElement | null) => {
217
- inputRef.current = element;
218
- if (ref) {
219
- if (typeof ref === "function") {
220
- ref(element);
221
- } else {
222
- ref.current = element;
223
- }
224
- }
225
- },
226
- [ref],
227
- );
228
-
229
- return (
230
- <div className={clsx(sCommon.container, className)} style={style}>
231
- <div
232
- className={clsx(
233
- sCommon.field_container,
234
- layout !== "default" && sCommon[layout],
235
- layout !== "default" &&
236
- layoutDirection !== "default" &&
237
- sCommon[layoutDirection],
238
- )}
239
- >
240
- {label && (
241
- <label className={sCommon.label} htmlFor={inputId}>
242
- {label}
243
- </label>
244
- )}
245
- <div
246
- className={clsx(
247
- sCommon.field_wrapper,
248
- s.native_field_wrapper,
249
- hasButtonStyle && sCommon.field_wrapper_variant,
250
- hasButtonStyle && buttonToneClass,
251
- stylesFitWidth && sCommon.field_wrapper_fit,
252
- )}
253
- data-start-actions={
254
- startActionCount > 0 ? startActionCount : undefined
255
- }
256
- data-end-actions={
257
- endActionCount > 0 ? Math.min(3, endActionCount) : undefined
258
- }
259
- >
260
- {iconStart ? (
261
- <div className={clsx(sCommon.icon_start)}>{iconStart}</div>
262
- ) : isSearch ? (
263
- <Icon className={clsx(sCommon.icon_start)}>
264
- <IconSearch />
265
- </Icon>
266
- ) : null}
267
-
268
- <input
269
- id={inputId}
270
- name={props.name}
271
- type={inputType}
272
- value={value}
273
- ref={refCallback}
274
- className={clsx(
275
- stylesToApplyFields &&
276
- buttonStyle === "none" &&
277
- sCommon.field_default,
278
- stylesToApplyFields &&
279
- hasButtonStyle && [
280
- sCommon.field_full_width,
281
- sCommon.field_cursor_text,
282
- sCommon.field_button_variant,
283
- buttonToneClass,
284
- sButton.base,
285
- sButton[buttonStyle],
286
- sButton.input_focus,
287
- ],
288
- )}
289
- style={dynamicInputStyle}
290
- {...props}
291
- onChange={handleInputChange}
292
- onFocus={handleFocus}
293
- />
294
-
295
- <div className={clsx(sCommon.action_end_wrapper)}>
296
- {showClearButton && (
297
- <Icon onClick={handleClear} aria-label="Clear input">
298
- <IconX />
299
- </Icon>
300
- )}
301
- {showPasswordToggle && (
302
- <Icon
303
- onClick={(e) => {
304
- e.preventDefault();
305
- setEyeVisible((v) => !v);
306
- }}
307
- aria-label={eyeVisible ? "Hide password" : "Show password"}
308
- >
309
- {eyeVisible ? <IconKeyOff /> : <IconKey />}
310
- </Icon>
311
- )}
312
- {showDateCalendarIcon && (
313
- <Icon
314
- onClick={() => {
315
- try {
316
- inputRef.current?.showPicker();
317
- } catch {
318
- // showPicker is not supported in all browsers.
319
- }
320
- }}
321
- clickable
322
- >
323
- <IconCalendar />
324
- </Icon>
325
- )}
326
- {showTimeClockIcon && (
327
- <Icon
328
- onClick={() => {
329
- try {
330
- inputRef.current?.showPicker();
331
- } catch {
332
- // showPicker is not supported in all browsers.
333
- }
334
- }}
335
- clickable
336
- >
337
- <IconClock />
338
- </Icon>
339
- )}
340
- {iconEnd}
341
- </div>
342
- </div>
343
- </div>
344
-
345
- <FieldMessage errors={errors} info={info} />
346
- </div>
347
- );
348
- };
1
+ import {
2
+ type ComponentProps,
3
+ useCallback,
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import clsx from "clsx";
9
+ import {
10
+ IconKey,
11
+ IconKeyOff,
12
+ IconSearch,
13
+ IconX,
14
+ IconCalendar,
15
+ IconClock,
16
+ } from "@tabler/icons-react";
17
+ import sCommon from "../common/input-field-common.module.css";
18
+ import sButton from "../../button/button.module.css";
19
+ import s from "../common/input-field-native.module.css";
20
+ import { FieldMessage } from "../common/field-message/field-message";
21
+ import { Icon } from "../../../primitives/icon/icon";
22
+ import type { BaseInputFieldProps } from "../common/types";
23
+
24
+ export type InputType =
25
+ | "text"
26
+ | "email"
27
+ | "tel"
28
+ | "url"
29
+ | "password"
30
+ | "search"
31
+ | "number"
32
+ | "file"
33
+ | "radio"
34
+ | "date"
35
+ | "month"
36
+ | "week"
37
+ | "time"
38
+ | "datetime-local"
39
+ | "checkbox"
40
+ | "color"
41
+ | "range";
42
+
43
+ const styledInputTypes: readonly InputType[] = [
44
+ "text",
45
+ "email",
46
+ "tel",
47
+ "url",
48
+ "password",
49
+ "search",
50
+ "number",
51
+ "file",
52
+ "date",
53
+ "month",
54
+ "week",
55
+ "time",
56
+ "datetime-local",
57
+ ] as const;
58
+
59
+ const clearableInputTypes: readonly InputType[] = [
60
+ "text",
61
+ "email",
62
+ "tel",
63
+ "url",
64
+ "password",
65
+ "search",
66
+ "number",
67
+ "file",
68
+ ] as const;
69
+
70
+ const fitWidthInputTypes: readonly InputType[] = [
71
+ "checkbox",
72
+ "radio",
73
+ "color",
74
+ ] as const;
75
+ const dateInputTypes: readonly InputType[] = [
76
+ "date",
77
+ "month",
78
+ "week",
79
+ "datetime-local",
80
+ ] as const;
81
+
82
+ type BaseProps = BaseInputFieldProps & {
83
+ type?: InputType;
84
+ onClear?: () => void;
85
+ showClear?: boolean;
86
+ };
87
+
88
+ export type InputProps = Omit<ComponentProps<"input">, "type"> & BaseProps;
89
+
90
+ export const Input = ({
91
+ type = "text",
92
+ label,
93
+ errors,
94
+ info,
95
+ className,
96
+ style,
97
+ value,
98
+ layout = "default",
99
+ layoutDirection = "default",
100
+ ref,
101
+ onClear,
102
+ showClear = false,
103
+ onChange,
104
+ buttonStyle = "none",
105
+ iconStart,
106
+ iconEnd,
107
+ ...props
108
+ }: InputProps) => {
109
+ const [eyeVisible, setEyeVisible] = useState(false);
110
+
111
+ // Derive initial hasValue from controlled value or defaultValue.
112
+ // IMPORTANT: do NOT call setHasValue inside a ref callback - it triggers
113
+ // a re-render which creates a new ref callback, which React treats as a
114
+ // changed ref, repeating the cycle indefinitely (infinite loop).
115
+ const [hasValue, setHasValue] = useState(() => {
116
+ if (value !== undefined && value !== null && value !== "") return true;
117
+ const dv = props.defaultValue;
118
+ if (dv !== undefined && dv !== null && dv !== "") return true;
119
+ return false;
120
+ });
121
+
122
+ // Sync hasValue when controlled value changes externally (e.g. form reset),
123
+ // in the render that sees the change.
124
+ const [seenValue, setSeenValue] = useState(value);
125
+ if (!Object.is(value, seenValue)) {
126
+ setSeenValue(value);
127
+ setHasValue(value !== undefined && String(value).length > 0);
128
+ }
129
+
130
+ const inputRef = useRef<HTMLInputElement | null>(null);
131
+
132
+ const isPassword = type === "password";
133
+ const isSearch = type === "search";
134
+
135
+ const inputType = isPassword && eyeVisible ? "text" : type;
136
+ // useId() guarantees a stable, unique id per Input instance. Falling back
137
+ // to `name` previously broke radio/checkbox siblings (they share a name,
138
+ // so every label's htmlFor pointed to the first input in the DOM).
139
+ const generatedId = useId();
140
+ const inputId = props.id ?? generatedId;
141
+
142
+ const showPasswordToggle = isPassword && hasValue;
143
+ const showClearButton =
144
+ showClear && hasValue && clearableInputTypes.includes(type);
145
+
146
+ const stylesToApplyFields = styledInputTypes.includes(type);
147
+ const stylesFitWidth = fitWidthInputTypes.includes(type);
148
+ const hasButtonStyle = buttonStyle !== "none";
149
+ const buttonToneClass = hasButtonStyle
150
+ ? sButton[`tone_${buttonStyle}`]
151
+ : undefined;
152
+
153
+ const isDateFamily = dateInputTypes.includes(type);
154
+ const isTimeFamily = type === "time";
155
+
156
+ const showDateCalendarIcon = isDateFamily && !iconEnd;
157
+ const showTimeClockIcon = isTimeFamily && !iconEnd;
158
+
159
+ const startActionCount = iconStart ? 1 : isSearch ? 1 : 0;
160
+ const endActionCount = [
161
+ showClearButton,
162
+ showPasswordToggle,
163
+ showDateCalendarIcon,
164
+ showTimeClockIcon,
165
+ iconEnd,
166
+ ].filter(Boolean).length;
167
+
168
+ const dynamicInputStyle = {
169
+ ...style,
170
+ };
171
+
172
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
173
+ setHasValue(e.target.value.length > 0);
174
+ onChange?.(e);
175
+ };
176
+
177
+ // A programmatic value set (e.g. react-hook-form populating a field on reset)
178
+ // sets the DOM value via the ref WITHOUT firing onChange, so `hasValue` can lag
179
+ // the real value and the reveal/clear actions stay hidden on a pre-filled
180
+ // field. Re-sync from the actual value on focus so they appear as soon as the
181
+ // field is interacted with.
182
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
183
+ setHasValue(e.target.value.length > 0);
184
+ props.onFocus?.(e);
185
+ };
186
+
187
+ const handleClear = () => {
188
+ const element = inputRef.current;
189
+ if (!element) return;
190
+
191
+ if (onClear) {
192
+ onClear();
193
+ return;
194
+ }
195
+
196
+ if (onChange) {
197
+ element.value = "";
198
+ setHasValue(false);
199
+ const event = {
200
+ target: element,
201
+ currentTarget: element,
202
+ } as React.ChangeEvent<HTMLInputElement>;
203
+ onChange(event);
204
+ element.focus();
205
+ return;
206
+ }
207
+
208
+ element.value = "";
209
+ setHasValue(false);
210
+ element.focus();
211
+ };
212
+
213
+ // Stable ref callback - memoized so React doesn't treat it as a new ref
214
+ // on every render. Does NOT call setState (would cause infinite loop).
215
+ const refCallback = useCallback(
216
+ (element: HTMLInputElement | null) => {
217
+ inputRef.current = element;
218
+ if (ref) {
219
+ if (typeof ref === "function") {
220
+ ref(element);
221
+ } else {
222
+ ref.current = element;
223
+ }
224
+ }
225
+ },
226
+ [ref],
227
+ );
228
+
229
+ return (
230
+ <div className={clsx(sCommon.container, className)} style={style}>
231
+ <div
232
+ className={clsx(
233
+ sCommon.field_container,
234
+ layout !== "default" && sCommon[layout],
235
+ layout !== "default" &&
236
+ layoutDirection !== "default" &&
237
+ sCommon[layoutDirection],
238
+ )}
239
+ >
240
+ {label && (
241
+ <label className={sCommon.label} htmlFor={inputId}>
242
+ {label}
243
+ </label>
244
+ )}
245
+ <div
246
+ className={clsx(
247
+ sCommon.field_wrapper,
248
+ s.native_field_wrapper,
249
+ hasButtonStyle && sCommon.field_wrapper_variant,
250
+ hasButtonStyle && buttonToneClass,
251
+ stylesFitWidth && sCommon.field_wrapper_fit,
252
+ )}
253
+ data-start-actions={
254
+ startActionCount > 0 ? startActionCount : undefined
255
+ }
256
+ data-end-actions={
257
+ endActionCount > 0 ? Math.min(3, endActionCount) : undefined
258
+ }
259
+ >
260
+ {iconStart ? (
261
+ <div className={clsx(sCommon.icon_start)}>{iconStart}</div>
262
+ ) : isSearch ? (
263
+ <Icon className={clsx(sCommon.icon_start)}>
264
+ <IconSearch />
265
+ </Icon>
266
+ ) : null}
267
+
268
+ <input
269
+ id={inputId}
270
+ name={props.name}
271
+ type={inputType}
272
+ value={value}
273
+ ref={refCallback}
274
+ className={clsx(
275
+ stylesToApplyFields &&
276
+ buttonStyle === "none" &&
277
+ sCommon.field_default,
278
+ stylesToApplyFields &&
279
+ hasButtonStyle && [
280
+ sCommon.field_full_width,
281
+ sCommon.field_cursor_text,
282
+ sCommon.field_button_variant,
283
+ buttonToneClass,
284
+ sButton.base,
285
+ sButton[buttonStyle],
286
+ sButton.input_focus,
287
+ ],
288
+ )}
289
+ style={dynamicInputStyle}
290
+ {...props}
291
+ onChange={handleInputChange}
292
+ onFocus={handleFocus}
293
+ />
294
+
295
+ <div className={clsx(sCommon.action_end_wrapper)}>
296
+ {showClearButton && (
297
+ <Icon onClick={handleClear} aria-label="Clear input">
298
+ <IconX />
299
+ </Icon>
300
+ )}
301
+ {showPasswordToggle && (
302
+ <Icon
303
+ onClick={(e) => {
304
+ e.preventDefault();
305
+ setEyeVisible((v) => !v);
306
+ }}
307
+ aria-label={eyeVisible ? "Hide password" : "Show password"}
308
+ >
309
+ {eyeVisible ? <IconKeyOff /> : <IconKey />}
310
+ </Icon>
311
+ )}
312
+ {showDateCalendarIcon && (
313
+ <Icon
314
+ onClick={() => {
315
+ try {
316
+ inputRef.current?.showPicker();
317
+ } catch {
318
+ // showPicker is not supported in all browsers.
319
+ }
320
+ }}
321
+ clickable
322
+ >
323
+ <IconCalendar />
324
+ </Icon>
325
+ )}
326
+ {showTimeClockIcon && (
327
+ <Icon
328
+ onClick={() => {
329
+ try {
330
+ inputRef.current?.showPicker();
331
+ } catch {
332
+ // showPicker is not supported in all browsers.
333
+ }
334
+ }}
335
+ clickable
336
+ >
337
+ <IconClock />
338
+ </Icon>
339
+ )}
340
+ {iconEnd}
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <FieldMessage errors={errors} info={info} />
346
+ </div>
347
+ );
348
+ };