@letar/forms 1.0.3 → 1.1.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.
@@ -0,0 +1,849 @@
1
+ import { createField, FieldLabel, FieldError, useDebounce, FieldTooltip, FieldWrapper, useDeclarativeForm, useDeclarativeFormOptional } from './chunk-HWVOFWAT.js';
2
+ import { Field, Box, Input, Spinner, List, Text, parseColor, ColorPicker, HStack, Portal, FileUpload, Button, Icon, PinInput, Group, useFileUploadContext, Float, IconButton } from '@chakra-ui/react';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { LuUpload, LuX, LuFile } from 'react-icons/lu';
6
+ import { withMask } from 'use-mask-input';
7
+
8
+ // src/lib/declarative/form-fields/specialized/providers/dadata.ts
9
+ var DADATA_URL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address";
10
+ function createDaDataProvider(config) {
11
+ const { token, baseUrl = DADATA_URL } = config;
12
+ return {
13
+ async getSuggestions(query, options) {
14
+ const body = {
15
+ query,
16
+ count: options?.count ?? 10
17
+ };
18
+ if (options?.bounds) {
19
+ if (options.bounds.from) body.from_bound = { value: options.bounds.from };
20
+ if (options.bounds.to) body.to_bound = { value: options.bounds.to };
21
+ }
22
+ if (options?.filters) {
23
+ body.locations = [options.filters];
24
+ }
25
+ const response = await fetch(baseUrl, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ Accept: "application/json",
30
+ Authorization: `Token ${token}`
31
+ },
32
+ body: JSON.stringify(body)
33
+ });
34
+ if (!response.ok) {
35
+ return [];
36
+ }
37
+ const data = await response.json();
38
+ const suggestions = data.suggestions ?? [];
39
+ return suggestions.map(
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ (s) => ({
42
+ label: s.value,
43
+ value: s.value,
44
+ data: s.data
45
+ })
46
+ );
47
+ }
48
+ };
49
+ }
50
+ function useAddressProvider(propProvider, token) {
51
+ const formContext = useDeclarativeFormOptional();
52
+ if (propProvider) return propProvider;
53
+ if (formContext?.addressProvider) return formContext.addressProvider;
54
+ if (token) return createDaDataProvider({ token });
55
+ return null;
56
+ }
57
+ var FieldAddress = createField({
58
+ displayName: "FieldAddress",
59
+ useFieldState: (props) => {
60
+ const { provider: propProvider, token, minChars = 3, debounceMs = 300, locations } = props;
61
+ const provider = useAddressProvider(propProvider, token);
62
+ const [inputValue, setInputValue] = useState("");
63
+ const [suggestions, setSuggestions] = useState([]);
64
+ const [isLoading, setIsLoading] = useState(false);
65
+ const [isOpen, setIsOpen] = useState(false);
66
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
67
+ const containerRef = useRef(null);
68
+ const initializedRef = useRef(false);
69
+ const debouncedQuery = useDebounce(inputValue, debounceMs);
70
+ const fetchSuggestions = useCallback(
71
+ async (query) => {
72
+ if (query.length < minChars || !provider) {
73
+ setSuggestions([]);
74
+ return;
75
+ }
76
+ setIsLoading(true);
77
+ try {
78
+ const results = await provider.getSuggestions(query, {
79
+ count: 10,
80
+ filters: locations ? Object.assign({}, ...locations) : void 0
81
+ });
82
+ setSuggestions(results);
83
+ setIsOpen(true);
84
+ } catch (error) {
85
+ console.error("Error loading address suggestions:", error);
86
+ setSuggestions([]);
87
+ } finally {
88
+ setIsLoading(false);
89
+ }
90
+ },
91
+ [provider, minChars, locations]
92
+ );
93
+ useEffect(() => {
94
+ if (debouncedQuery) {
95
+ fetchSuggestions(debouncedQuery);
96
+ } else {
97
+ setSuggestions([]);
98
+ setIsOpen(false);
99
+ }
100
+ }, [debouncedQuery, fetchSuggestions]);
101
+ useEffect(() => {
102
+ const handleClickOutside = (event) => {
103
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
104
+ setIsOpen(false);
105
+ }
106
+ };
107
+ document.addEventListener("mousedown", handleClickOutside);
108
+ return () => document.removeEventListener("mousedown", handleClickOutside);
109
+ }, []);
110
+ return {
111
+ inputValue,
112
+ setInputValue,
113
+ suggestions,
114
+ setSuggestions,
115
+ isLoading,
116
+ setIsLoading,
117
+ isOpen,
118
+ setIsOpen,
119
+ highlightedIndex,
120
+ setHighlightedIndex,
121
+ containerRef,
122
+ debouncedQuery,
123
+ fetchSuggestions,
124
+ initializedRef
125
+ };
126
+ },
127
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
128
+ const { valueOnly = false } = componentProps;
129
+ const {
130
+ inputValue,
131
+ setInputValue,
132
+ suggestions,
133
+ setSuggestions,
134
+ isLoading,
135
+ isOpen,
136
+ setIsOpen,
137
+ highlightedIndex,
138
+ setHighlightedIndex,
139
+ containerRef,
140
+ initializedRef
141
+ } = fieldState;
142
+ const fieldValue = field.state.value;
143
+ if (!initializedRef.current && fieldValue) {
144
+ const displayValue = typeof fieldValue === "string" ? fieldValue : fieldValue.value;
145
+ if (displayValue && displayValue !== inputValue) {
146
+ setInputValue(displayValue);
147
+ }
148
+ initializedRef.current = true;
149
+ }
150
+ const handleSelect = (suggestion) => {
151
+ setInputValue(suggestion.value);
152
+ setIsOpen(false);
153
+ setSuggestions([]);
154
+ if (valueOnly) {
155
+ field.handleChange(suggestion.value);
156
+ } else {
157
+ const addressValue = {
158
+ value: suggestion.value,
159
+ data: suggestion.data
160
+ };
161
+ field.handleChange(addressValue);
162
+ }
163
+ };
164
+ const handleKeyDown = (e) => {
165
+ if (!isOpen || suggestions.length === 0) {
166
+ return;
167
+ }
168
+ switch (e.key) {
169
+ case "ArrowDown":
170
+ e.preventDefault();
171
+ setHighlightedIndex(highlightedIndex < suggestions.length - 1 ? highlightedIndex + 1 : 0);
172
+ break;
173
+ case "ArrowUp":
174
+ e.preventDefault();
175
+ setHighlightedIndex(highlightedIndex > 0 ? highlightedIndex - 1 : suggestions.length - 1);
176
+ break;
177
+ case "Enter":
178
+ e.preventDefault();
179
+ if (highlightedIndex >= 0) {
180
+ handleSelect(suggestions[highlightedIndex]);
181
+ }
182
+ break;
183
+ case "Escape":
184
+ setIsOpen(false);
185
+ break;
186
+ }
187
+ };
188
+ return /* @__PURE__ */ jsxs(
189
+ Field.Root,
190
+ {
191
+ invalid: hasError,
192
+ required: resolved.required,
193
+ disabled: resolved.disabled,
194
+ readOnly: resolved.readOnly,
195
+ children: [
196
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
197
+ /* @__PURE__ */ jsxs(Box, { ref: containerRef, position: "relative", width: "100%", children: [
198
+ /* @__PURE__ */ jsx(
199
+ Input,
200
+ {
201
+ value: inputValue,
202
+ onChange: (e) => {
203
+ setInputValue(e.target.value);
204
+ setHighlightedIndex(-1);
205
+ },
206
+ onFocus: () => {
207
+ if (suggestions.length > 0) {
208
+ setIsOpen(true);
209
+ }
210
+ },
211
+ onBlur: field.handleBlur,
212
+ onKeyDown: handleKeyDown,
213
+ placeholder: resolved.placeholder ?? "Start typing address...",
214
+ "data-field-name": fullPath
215
+ }
216
+ ),
217
+ isLoading && /* @__PURE__ */ jsx(Box, { position: "absolute", right: 3, top: "50%", transform: "translateY(-50%)", children: /* @__PURE__ */ jsx(Spinner, { size: "sm" }) }),
218
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
219
+ List.Root,
220
+ {
221
+ position: "absolute",
222
+ zIndex: 10,
223
+ width: "100%",
224
+ bg: "bg.panel",
225
+ borderWidth: "1px",
226
+ borderRadius: "md",
227
+ shadow: "md",
228
+ maxH: "200px",
229
+ overflowY: "auto",
230
+ mt: 1,
231
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
232
+ List.Item,
233
+ {
234
+ px: 3,
235
+ py: 2,
236
+ cursor: "pointer",
237
+ bg: highlightedIndex === index ? "bg.muted" : void 0,
238
+ _hover: { bg: "bg.muted" },
239
+ onClick: () => handleSelect(suggestion),
240
+ onMouseEnter: () => setHighlightedIndex(index),
241
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "sm", children: suggestion.label })
242
+ },
243
+ suggestion.value + index
244
+ ))
245
+ }
246
+ )
247
+ ] }),
248
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
249
+ ]
250
+ }
251
+ );
252
+ }
253
+ });
254
+ var defaultSwatches = [
255
+ "#000000",
256
+ "#4A5568",
257
+ "#F56565",
258
+ "#ED64A6",
259
+ "#9F7AEA",
260
+ "#6B46C1",
261
+ "#4299E1",
262
+ "#0BC5EA",
263
+ "#38B2AC",
264
+ "#48BB78",
265
+ "#ECC94B",
266
+ "#DD6B20"
267
+ ];
268
+ var FieldColorPicker = createField({
269
+ displayName: "FieldColorPicker",
270
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps }) => {
271
+ const {
272
+ swatches = defaultSwatches,
273
+ size = "md",
274
+ showArea = true,
275
+ showEyeDropper = true,
276
+ showSliders = true,
277
+ showInput = true
278
+ } = componentProps;
279
+ const currentValue = field.state.value || "#000000";
280
+ let parsedColor;
281
+ try {
282
+ parsedColor = parseColor(currentValue);
283
+ } catch {
284
+ parsedColor = parseColor("#000000");
285
+ }
286
+ return /* @__PURE__ */ jsxs(
287
+ Field.Root,
288
+ {
289
+ invalid: hasError,
290
+ required: resolved.required,
291
+ disabled: resolved.disabled,
292
+ readOnly: resolved.readOnly,
293
+ children: [
294
+ /* @__PURE__ */ jsxs(
295
+ ColorPicker.Root,
296
+ {
297
+ value: parsedColor,
298
+ onValueChange: (details) => {
299
+ field.handleChange(details.valueAsString);
300
+ },
301
+ disabled: resolved.disabled,
302
+ readOnly: resolved.readOnly,
303
+ size,
304
+ children: [
305
+ /* @__PURE__ */ jsx(ColorPicker.HiddenInput, { name: fullPath }),
306
+ resolved.label && /* @__PURE__ */ jsxs(ColorPicker.Label, { children: [
307
+ resolved.tooltip ? /* @__PURE__ */ jsxs(HStack, { gap: 1, children: [
308
+ /* @__PURE__ */ jsx("span", { children: resolved.label }),
309
+ /* @__PURE__ */ jsx(FieldTooltip, { ...resolved.tooltip })
310
+ ] }) : resolved.label,
311
+ resolved.required && /* @__PURE__ */ jsx(Field.RequiredIndicator, {})
312
+ ] }),
313
+ /* @__PURE__ */ jsxs(ColorPicker.Control, { children: [
314
+ showInput && /* @__PURE__ */ jsx(ColorPicker.ChannelInput, { channel: "hex" }),
315
+ /* @__PURE__ */ jsx(ColorPicker.Trigger, {})
316
+ ] }),
317
+ /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(ColorPicker.Positioner, { children: /* @__PURE__ */ jsxs(ColorPicker.Content, { children: [
318
+ showArea && /* @__PURE__ */ jsx(ColorPicker.Area, {}),
319
+ (showEyeDropper || showSliders) && /* @__PURE__ */ jsxs(HStack, { children: [
320
+ showEyeDropper && /* @__PURE__ */ jsx(ColorPicker.EyeDropper, { size: "xs", variant: "outline" }),
321
+ showSliders && /* @__PURE__ */ jsx(ColorPicker.Sliders, {})
322
+ ] }),
323
+ swatches.length > 0 && /* @__PURE__ */ jsx(ColorPicker.SwatchGroup, { children: swatches.map((swatch) => /* @__PURE__ */ jsx(ColorPicker.SwatchTrigger, { value: swatch, children: /* @__PURE__ */ jsx(ColorPicker.Swatch, { value: swatch, boxSize: "4.5" }) }, swatch)) })
324
+ ] }) }) })
325
+ ]
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
329
+ ]
330
+ }
331
+ );
332
+ }
333
+ });
334
+ function FileImageList({ clearable }) {
335
+ const fileUpload = useFileUploadContext();
336
+ if (fileUpload.acceptedFiles.length === 0) {
337
+ return null;
338
+ }
339
+ return /* @__PURE__ */ jsx(HStack, { wrap: "wrap", gap: "3", mt: "2", children: fileUpload.acceptedFiles.map((file) => /* @__PURE__ */ jsxs(FileUpload.Item, { file, p: "2", width: "auto", pos: "relative", children: [
340
+ clearable && /* @__PURE__ */ jsx(Float, { placement: "top-end", children: /* @__PURE__ */ jsx(FileUpload.ItemDeleteTrigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "2xs", variant: "solid", colorPalette: "red", rounded: "full", children: /* @__PURE__ */ jsx(LuX, {}) }) }) }),
341
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { type: "image/*", asChild: true, children: /* @__PURE__ */ jsx(FileUpload.ItemPreviewImage, { boxSize: "16", rounded: "md", objectFit: "cover" }) }),
342
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { type: ".*", asChild: true, children: /* @__PURE__ */ jsx(Icon, { fontSize: "4xl", color: "fg.muted", children: /* @__PURE__ */ jsx(LuFile, {}) }) })
343
+ ] }, file.name)) });
344
+ }
345
+ function FileList({ showSize, clearable }) {
346
+ const fileUpload = useFileUploadContext();
347
+ if (fileUpload.acceptedFiles.length === 0) {
348
+ return null;
349
+ }
350
+ return /* @__PURE__ */ jsx(FileUpload.ItemGroup, { mt: "2", children: fileUpload.acceptedFiles.map((file) => /* @__PURE__ */ jsxs(FileUpload.Item, { file, children: [
351
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { asChild: true, children: /* @__PURE__ */ jsx(Icon, { fontSize: "lg", color: "fg.muted", children: /* @__PURE__ */ jsx(LuFile, {}) }) }),
352
+ showSize ? /* @__PURE__ */ jsxs(FileUpload.ItemContent, { children: [
353
+ /* @__PURE__ */ jsx(FileUpload.ItemName, {}),
354
+ /* @__PURE__ */ jsx(FileUpload.ItemSizeText, {})
355
+ ] }) : /* @__PURE__ */ jsx(FileUpload.ItemName, { flex: "1" }),
356
+ clearable && /* @__PURE__ */ jsx(FileUpload.ItemDeleteTrigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { variant: "ghost", color: "fg.muted", size: "xs", children: /* @__PURE__ */ jsx(LuX, {}) }) })
357
+ ] }, file.name)) });
358
+ }
359
+ var FieldFileUpload = createField({
360
+ displayName: "FieldFileUpload",
361
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps }) => {
362
+ const {
363
+ accept,
364
+ maxFileSize,
365
+ maxFiles = 1,
366
+ variant = "button",
367
+ showSize = false,
368
+ clearable = true,
369
+ dropzoneLabel = "Drag and drop files here",
370
+ dropzoneDescription,
371
+ buttonText = "Upload file"
372
+ } = componentProps;
373
+ const placeholder = resolved.placeholder ?? "Select file(s)";
374
+ const normalizedAccept = accept ? typeof accept === "string" ? accept.split(",").map((s) => s.trim()) : accept : void 0;
375
+ const isImageUpload = normalizedAccept?.some((type) => type.startsWith("image/") || type === "image/*");
376
+ return /* @__PURE__ */ jsxs(Field.Root, { invalid: hasError, required: resolved.required, disabled: resolved.disabled, children: [
377
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
378
+ /* @__PURE__ */ jsxs(
379
+ FileUpload.Root,
380
+ {
381
+ maxFiles,
382
+ maxFileSize,
383
+ accept: normalizedAccept,
384
+ disabled: resolved.disabled,
385
+ onFileChange: (details) => {
386
+ field.handleChange(details.acceptedFiles);
387
+ },
388
+ "data-field-name": fullPath,
389
+ children: [
390
+ /* @__PURE__ */ jsx(FileUpload.HiddenInput, { onBlur: field.handleBlur }),
391
+ variant === "button" && /* @__PURE__ */ jsxs(Fragment, { children: [
392
+ /* @__PURE__ */ jsx(FileUpload.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
393
+ /* @__PURE__ */ jsx(LuUpload, {}),
394
+ buttonText
395
+ ] }) }),
396
+ isImageUpload ? /* @__PURE__ */ jsx(FileImageList, { clearable }) : /* @__PURE__ */ jsx(FileList, { showSize, clearable })
397
+ ] }),
398
+ variant === "dropzone" && /* @__PURE__ */ jsxs(Fragment, { children: [
399
+ /* @__PURE__ */ jsxs(FileUpload.Dropzone, { children: [
400
+ /* @__PURE__ */ jsx(Icon, { size: "md", color: "fg.muted", children: /* @__PURE__ */ jsx(LuUpload, {}) }),
401
+ /* @__PURE__ */ jsxs(FileUpload.DropzoneContent, { children: [
402
+ /* @__PURE__ */ jsx(Box, { children: dropzoneLabel }),
403
+ dropzoneDescription && /* @__PURE__ */ jsx(Text, { color: "fg.muted", children: dropzoneDescription })
404
+ ] })
405
+ ] }),
406
+ isImageUpload ? /* @__PURE__ */ jsx(FileImageList, { clearable }) : /* @__PURE__ */ jsx(FileList, { showSize, clearable })
407
+ ] }),
408
+ variant === "input" && /* @__PURE__ */ jsx(Input, { asChild: true, children: /* @__PURE__ */ jsx(FileUpload.Trigger, { children: /* @__PURE__ */ jsx(FileUpload.Context, { children: ({ acceptedFiles }) => {
409
+ if (acceptedFiles.length === 1) {
410
+ return /* @__PURE__ */ jsx("span", { children: acceptedFiles[0].name });
411
+ }
412
+ if (acceptedFiles.length > 1) {
413
+ return /* @__PURE__ */ jsxs("span", { children: [
414
+ acceptedFiles.length,
415
+ " files"
416
+ ] });
417
+ }
418
+ return /* @__PURE__ */ jsx(Text, { color: "fg.subtle", children: placeholder });
419
+ } }) }) })
420
+ ]
421
+ }
422
+ ),
423
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
424
+ ] });
425
+ }
426
+ });
427
+ var FieldOTPInput = createField({
428
+ displayName: "FieldOTPInput",
429
+ useFieldState: (props) => {
430
+ const [countdown, setCountdown] = useState(0);
431
+ const [isResending, setIsResending] = useState(false);
432
+ useEffect(() => {
433
+ if (countdown <= 0) {
434
+ return;
435
+ }
436
+ const timer = setInterval(() => {
437
+ setCountdown((prev) => prev - 1);
438
+ }, 1e3);
439
+ return () => clearInterval(timer);
440
+ }, [countdown]);
441
+ const handleResend = useCallback(async () => {
442
+ if (!props.onResend || countdown > 0) {
443
+ return;
444
+ }
445
+ setIsResending(true);
446
+ try {
447
+ await props.onResend();
448
+ setCountdown(props.resendTimeout ?? 60);
449
+ } finally {
450
+ setIsResending(false);
451
+ }
452
+ }, [props.onResend, countdown, props.resendTimeout]);
453
+ const formatCountdown = (seconds) => {
454
+ const mins = Math.floor(seconds / 60);
455
+ const secs = seconds % 60;
456
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
457
+ };
458
+ const formContext = useDeclarativeForm();
459
+ return { countdown, isResending, handleResend, formatCountdown, formContext };
460
+ },
461
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
462
+ const { length = 6, autoSubmit = false, type = "numeric", mask = false, onResend } = componentProps;
463
+ const { countdown, isResending, handleResend, formatCountdown, formContext } = fieldState;
464
+ const value = field.state.value ?? "";
465
+ const handleValueComplete = (details) => {
466
+ field.handleChange(details.valueAsString);
467
+ if (autoSubmit && details.valueAsString.length === length) {
468
+ formContext.form.handleSubmit();
469
+ }
470
+ };
471
+ return /* @__PURE__ */ jsx(FieldWrapper, { resolved, hasError, errorMessage, fullPath, children: /* @__PURE__ */ jsxs(Box, { children: [
472
+ /* @__PURE__ */ jsxs(
473
+ PinInput.Root,
474
+ {
475
+ value: value.split(""),
476
+ onValueComplete: handleValueComplete,
477
+ onValueChange: (details) => field.handleChange(details.valueAsString),
478
+ count: length,
479
+ type,
480
+ mask,
481
+ otp: true,
482
+ children: [
483
+ /* @__PURE__ */ jsx(PinInput.Control, { children: /* @__PURE__ */ jsx(HStack, { gap: 2, children: Array.from({ length }).map((_, index) => /* @__PURE__ */ jsx(PinInput.Input, { index, "data-field-name": index === 0 ? fullPath : void 0 }, index)) }) }),
484
+ /* @__PURE__ */ jsx(PinInput.HiddenInput, {})
485
+ ]
486
+ }
487
+ ),
488
+ onResend && /* @__PURE__ */ jsx(HStack, { mt: 3, justify: "center", children: countdown > 0 ? /* @__PURE__ */ jsxs(Text, { fontSize: "sm", color: "fg.muted", children: [
489
+ "Redo in ",
490
+ formatCountdown(countdown)
491
+ ] }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: handleResend, disabled: isResending, loading: isResending, children: "Submit again" }) })
492
+ ] }) });
493
+ }
494
+ });
495
+ var PHONE_MASKS = {
496
+ RU: "+7 (999) 999-99-99",
497
+ US: "+1 (999) 999-9999",
498
+ UK: "+44 9999 999999",
499
+ DE: "+49 999 99999999",
500
+ FR: "+33 9 99 99 99 99",
501
+ IT: "+39 999 999 9999",
502
+ ES: "+34 999 99 99 99",
503
+ CN: "+86 999 9999 9999",
504
+ JP: "+81 99 9999 9999",
505
+ KR: "+82 99 9999 9999",
506
+ BY: "+375 (99) 999-99-99",
507
+ KZ: "+7 (999) 999-99-99",
508
+ UA: "+380 (99) 999-99-99"
509
+ };
510
+ var COUNTRY_FLAGS = {
511
+ RU: "\u{1F1F7}\u{1F1FA}",
512
+ US: "\u{1F1FA}\u{1F1F8}",
513
+ UK: "\u{1F1EC}\u{1F1E7}",
514
+ DE: "\u{1F1E9}\u{1F1EA}",
515
+ FR: "\u{1F1EB}\u{1F1F7}",
516
+ IT: "\u{1F1EE}\u{1F1F9}",
517
+ ES: "\u{1F1EA}\u{1F1F8}",
518
+ CN: "\u{1F1E8}\u{1F1F3}",
519
+ JP: "\u{1F1EF}\u{1F1F5}",
520
+ KR: "\u{1F1F0}\u{1F1F7}",
521
+ BY: "\u{1F1E7}\u{1F1FE}",
522
+ KZ: "\u{1F1F0}\u{1F1FF}",
523
+ UA: "\u{1F1FA}\u{1F1E6}"
524
+ };
525
+ var FieldPhone = createField({
526
+ displayName: "FieldPhone",
527
+ useFieldState: (props) => {
528
+ const { country = "RU", autoUnmask = false } = props;
529
+ const mask = PHONE_MASKS[country];
530
+ const maskRef = useCallback(
531
+ (element) => {
532
+ if (element && mask) {
533
+ const maskCallback = withMask(mask, {
534
+ showMaskOnFocus: true,
535
+ clearIncomplete: true,
536
+ autoUnmask
537
+ });
538
+ maskCallback(element);
539
+ }
540
+ },
541
+ [mask, autoUnmask]
542
+ );
543
+ return { maskRef };
544
+ },
545
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
546
+ const { country = "RU", showFlag = false } = componentProps;
547
+ const flag = COUNTRY_FLAGS[country];
548
+ const mask = PHONE_MASKS[country];
549
+ const value = field.state.value ?? "";
550
+ const resolvedPlaceholder = resolved.placeholder ?? mask?.toString().replace(/9/g, "_");
551
+ return /* @__PURE__ */ jsxs(
552
+ Field.Root,
553
+ {
554
+ invalid: hasError,
555
+ required: resolved.required,
556
+ disabled: resolved.disabled,
557
+ readOnly: resolved.readOnly,
558
+ children: [
559
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
560
+ /* @__PURE__ */ jsxs(Group, { attached: true, children: [
561
+ showFlag && /* @__PURE__ */ jsx(Text, { px: 3, display: "flex", alignItems: "center", bg: "bg.muted", borderWidth: "1px", borderRightWidth: "0", children: flag }),
562
+ /* @__PURE__ */ jsx(
563
+ Input,
564
+ {
565
+ ref: fieldState.maskRef,
566
+ value,
567
+ onChange: (e) => field.handleChange(e.target.value),
568
+ onBlur: field.handleBlur,
569
+ placeholder: resolvedPlaceholder,
570
+ "data-field-name": fullPath,
571
+ type: "tel",
572
+ inputMode: "tel",
573
+ autoComplete: "tel"
574
+ }
575
+ )
576
+ ] }),
577
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
578
+ ]
579
+ }
580
+ );
581
+ }
582
+ });
583
+ var FieldPinInput = createField({
584
+ displayName: "FieldPinInput",
585
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps }) => {
586
+ const {
587
+ count = 4,
588
+ mask,
589
+ otp,
590
+ type = "numeric",
591
+ size = "md",
592
+ variant = "outline",
593
+ attached,
594
+ onComplete
595
+ } = componentProps;
596
+ const stringValue = field.state.value ?? "";
597
+ const arrayValue = stringValue.split("").slice(0, count);
598
+ while (arrayValue.length < count) {
599
+ arrayValue.push("");
600
+ }
601
+ const handleValueChange = (details) => {
602
+ const newValue = details.value.join("");
603
+ field.handleChange(newValue);
604
+ };
605
+ const handleValueComplete = (details) => {
606
+ const completeValue = details.value.join("");
607
+ onComplete?.(completeValue);
608
+ };
609
+ return /* @__PURE__ */ jsx(FieldWrapper, { resolved, hasError, errorMessage, fullPath, children: /* @__PURE__ */ jsxs(
610
+ PinInput.Root,
611
+ {
612
+ value: arrayValue,
613
+ onValueChange: handleValueChange,
614
+ onValueComplete: handleValueComplete,
615
+ placeholder: resolved.placeholder,
616
+ mask,
617
+ otp,
618
+ type,
619
+ size,
620
+ variant,
621
+ attached,
622
+ disabled: resolved.disabled,
623
+ readOnly: resolved.readOnly,
624
+ invalid: hasError,
625
+ count,
626
+ onBlur: field.handleBlur,
627
+ "data-field-name": fullPath,
628
+ children: [
629
+ /* @__PURE__ */ jsx(PinInput.HiddenInput, {}),
630
+ /* @__PURE__ */ jsx(PinInput.Control, { children: Array.from({ length: count }).map((_, index) => /* @__PURE__ */ jsx(PinInput.Input, { index }, index)) })
631
+ ]
632
+ }
633
+ ) });
634
+ }
635
+ });
636
+ function useCityProvider(propProvider, token) {
637
+ const formContext = useDeclarativeFormOptional();
638
+ if (propProvider) return propProvider;
639
+ if (formContext?.addressProvider) return formContext.addressProvider;
640
+ if (token) return createDaDataProvider({ token });
641
+ const envKey = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_DADATA_API_KEY : "";
642
+ if (envKey) return createDaDataProvider({ token: envKey });
643
+ return null;
644
+ }
645
+ var FieldCity = createField({
646
+ displayName: "FieldCity",
647
+ useFieldState: (props) => {
648
+ const { provider: propProvider, token, minChars = 2, debounceMs = 300 } = props;
649
+ const provider = useCityProvider(propProvider, token);
650
+ const [inputValue, setInputValue] = useState("");
651
+ const [suggestions, setSuggestions] = useState([]);
652
+ const [isLoading, setIsLoading] = useState(false);
653
+ const [isOpen, setIsOpen] = useState(false);
654
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
655
+ const containerRef = useRef(null);
656
+ const debouncedQuery = useDebounce(inputValue, debounceMs);
657
+ const justSelectedRef = useRef(false);
658
+ const initializedRef = useRef(false);
659
+ const fetchSuggestions = useCallback(
660
+ async (query) => {
661
+ if (query.length < minChars || !provider) {
662
+ setSuggestions([]);
663
+ return;
664
+ }
665
+ setIsLoading(true);
666
+ try {
667
+ const results = await provider.getSuggestions(query, {
668
+ count: 7,
669
+ bounds: { from: "city", to: "settlement" }
670
+ });
671
+ setSuggestions(results);
672
+ setIsOpen(results.length > 0);
673
+ } catch (error) {
674
+ console.error("Error loading city suggestions:", error);
675
+ setSuggestions([]);
676
+ } finally {
677
+ setIsLoading(false);
678
+ }
679
+ },
680
+ [provider, minChars]
681
+ );
682
+ useEffect(() => {
683
+ if (justSelectedRef.current) {
684
+ justSelectedRef.current = false;
685
+ return;
686
+ }
687
+ if (debouncedQuery) {
688
+ fetchSuggestions(debouncedQuery);
689
+ } else {
690
+ setSuggestions([]);
691
+ setIsOpen(false);
692
+ }
693
+ }, [debouncedQuery, fetchSuggestions]);
694
+ useEffect(() => {
695
+ const handleClickOutside = (event) => {
696
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
697
+ setIsOpen(false);
698
+ }
699
+ };
700
+ document.addEventListener("mousedown", handleClickOutside);
701
+ return () => document.removeEventListener("mousedown", handleClickOutside);
702
+ }, []);
703
+ return {
704
+ inputValue,
705
+ setInputValue,
706
+ suggestions,
707
+ setSuggestions,
708
+ isLoading,
709
+ setIsLoading,
710
+ isOpen,
711
+ setIsOpen,
712
+ highlightedIndex,
713
+ setHighlightedIndex,
714
+ containerRef,
715
+ debouncedQuery,
716
+ justSelectedRef,
717
+ initializedRef
718
+ };
719
+ },
720
+ render: ({ field, fullPath, resolved, hasError, errorMessage, fieldState }) => {
721
+ const {
722
+ inputValue,
723
+ setInputValue,
724
+ suggestions,
725
+ setSuggestions,
726
+ isLoading,
727
+ isOpen,
728
+ setIsOpen,
729
+ highlightedIndex,
730
+ setHighlightedIndex,
731
+ containerRef
732
+ } = fieldState;
733
+ const { justSelectedRef, initializedRef } = fieldState;
734
+ const fieldValue = field.state.value;
735
+ if (!initializedRef.current && fieldValue && fieldValue !== inputValue) {
736
+ initializedRef.current = true;
737
+ setInputValue(fieldValue);
738
+ }
739
+ const handleSelect = (suggestion) => {
740
+ const cityName = suggestion.data?.city || suggestion.data?.settlement || suggestion.value;
741
+ justSelectedRef.current = true;
742
+ setInputValue(cityName);
743
+ setIsOpen(false);
744
+ setSuggestions([]);
745
+ field.handleChange(cityName);
746
+ };
747
+ const handleKeyDown = (e) => {
748
+ if (!isOpen || suggestions.length === 0) {
749
+ return;
750
+ }
751
+ switch (e.key) {
752
+ case "ArrowDown":
753
+ e.preventDefault();
754
+ setHighlightedIndex(highlightedIndex < suggestions.length - 1 ? highlightedIndex + 1 : 0);
755
+ break;
756
+ case "ArrowUp":
757
+ e.preventDefault();
758
+ setHighlightedIndex(highlightedIndex > 0 ? highlightedIndex - 1 : suggestions.length - 1);
759
+ break;
760
+ case "Enter":
761
+ e.preventDefault();
762
+ if (highlightedIndex >= 0) {
763
+ handleSelect(suggestions[highlightedIndex]);
764
+ }
765
+ break;
766
+ case "Escape":
767
+ setIsOpen(false);
768
+ break;
769
+ }
770
+ };
771
+ return /* @__PURE__ */ jsxs(
772
+ Field.Root,
773
+ {
774
+ invalid: hasError,
775
+ required: resolved.required,
776
+ disabled: resolved.disabled,
777
+ readOnly: resolved.readOnly,
778
+ children: [
779
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
780
+ /* @__PURE__ */ jsxs(Box, { ref: containerRef, position: "relative", width: "100%", children: [
781
+ /* @__PURE__ */ jsx(
782
+ Input,
783
+ {
784
+ value: inputValue,
785
+ onChange: (e) => {
786
+ setInputValue(e.target.value);
787
+ setHighlightedIndex(-1);
788
+ if (!e.target.value) {
789
+ field.handleChange("");
790
+ }
791
+ },
792
+ onFocus: () => {
793
+ if (suggestions.length > 0) {
794
+ setIsOpen(true);
795
+ }
796
+ },
797
+ onBlur: () => {
798
+ if (inputValue && inputValue !== field.state.value) {
799
+ field.handleChange(inputValue);
800
+ }
801
+ field.handleBlur();
802
+ },
803
+ onKeyDown: handleKeyDown,
804
+ placeholder: resolved.placeholder ?? "Enter city",
805
+ "data-field-name": fullPath
806
+ }
807
+ ),
808
+ isLoading && /* @__PURE__ */ jsx(Box, { position: "absolute", right: 3, top: "50%", transform: "translateY(-50%)", children: /* @__PURE__ */ jsx(Spinner, { size: "sm" }) }),
809
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
810
+ List.Root,
811
+ {
812
+ position: "absolute",
813
+ zIndex: 10,
814
+ width: "100%",
815
+ bg: "bg.panel",
816
+ borderWidth: "1px",
817
+ borderRadius: "md",
818
+ shadow: "md",
819
+ maxH: "250px",
820
+ overflowY: "auto",
821
+ mt: 1,
822
+ listStyle: "none",
823
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
824
+ List.Item,
825
+ {
826
+ px: 3,
827
+ py: 2,
828
+ cursor: "pointer",
829
+ bg: highlightedIndex === index ? "bg.muted" : void 0,
830
+ _hover: { bg: "bg.muted" },
831
+ onClick: () => handleSelect(suggestion),
832
+ onMouseEnter: () => setHighlightedIndex(index),
833
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "sm", children: suggestion.label })
834
+ },
835
+ `${suggestion.value}-${index}`
836
+ ))
837
+ }
838
+ )
839
+ ] }),
840
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
841
+ ]
842
+ }
843
+ );
844
+ }
845
+ });
846
+
847
+ export { FieldAddress, FieldCity, FieldColorPicker, FieldFileUpload, FieldOTPInput, FieldPhone, FieldPinInput };
848
+ //# sourceMappingURL=chunk-GOELIS6T.js.map
849
+ //# sourceMappingURL=chunk-GOELIS6T.js.map