@letar/forms 1.1.0 → 1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/README.md +9 -9
  3. package/README.ru.md +115 -30
  4. package/analytics.js +3 -0
  5. package/analytics.js.map +1 -0
  6. package/chunk-2PSXYC3I.js +1782 -0
  7. package/chunk-2PSXYC3I.js.map +1 -0
  8. package/chunk-5D6S6EGF.js +206 -0
  9. package/chunk-5D6S6EGF.js.map +1 -0
  10. package/{chunk-6QOPSQ3Z.js → chunk-6E7VJAJT.js} +3 -3
  11. package/{chunk-6QOPSQ3Z.js.map → chunk-6E7VJAJT.js.map} +1 -1
  12. package/chunk-CGXKRCSM.js +117 -0
  13. package/chunk-CGXKRCSM.js.map +1 -0
  14. package/{chunk-M2PNAAIR.js → chunk-DQUVUMCX.js} +30 -19
  15. package/chunk-DQUVUMCX.js.map +1 -0
  16. package/chunk-K3J4L26K.js +345 -0
  17. package/chunk-K3J4L26K.js.map +1 -0
  18. package/{chunk-PJETA6YN.js → chunk-MAYUFA5K.js} +5 -4
  19. package/chunk-MAYUFA5K.js.map +1 -0
  20. package/{chunk-4V6WBJ76.js → chunk-MVGXZNHP.js} +2 -2
  21. package/{chunk-4V6WBJ76.js.map → chunk-MVGXZNHP.js.map} +1 -1
  22. package/{chunk-XKKJKYWZ.js → chunk-MZDTJSF7.js} +3 -3
  23. package/{chunk-XKKJKYWZ.js.map → chunk-MZDTJSF7.js.map} +1 -1
  24. package/{chunk-KUNT5MSU.js → chunk-Q5EOF36Y.js} +3 -3
  25. package/chunk-Q5EOF36Y.js.map +1 -0
  26. package/{chunk-7FEQFDJ7.js → chunk-R2RTCKXY.js} +2 -2
  27. package/{chunk-7FEQFDJ7.js.map → chunk-R2RTCKXY.js.map} +1 -1
  28. package/{chunk-HWVOFWAT.js → chunk-XFWLD5EO.js} +225 -26
  29. package/chunk-XFWLD5EO.js.map +1 -0
  30. package/fields/boolean.js +3 -3
  31. package/fields/datetime.js +3 -3
  32. package/fields/number.js +3 -3
  33. package/fields/selection.js +3 -3
  34. package/fields/specialized.js +3 -3
  35. package/fields/text.js +3 -3
  36. package/hcaptcha-U4XIT3HS.js +64 -0
  37. package/hcaptcha-U4XIT3HS.js.map +1 -0
  38. package/i18n.js +1 -1
  39. package/index.js +3268 -51
  40. package/index.js.map +1 -1
  41. package/offline.js +1 -1
  42. package/package.json +33 -4
  43. package/recaptcha-PKAUAY2S.js +56 -0
  44. package/recaptcha-PKAUAY2S.js.map +1 -0
  45. package/server-errors.js +3 -0
  46. package/server-errors.js.map +1 -0
  47. package/turnstile-7FXTBSLW.js +36 -0
  48. package/turnstile-7FXTBSLW.js.map +1 -0
  49. package/validators/ru.js +73 -0
  50. package/validators/ru.js.map +1 -0
  51. package/chunk-GOELIS6T.js +0 -849
  52. package/chunk-GOELIS6T.js.map +0 -1
  53. package/chunk-HWVOFWAT.js.map +0 -1
  54. package/chunk-KUNT5MSU.js.map +0 -1
  55. package/chunk-M2PNAAIR.js.map +0 -1
  56. package/chunk-PJETA6YN.js.map +0 -1
@@ -0,0 +1,1782 @@
1
+ import { createField, FieldLabel, FieldError, useDebounce, FieldTooltip, FieldWrapper, useDeclarativeForm, useDeclarativeFormOptional } from './chunk-XFWLD5EO.js';
2
+ import { Field, Box, Input, Spinner, List, Text, parseColor, ColorPicker, HStack, Portal, FileUpload, Button, Icon, PinInput, Group, SegmentGroup, useFileUploadContext, Float, IconButton, Flex, Tooltip } from '@chakra-ui/react';
3
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { LuUpload, LuPen, LuType, LuEraser, LuX, LuFile } from 'react-icons/lu';
6
+ import { withMask } from 'use-mask-input';
7
+ import { z } from 'zod/v4';
8
+
9
+ // src/lib/declarative/form-fields/specialized/providers/dadata.ts
10
+ var DADATA_URL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address";
11
+ function createDaDataProvider(config) {
12
+ const { token, baseUrl = DADATA_URL } = config;
13
+ return {
14
+ async getSuggestions(query, options) {
15
+ const body = {
16
+ query,
17
+ count: options?.count ?? 10
18
+ };
19
+ if (options?.bounds) {
20
+ if (options.bounds.from) body.from_bound = { value: options.bounds.from };
21
+ if (options.bounds.to) body.to_bound = { value: options.bounds.to };
22
+ }
23
+ if (options?.filters) {
24
+ body.locations = [options.filters];
25
+ }
26
+ const response = await fetch(baseUrl, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ Accept: "application/json",
31
+ Authorization: `Token ${token}`
32
+ },
33
+ body: JSON.stringify(body)
34
+ });
35
+ if (!response.ok) {
36
+ return [];
37
+ }
38
+ const data = await response.json();
39
+ const suggestions = data.suggestions ?? [];
40
+ return suggestions.map(
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ (s) => ({
43
+ label: s.value,
44
+ value: s.value,
45
+ data: s.data
46
+ })
47
+ );
48
+ }
49
+ };
50
+ }
51
+ function useAddressProvider(propProvider, token) {
52
+ const formContext = useDeclarativeFormOptional();
53
+ if (propProvider) return propProvider;
54
+ if (formContext?.addressProvider) return formContext.addressProvider;
55
+ if (token) return createDaDataProvider({ token });
56
+ return null;
57
+ }
58
+ var FieldAddress = createField({
59
+ displayName: "FieldAddress",
60
+ useFieldState: (props) => {
61
+ const { provider: propProvider, token, minChars = 3, debounceMs = 300, locations } = props;
62
+ const provider = useAddressProvider(propProvider, token);
63
+ const [inputValue, setInputValue] = useState("");
64
+ const [suggestions, setSuggestions] = useState([]);
65
+ const [isLoading, setIsLoading] = useState(false);
66
+ const [isOpen, setIsOpen] = useState(false);
67
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
68
+ const containerRef = useRef(null);
69
+ const initializedRef = useRef(false);
70
+ const debouncedQuery = useDebounce(inputValue, debounceMs);
71
+ const fetchSuggestions = useCallback(
72
+ async (query) => {
73
+ if (query.length < minChars || !provider) {
74
+ setSuggestions([]);
75
+ return;
76
+ }
77
+ setIsLoading(true);
78
+ try {
79
+ const results = await provider.getSuggestions(query, {
80
+ count: 10,
81
+ filters: locations ? Object.assign({}, ...locations) : void 0
82
+ });
83
+ setSuggestions(results);
84
+ setIsOpen(true);
85
+ } catch (error) {
86
+ console.error("Error loading address suggestions:", error);
87
+ setSuggestions([]);
88
+ } finally {
89
+ setIsLoading(false);
90
+ }
91
+ },
92
+ [provider, minChars, locations]
93
+ );
94
+ useEffect(() => {
95
+ if (debouncedQuery) {
96
+ fetchSuggestions(debouncedQuery);
97
+ } else {
98
+ setSuggestions([]);
99
+ setIsOpen(false);
100
+ }
101
+ }, [debouncedQuery, fetchSuggestions]);
102
+ useEffect(() => {
103
+ const handleClickOutside = (event) => {
104
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
105
+ setIsOpen(false);
106
+ }
107
+ };
108
+ document.addEventListener("mousedown", handleClickOutside);
109
+ return () => document.removeEventListener("mousedown", handleClickOutside);
110
+ }, []);
111
+ return {
112
+ inputValue,
113
+ setInputValue,
114
+ suggestions,
115
+ setSuggestions,
116
+ isLoading,
117
+ setIsLoading,
118
+ isOpen,
119
+ setIsOpen,
120
+ highlightedIndex,
121
+ setHighlightedIndex,
122
+ containerRef,
123
+ debouncedQuery,
124
+ fetchSuggestions,
125
+ initializedRef
126
+ };
127
+ },
128
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
129
+ const { valueOnly = false } = componentProps;
130
+ const {
131
+ inputValue,
132
+ setInputValue,
133
+ suggestions,
134
+ setSuggestions,
135
+ isLoading,
136
+ isOpen,
137
+ setIsOpen,
138
+ highlightedIndex,
139
+ setHighlightedIndex,
140
+ containerRef,
141
+ initializedRef
142
+ } = fieldState;
143
+ const fieldValue = field.state.value;
144
+ if (!initializedRef.current && fieldValue) {
145
+ const displayValue = typeof fieldValue === "string" ? fieldValue : fieldValue.value;
146
+ if (displayValue && displayValue !== inputValue) {
147
+ setInputValue(displayValue);
148
+ }
149
+ initializedRef.current = true;
150
+ }
151
+ const handleSelect = (suggestion) => {
152
+ setInputValue(suggestion.value);
153
+ setIsOpen(false);
154
+ setSuggestions([]);
155
+ if (valueOnly) {
156
+ field.handleChange(suggestion.value);
157
+ } else {
158
+ const addressValue = {
159
+ value: suggestion.value,
160
+ data: suggestion.data
161
+ };
162
+ field.handleChange(addressValue);
163
+ }
164
+ };
165
+ const handleKeyDown = (e) => {
166
+ if (!isOpen || suggestions.length === 0) {
167
+ return;
168
+ }
169
+ switch (e.key) {
170
+ case "ArrowDown":
171
+ e.preventDefault();
172
+ setHighlightedIndex(highlightedIndex < suggestions.length - 1 ? highlightedIndex + 1 : 0);
173
+ break;
174
+ case "ArrowUp":
175
+ e.preventDefault();
176
+ setHighlightedIndex(highlightedIndex > 0 ? highlightedIndex - 1 : suggestions.length - 1);
177
+ break;
178
+ case "Enter":
179
+ e.preventDefault();
180
+ if (highlightedIndex >= 0) {
181
+ handleSelect(suggestions[highlightedIndex]);
182
+ }
183
+ break;
184
+ case "Escape":
185
+ setIsOpen(false);
186
+ break;
187
+ }
188
+ };
189
+ return /* @__PURE__ */ jsxs(
190
+ Field.Root,
191
+ {
192
+ invalid: hasError,
193
+ required: resolved.required,
194
+ disabled: resolved.disabled,
195
+ readOnly: resolved.readOnly,
196
+ children: [
197
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
198
+ /* @__PURE__ */ jsxs(Box, { ref: containerRef, position: "relative", width: "100%", children: [
199
+ /* @__PURE__ */ jsx(
200
+ Input,
201
+ {
202
+ value: inputValue,
203
+ onChange: (e) => {
204
+ setInputValue(e.target.value);
205
+ setHighlightedIndex(-1);
206
+ },
207
+ onFocus: () => {
208
+ if (suggestions.length > 0) {
209
+ setIsOpen(true);
210
+ }
211
+ },
212
+ onBlur: field.handleBlur,
213
+ onKeyDown: handleKeyDown,
214
+ placeholder: resolved.placeholder ?? "Start typing address...",
215
+ "data-field-name": fullPath
216
+ }
217
+ ),
218
+ isLoading && /* @__PURE__ */ jsx(Box, { position: "absolute", right: 3, top: "50%", transform: "translateY(-50%)", children: /* @__PURE__ */ jsx(Spinner, { size: "sm" }) }),
219
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
220
+ List.Root,
221
+ {
222
+ position: "absolute",
223
+ zIndex: 10,
224
+ width: "100%",
225
+ bg: "bg.panel",
226
+ borderWidth: "1px",
227
+ borderRadius: "md",
228
+ shadow: "md",
229
+ maxH: "200px",
230
+ overflowY: "auto",
231
+ mt: 1,
232
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
233
+ List.Item,
234
+ {
235
+ px: 3,
236
+ py: 2,
237
+ cursor: "pointer",
238
+ bg: highlightedIndex === index ? "bg.muted" : void 0,
239
+ _hover: { bg: "bg.muted" },
240
+ onClick: () => handleSelect(suggestion),
241
+ onMouseEnter: () => setHighlightedIndex(index),
242
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "sm", children: suggestion.label })
243
+ },
244
+ suggestion.value + index
245
+ ))
246
+ }
247
+ )
248
+ ] }),
249
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
250
+ ]
251
+ }
252
+ );
253
+ }
254
+ });
255
+ var defaultSwatches = [
256
+ "#000000",
257
+ "#4A5568",
258
+ "#F56565",
259
+ "#ED64A6",
260
+ "#9F7AEA",
261
+ "#6B46C1",
262
+ "#4299E1",
263
+ "#0BC5EA",
264
+ "#38B2AC",
265
+ "#48BB78",
266
+ "#ECC94B",
267
+ "#DD6B20"
268
+ ];
269
+ var FieldColorPicker = createField({
270
+ displayName: "FieldColorPicker",
271
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps }) => {
272
+ const {
273
+ swatches = defaultSwatches,
274
+ size = "md",
275
+ showArea = true,
276
+ showEyeDropper = true,
277
+ showSliders = true,
278
+ showInput = true
279
+ } = componentProps;
280
+ const currentValue = field.state.value || "#000000";
281
+ let parsedColor;
282
+ try {
283
+ parsedColor = parseColor(currentValue);
284
+ } catch {
285
+ parsedColor = parseColor("#000000");
286
+ }
287
+ return /* @__PURE__ */ jsxs(
288
+ Field.Root,
289
+ {
290
+ invalid: hasError,
291
+ required: resolved.required,
292
+ disabled: resolved.disabled,
293
+ readOnly: resolved.readOnly,
294
+ children: [
295
+ /* @__PURE__ */ jsxs(
296
+ ColorPicker.Root,
297
+ {
298
+ value: parsedColor,
299
+ onValueChange: (details) => {
300
+ field.handleChange(details.valueAsString);
301
+ },
302
+ disabled: resolved.disabled,
303
+ readOnly: resolved.readOnly,
304
+ size,
305
+ children: [
306
+ /* @__PURE__ */ jsx(ColorPicker.HiddenInput, { name: fullPath }),
307
+ resolved.label && /* @__PURE__ */ jsxs(ColorPicker.Label, { children: [
308
+ resolved.tooltip ? /* @__PURE__ */ jsxs(HStack, { gap: 1, children: [
309
+ /* @__PURE__ */ jsx("span", { children: resolved.label }),
310
+ /* @__PURE__ */ jsx(FieldTooltip, { ...resolved.tooltip })
311
+ ] }) : resolved.label,
312
+ resolved.required && /* @__PURE__ */ jsx(Field.RequiredIndicator, {})
313
+ ] }),
314
+ /* @__PURE__ */ jsxs(ColorPicker.Control, { children: [
315
+ showInput && /* @__PURE__ */ jsx(ColorPicker.ChannelInput, { channel: "hex" }),
316
+ /* @__PURE__ */ jsx(ColorPicker.Trigger, {})
317
+ ] }),
318
+ /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(ColorPicker.Positioner, { children: /* @__PURE__ */ jsxs(ColorPicker.Content, { children: [
319
+ showArea && /* @__PURE__ */ jsx(ColorPicker.Area, {}),
320
+ (showEyeDropper || showSliders) && /* @__PURE__ */ jsxs(HStack, { children: [
321
+ showEyeDropper && /* @__PURE__ */ jsx(ColorPicker.EyeDropper, { size: "xs", variant: "outline" }),
322
+ showSliders && /* @__PURE__ */ jsx(ColorPicker.Sliders, {})
323
+ ] }),
324
+ 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)) })
325
+ ] }) }) })
326
+ ]
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
330
+ ]
331
+ }
332
+ );
333
+ }
334
+ });
335
+
336
+ // src/lib/declarative/security/file-security.ts
337
+ var MIME_SIGNATURES = [
338
+ // Изображения
339
+ { bytes: [255, 216, 255], mime: "image/jpeg" },
340
+ { bytes: [137, 80, 78, 71], mime: "image/png" },
341
+ { bytes: [71, 73, 70, 56], mime: "image/gif" },
342
+ { bytes: [82, 73, 70, 70], mime: "image/webp" },
343
+ // RIFF (WebP контейнер)
344
+ // Документы
345
+ { bytes: [37, 80, 68, 70], mime: "application/pdf" },
346
+ // Архивы
347
+ { bytes: [80, 75, 3, 4], mime: "application/zip" }
348
+ ];
349
+ function parseFileSize(size) {
350
+ if (typeof size === "number") return size;
351
+ const match = size.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)$/i);
352
+ if (!match) {
353
+ throw new Error(`Invalid file size format: "${size}". Use "10MB", "500KB", etc.`);
354
+ }
355
+ const value = Number.parseFloat(match[1]);
356
+ const unit = match[2].toUpperCase();
357
+ const multipliers = {
358
+ B: 1,
359
+ KB: 1024,
360
+ MB: 1024 * 1024,
361
+ GB: 1024 * 1024 * 1024
362
+ };
363
+ return Math.floor(value * multipliers[unit]);
364
+ }
365
+ async function detectMimeType(file) {
366
+ const buffer = await file.slice(0, 8).arrayBuffer();
367
+ const bytes = new Uint8Array(buffer);
368
+ for (const sig of MIME_SIGNATURES) {
369
+ if (sig.bytes.every((b, i) => bytes[i] === b)) {
370
+ return sig.mime;
371
+ }
372
+ }
373
+ return null;
374
+ }
375
+ async function validateMimeType(file, allowedTypes) {
376
+ const detectedMime = await detectMimeType(file);
377
+ const mimeToCheck = detectedMime ?? file.type;
378
+ if (!mimeToCheck) {
379
+ return { valid: false, detectedMime, reason: "Unable to determine file type" };
380
+ }
381
+ const isAllowed = allowedTypes.some((allowed) => {
382
+ if (allowed.endsWith("/*")) {
383
+ const category = allowed.split("/")[0];
384
+ return mimeToCheck.startsWith(`${category}/`);
385
+ }
386
+ return allowed === mimeToCheck;
387
+ });
388
+ if (!isAllowed) {
389
+ return {
390
+ valid: false,
391
+ detectedMime,
392
+ reason: `File type "${mimeToCheck}" is not allowed. Allowed: ${allowedTypes.join(", ")}`
393
+ };
394
+ }
395
+ return { valid: true, detectedMime };
396
+ }
397
+ async function stripExifMetadata(file) {
398
+ if (!file.type.startsWith("image/")) {
399
+ return file;
400
+ }
401
+ if (file.type === "image/svg+xml") {
402
+ return file;
403
+ }
404
+ return new Promise((resolve) => {
405
+ const img = new Image();
406
+ const url = URL.createObjectURL(file);
407
+ img.onload = () => {
408
+ const canvas = document.createElement("canvas");
409
+ canvas.width = img.naturalWidth;
410
+ canvas.height = img.naturalHeight;
411
+ const ctx = canvas.getContext("2d");
412
+ if (!ctx) {
413
+ URL.revokeObjectURL(url);
414
+ resolve(file);
415
+ return;
416
+ }
417
+ ctx.drawImage(img, 0, 0);
418
+ URL.revokeObjectURL(url);
419
+ const outputType = file.type === "image/png" ? "image/png" : "image/jpeg";
420
+ const quality = outputType === "image/jpeg" ? 0.92 : void 0;
421
+ canvas.toBlob(
422
+ (blob) => {
423
+ if (!blob) {
424
+ resolve(file);
425
+ return;
426
+ }
427
+ resolve(new File([blob], file.name, { type: outputType, lastModified: Date.now() }));
428
+ },
429
+ outputType,
430
+ quality
431
+ );
432
+ };
433
+ img.onerror = () => {
434
+ URL.revokeObjectURL(url);
435
+ resolve(file);
436
+ };
437
+ img.src = url;
438
+ });
439
+ }
440
+ function sanitizeFileName(file) {
441
+ const baseName = file.name.split(/[/\\]/).pop() ?? file.name;
442
+ const lastDot = baseName.lastIndexOf(".");
443
+ const ext = lastDot > 0 ? baseName.slice(lastDot) : "";
444
+ const uuid = crypto.randomUUID();
445
+ const safeName = `${uuid}${ext}`;
446
+ return new File([file], safeName, { type: file.type, lastModified: file.lastModified });
447
+ }
448
+ async function processFileWithSecurity(file, config) {
449
+ if (config.maxSize) {
450
+ const maxBytes = parseFileSize(config.maxSize);
451
+ if (file.size > maxBytes) {
452
+ return {
453
+ valid: false,
454
+ file,
455
+ reason: `File size ${formatSize(file.size)} exceeds limit ${formatSize(maxBytes)}`
456
+ };
457
+ }
458
+ }
459
+ if (config.allowedTypes) {
460
+ const mimeResult = await validateMimeType(file, config.allowedTypes);
461
+ if (!mimeResult.valid) {
462
+ return { valid: false, file, reason: mimeResult.reason };
463
+ }
464
+ }
465
+ let processedFile = file;
466
+ if (config.stripMetadata) {
467
+ processedFile = await stripExifMetadata(processedFile);
468
+ }
469
+ if (config.renameFile) {
470
+ processedFile = sanitizeFileName(processedFile);
471
+ }
472
+ return { valid: true, file: processedFile };
473
+ }
474
+ function formatSize(bytes) {
475
+ if (bytes < 1024) return `${bytes} B`;
476
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
477
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
478
+ }
479
+ function FileImageList({ clearable }) {
480
+ const fileUpload = useFileUploadContext();
481
+ if (fileUpload.acceptedFiles.length === 0) {
482
+ return null;
483
+ }
484
+ 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: [
485
+ 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, {}) }) }) }),
486
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { type: "image/*", asChild: true, children: /* @__PURE__ */ jsx(FileUpload.ItemPreviewImage, { boxSize: "16", rounded: "md", objectFit: "cover" }) }),
487
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { type: ".*", asChild: true, children: /* @__PURE__ */ jsx(Icon, { fontSize: "4xl", color: "fg.muted", children: /* @__PURE__ */ jsx(LuFile, {}) }) })
488
+ ] }, file.name)) });
489
+ }
490
+ function FileList({ showSize, clearable }) {
491
+ const fileUpload = useFileUploadContext();
492
+ if (fileUpload.acceptedFiles.length === 0) {
493
+ return null;
494
+ }
495
+ return /* @__PURE__ */ jsx(FileUpload.ItemGroup, { mt: "2", children: fileUpload.acceptedFiles.map((file) => /* @__PURE__ */ jsxs(FileUpload.Item, { file, children: [
496
+ /* @__PURE__ */ jsx(FileUpload.ItemPreview, { asChild: true, children: /* @__PURE__ */ jsx(Icon, { fontSize: "lg", color: "fg.muted", children: /* @__PURE__ */ jsx(LuFile, {}) }) }),
497
+ showSize ? /* @__PURE__ */ jsxs(FileUpload.ItemContent, { children: [
498
+ /* @__PURE__ */ jsx(FileUpload.ItemName, {}),
499
+ /* @__PURE__ */ jsx(FileUpload.ItemSizeText, {})
500
+ ] }) : /* @__PURE__ */ jsx(FileUpload.ItemName, { flex: "1" }),
501
+ clearable && /* @__PURE__ */ jsx(FileUpload.ItemDeleteTrigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { variant: "ghost", color: "fg.muted", size: "xs", children: /* @__PURE__ */ jsx(LuX, {}) }) })
502
+ ] }, file.name)) });
503
+ }
504
+ var FieldFileUpload = createField({
505
+ displayName: "FieldFileUpload",
506
+ useFieldState: (componentProps) => {
507
+ const [securityError, setSecurityError] = useState(null);
508
+ return { securityError, setSecurityError };
509
+ },
510
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
511
+ const {
512
+ accept,
513
+ maxFileSize,
514
+ maxFiles = 1,
515
+ variant = "button",
516
+ showSize = false,
517
+ clearable = true,
518
+ dropzoneLabel = "Drag and drop files here",
519
+ dropzoneDescription,
520
+ buttonText = "Upload file",
521
+ security
522
+ } = componentProps;
523
+ const { securityError, setSecurityError } = fieldState;
524
+ const placeholder = resolved.placeholder ?? "Select file(s)";
525
+ const normalizedAccept = accept ? typeof accept === "string" ? accept.split(",").map((s) => s.trim()) : accept : void 0;
526
+ const isImageUpload = normalizedAccept?.some((type) => type.startsWith("image/") || type === "image/*");
527
+ return /* @__PURE__ */ jsxs(Field.Root, { invalid: hasError, required: resolved.required, disabled: resolved.disabled, children: [
528
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
529
+ /* @__PURE__ */ jsxs(
530
+ FileUpload.Root,
531
+ {
532
+ maxFiles,
533
+ maxFileSize,
534
+ accept: normalizedAccept,
535
+ disabled: resolved.disabled,
536
+ onFileChange: async (details) => {
537
+ if (!security || details.acceptedFiles.length === 0) {
538
+ setSecurityError(null);
539
+ field.handleChange(details.acceptedFiles);
540
+ return;
541
+ }
542
+ const results = await Promise.all(details.acceptedFiles.map((f) => processFileWithSecurity(f, security)));
543
+ const rejected = results.filter((r) => !r.valid);
544
+ if (rejected.length > 0) {
545
+ setSecurityError(rejected.map((r) => r.reason).join("; "));
546
+ const validFiles = results.filter((r) => r.valid).map((r) => r.file);
547
+ field.handleChange(validFiles.length > 0 ? validFiles : []);
548
+ return;
549
+ }
550
+ setSecurityError(null);
551
+ field.handleChange(results.map((r) => r.file));
552
+ },
553
+ "data-field-name": fullPath,
554
+ children: [
555
+ /* @__PURE__ */ jsx(FileUpload.HiddenInput, { onBlur: field.handleBlur }),
556
+ variant === "button" && /* @__PURE__ */ jsxs(Fragment, { children: [
557
+ /* @__PURE__ */ jsx(FileUpload.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", children: [
558
+ /* @__PURE__ */ jsx(LuUpload, {}),
559
+ buttonText
560
+ ] }) }),
561
+ isImageUpload ? /* @__PURE__ */ jsx(FileImageList, { clearable }) : /* @__PURE__ */ jsx(FileList, { showSize, clearable })
562
+ ] }),
563
+ variant === "dropzone" && /* @__PURE__ */ jsxs(Fragment, { children: [
564
+ /* @__PURE__ */ jsxs(FileUpload.Dropzone, { children: [
565
+ /* @__PURE__ */ jsx(Icon, { size: "md", color: "fg.muted", children: /* @__PURE__ */ jsx(LuUpload, {}) }),
566
+ /* @__PURE__ */ jsxs(FileUpload.DropzoneContent, { children: [
567
+ /* @__PURE__ */ jsx(Box, { children: dropzoneLabel }),
568
+ dropzoneDescription && /* @__PURE__ */ jsx(Text, { color: "fg.muted", children: dropzoneDescription })
569
+ ] })
570
+ ] }),
571
+ isImageUpload ? /* @__PURE__ */ jsx(FileImageList, { clearable }) : /* @__PURE__ */ jsx(FileList, { showSize, clearable })
572
+ ] }),
573
+ variant === "input" && /* @__PURE__ */ jsx(Input, { asChild: true, children: /* @__PURE__ */ jsx(FileUpload.Trigger, { children: /* @__PURE__ */ jsx(FileUpload.Context, { children: ({ acceptedFiles }) => {
574
+ if (acceptedFiles.length === 1) {
575
+ return /* @__PURE__ */ jsx("span", { children: acceptedFiles[0].name });
576
+ }
577
+ if (acceptedFiles.length > 1) {
578
+ return /* @__PURE__ */ jsxs("span", { children: [
579
+ acceptedFiles.length,
580
+ " files"
581
+ ] });
582
+ }
583
+ return /* @__PURE__ */ jsx(Text, { color: "fg.subtle", children: placeholder });
584
+ } }) }) })
585
+ ]
586
+ }
587
+ ),
588
+ /* @__PURE__ */ jsx(
589
+ FieldError,
590
+ {
591
+ hasError: hasError || !!securityError,
592
+ errorMessage: securityError ?? errorMessage,
593
+ helperText: resolved.helperText
594
+ }
595
+ )
596
+ ] });
597
+ }
598
+ });
599
+ var FieldOTPInput = createField({
600
+ displayName: "FieldOTPInput",
601
+ useFieldState: (props) => {
602
+ const [countdown, setCountdown] = useState(0);
603
+ const [isResending, setIsResending] = useState(false);
604
+ useEffect(() => {
605
+ if (countdown <= 0) {
606
+ return;
607
+ }
608
+ const timer = setInterval(() => {
609
+ setCountdown((prev) => prev - 1);
610
+ }, 1e3);
611
+ return () => clearInterval(timer);
612
+ }, [countdown]);
613
+ const handleResend = useCallback(async () => {
614
+ if (!props.onResend || countdown > 0) {
615
+ return;
616
+ }
617
+ setIsResending(true);
618
+ try {
619
+ await props.onResend();
620
+ setCountdown(props.resendTimeout ?? 60);
621
+ } finally {
622
+ setIsResending(false);
623
+ }
624
+ }, [props.onResend, countdown, props.resendTimeout]);
625
+ const formatCountdown = (seconds) => {
626
+ const mins = Math.floor(seconds / 60);
627
+ const secs = seconds % 60;
628
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
629
+ };
630
+ const formContext = useDeclarativeForm();
631
+ return { countdown, isResending, handleResend, formatCountdown, formContext };
632
+ },
633
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
634
+ const { length = 6, autoSubmit = false, type = "numeric", mask = false, onResend } = componentProps;
635
+ const { countdown, isResending, handleResend, formatCountdown, formContext } = fieldState;
636
+ const value = field.state.value ?? "";
637
+ const handleValueComplete = (details) => {
638
+ field.handleChange(details.valueAsString);
639
+ if (autoSubmit && details.valueAsString.length === length) {
640
+ formContext.form.handleSubmit();
641
+ }
642
+ };
643
+ return /* @__PURE__ */ jsx(FieldWrapper, { resolved, hasError, errorMessage, fullPath, children: /* @__PURE__ */ jsxs(Box, { children: [
644
+ /* @__PURE__ */ jsxs(
645
+ PinInput.Root,
646
+ {
647
+ value: value.split(""),
648
+ onValueComplete: handleValueComplete,
649
+ onValueChange: (details) => field.handleChange(details.valueAsString),
650
+ count: length,
651
+ type,
652
+ mask,
653
+ otp: true,
654
+ children: [
655
+ /* @__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)) }) }),
656
+ /* @__PURE__ */ jsx(PinInput.HiddenInput, {})
657
+ ]
658
+ }
659
+ ),
660
+ onResend && /* @__PURE__ */ jsx(HStack, { mt: 3, justify: "center", children: countdown > 0 ? /* @__PURE__ */ jsxs(Text, { fontSize: "sm", color: "fg.muted", children: [
661
+ "Redo in ",
662
+ formatCountdown(countdown)
663
+ ] }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: handleResend, disabled: isResending, loading: isResending, children: "Submit again" }) })
664
+ ] }) });
665
+ }
666
+ });
667
+ var PHONE_MASKS = {
668
+ RU: "+7 (999) 999-99-99",
669
+ US: "+1 (999) 999-9999",
670
+ UK: "+44 9999 999999",
671
+ DE: "+49 999 99999999",
672
+ FR: "+33 9 99 99 99 99",
673
+ IT: "+39 999 999 9999",
674
+ ES: "+34 999 99 99 99",
675
+ CN: "+86 999 9999 9999",
676
+ JP: "+81 99 9999 9999",
677
+ KR: "+82 99 9999 9999",
678
+ BY: "+375 (99) 999-99-99",
679
+ KZ: "+7 (999) 999-99-99",
680
+ UA: "+380 (99) 999-99-99"
681
+ };
682
+ var COUNTRY_FLAGS = {
683
+ RU: "\u{1F1F7}\u{1F1FA}",
684
+ US: "\u{1F1FA}\u{1F1F8}",
685
+ UK: "\u{1F1EC}\u{1F1E7}",
686
+ DE: "\u{1F1E9}\u{1F1EA}",
687
+ FR: "\u{1F1EB}\u{1F1F7}",
688
+ IT: "\u{1F1EE}\u{1F1F9}",
689
+ ES: "\u{1F1EA}\u{1F1F8}",
690
+ CN: "\u{1F1E8}\u{1F1F3}",
691
+ JP: "\u{1F1EF}\u{1F1F5}",
692
+ KR: "\u{1F1F0}\u{1F1F7}",
693
+ BY: "\u{1F1E7}\u{1F1FE}",
694
+ KZ: "\u{1F1F0}\u{1F1FF}",
695
+ UA: "\u{1F1FA}\u{1F1E6}"
696
+ };
697
+ var FieldPhone = createField({
698
+ displayName: "FieldPhone",
699
+ useFieldState: (props) => {
700
+ const { country = "RU", autoUnmask = false } = props;
701
+ const mask = PHONE_MASKS[country];
702
+ const maskRef = useCallback(
703
+ (element) => {
704
+ if (element && mask) {
705
+ const maskCallback = withMask(mask, {
706
+ showMaskOnFocus: true,
707
+ clearIncomplete: true,
708
+ autoUnmask
709
+ });
710
+ maskCallback(element);
711
+ }
712
+ },
713
+ [mask, autoUnmask]
714
+ );
715
+ return { maskRef };
716
+ },
717
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps, fieldState }) => {
718
+ const { country = "RU", showFlag = false } = componentProps;
719
+ const flag = COUNTRY_FLAGS[country];
720
+ const mask = PHONE_MASKS[country];
721
+ const value = field.state.value ?? "";
722
+ const resolvedPlaceholder = resolved.placeholder ?? mask?.toString().replace(/9/g, "_");
723
+ return /* @__PURE__ */ jsxs(
724
+ Field.Root,
725
+ {
726
+ invalid: hasError,
727
+ required: resolved.required,
728
+ disabled: resolved.disabled,
729
+ readOnly: resolved.readOnly,
730
+ children: [
731
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
732
+ /* @__PURE__ */ jsxs(Group, { attached: true, children: [
733
+ showFlag && /* @__PURE__ */ jsx(Text, { px: 3, display: "flex", alignItems: "center", bg: "bg.muted", borderWidth: "1px", borderRightWidth: "0", children: flag }),
734
+ /* @__PURE__ */ jsx(
735
+ Input,
736
+ {
737
+ ref: fieldState.maskRef,
738
+ value,
739
+ onChange: (e) => field.handleChange(e.target.value),
740
+ onBlur: field.handleBlur,
741
+ placeholder: resolvedPlaceholder,
742
+ "data-field-name": fullPath,
743
+ type: "tel",
744
+ inputMode: "tel",
745
+ autoComplete: "tel"
746
+ }
747
+ )
748
+ ] }),
749
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
750
+ ]
751
+ }
752
+ );
753
+ }
754
+ });
755
+ var FieldPinInput = createField({
756
+ displayName: "FieldPinInput",
757
+ render: ({ field, fullPath, resolved, hasError, errorMessage, componentProps }) => {
758
+ const {
759
+ count = 4,
760
+ mask,
761
+ otp,
762
+ type = "numeric",
763
+ size = "md",
764
+ variant = "outline",
765
+ attached,
766
+ onComplete
767
+ } = componentProps;
768
+ const stringValue = field.state.value ?? "";
769
+ const arrayValue = stringValue.split("").slice(0, count);
770
+ while (arrayValue.length < count) {
771
+ arrayValue.push("");
772
+ }
773
+ const handleValueChange = (details) => {
774
+ const newValue = details.value.join("");
775
+ field.handleChange(newValue);
776
+ };
777
+ const handleValueComplete = (details) => {
778
+ const completeValue = details.value.join("");
779
+ onComplete?.(completeValue);
780
+ };
781
+ return /* @__PURE__ */ jsx(FieldWrapper, { resolved, hasError, errorMessage, fullPath, children: /* @__PURE__ */ jsxs(
782
+ PinInput.Root,
783
+ {
784
+ value: arrayValue,
785
+ onValueChange: handleValueChange,
786
+ onValueComplete: handleValueComplete,
787
+ placeholder: resolved.placeholder,
788
+ mask,
789
+ otp,
790
+ type,
791
+ size,
792
+ variant,
793
+ attached,
794
+ disabled: resolved.disabled,
795
+ readOnly: resolved.readOnly,
796
+ invalid: hasError,
797
+ count,
798
+ onBlur: field.handleBlur,
799
+ "data-field-name": fullPath,
800
+ children: [
801
+ /* @__PURE__ */ jsx(PinInput.HiddenInput, {}),
802
+ /* @__PURE__ */ jsx(PinInput.Control, { children: Array.from({ length: count }).map((_, index) => /* @__PURE__ */ jsx(PinInput.Input, { index }, index)) })
803
+ ]
804
+ }
805
+ ) });
806
+ }
807
+ });
808
+ function useCityProvider(propProvider, token) {
809
+ const formContext = useDeclarativeFormOptional();
810
+ if (propProvider) return propProvider;
811
+ if (formContext?.addressProvider) return formContext.addressProvider;
812
+ if (token) return createDaDataProvider({ token });
813
+ const envKey = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_DADATA_API_KEY : "";
814
+ if (envKey) return createDaDataProvider({ token: envKey });
815
+ return null;
816
+ }
817
+ var FieldCity = createField({
818
+ displayName: "FieldCity",
819
+ useFieldState: (props) => {
820
+ const { provider: propProvider, token, minChars = 2, debounceMs = 300 } = props;
821
+ const provider = useCityProvider(propProvider, token);
822
+ const [inputValue, setInputValue] = useState("");
823
+ const [suggestions, setSuggestions] = useState([]);
824
+ const [isLoading, setIsLoading] = useState(false);
825
+ const [isOpen, setIsOpen] = useState(false);
826
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
827
+ const containerRef = useRef(null);
828
+ const debouncedQuery = useDebounce(inputValue, debounceMs);
829
+ const justSelectedRef = useRef(false);
830
+ const initializedRef = useRef(false);
831
+ const fetchSuggestions = useCallback(
832
+ async (query) => {
833
+ if (query.length < minChars || !provider) {
834
+ setSuggestions([]);
835
+ return;
836
+ }
837
+ setIsLoading(true);
838
+ try {
839
+ const results = await provider.getSuggestions(query, {
840
+ count: 7,
841
+ bounds: { from: "city", to: "settlement" }
842
+ });
843
+ setSuggestions(results);
844
+ setIsOpen(results.length > 0);
845
+ } catch (error) {
846
+ console.error("Error loading city suggestions:", error);
847
+ setSuggestions([]);
848
+ } finally {
849
+ setIsLoading(false);
850
+ }
851
+ },
852
+ [provider, minChars]
853
+ );
854
+ useEffect(() => {
855
+ if (justSelectedRef.current) {
856
+ justSelectedRef.current = false;
857
+ return;
858
+ }
859
+ if (debouncedQuery) {
860
+ fetchSuggestions(debouncedQuery);
861
+ } else {
862
+ setSuggestions([]);
863
+ setIsOpen(false);
864
+ }
865
+ }, [debouncedQuery, fetchSuggestions]);
866
+ useEffect(() => {
867
+ const handleClickOutside = (event) => {
868
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
869
+ setIsOpen(false);
870
+ }
871
+ };
872
+ document.addEventListener("mousedown", handleClickOutside);
873
+ return () => document.removeEventListener("mousedown", handleClickOutside);
874
+ }, []);
875
+ return {
876
+ inputValue,
877
+ setInputValue,
878
+ suggestions,
879
+ setSuggestions,
880
+ isLoading,
881
+ setIsLoading,
882
+ isOpen,
883
+ setIsOpen,
884
+ highlightedIndex,
885
+ setHighlightedIndex,
886
+ containerRef,
887
+ debouncedQuery,
888
+ justSelectedRef,
889
+ initializedRef
890
+ };
891
+ },
892
+ render: ({ field, fullPath, resolved, hasError, errorMessage, fieldState }) => {
893
+ const {
894
+ inputValue,
895
+ setInputValue,
896
+ suggestions,
897
+ setSuggestions,
898
+ isLoading,
899
+ isOpen,
900
+ setIsOpen,
901
+ highlightedIndex,
902
+ setHighlightedIndex,
903
+ containerRef
904
+ } = fieldState;
905
+ const { justSelectedRef, initializedRef } = fieldState;
906
+ const fieldValue = field.state.value;
907
+ if (!initializedRef.current && fieldValue && fieldValue !== inputValue) {
908
+ initializedRef.current = true;
909
+ setInputValue(fieldValue);
910
+ }
911
+ const handleSelect = (suggestion) => {
912
+ const cityName = suggestion.data?.city || suggestion.data?.settlement || suggestion.value;
913
+ justSelectedRef.current = true;
914
+ setInputValue(cityName);
915
+ setIsOpen(false);
916
+ setSuggestions([]);
917
+ field.handleChange(cityName);
918
+ };
919
+ const handleKeyDown = (e) => {
920
+ if (!isOpen || suggestions.length === 0) {
921
+ return;
922
+ }
923
+ switch (e.key) {
924
+ case "ArrowDown":
925
+ e.preventDefault();
926
+ setHighlightedIndex(highlightedIndex < suggestions.length - 1 ? highlightedIndex + 1 : 0);
927
+ break;
928
+ case "ArrowUp":
929
+ e.preventDefault();
930
+ setHighlightedIndex(highlightedIndex > 0 ? highlightedIndex - 1 : suggestions.length - 1);
931
+ break;
932
+ case "Enter":
933
+ e.preventDefault();
934
+ if (highlightedIndex >= 0) {
935
+ handleSelect(suggestions[highlightedIndex]);
936
+ }
937
+ break;
938
+ case "Escape":
939
+ setIsOpen(false);
940
+ break;
941
+ }
942
+ };
943
+ return /* @__PURE__ */ jsxs(
944
+ Field.Root,
945
+ {
946
+ invalid: hasError,
947
+ required: resolved.required,
948
+ disabled: resolved.disabled,
949
+ readOnly: resolved.readOnly,
950
+ children: [
951
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
952
+ /* @__PURE__ */ jsxs(Box, { ref: containerRef, position: "relative", width: "100%", children: [
953
+ /* @__PURE__ */ jsx(
954
+ Input,
955
+ {
956
+ value: inputValue,
957
+ onChange: (e) => {
958
+ setInputValue(e.target.value);
959
+ setHighlightedIndex(-1);
960
+ if (!e.target.value) {
961
+ field.handleChange("");
962
+ }
963
+ },
964
+ onFocus: () => {
965
+ if (suggestions.length > 0) {
966
+ setIsOpen(true);
967
+ }
968
+ },
969
+ onBlur: () => {
970
+ if (inputValue && inputValue !== field.state.value) {
971
+ field.handleChange(inputValue);
972
+ }
973
+ field.handleBlur();
974
+ },
975
+ onKeyDown: handleKeyDown,
976
+ placeholder: resolved.placeholder ?? "Enter city",
977
+ "data-field-name": fullPath
978
+ }
979
+ ),
980
+ isLoading && /* @__PURE__ */ jsx(Box, { position: "absolute", right: 3, top: "50%", transform: "translateY(-50%)", children: /* @__PURE__ */ jsx(Spinner, { size: "sm" }) }),
981
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
982
+ List.Root,
983
+ {
984
+ position: "absolute",
985
+ zIndex: 10,
986
+ width: "100%",
987
+ bg: "bg.panel",
988
+ borderWidth: "1px",
989
+ borderRadius: "md",
990
+ shadow: "md",
991
+ maxH: "250px",
992
+ overflowY: "auto",
993
+ mt: 1,
994
+ listStyle: "none",
995
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
996
+ List.Item,
997
+ {
998
+ px: 3,
999
+ py: 2,
1000
+ cursor: "pointer",
1001
+ bg: highlightedIndex === index ? "bg.muted" : void 0,
1002
+ _hover: { bg: "bg.muted" },
1003
+ onClick: () => handleSelect(suggestion),
1004
+ onMouseEnter: () => setHighlightedIndex(index),
1005
+ children: /* @__PURE__ */ jsx(Text, { fontSize: "sm", children: suggestion.label })
1006
+ },
1007
+ `${suggestion.value}-${index}`
1008
+ ))
1009
+ }
1010
+ )
1011
+ ] }),
1012
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
1013
+ ]
1014
+ }
1015
+ );
1016
+ }
1017
+ });
1018
+ function getCoords(e, canvas) {
1019
+ const rect = canvas.getBoundingClientRect();
1020
+ if ("touches" in e) {
1021
+ const touch = e.touches[0];
1022
+ return { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
1023
+ }
1024
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
1025
+ }
1026
+ var FieldSignature = createField({
1027
+ displayName: "FieldSignature",
1028
+ useFieldState: (props) => {
1029
+ const canvasRef = useRef(null);
1030
+ const isDrawingRef = useRef(false);
1031
+ const [mode, setMode] = useState("draw");
1032
+ const [typedText, setTypedText] = useState("");
1033
+ const [isEmpty, setIsEmpty] = useState(true);
1034
+ const strokeColor = props.strokeColor ?? "black";
1035
+ const strokeWidth = props.strokeWidth ?? 2;
1036
+ const backgroundColor = props.backgroundColor ?? "white";
1037
+ const typedFont = props.typedFont ?? "'Segoe Script', 'Dancing Script', cursive";
1038
+ const initCanvas = useCallback(() => {
1039
+ const canvas = canvasRef.current;
1040
+ if (!canvas) return;
1041
+ const ctx = canvas.getContext("2d");
1042
+ if (!ctx) return;
1043
+ ctx.fillStyle = backgroundColor;
1044
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1045
+ }, [backgroundColor]);
1046
+ useEffect(() => {
1047
+ initCanvas();
1048
+ }, [initCanvas]);
1049
+ const startDrawing = useCallback(
1050
+ (e) => {
1051
+ const canvas = canvasRef.current;
1052
+ if (!canvas) return;
1053
+ if ("touches" in e) e.preventDefault();
1054
+ const ctx = canvas.getContext("2d");
1055
+ if (!ctx) return;
1056
+ isDrawingRef.current = true;
1057
+ const { x, y } = getCoords(e, canvas);
1058
+ ctx.strokeStyle = strokeColor;
1059
+ ctx.lineWidth = strokeWidth;
1060
+ ctx.lineCap = "round";
1061
+ ctx.lineJoin = "round";
1062
+ ctx.beginPath();
1063
+ ctx.moveTo(x, y);
1064
+ },
1065
+ [strokeColor, strokeWidth]
1066
+ );
1067
+ const draw = useCallback((e) => {
1068
+ if (!isDrawingRef.current) return;
1069
+ const canvas = canvasRef.current;
1070
+ if (!canvas) return;
1071
+ if ("touches" in e) e.preventDefault();
1072
+ const ctx = canvas.getContext("2d");
1073
+ if (!ctx) return;
1074
+ const { x, y } = getCoords(e, canvas);
1075
+ ctx.lineTo(x, y);
1076
+ ctx.stroke();
1077
+ }, []);
1078
+ const stopDrawing = useCallback(() => {
1079
+ isDrawingRef.current = false;
1080
+ const canvas = canvasRef.current;
1081
+ if (!canvas) return "";
1082
+ setIsEmpty(false);
1083
+ return canvas.toDataURL("image/png");
1084
+ }, []);
1085
+ const clearCanvas = useCallback(() => {
1086
+ initCanvas();
1087
+ setIsEmpty(true);
1088
+ setTypedText("");
1089
+ }, [initCanvas]);
1090
+ const renderTypedSignature = useCallback(
1091
+ (text) => {
1092
+ const canvas = canvasRef.current;
1093
+ if (!canvas) return "";
1094
+ const ctx = canvas.getContext("2d");
1095
+ if (!ctx) return "";
1096
+ ctx.fillStyle = backgroundColor;
1097
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1098
+ if (!text.trim()) {
1099
+ setIsEmpty(true);
1100
+ return "";
1101
+ }
1102
+ const fontSize = Math.min(canvas.height * 0.4, 48);
1103
+ ctx.font = `${fontSize}px ${typedFont}`;
1104
+ ctx.fillStyle = strokeColor;
1105
+ ctx.textAlign = "center";
1106
+ ctx.textBaseline = "middle";
1107
+ ctx.fillText(text, canvas.width / 2, canvas.height / 2);
1108
+ setIsEmpty(false);
1109
+ return canvas.toDataURL("image/png");
1110
+ },
1111
+ [backgroundColor, strokeColor, typedFont]
1112
+ );
1113
+ return {
1114
+ canvasRef,
1115
+ mode,
1116
+ setMode,
1117
+ typedText,
1118
+ setTypedText,
1119
+ isEmpty,
1120
+ startDrawing,
1121
+ draw,
1122
+ stopDrawing,
1123
+ clearCanvas,
1124
+ renderTypedSignature
1125
+ };
1126
+ },
1127
+ render: ({ field, resolved, hasError, errorMessage, componentProps, fieldState }) => {
1128
+ const {
1129
+ width = 400,
1130
+ height = 150,
1131
+ clearLabel = "Clear",
1132
+ placeholder = "Sign here",
1133
+ allowTyped = true,
1134
+ disabled: _disabled
1135
+ } = componentProps;
1136
+ const {
1137
+ canvasRef,
1138
+ mode,
1139
+ setMode,
1140
+ typedText,
1141
+ setTypedText,
1142
+ isEmpty,
1143
+ startDrawing,
1144
+ draw,
1145
+ stopDrawing,
1146
+ clearCanvas,
1147
+ renderTypedSignature
1148
+ } = fieldState;
1149
+ const isDisabled = resolved.disabled;
1150
+ return /* @__PURE__ */ jsxs(Field.Root, { invalid: hasError, required: resolved.required, disabled: isDisabled, children: [
1151
+ /* @__PURE__ */ jsx(FieldLabel, { label: resolved.label, tooltip: resolved.tooltip, required: resolved.required }),
1152
+ /* @__PURE__ */ jsxs(
1153
+ Box,
1154
+ {
1155
+ position: "relative",
1156
+ borderWidth: "1px",
1157
+ borderColor: hasError ? "border.error" : "border",
1158
+ borderRadius: "md",
1159
+ overflow: "hidden",
1160
+ maxW: `${width}px`,
1161
+ opacity: isDisabled ? 0.5 : 1,
1162
+ pointerEvents: isDisabled ? "none" : "auto",
1163
+ children: [
1164
+ allowTyped && /* @__PURE__ */ jsx(HStack, { p: 2, borderBottomWidth: "1px", borderColor: "border", gap: 2, children: /* @__PURE__ */ jsxs(
1165
+ SegmentGroup.Root,
1166
+ {
1167
+ size: "xs",
1168
+ value: mode,
1169
+ onValueChange: (details) => {
1170
+ const newMode = details.value;
1171
+ setMode(newMode);
1172
+ if (newMode === "draw") {
1173
+ clearCanvas();
1174
+ field.handleChange("");
1175
+ }
1176
+ },
1177
+ children: [
1178
+ /* @__PURE__ */ jsxs(SegmentGroup.Item, { value: "draw", children: [
1179
+ /* @__PURE__ */ jsxs(SegmentGroup.ItemText, { children: [
1180
+ /* @__PURE__ */ jsx(LuPen, {}),
1181
+ " Draw"
1182
+ ] }),
1183
+ /* @__PURE__ */ jsx(SegmentGroup.ItemHiddenInput, {})
1184
+ ] }),
1185
+ /* @__PURE__ */ jsxs(SegmentGroup.Item, { value: "typed", children: [
1186
+ /* @__PURE__ */ jsxs(SegmentGroup.ItemText, { children: [
1187
+ /* @__PURE__ */ jsx(LuType, {}),
1188
+ " Type"
1189
+ ] }),
1190
+ /* @__PURE__ */ jsx(SegmentGroup.ItemHiddenInput, {})
1191
+ ] })
1192
+ ]
1193
+ }
1194
+ ) }),
1195
+ mode === "typed" && /* @__PURE__ */ jsx(Box, { p: 2, borderBottomWidth: "1px", borderColor: "border", children: /* @__PURE__ */ jsx(
1196
+ Input,
1197
+ {
1198
+ placeholder: "Type your name...",
1199
+ value: typedText,
1200
+ onChange: (e) => {
1201
+ const text = e.target.value;
1202
+ setTypedText(text);
1203
+ const dataUrl = renderTypedSignature(text);
1204
+ field.handleChange(dataUrl || "");
1205
+ },
1206
+ fontFamily: "cursive",
1207
+ fontSize: "lg"
1208
+ }
1209
+ ) }),
1210
+ /* @__PURE__ */ jsxs(Box, { position: "relative", children: [
1211
+ /* @__PURE__ */ jsx(
1212
+ "canvas",
1213
+ {
1214
+ ref: canvasRef,
1215
+ width,
1216
+ height,
1217
+ style: {
1218
+ display: "block",
1219
+ maxWidth: "100%",
1220
+ cursor: mode === "draw" ? "crosshair" : "default",
1221
+ touchAction: "none"
1222
+ // Отключить scroll при рисовании
1223
+ },
1224
+ role: "img",
1225
+ "aria-label": "Signature pad",
1226
+ tabIndex: 0,
1227
+ onMouseDown: mode === "draw" ? startDrawing : void 0,
1228
+ onMouseMove: mode === "draw" ? draw : void 0,
1229
+ onMouseUp: mode === "draw" ? () => {
1230
+ const dataUrl = stopDrawing();
1231
+ if (dataUrl) field.handleChange(dataUrl);
1232
+ } : void 0,
1233
+ onMouseLeave: mode === "draw" ? () => {
1234
+ const dataUrl = stopDrawing();
1235
+ if (dataUrl) field.handleChange(dataUrl);
1236
+ } : void 0,
1237
+ onTouchStart: mode === "draw" ? startDrawing : void 0,
1238
+ onTouchMove: mode === "draw" ? draw : void 0,
1239
+ onTouchEnd: mode === "draw" ? () => {
1240
+ const dataUrl = stopDrawing();
1241
+ if (dataUrl) field.handleChange(dataUrl);
1242
+ } : void 0
1243
+ }
1244
+ ),
1245
+ isEmpty && mode === "draw" && /* @__PURE__ */ jsx(
1246
+ Box,
1247
+ {
1248
+ position: "absolute",
1249
+ inset: 0,
1250
+ display: "flex",
1251
+ alignItems: "center",
1252
+ justifyContent: "center",
1253
+ pointerEvents: "none",
1254
+ color: "fg.subtle",
1255
+ fontSize: "sm",
1256
+ children: placeholder
1257
+ }
1258
+ )
1259
+ ] }),
1260
+ !isEmpty && /* @__PURE__ */ jsx(HStack, { p: 2, borderTopWidth: "1px", borderColor: "border", justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(
1261
+ Button,
1262
+ {
1263
+ size: "xs",
1264
+ variant: "ghost",
1265
+ colorPalette: "red",
1266
+ onClick: () => {
1267
+ clearCanvas();
1268
+ field.handleChange("");
1269
+ },
1270
+ children: [
1271
+ /* @__PURE__ */ jsx(LuEraser, {}),
1272
+ clearLabel
1273
+ ]
1274
+ }
1275
+ ) })
1276
+ ]
1277
+ }
1278
+ ),
1279
+ /* @__PURE__ */ jsx(FieldError, { hasError, errorMessage, helperText: resolved.helperText })
1280
+ ] });
1281
+ }
1282
+ });
1283
+ var BRAND_ICONS = {
1284
+ visa: (s) => /* @__PURE__ */ jsx("svg", { width: s, height: s * 0.64, viewBox: "0 0 24 24", fill: "#1A1F71", "aria-label": "Visa", children: /* @__PURE__ */ jsx("path", { d: "M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z" }) }),
1285
+ mastercard: (s) => /* @__PURE__ */ jsxs("svg", { width: s, height: s * 0.64, viewBox: "0 0 24 24", fill: "none", "aria-label": "Mastercard", children: [
1286
+ /* @__PURE__ */ jsx("circle", { cx: "7.416", cy: "12", r: "7.416", fill: "#EB001B" }),
1287
+ /* @__PURE__ */ jsx("circle", { cx: "16.584", cy: "12", r: "7.416", fill: "#F79E1B" }),
1288
+ /* @__PURE__ */ jsx(
1289
+ "path",
1290
+ {
1291
+ d: "M12 6.174a7.4 7.4 0 00-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232C13.844 16.236 14.831 14.236 14.831 12c0-2.235-.987-4.236-2.551-5.595a7.4 7.4 0 00-.28-.231z",
1292
+ fill: "#FF5F00"
1293
+ }
1294
+ )
1295
+ ] }),
1296
+ amex: (s) => /* @__PURE__ */ jsx("svg", { width: s, height: s * 0.64, viewBox: "0 0 24 24", fill: "#2E77BC", "aria-label": "American Express", children: /* @__PURE__ */ jsx("path", { d: "M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234.18-.072.48-.297.48-.747zm-.87.407c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0z" }) }),
1297
+ mir: (s) => /* @__PURE__ */ jsxs("svg", { width: s, height: s * 0.3, viewBox: "0 0 400 120", fill: "none", "aria-label": "\u041C\u0418\u0420", children: [
1298
+ /* @__PURE__ */ jsxs("linearGradient", { id: "mir-g", x1: "370", x2: "290", gradientUnits: "userSpaceOnUse", children: [
1299
+ /* @__PURE__ */ jsx("stop", { stopColor: "#1F5CD7" }),
1300
+ /* @__PURE__ */ jsx("stop", { stopColor: "#02AEFF", offset: "1" })
1301
+ ] }),
1302
+ /* @__PURE__ */ jsx(
1303
+ "path",
1304
+ {
1305
+ d: "m31 13h33c3 0 12-1 16 13 3 9 7 23 13 44h2c6-22 11-37 13-44 4-14 14-13 18-13h31v96h-32v-57h-2l-17 57h-24l-17-57h-3v57h-31m139-96h32v57h3l21-47c4-9 13-10 13-10h30v96h-32v-57h-2l-21 47c-4 9-14 10-14 10h-30m142-29v29h-30v-50h98c-4 12-18 21-34 21",
1306
+ fill: "#0F754E"
1307
+ }
1308
+ ),
1309
+ /* @__PURE__ */ jsx("path", { d: "m382 53c4-18-8-40-34-40h-68c2 21 20 40 39 40", fill: "url(#mir-g)" })
1310
+ ] })
1311
+ };
1312
+ function GenericCardIcon({ size }) {
1313
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size * 0.64, viewBox: "0 0 24 24", fill: "#a0aec0", "aria-label": "\u041A\u0430\u0440\u0442\u0430", children: /* @__PURE__ */ jsx("path", { d: "M22 4H2c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H2V10h20v8zm0-12H2V6h20v2z" }) });
1314
+ }
1315
+ function CardBrandIcon({ brand, size = 32 }) {
1316
+ const renderIcon = BRAND_ICONS[brand];
1317
+ return /* @__PURE__ */ jsx(
1318
+ "span",
1319
+ {
1320
+ style: {
1321
+ display: "inline-flex",
1322
+ alignItems: "center",
1323
+ justifyContent: "center",
1324
+ transition: "opacity 200ms ease",
1325
+ width: size,
1326
+ minWidth: size
1327
+ },
1328
+ children: renderIcon ? renderIcon(size) : /* @__PURE__ */ jsx(GenericCardIcon, { size })
1329
+ }
1330
+ );
1331
+ }
1332
+
1333
+ // src/lib/declarative/form-fields/specialized/credit-card/utils/detect-brand.ts
1334
+ var BRANDS = {
1335
+ visa: {
1336
+ brand: "visa",
1337
+ name: "Visa",
1338
+ lengths: [16, 18, 19],
1339
+ cvcLength: 3,
1340
+ gaps: [4, 4, 4, 4]
1341
+ },
1342
+ mastercard: {
1343
+ brand: "mastercard",
1344
+ name: "Mastercard",
1345
+ lengths: [16],
1346
+ cvcLength: 3,
1347
+ gaps: [4, 4, 4, 4]
1348
+ },
1349
+ amex: {
1350
+ brand: "amex",
1351
+ name: "American Express",
1352
+ lengths: [15],
1353
+ cvcLength: 4,
1354
+ gaps: [4, 6, 5]
1355
+ },
1356
+ mir: {
1357
+ brand: "mir",
1358
+ name: "\u041C\u0418\u0420",
1359
+ lengths: [16, 17, 18, 19],
1360
+ cvcLength: 3,
1361
+ gaps: [4, 4, 4, 4]
1362
+ },
1363
+ unionpay: {
1364
+ brand: "unionpay",
1365
+ name: "UnionPay",
1366
+ lengths: [16, 17, 18, 19],
1367
+ cvcLength: 3,
1368
+ gaps: [4, 4, 4, 4]
1369
+ },
1370
+ maestro: {
1371
+ brand: "maestro",
1372
+ name: "Maestro",
1373
+ lengths: [12, 13, 14, 15, 16, 17, 18, 19],
1374
+ cvcLength: 3,
1375
+ gaps: [4, 4, 4, 4]
1376
+ },
1377
+ jcb: {
1378
+ brand: "jcb",
1379
+ name: "JCB",
1380
+ lengths: [16, 17, 18, 19],
1381
+ cvcLength: 3,
1382
+ gaps: [4, 4, 4, 4]
1383
+ },
1384
+ discover: {
1385
+ brand: "discover",
1386
+ name: "Discover",
1387
+ lengths: [16, 19],
1388
+ cvcLength: 3,
1389
+ gaps: [4, 4, 4, 4]
1390
+ }
1391
+ };
1392
+ var UNKNOWN_BRAND = {
1393
+ brand: "unknown",
1394
+ name: "Unknown",
1395
+ lengths: [16],
1396
+ cvcLength: 3,
1397
+ gaps: [4, 4, 4, 4]
1398
+ };
1399
+ function detectBrand(number) {
1400
+ const digits = number.replace(/\D/g, "");
1401
+ if (!digits) return UNKNOWN_BRAND;
1402
+ const n2 = Number(digits.slice(0, 2));
1403
+ const n4 = Number(digits.slice(0, 4));
1404
+ const n6 = Number(digits.slice(0, 6));
1405
+ if (n4 >= 2200 && n4 <= 2204) return BRANDS.mir;
1406
+ if (n2 === 34 || n2 === 37) return BRANDS.amex;
1407
+ if (digits[0] === "4") return BRANDS.visa;
1408
+ if (n2 >= 51 && n2 <= 55 || n4 >= 2221 && n4 <= 2720) return BRANDS.mastercard;
1409
+ if (n4 === 6011 || n6 >= 622126 && n6 <= 622925 || n2 >= 64 && n2 <= 65) return BRANDS.discover;
1410
+ if (n4 >= 3528 && n4 <= 3589) return BRANDS.jcb;
1411
+ if (n2 === 62) return BRANDS.unionpay;
1412
+ if (n2 === 50 || n2 >= 56 && n2 <= 69) return BRANDS.maestro;
1413
+ return UNKNOWN_BRAND;
1414
+ }
1415
+
1416
+ // src/lib/declarative/form-fields/specialized/credit-card/utils/format-expiry.ts
1417
+ function formatExpiry(raw) {
1418
+ const digits = raw.replace(/\D/g, "");
1419
+ if (!digits) return "";
1420
+ if (digits.length <= 2) {
1421
+ return digits;
1422
+ }
1423
+ return `${digits.slice(0, 2)}/${digits.slice(2, 4)}`;
1424
+ }
1425
+ function isExpiryValid(expiry) {
1426
+ const match = expiry.match(/^(\d{2})\/(\d{2})$/);
1427
+ if (!match) return false;
1428
+ const month = Number(match[1]);
1429
+ const year = Number(match[2]);
1430
+ if (month < 1 || month > 12) return false;
1431
+ const now = /* @__PURE__ */ new Date();
1432
+ const currentYear = now.getFullYear() % 100;
1433
+ const currentMonth = now.getMonth() + 1;
1434
+ if (year < currentYear) return false;
1435
+ if (year === currentYear && month < currentMonth) return false;
1436
+ return true;
1437
+ }
1438
+
1439
+ // src/lib/declarative/form-fields/specialized/credit-card/utils/format-number.ts
1440
+ function formatCardNumber(raw) {
1441
+ const digits = raw.replace(/\D/g, "");
1442
+ if (!digits) return "";
1443
+ const brand = detectBrand(digits);
1444
+ const { gaps } = brand;
1445
+ const parts = [];
1446
+ let pos = 0;
1447
+ for (const gap of gaps) {
1448
+ if (pos >= digits.length) break;
1449
+ parts.push(digits.slice(pos, pos + gap));
1450
+ pos += gap;
1451
+ }
1452
+ if (pos < digits.length) {
1453
+ parts.push(digits.slice(pos));
1454
+ }
1455
+ return parts.join(" ");
1456
+ }
1457
+ function stripCardNumber(formatted) {
1458
+ return formatted.replace(/\D/g, "");
1459
+ }
1460
+ function maxFormattedLength(raw) {
1461
+ const brand = detectBrand(raw);
1462
+ const maxDigits = Math.max(...brand.lengths);
1463
+ const spaces = brand.gaps.length - 1;
1464
+ return maxDigits + spaces;
1465
+ }
1466
+
1467
+ // src/lib/declarative/form-fields/specialized/credit-card/utils/luhn.ts
1468
+ function luhn(cardNumber) {
1469
+ const digits = cardNumber.replace(/\D/g, "");
1470
+ if (digits.length < 12 || digits.length > 19) return false;
1471
+ let sum = 0;
1472
+ let isDouble = false;
1473
+ for (let i = digits.length - 1; i >= 0; i--) {
1474
+ let digit = Number(digits[i]);
1475
+ if (isDouble) {
1476
+ digit *= 2;
1477
+ if (digit > 9) digit -= 9;
1478
+ }
1479
+ sum += digit;
1480
+ isDouble = !isDouble;
1481
+ }
1482
+ return sum % 10 === 0;
1483
+ }
1484
+ function CreditCardField({
1485
+ name = "card",
1486
+ label = "\u0414\u0430\u043D\u043D\u044B\u0435 \u043A\u0430\u0440\u0442\u044B",
1487
+ brands,
1488
+ showBrandIcon = true,
1489
+ layout = "inline",
1490
+ disabled,
1491
+ readOnly,
1492
+ numberPlaceholder = "0000 0000 0000 0000",
1493
+ expiryPlaceholder = "MM / YY",
1494
+ cvcPlaceholder = "CVC"
1495
+ }) {
1496
+ const formCtx = useDeclarativeFormOptional();
1497
+ const expiryRef = useRef(null);
1498
+ const cvcRef = useRef(null);
1499
+ const [numberDisplay, setNumberDisplay] = useState("");
1500
+ const [expiryDisplay, setExpiryDisplay] = useState("");
1501
+ const [cvcValue, setCvcValue] = useState("");
1502
+ const [numberStatus, setNumberStatus] = useState("idle");
1503
+ const [expiryStatus, setExpiryStatus] = useState("idle");
1504
+ const [cvcStatus, setCvcStatus] = useState("idle");
1505
+ const [numberError, setNumberError] = useState();
1506
+ const [expiryError, setExpiryError] = useState();
1507
+ const brandInfo = useMemo(() => detectBrand(numberDisplay), [numberDisplay]);
1508
+ const isBrandAllowed = useMemo(() => {
1509
+ if (!brands || brands.length === 0) return true;
1510
+ return brands.includes(brandInfo.brand);
1511
+ }, [brands, brandInfo.brand]);
1512
+ const handleNumberChange = useCallback(
1513
+ (e) => {
1514
+ const raw = stripCardNumber(e.target.value);
1515
+ const formatted = formatCardNumber(raw);
1516
+ setNumberDisplay(formatted);
1517
+ setNumberStatus("idle");
1518
+ setNumberError(void 0);
1519
+ if (formCtx?.form) {
1520
+ formCtx.form.setFieldValue(`${name}.number`, raw);
1521
+ }
1522
+ const maxLen = Math.max(...brandInfo.lengths);
1523
+ if (raw.length >= maxLen) {
1524
+ expiryRef.current?.focus();
1525
+ }
1526
+ },
1527
+ [formCtx, name, brandInfo.lengths]
1528
+ );
1529
+ const handleNumberBlur = useCallback(() => {
1530
+ const raw = stripCardNumber(numberDisplay);
1531
+ if (!raw) return;
1532
+ if (raw.length < 12) {
1533
+ setNumberStatus("error");
1534
+ setNumberError("\u041D\u043E\u043C\u0435\u0440 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439");
1535
+ } else if (!luhn(raw)) {
1536
+ setNumberStatus("error");
1537
+ setNumberError("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043D\u043E\u043C\u0435\u0440 \u043A\u0430\u0440\u0442\u044B");
1538
+ } else if (!isBrandAllowed) {
1539
+ setNumberStatus("error");
1540
+ setNumberError("\u042D\u0442\u043E\u0442 \u0442\u0438\u043F \u043A\u0430\u0440\u0442\u044B \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F");
1541
+ } else {
1542
+ setNumberStatus("valid");
1543
+ setNumberError(void 0);
1544
+ }
1545
+ }, [numberDisplay, isBrandAllowed]);
1546
+ const handleExpiryChange = useCallback(
1547
+ (e) => {
1548
+ let raw = e.target.value.replace(/\D/g, "");
1549
+ if (raw.length === 1 && Number(raw) > 1) {
1550
+ raw = `0${raw}`;
1551
+ }
1552
+ const formatted = formatExpiry(raw);
1553
+ setExpiryDisplay(formatted);
1554
+ setExpiryStatus("idle");
1555
+ setExpiryError(void 0);
1556
+ if (formCtx?.form) {
1557
+ formCtx.form.setFieldValue(`${name}.expiry`, formatted);
1558
+ }
1559
+ if (formatted.length === 5) {
1560
+ cvcRef.current?.focus();
1561
+ }
1562
+ },
1563
+ [formCtx, name]
1564
+ );
1565
+ const handleExpiryBlur = useCallback(() => {
1566
+ if (!expiryDisplay) return;
1567
+ if (expiryDisplay.length < 5) {
1568
+ setExpiryStatus("error");
1569
+ setExpiryError("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 MM/YY");
1570
+ } else if (!isExpiryValid(expiryDisplay)) {
1571
+ setExpiryStatus("error");
1572
+ setExpiryError("\u041A\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u0430");
1573
+ } else {
1574
+ setExpiryStatus("valid");
1575
+ setExpiryError(void 0);
1576
+ }
1577
+ }, [expiryDisplay]);
1578
+ const handleCvcChange = useCallback(
1579
+ (e) => {
1580
+ const raw = e.target.value.replace(/\D/g, "").slice(0, brandInfo.cvcLength);
1581
+ setCvcValue(raw);
1582
+ setCvcStatus("idle");
1583
+ if (formCtx?.form) {
1584
+ formCtx.form.setFieldValue(`${name}.cvc`, raw);
1585
+ }
1586
+ },
1587
+ [formCtx, name, brandInfo.cvcLength]
1588
+ );
1589
+ const handleCvcBlur = useCallback(() => {
1590
+ if (!cvcValue) return;
1591
+ if (cvcValue.length < brandInfo.cvcLength) {
1592
+ setCvcStatus("error");
1593
+ } else {
1594
+ setCvcStatus("valid");
1595
+ }
1596
+ }, [cvcValue, brandInfo.cvcLength]);
1597
+ const isInline = layout === "inline";
1598
+ const statusBorder = (status) => {
1599
+ if (status === "valid") return "1px solid var(--chakra-colors-green-500, #38a169)";
1600
+ if (status === "error") return "1px solid var(--chakra-colors-red-500, #e53e3e)";
1601
+ return void 0;
1602
+ };
1603
+ const statusIcon = (status) => {
1604
+ if (status === "valid") return "\u2713";
1605
+ return null;
1606
+ };
1607
+ return /* @__PURE__ */ jsxs(Field.Root, { invalid: numberStatus === "error" || expiryStatus === "error", children: [
1608
+ label && /* @__PURE__ */ jsx(Field.Label, { children: label }),
1609
+ /* @__PURE__ */ jsxs(
1610
+ Flex,
1611
+ {
1612
+ role: "group",
1613
+ "aria-label": label,
1614
+ direction: isInline ? "row" : "column",
1615
+ gap: isInline ? 0 : 3,
1616
+ align: isInline ? "center" : "stretch",
1617
+ borderWidth: isInline ? "1px" : 0,
1618
+ borderColor: isInline ? "border" : void 0,
1619
+ borderRadius: isInline ? "md" : void 0,
1620
+ overflow: isInline ? "hidden" : void 0,
1621
+ _focusWithin: isInline ? { borderColor: "colorPalette.500", boxShadow: "0 0 0 1px var(--chakra-colors-colorPalette-500)" } : void 0,
1622
+ children: [
1623
+ /* @__PURE__ */ jsxs(
1624
+ Group,
1625
+ {
1626
+ attached: !isInline,
1627
+ flex: isInline ? "1" : void 0,
1628
+ gap: 0,
1629
+ children: [
1630
+ showBrandIcon && /* @__PURE__ */ jsx(
1631
+ Box,
1632
+ {
1633
+ px: 2,
1634
+ display: "flex",
1635
+ alignItems: "center",
1636
+ borderRightWidth: isInline ? "1px" : 0,
1637
+ borderColor: "border",
1638
+ children: /* @__PURE__ */ jsx(CardBrandIcon, { brand: brandInfo.brand, size: 28 })
1639
+ }
1640
+ ),
1641
+ /* @__PURE__ */ jsxs(Box, { position: "relative", flex: 1, children: [
1642
+ /* @__PURE__ */ jsx(
1643
+ Input,
1644
+ {
1645
+ value: numberDisplay,
1646
+ onChange: handleNumberChange,
1647
+ onBlur: handleNumberBlur,
1648
+ placeholder: numberPlaceholder,
1649
+ inputMode: "numeric",
1650
+ autoComplete: "cc-number",
1651
+ name: "cardnumber",
1652
+ maxLength: maxFormattedLength(numberDisplay),
1653
+ disabled,
1654
+ readOnly,
1655
+ "aria-label": "\u041D\u043E\u043C\u0435\u0440 \u043A\u0430\u0440\u0442\u044B",
1656
+ fontSize: "16px",
1657
+ border: isInline ? "none" : void 0,
1658
+ borderColor: !isInline ? statusBorder(numberStatus) ? void 0 : "border" : void 0,
1659
+ style: !isInline ? { border: statusBorder(numberStatus) } : void 0,
1660
+ _focus: isInline ? { boxShadow: "none" } : void 0
1661
+ }
1662
+ ),
1663
+ statusIcon(numberStatus) && /* @__PURE__ */ jsx(
1664
+ Box,
1665
+ {
1666
+ position: "absolute",
1667
+ right: 2,
1668
+ top: "50%",
1669
+ transform: "translateY(-50%)",
1670
+ color: "green.500",
1671
+ fontSize: "sm",
1672
+ fontWeight: "bold",
1673
+ children: statusIcon(numberStatus)
1674
+ }
1675
+ )
1676
+ ] })
1677
+ ]
1678
+ }
1679
+ ),
1680
+ /* @__PURE__ */ jsxs(Flex, { gap: isInline ? 0 : 2, children: [
1681
+ /* @__PURE__ */ jsxs(Box, { position: "relative", children: [
1682
+ /* @__PURE__ */ jsx(
1683
+ Input,
1684
+ {
1685
+ ref: expiryRef,
1686
+ value: expiryDisplay,
1687
+ onChange: handleExpiryChange,
1688
+ onBlur: handleExpiryBlur,
1689
+ placeholder: expiryPlaceholder,
1690
+ inputMode: "numeric",
1691
+ autoComplete: "cc-exp",
1692
+ name: "cc-exp",
1693
+ maxLength: 5,
1694
+ disabled,
1695
+ readOnly,
1696
+ "aria-label": "\u0421\u0440\u043E\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F",
1697
+ width: isInline ? "100px" : void 0,
1698
+ fontSize: "16px",
1699
+ border: isInline ? "none" : void 0,
1700
+ borderLeft: isInline ? "1px solid" : void 0,
1701
+ borderColor: isInline ? "border" : void 0,
1702
+ borderRadius: isInline ? 0 : void 0,
1703
+ style: !isInline ? { border: statusBorder(expiryStatus) } : void 0,
1704
+ _focus: isInline ? { boxShadow: "none" } : void 0
1705
+ }
1706
+ ),
1707
+ statusIcon(expiryStatus) && /* @__PURE__ */ jsx(
1708
+ Box,
1709
+ {
1710
+ position: "absolute",
1711
+ right: 2,
1712
+ top: "50%",
1713
+ transform: "translateY(-50%)",
1714
+ color: "green.500",
1715
+ fontSize: "sm",
1716
+ fontWeight: "bold",
1717
+ children: statusIcon(expiryStatus)
1718
+ }
1719
+ )
1720
+ ] }),
1721
+ /* @__PURE__ */ jsxs(Box, { position: "relative", children: [
1722
+ /* @__PURE__ */ jsxs(Tooltip.Root, { children: [
1723
+ /* @__PURE__ */ jsx(Tooltip.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
1724
+ Input,
1725
+ {
1726
+ ref: cvcRef,
1727
+ value: cvcValue,
1728
+ onChange: handleCvcChange,
1729
+ onBlur: handleCvcBlur,
1730
+ placeholder: cvcPlaceholder,
1731
+ inputMode: "numeric",
1732
+ autoComplete: "cc-csc",
1733
+ name: "cvc",
1734
+ maxLength: brandInfo.cvcLength,
1735
+ disabled,
1736
+ readOnly,
1737
+ "aria-label": `CVC (${brandInfo.cvcLength} \u0446\u0438\u0444\u0440\u044B)`,
1738
+ width: isInline ? "80px" : void 0,
1739
+ fontSize: "16px",
1740
+ border: isInline ? "none" : void 0,
1741
+ borderLeft: isInline ? "1px solid" : void 0,
1742
+ borderColor: isInline ? "border" : void 0,
1743
+ borderRadius: isInline ? 0 : void 0,
1744
+ style: !isInline ? { border: statusBorder(cvcStatus) } : void 0,
1745
+ _focus: isInline ? { boxShadow: "none" } : void 0
1746
+ }
1747
+ ) }),
1748
+ /* @__PURE__ */ jsx(Tooltip.Positioner, { children: /* @__PURE__ */ jsx(Tooltip.Content, { children: /* @__PURE__ */ jsx(Text, { fontSize: "xs", children: brandInfo.brand === "amex" ? "4 \u0446\u0438\u0444\u0440\u044B \u043D\u0430 \u043B\u0438\u0446\u0435\u0432\u043E\u0439 \u0441\u0442\u043E\u0440\u043E\u043D\u0435 \u043A\u0430\u0440\u0442\u044B" : "3 \u0446\u0438\u0444\u0440\u044B \u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u043D\u043E\u0439 \u0441\u0442\u043E\u0440\u043E\u043D\u0435 \u043A\u0430\u0440\u0442\u044B" }) }) })
1749
+ ] }),
1750
+ statusIcon(cvcStatus) && /* @__PURE__ */ jsx(
1751
+ Box,
1752
+ {
1753
+ position: "absolute",
1754
+ right: 2,
1755
+ top: "50%",
1756
+ transform: "translateY(-50%)",
1757
+ color: "green.500",
1758
+ fontSize: "sm",
1759
+ fontWeight: "bold",
1760
+ children: statusIcon(cvcStatus)
1761
+ }
1762
+ )
1763
+ ] })
1764
+ ] })
1765
+ ]
1766
+ }
1767
+ ),
1768
+ numberError && /* @__PURE__ */ jsx(Field.ErrorText, { children: numberError }),
1769
+ expiryError && /* @__PURE__ */ jsx(Field.ErrorText, { children: expiryError })
1770
+ ] });
1771
+ }
1772
+ function creditCardSchema() {
1773
+ return z.object({
1774
+ number: z.string().min(12, "\u041C\u0438\u043D\u0438\u043C\u0443\u043C 12 \u0446\u0438\u0444\u0440").max(19, "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 19 \u0446\u0438\u0444\u0440").refine((val) => luhn(val), "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043D\u043E\u043C\u0435\u0440 \u043A\u0430\u0440\u0442\u044B"),
1775
+ expiry: z.string().regex(/^\d{2}\/\d{2}$/, "\u0424\u043E\u0440\u043C\u0430\u0442 MM/YY").refine((val) => isExpiryValid(val), "\u041A\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u0430"),
1776
+ cvc: z.string().min(3, "\u041C\u0438\u043D\u0438\u043C\u0443\u043C 3 \u0446\u0438\u0444\u0440\u044B").max(4, "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 4 \u0446\u0438\u0444\u0440\u044B")
1777
+ });
1778
+ }
1779
+
1780
+ export { CardBrandIcon, CreditCardField, FieldAddress, FieldCity, FieldColorPicker, FieldFileUpload, FieldOTPInput, FieldPhone, FieldPinInput, FieldSignature, creditCardSchema, detectBrand, formatCardNumber, formatExpiry, isExpiryValid, luhn, parseFileSize, processFileWithSecurity, sanitizeFileName, validateMimeType };
1781
+ //# sourceMappingURL=chunk-2PSXYC3I.js.map
1782
+ //# sourceMappingURL=chunk-2PSXYC3I.js.map