@dmitryvim/form-builder 0.2.11 → 0.2.12

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.
@@ -2,58 +2,87 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ // src/utils/translation.ts
6
+ function t(key, state, params) {
7
+ const locale = state.config.locale || "en";
8
+ const localeTranslations = state.config.translations[locale];
9
+ const fallbackTranslations = state.config.translations.en;
10
+ let text = (localeTranslations == null ? void 0 : localeTranslations[key]) || (fallbackTranslations == null ? void 0 : fallbackTranslations[key]) || key;
11
+ if (params) {
12
+ for (const [paramKey, paramValue] of Object.entries(params)) {
13
+ text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), String(paramValue));
14
+ }
15
+ }
16
+ return text;
17
+ }
18
+
5
19
  // src/utils/validation.ts
6
- function addLengthHint(element, parts) {
7
- if (element.minLength !== null || element.maxLength !== null) {
8
- if (element.minLength !== null && element.maxLength !== null) {
9
- parts.push(`length=${element.minLength}-${element.maxLength} characters`);
10
- } else if (element.maxLength !== null) {
11
- parts.push(`max=${element.maxLength} characters`);
12
- } else if (element.minLength !== null) {
13
- parts.push(`min=${element.minLength} characters`);
20
+ function addLengthHint(element, parts, state) {
21
+ if (element.minLength != null || element.maxLength != null) {
22
+ if (element.minLength != null && element.maxLength != null) {
23
+ parts.push(
24
+ t("hintLengthRange", state, {
25
+ min: element.minLength,
26
+ max: element.maxLength
27
+ })
28
+ );
29
+ } else if (element.maxLength != null) {
30
+ parts.push(t("hintMaxLength", state, { max: element.maxLength }));
31
+ } else if (element.minLength != null) {
32
+ parts.push(t("hintMinLength", state, { min: element.minLength }));
14
33
  }
15
34
  }
16
35
  }
17
- function addRangeHint(element, parts) {
18
- if (element.min !== null || element.max !== null) {
19
- if (element.min !== null && element.max !== null) {
20
- parts.push(`range=${element.min}-${element.max}`);
21
- } else if (element.max !== null) {
22
- parts.push(`max=${element.max}`);
23
- } else if (element.min !== null) {
24
- parts.push(`min=${element.min}`);
36
+ function addRangeHint(element, parts, state) {
37
+ if (element.min != null || element.max != null) {
38
+ if (element.min != null && element.max != null) {
39
+ parts.push(
40
+ t("hintValueRange", state, { min: element.min, max: element.max })
41
+ );
42
+ } else if (element.max != null) {
43
+ parts.push(t("hintMaxValue", state, { max: element.max }));
44
+ } else if (element.min != null) {
45
+ parts.push(t("hintMinValue", state, { min: element.min }));
25
46
  }
26
47
  }
27
48
  }
28
- function addFileSizeHint(element, parts) {
49
+ function addFileSizeHint(element, parts, state) {
29
50
  if (element.maxSizeMB) {
30
- parts.push(`max_size=${element.maxSizeMB}MB`);
51
+ parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
31
52
  }
32
53
  }
33
- function addFormatHint(element, parts) {
54
+ function addFormatHint(element, parts, state) {
34
55
  var _a;
35
56
  if ((_a = element.accept) == null ? void 0 : _a.extensions) {
36
57
  parts.push(
37
- `formats=${element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")}`
58
+ t("hintFormats", state, {
59
+ formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
60
+ })
38
61
  );
39
62
  }
40
63
  }
41
- function addPatternHint(element, parts) {
42
- var _a;
43
- if (element.pattern && !element.pattern.includes("\u0410-\u042F")) {
44
- parts.push("plain text only");
45
- } else if ((_a = element.pattern) == null ? void 0 : _a.includes("\u0410-\u042F")) {
46
- parts.push("text with punctuation");
64
+ function addRequiredHint(element, parts, state) {
65
+ if (element.required) {
66
+ parts.push(t("hintRequired", state));
67
+ } else {
68
+ parts.push(t("hintOptional", state));
69
+ }
70
+ }
71
+ function addPatternHint(element, parts, state) {
72
+ if (element.pattern) {
73
+ parts.push(t("hintPattern", state, { pattern: element.pattern }));
47
74
  }
48
75
  }
49
- function makeFieldHint(element) {
76
+ function makeFieldHint(element, state) {
50
77
  const parts = [];
51
- parts.push(element.required ? "required" : "optional");
52
- addLengthHint(element, parts);
53
- addRangeHint(element, parts);
54
- addFileSizeHint(element, parts);
55
- addFormatHint(element, parts);
56
- addPatternHint(element, parts);
78
+ addRequiredHint(element, parts, state);
79
+ addLengthHint(element, parts, state);
80
+ if (element.type !== "slider") {
81
+ addRangeHint(element, parts, state);
82
+ }
83
+ addFileSizeHint(element, parts, state);
84
+ addFormatHint(element, parts, state);
85
+ addPatternHint(element, parts, state);
57
86
  return parts.join(" \u2022 ");
58
87
  }
59
88
  function validateSchema(schema) {
@@ -191,6 +220,11 @@ function validateSchema(schema) {
191
220
  function isPlainObject(obj) {
192
221
  return obj && typeof obj === "object" && obj.constructor === Object;
193
222
  }
223
+ function escapeHtml(text) {
224
+ const div = document.createElement("div");
225
+ div.textContent = text;
226
+ return div.innerHTML;
227
+ }
194
228
  function pathJoin(base, key) {
195
229
  return base ? `${base}.${key}` : key;
196
230
  }
@@ -269,13 +303,62 @@ function deepEqual(a, b) {
269
303
  }
270
304
 
271
305
  // src/components/text.ts
306
+ function createCharCounter(element, input, isTextarea = false) {
307
+ const counter = document.createElement("span");
308
+ counter.className = "char-counter";
309
+ counter.style.cssText = `
310
+ position: absolute;
311
+ ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
312
+ right: 10px;
313
+ font-size: var(--fb-font-size-small);
314
+ color: var(--fb-text-secondary-color);
315
+ pointer-events: none;
316
+ background: var(--fb-background-color);
317
+ padding: 0 4px;
318
+ `;
319
+ const updateCounter = () => {
320
+ const len = input.value.length;
321
+ const min = element.minLength;
322
+ const max = element.maxLength;
323
+ if (min == null && max == null) {
324
+ counter.textContent = "";
325
+ return;
326
+ }
327
+ if (len === 0 || min != null && len < min) {
328
+ if (min != null && max != null) {
329
+ counter.textContent = `${min}-${max}`;
330
+ } else if (max != null) {
331
+ counter.textContent = `\u2264${max}`;
332
+ } else if (min != null) {
333
+ counter.textContent = `\u2265${min}`;
334
+ }
335
+ counter.style.color = "var(--fb-text-secondary-color)";
336
+ } else if (max != null && len > max) {
337
+ counter.textContent = `${len}/${max}`;
338
+ counter.style.color = "var(--fb-error-color)";
339
+ } else {
340
+ if (max != null) {
341
+ counter.textContent = `${len}/${max}`;
342
+ } else {
343
+ counter.textContent = `${len}`;
344
+ }
345
+ counter.style.color = "var(--fb-text-secondary-color)";
346
+ }
347
+ };
348
+ input.addEventListener("input", updateCounter);
349
+ updateCounter();
350
+ return counter;
351
+ }
272
352
  function renderTextElement(element, ctx, wrapper, pathKey) {
273
353
  const state = ctx.state;
354
+ const inputWrapper = document.createElement("div");
355
+ inputWrapper.style.cssText = "position: relative;";
274
356
  const textInput = document.createElement("input");
275
357
  textInput.type = "text";
276
358
  textInput.className = "w-full rounded-lg";
277
359
  textInput.style.cssText = `
278
360
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
361
+ padding-right: 60px;
279
362
  border: var(--fb-border-width) solid var(--fb-border-color);
280
363
  border-radius: var(--fb-border-radius);
281
364
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -283,6 +366,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
283
366
  font-size: var(--fb-font-size);
284
367
  font-family: var(--fb-font-family);
285
368
  transition: all var(--fb-transition-duration) ease-in-out;
369
+ width: 100%;
370
+ box-sizing: border-box;
286
371
  `;
287
372
  textInput.name = pathKey;
288
373
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
@@ -317,15 +402,12 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
317
402
  textInput.addEventListener("blur", handleChange);
318
403
  textInput.addEventListener("input", handleChange);
319
404
  }
320
- wrapper.appendChild(textInput);
321
- const textHint = document.createElement("p");
322
- textHint.className = "mt-1";
323
- textHint.style.cssText = `
324
- font-size: var(--fb-font-size-small);
325
- color: var(--fb-text-secondary-color);
326
- `;
327
- textHint.textContent = makeFieldHint(element);
328
- wrapper.appendChild(textHint);
405
+ inputWrapper.appendChild(textInput);
406
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
407
+ const counter = createCharCounter(element, textInput, false);
408
+ inputWrapper.appendChild(counter);
409
+ }
410
+ wrapper.appendChild(inputWrapper);
329
411
  }
330
412
  function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
331
413
  var _a, _b;
@@ -352,11 +434,13 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
352
434
  function addTextItem(value = "", index = -1) {
353
435
  const itemWrapper = document.createElement("div");
354
436
  itemWrapper.className = "multiple-text-item flex items-center gap-2";
437
+ const inputContainer = document.createElement("div");
438
+ inputContainer.style.cssText = "position: relative; flex: 1;";
355
439
  const textInput = document.createElement("input");
356
440
  textInput.type = "text";
357
- textInput.className = "flex-1";
358
441
  textInput.style.cssText = `
359
442
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
443
+ padding-right: 60px;
360
444
  border: var(--fb-border-width) solid var(--fb-border-color);
361
445
  border-radius: var(--fb-border-radius);
362
446
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -364,8 +448,10 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
364
448
  font-size: var(--fb-font-size);
365
449
  font-family: var(--fb-font-family);
366
450
  transition: all var(--fb-transition-duration) ease-in-out;
451
+ width: 100%;
452
+ box-sizing: border-box;
367
453
  `;
368
- textInput.placeholder = element.placeholder || "Enter text";
454
+ textInput.placeholder = element.placeholder || t("placeholderText", state);
369
455
  textInput.value = value;
370
456
  textInput.readOnly = state.config.readonly;
371
457
  if (!state.config.readonly) {
@@ -397,7 +483,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
397
483
  textInput.addEventListener("blur", handleChange);
398
484
  textInput.addEventListener("input", handleChange);
399
485
  }
400
- itemWrapper.appendChild(textInput);
486
+ inputContainer.appendChild(textInput);
487
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
488
+ const counter = createCharCounter(element, textInput, false);
489
+ inputContainer.appendChild(counter);
490
+ }
491
+ itemWrapper.appendChild(inputContainer);
401
492
  if (index === -1) {
402
493
  container.appendChild(itemWrapper);
403
494
  } else {
@@ -450,47 +541,54 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
450
541
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
451
542
  });
452
543
  }
544
+ let addRow = null;
545
+ let countDisplay = null;
546
+ if (!state.config.readonly) {
547
+ addRow = document.createElement("div");
548
+ addRow.className = "flex items-center gap-3 mt-2";
549
+ const addBtn = document.createElement("button");
550
+ addBtn.type = "button";
551
+ addBtn.className = "add-text-btn px-3 py-1 rounded";
552
+ addBtn.style.cssText = `
553
+ color: var(--fb-primary-color);
554
+ border: var(--fb-border-width) solid var(--fb-primary-color);
555
+ background-color: transparent;
556
+ font-size: var(--fb-font-size);
557
+ transition: all var(--fb-transition-duration);
558
+ `;
559
+ addBtn.textContent = "+";
560
+ addBtn.addEventListener("mouseenter", () => {
561
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
562
+ });
563
+ addBtn.addEventListener("mouseleave", () => {
564
+ addBtn.style.backgroundColor = "transparent";
565
+ });
566
+ addBtn.onclick = () => {
567
+ values.push(element.default || "");
568
+ addTextItem(element.default || "");
569
+ updateAddButton();
570
+ updateRemoveButtons();
571
+ };
572
+ countDisplay = document.createElement("span");
573
+ countDisplay.className = "text-sm text-gray-500";
574
+ addRow.appendChild(addBtn);
575
+ addRow.appendChild(countDisplay);
576
+ wrapper.appendChild(addRow);
577
+ }
453
578
  function updateAddButton() {
454
- const existingAddBtn = wrapper.querySelector(".add-text-btn");
455
- if (existingAddBtn) existingAddBtn.remove();
456
- if (!state.config.readonly && values.length < maxCount) {
457
- const addBtn = document.createElement("button");
458
- addBtn.type = "button";
459
- addBtn.className = "add-text-btn mt-2 px-3 py-1 rounded";
460
- addBtn.style.cssText = `
461
- color: var(--fb-primary-color);
462
- border: var(--fb-border-width) solid var(--fb-primary-color);
463
- background-color: transparent;
464
- font-size: var(--fb-font-size);
465
- transition: all var(--fb-transition-duration);
466
- `;
467
- addBtn.textContent = "+";
468
- addBtn.addEventListener("mouseenter", () => {
469
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
470
- });
471
- addBtn.addEventListener("mouseleave", () => {
472
- addBtn.style.backgroundColor = "transparent";
473
- });
474
- addBtn.onclick = () => {
475
- values.push(element.default || "");
476
- addTextItem(element.default || "");
477
- updateAddButton();
478
- updateRemoveButtons();
479
- };
480
- wrapper.appendChild(addBtn);
579
+ if (!addRow || !countDisplay) return;
580
+ const addBtn = addRow.querySelector(".add-text-btn");
581
+ if (addBtn) {
582
+ const disabled = values.length >= maxCount;
583
+ addBtn.disabled = disabled;
584
+ addBtn.style.opacity = disabled ? "0.5" : "1";
585
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
481
586
  }
587
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
482
588
  }
483
589
  values.forEach((value) => addTextItem(value));
484
590
  updateAddButton();
485
591
  updateRemoveButtons();
486
- const hint = document.createElement("p");
487
- hint.className = "mt-1";
488
- hint.style.cssText = `
489
- font-size: var(--fb-font-size-small);
490
- color: var(--fb-text-secondary-color);
491
- `;
492
- hint.textContent = makeFieldHint(element);
493
- wrapper.appendChild(hint);
494
592
  }
495
593
  function validateTextElement(element, key, context) {
496
594
  var _a, _b, _c;
@@ -531,26 +629,31 @@ function validateTextElement(element, key, context) {
531
629
  };
532
630
  const validateTextInput = (input, val, fieldKey) => {
533
631
  let hasError = false;
632
+ const { state } = context;
534
633
  if (!skipValidation && val) {
535
634
  if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
536
- errors.push(`${fieldKey}: minLength=${element.minLength}`);
537
- markValidity(input, `minLength=${element.minLength}`);
635
+ const msg = t("minLength", state, { min: element.minLength });
636
+ errors.push(`${fieldKey}: ${msg}`);
637
+ markValidity(input, msg);
538
638
  hasError = true;
539
639
  } else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
540
- errors.push(`${fieldKey}: maxLength=${element.maxLength}`);
541
- markValidity(input, `maxLength=${element.maxLength}`);
640
+ const msg = t("maxLength", state, { max: element.maxLength });
641
+ errors.push(`${fieldKey}: ${msg}`);
642
+ markValidity(input, msg);
542
643
  hasError = true;
543
644
  } else if (element.pattern) {
544
645
  try {
545
646
  const re = new RegExp(element.pattern);
546
647
  if (!re.test(val)) {
547
- errors.push(`${fieldKey}: pattern mismatch`);
548
- markValidity(input, "pattern mismatch");
648
+ const msg = t("patternMismatch", state);
649
+ errors.push(`${fieldKey}: ${msg}`);
650
+ markValidity(input, msg);
549
651
  hasError = true;
550
652
  }
551
653
  } catch {
552
- errors.push(`${fieldKey}: invalid pattern`);
553
- markValidity(input, "invalid pattern");
654
+ const msg = t("invalidPattern", state);
655
+ errors.push(`${fieldKey}: ${msg}`);
656
+ markValidity(input, msg);
554
657
  hasError = true;
555
658
  }
556
659
  }
@@ -571,17 +674,18 @@ function validateTextElement(element, key, context) {
571
674
  validateTextInput(input, val, `${key}[${index}]`);
572
675
  });
573
676
  if (!skipValidation) {
677
+ const { state } = context;
574
678
  const minCount = (_a = element.minCount) != null ? _a : 1;
575
679
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
576
680
  const filteredValues = rawValues.filter((v) => v.trim() !== "");
577
681
  if (element.required && filteredValues.length === 0) {
578
- errors.push(`${key}: required`);
682
+ errors.push(`${key}: ${t("required", state)}`);
579
683
  }
580
684
  if (filteredValues.length < minCount) {
581
- errors.push(`${key}: minimum ${minCount} items required`);
685
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
582
686
  }
583
687
  if (filteredValues.length > maxCount) {
584
- errors.push(`${key}: maximum ${maxCount} items allowed`);
688
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
585
689
  }
586
690
  }
587
691
  return { value: values, errors };
@@ -589,8 +693,9 @@ function validateTextElement(element, key, context) {
589
693
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
590
694
  const val = (_c = input == null ? void 0 : input.value) != null ? _c : "";
591
695
  if (!skipValidation && element.required && val === "") {
592
- errors.push(`${key}: required`);
593
- markValidity(input, "required");
696
+ const msg = t("required", context.state);
697
+ errors.push(`${key}: ${msg}`);
698
+ markValidity(input, msg);
594
699
  return { value: null, errors };
595
700
  }
596
701
  if (input) {
@@ -634,8 +739,11 @@ function updateTextField(element, fieldPath, value, context) {
634
739
  // src/components/textarea.ts
635
740
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
636
741
  const state = ctx.state;
742
+ const textareaWrapper = document.createElement("div");
743
+ textareaWrapper.style.cssText = "position: relative;";
637
744
  const textareaInput = document.createElement("textarea");
638
745
  textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
746
+ textareaInput.style.cssText = "padding-bottom: 24px;";
639
747
  textareaInput.name = pathKey;
640
748
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
641
749
  textareaInput.rows = element.rows || 4;
@@ -649,11 +757,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
649
757
  textareaInput.addEventListener("blur", handleChange);
650
758
  textareaInput.addEventListener("input", handleChange);
651
759
  }
652
- wrapper.appendChild(textareaInput);
653
- const textareaHint = document.createElement("p");
654
- textareaHint.className = "text-xs text-gray-500 mt-1";
655
- textareaHint.textContent = makeFieldHint(element);
656
- wrapper.appendChild(textareaHint);
760
+ textareaWrapper.appendChild(textareaInput);
761
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
762
+ const counter = createCharCounter(element, textareaInput, true);
763
+ textareaWrapper.appendChild(counter);
764
+ }
765
+ wrapper.appendChild(textareaWrapper);
657
766
  }
658
767
  function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
659
768
  var _a, _b;
@@ -680,9 +789,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
680
789
  function addTextareaItem(value = "", index = -1) {
681
790
  const itemWrapper = document.createElement("div");
682
791
  itemWrapper.className = "multiple-textarea-item";
792
+ const textareaContainer = document.createElement("div");
793
+ textareaContainer.style.cssText = "position: relative;";
683
794
  const textareaInput = document.createElement("textarea");
684
795
  textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
685
- textareaInput.placeholder = element.placeholder || "Enter text";
796
+ textareaInput.style.cssText = "padding-bottom: 24px;";
797
+ textareaInput.placeholder = element.placeholder || t("placeholderText", state);
686
798
  textareaInput.rows = element.rows || 4;
687
799
  textareaInput.value = value;
688
800
  textareaInput.readOnly = state.config.readonly;
@@ -694,7 +806,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
694
806
  textareaInput.addEventListener("blur", handleChange);
695
807
  textareaInput.addEventListener("input", handleChange);
696
808
  }
697
- itemWrapper.appendChild(textareaInput);
809
+ textareaContainer.appendChild(textareaInput);
810
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
811
+ const counter = createCharCounter(element, textareaInput, true);
812
+ textareaContainer.appendChild(counter);
813
+ }
814
+ itemWrapper.appendChild(textareaContainer);
698
815
  if (index === -1) {
699
816
  container.appendChild(itemWrapper);
700
817
  } else {
@@ -736,30 +853,54 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
736
853
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
737
854
  });
738
855
  }
856
+ let addRow = null;
857
+ let countDisplay = null;
858
+ if (!state.config.readonly) {
859
+ addRow = document.createElement("div");
860
+ addRow.className = "flex items-center gap-3 mt-2";
861
+ const addBtn = document.createElement("button");
862
+ addBtn.type = "button";
863
+ addBtn.className = "add-textarea-btn px-3 py-1 rounded";
864
+ addBtn.style.cssText = `
865
+ color: var(--fb-primary-color);
866
+ border: var(--fb-border-width) solid var(--fb-primary-color);
867
+ background-color: transparent;
868
+ font-size: var(--fb-font-size);
869
+ transition: all var(--fb-transition-duration);
870
+ `;
871
+ addBtn.textContent = "+";
872
+ addBtn.addEventListener("mouseenter", () => {
873
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
874
+ });
875
+ addBtn.addEventListener("mouseleave", () => {
876
+ addBtn.style.backgroundColor = "transparent";
877
+ });
878
+ addBtn.onclick = () => {
879
+ values.push(element.default || "");
880
+ addTextareaItem(element.default || "");
881
+ updateAddButton();
882
+ updateRemoveButtons();
883
+ };
884
+ countDisplay = document.createElement("span");
885
+ countDisplay.className = "text-sm text-gray-500";
886
+ addRow.appendChild(addBtn);
887
+ addRow.appendChild(countDisplay);
888
+ wrapper.appendChild(addRow);
889
+ }
739
890
  function updateAddButton() {
740
- const existingAddBtn = wrapper.querySelector(".add-textarea-btn");
741
- if (existingAddBtn) existingAddBtn.remove();
742
- if (!state.config.readonly && values.length < maxCount) {
743
- const addBtn = document.createElement("button");
744
- addBtn.type = "button";
745
- addBtn.className = "add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
746
- addBtn.textContent = "+";
747
- addBtn.onclick = () => {
748
- values.push(element.default || "");
749
- addTextareaItem(element.default || "");
750
- updateAddButton();
751
- updateRemoveButtons();
752
- };
753
- wrapper.appendChild(addBtn);
891
+ if (!addRow || !countDisplay) return;
892
+ const addBtn = addRow.querySelector(".add-textarea-btn");
893
+ if (addBtn) {
894
+ const disabled = values.length >= maxCount;
895
+ addBtn.disabled = disabled;
896
+ addBtn.style.opacity = disabled ? "0.5" : "1";
897
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
754
898
  }
899
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
755
900
  }
756
901
  values.forEach((value) => addTextareaItem(value));
757
902
  updateAddButton();
758
903
  updateRemoveButtons();
759
- const hint = document.createElement("p");
760
- hint.className = "text-xs text-gray-500 mt-1";
761
- hint.textContent = makeFieldHint(element);
762
- wrapper.appendChild(hint);
763
904
  }
764
905
  function validateTextareaElement(element, key, context) {
765
906
  return validateTextElement(element, key, context);
@@ -769,11 +910,61 @@ function updateTextareaField(element, fieldPath, value, context) {
769
910
  }
770
911
 
771
912
  // src/components/number.ts
913
+ function createNumberCounter(element, input) {
914
+ const counter = document.createElement("span");
915
+ counter.className = "number-counter";
916
+ counter.style.cssText = `
917
+ position: absolute;
918
+ top: 50%;
919
+ transform: translateY(-50%);
920
+ right: 10px;
921
+ font-size: var(--fb-font-size-small);
922
+ color: var(--fb-text-secondary-color);
923
+ pointer-events: none;
924
+ background: var(--fb-background-color);
925
+ padding: 0 4px;
926
+ `;
927
+ const updateCounter = () => {
928
+ const val = input.value ? parseFloat(input.value) : null;
929
+ const min = element.min;
930
+ const max = element.max;
931
+ if (min == null && max == null) {
932
+ counter.textContent = "";
933
+ return;
934
+ }
935
+ if (val == null || min != null && val < min) {
936
+ if (min != null && max != null) {
937
+ counter.textContent = `${min}-${max}`;
938
+ } else if (max != null) {
939
+ counter.textContent = `\u2264${max}`;
940
+ } else if (min != null) {
941
+ counter.textContent = `\u2265${min}`;
942
+ }
943
+ counter.style.color = "var(--fb-text-secondary-color)";
944
+ } else if (max != null && val > max) {
945
+ counter.textContent = `${val}/${max}`;
946
+ counter.style.color = "var(--fb-error-color)";
947
+ } else {
948
+ if (max != null) {
949
+ counter.textContent = `${val}/${max}`;
950
+ } else {
951
+ counter.textContent = `${val}`;
952
+ }
953
+ counter.style.color = "var(--fb-text-secondary-color)";
954
+ }
955
+ };
956
+ input.addEventListener("input", updateCounter);
957
+ updateCounter();
958
+ return counter;
959
+ }
772
960
  function renderNumberElement(element, ctx, wrapper, pathKey) {
773
961
  const state = ctx.state;
962
+ const inputWrapper = document.createElement("div");
963
+ inputWrapper.style.cssText = "position: relative;";
774
964
  const numberInput = document.createElement("input");
775
965
  numberInput.type = "number";
776
966
  numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
967
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
777
968
  numberInput.name = pathKey;
778
969
  numberInput.placeholder = element.placeholder || "0";
779
970
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -789,11 +980,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
789
980
  numberInput.addEventListener("blur", handleChange);
790
981
  numberInput.addEventListener("input", handleChange);
791
982
  }
792
- wrapper.appendChild(numberInput);
793
- const numberHint = document.createElement("p");
794
- numberHint.className = "text-xs text-gray-500 mt-1";
795
- numberHint.textContent = makeFieldHint(element);
796
- wrapper.appendChild(numberHint);
983
+ inputWrapper.appendChild(numberInput);
984
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
985
+ const counter = createNumberCounter(element, numberInput);
986
+ inputWrapper.appendChild(counter);
987
+ }
988
+ wrapper.appendChild(inputWrapper);
797
989
  }
798
990
  function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
799
991
  var _a, _b;
@@ -820,9 +1012,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
820
1012
  function addNumberItem(value = "", index = -1) {
821
1013
  const itemWrapper = document.createElement("div");
822
1014
  itemWrapper.className = "multiple-number-item flex items-center gap-2";
1015
+ const inputContainer = document.createElement("div");
1016
+ inputContainer.style.cssText = "position: relative; flex: 1;";
823
1017
  const numberInput = document.createElement("input");
824
1018
  numberInput.type = "number";
825
- numberInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1019
+ numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1020
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
826
1021
  numberInput.placeholder = element.placeholder || "0";
827
1022
  if (element.min !== void 0) numberInput.min = element.min.toString();
828
1023
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -837,7 +1032,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
837
1032
  numberInput.addEventListener("blur", handleChange);
838
1033
  numberInput.addEventListener("input", handleChange);
839
1034
  }
840
- itemWrapper.appendChild(numberInput);
1035
+ inputContainer.appendChild(numberInput);
1036
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
1037
+ const counter = createNumberCounter(element, numberInput);
1038
+ inputContainer.appendChild(counter);
1039
+ }
1040
+ itemWrapper.appendChild(inputContainer);
841
1041
  if (index === -1) {
842
1042
  container.appendChild(itemWrapper);
843
1043
  } else {
@@ -879,30 +1079,54 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
879
1079
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
880
1080
  });
881
1081
  }
1082
+ let addRow = null;
1083
+ let countDisplay = null;
1084
+ if (!state.config.readonly) {
1085
+ addRow = document.createElement("div");
1086
+ addRow.className = "flex items-center gap-3 mt-2";
1087
+ const addBtn = document.createElement("button");
1088
+ addBtn.type = "button";
1089
+ addBtn.className = "add-number-btn px-3 py-1 rounded";
1090
+ addBtn.style.cssText = `
1091
+ color: var(--fb-primary-color);
1092
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1093
+ background-color: transparent;
1094
+ font-size: var(--fb-font-size);
1095
+ transition: all var(--fb-transition-duration);
1096
+ `;
1097
+ addBtn.textContent = "+";
1098
+ addBtn.addEventListener("mouseenter", () => {
1099
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1100
+ });
1101
+ addBtn.addEventListener("mouseleave", () => {
1102
+ addBtn.style.backgroundColor = "transparent";
1103
+ });
1104
+ addBtn.onclick = () => {
1105
+ values.push(element.default || "");
1106
+ addNumberItem(element.default || "");
1107
+ updateAddButton();
1108
+ updateRemoveButtons();
1109
+ };
1110
+ countDisplay = document.createElement("span");
1111
+ countDisplay.className = "text-sm text-gray-500";
1112
+ addRow.appendChild(addBtn);
1113
+ addRow.appendChild(countDisplay);
1114
+ wrapper.appendChild(addRow);
1115
+ }
882
1116
  function updateAddButton() {
883
- const existingAddBtn = wrapper.querySelector(".add-number-btn");
884
- if (existingAddBtn) existingAddBtn.remove();
885
- if (!state.config.readonly && values.length < maxCount) {
886
- const addBtn = document.createElement("button");
887
- addBtn.type = "button";
888
- addBtn.className = "add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
889
- addBtn.textContent = "+";
890
- addBtn.onclick = () => {
891
- values.push(element.default || "");
892
- addNumberItem(element.default || "");
893
- updateAddButton();
894
- updateRemoveButtons();
895
- };
896
- wrapper.appendChild(addBtn);
1117
+ if (!addRow || !countDisplay) return;
1118
+ const addBtn = addRow.querySelector(".add-number-btn");
1119
+ if (addBtn) {
1120
+ const disabled = values.length >= maxCount;
1121
+ addBtn.disabled = disabled;
1122
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1123
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
897
1124
  }
1125
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
898
1126
  }
899
1127
  values.forEach((value) => addNumberItem(value));
900
1128
  updateAddButton();
901
1129
  updateRemoveButtons();
902
- const hint = document.createElement("p");
903
- hint.className = "text-xs text-gray-500 mt-1";
904
- hint.textContent = makeFieldHint(element);
905
- wrapper.appendChild(hint);
906
1130
  }
907
1131
  function validateNumberElement(element, key, context) {
908
1132
  var _a, _b, _c, _d, _e;
@@ -943,13 +1167,16 @@ function validateNumberElement(element, key, context) {
943
1167
  };
944
1168
  const validateNumberInput = (input, v, fieldKey) => {
945
1169
  let hasError = false;
1170
+ const { state } = context;
946
1171
  if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
947
- errors.push(`${fieldKey}: < min=${element.min}`);
948
- markValidity(input, `< min=${element.min}`);
1172
+ const msg = t("minValue", state, { min: element.min });
1173
+ errors.push(`${fieldKey}: ${msg}`);
1174
+ markValidity(input, msg);
949
1175
  hasError = true;
950
1176
  } else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
951
- errors.push(`${fieldKey}: > max=${element.max}`);
952
- markValidity(input, `> max=${element.max}`);
1177
+ const msg = t("maxValue", state, { max: element.max });
1178
+ errors.push(`${fieldKey}: ${msg}`);
1179
+ markValidity(input, msg);
953
1180
  hasError = true;
954
1181
  }
955
1182
  if (!hasError) {
@@ -971,8 +1198,9 @@ function validateNumberElement(element, key, context) {
971
1198
  }
972
1199
  const v = parseFloat(raw);
973
1200
  if (!skipValidation && !Number.isFinite(v)) {
974
- errors.push(`${key}[${index}]: not a number`);
975
- markValidity(input, "not a number");
1201
+ const msg = t("notANumber", context.state);
1202
+ errors.push(`${key}[${index}]: ${msg}`);
1203
+ markValidity(input, msg);
976
1204
  values.push(null);
977
1205
  return;
978
1206
  }
@@ -981,26 +1209,29 @@ function validateNumberElement(element, key, context) {
981
1209
  values.push(Number(v.toFixed(d)));
982
1210
  });
983
1211
  if (!skipValidation) {
1212
+ const { state } = context;
984
1213
  const minCount = (_a = element.minCount) != null ? _a : 1;
985
1214
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
986
1215
  const filteredValues = values.filter((v) => v !== null);
987
1216
  if (element.required && filteredValues.length === 0) {
988
- errors.push(`${key}: required`);
1217
+ errors.push(`${key}: ${t("required", state)}`);
989
1218
  }
990
1219
  if (filteredValues.length < minCount) {
991
- errors.push(`${key}: minimum ${minCount} items required`);
1220
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
992
1221
  }
993
1222
  if (filteredValues.length > maxCount) {
994
- errors.push(`${key}: maximum ${maxCount} items allowed`);
1223
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
995
1224
  }
996
1225
  }
997
1226
  return { value: values, errors };
998
1227
  } else {
999
1228
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
1000
1229
  const raw = (_c = input == null ? void 0 : input.value) != null ? _c : "";
1230
+ const { state } = context;
1001
1231
  if (!skipValidation && element.required && raw === "") {
1002
- errors.push(`${key}: required`);
1003
- markValidity(input, "required");
1232
+ const msg = t("required", state);
1233
+ errors.push(`${key}: ${msg}`);
1234
+ markValidity(input, msg);
1004
1235
  return { value: null, errors };
1005
1236
  }
1006
1237
  if (raw === "") {
@@ -1009,8 +1240,9 @@ function validateNumberElement(element, key, context) {
1009
1240
  }
1010
1241
  const v = parseFloat(raw);
1011
1242
  if (!skipValidation && !Number.isFinite(v)) {
1012
- errors.push(`${key}: not a number`);
1013
- markValidity(input, "not a number");
1243
+ const msg = t("notANumber", state);
1244
+ errors.push(`${key}: ${msg}`);
1245
+ markValidity(input, msg);
1014
1246
  return { value: null, errors };
1015
1247
  }
1016
1248
  validateNumberInput(input, v, key);
@@ -1079,7 +1311,7 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1079
1311
  wrapper.appendChild(selectInput);
1080
1312
  const selectHint = document.createElement("p");
1081
1313
  selectHint.className = "text-xs text-gray-500 mt-1";
1082
- selectHint.textContent = makeFieldHint(element);
1314
+ selectHint.textContent = makeFieldHint(element, state);
1083
1315
  wrapper.appendChild(selectHint);
1084
1316
  }
1085
1317
  function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
@@ -1165,31 +1397,59 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1165
1397
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1166
1398
  });
1167
1399
  }
1400
+ let addRow = null;
1401
+ let countDisplay = null;
1402
+ if (!state.config.readonly) {
1403
+ addRow = document.createElement("div");
1404
+ addRow.className = "flex items-center gap-3 mt-2";
1405
+ const addBtn = document.createElement("button");
1406
+ addBtn.type = "button";
1407
+ addBtn.className = "add-select-btn px-3 py-1 rounded";
1408
+ addBtn.style.cssText = `
1409
+ color: var(--fb-primary-color);
1410
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1411
+ background-color: transparent;
1412
+ font-size: var(--fb-font-size);
1413
+ transition: all var(--fb-transition-duration);
1414
+ `;
1415
+ addBtn.textContent = "+";
1416
+ addBtn.addEventListener("mouseenter", () => {
1417
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1418
+ });
1419
+ addBtn.addEventListener("mouseleave", () => {
1420
+ addBtn.style.backgroundColor = "transparent";
1421
+ });
1422
+ addBtn.onclick = () => {
1423
+ var _a2, _b2;
1424
+ const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1425
+ values.push(defaultValue);
1426
+ addSelectItem(defaultValue);
1427
+ updateAddButton();
1428
+ updateRemoveButtons();
1429
+ };
1430
+ countDisplay = document.createElement("span");
1431
+ countDisplay.className = "text-sm text-gray-500";
1432
+ addRow.appendChild(addBtn);
1433
+ addRow.appendChild(countDisplay);
1434
+ wrapper.appendChild(addRow);
1435
+ }
1168
1436
  function updateAddButton() {
1169
- const existingAddBtn = wrapper.querySelector(".add-select-btn");
1170
- if (existingAddBtn) existingAddBtn.remove();
1171
- if (!state.config.readonly && values.length < maxCount) {
1172
- const addBtn = document.createElement("button");
1173
- addBtn.type = "button";
1174
- addBtn.className = "add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1175
- addBtn.textContent = "+";
1176
- addBtn.onclick = () => {
1177
- var _a2, _b2;
1178
- const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1179
- values.push(defaultValue);
1180
- addSelectItem(defaultValue);
1181
- updateAddButton();
1182
- updateRemoveButtons();
1183
- };
1184
- wrapper.appendChild(addBtn);
1437
+ if (!addRow || !countDisplay) return;
1438
+ const addBtn = addRow.querySelector(".add-select-btn");
1439
+ if (addBtn) {
1440
+ const disabled = values.length >= maxCount;
1441
+ addBtn.disabled = disabled;
1442
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1443
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
1185
1444
  }
1445
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1186
1446
  }
1187
1447
  values.forEach((value) => addSelectItem(value));
1188
1448
  updateAddButton();
1189
1449
  updateRemoveButtons();
1190
1450
  const hint = document.createElement("p");
1191
1451
  hint.className = "text-xs text-gray-500 mt-1";
1192
- hint.textContent = makeFieldHint(element);
1452
+ hint.textContent = makeFieldHint(element, state);
1193
1453
  wrapper.appendChild(hint);
1194
1454
  }
1195
1455
  function validateSelectElement(element, key, context) {
@@ -1232,17 +1492,18 @@ function validateSelectElement(element, key, context) {
1232
1492
  const validateMultipleCount = (key2, values, element2, filterFn) => {
1233
1493
  var _a2, _b;
1234
1494
  if (skipValidation) return;
1495
+ const { state } = context;
1235
1496
  const filteredValues = values.filter(filterFn);
1236
1497
  const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 1 : 1;
1237
1498
  const maxCount = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
1238
1499
  if (element2.required && filteredValues.length === 0) {
1239
- errors.push(`${key2}: required`);
1500
+ errors.push(`${key2}: ${t("required", state)}`);
1240
1501
  }
1241
1502
  if (filteredValues.length < minCount) {
1242
- errors.push(`${key2}: minimum ${minCount} items required`);
1503
+ errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
1243
1504
  }
1244
1505
  if (filteredValues.length > maxCount) {
1245
- errors.push(`${key2}: maximum ${maxCount} items allowed`);
1506
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
1246
1507
  }
1247
1508
  };
1248
1509
  if ("multiple" in element && element.multiple) {
@@ -1264,8 +1525,9 @@ function validateSelectElement(element, key, context) {
1264
1525
  );
1265
1526
  const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
1266
1527
  if (!skipValidation && element.required && val === "") {
1267
- errors.push(`${key}: required`);
1268
- markValidity(input, "required");
1528
+ const msg = t("required", context.state);
1529
+ errors.push(`${key}: ${msg}`);
1530
+ markValidity(input, msg);
1269
1531
  return { value: null, errors };
1270
1532
  } else {
1271
1533
  markValidity(input, null);
@@ -1317,18 +1579,11 @@ function updateSelectField(element, fieldPath, value, context) {
1317
1579
  }
1318
1580
  }
1319
1581
 
1320
- // src/utils/translation.ts
1321
- function t(key, state) {
1322
- const locale = state.config.locale || "en";
1323
- const translations = state.config.translations[locale] || state.config.translations.en;
1324
- return translations[key] || key;
1325
- }
1326
-
1327
1582
  // src/components/file.ts
1328
- function renderLocalImagePreview(container, file, fileName) {
1583
+ function renderLocalImagePreview(container, file, fileName, state) {
1329
1584
  const img = document.createElement("img");
1330
1585
  img.className = "w-full h-full object-contain";
1331
- img.alt = fileName || "Preview";
1586
+ img.alt = fileName || t("previewAlt", state);
1332
1587
  const reader = new FileReader();
1333
1588
  reader.onload = (e) => {
1334
1589
  var _a;
@@ -1348,14 +1603,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
1348
1603
  <div class="relative group h-full">
1349
1604
  <video class="w-full h-full object-contain" controls preload="auto" muted>
1350
1605
  <source src="${videoUrl}" type="${videoType}">
1351
- Your browser does not support the video tag.
1606
+ ${escapeHtml(t("videoNotSupported", state))}
1352
1607
  </video>
1353
1608
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1354
1609
  <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1355
- ${t("removeElement", state)}
1610
+ ${escapeHtml(t("removeElement", state))}
1356
1611
  </button>
1357
1612
  <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1358
- Change
1613
+ ${escapeHtml(t("changeButton", state))}
1359
1614
  </button>
1360
1615
  </div>
1361
1616
  </div>
@@ -1405,11 +1660,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
1405
1660
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1406
1661
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1407
1662
  </svg>
1408
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1663
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1409
1664
  </div>
1410
1665
  `;
1411
1666
  }
1412
- function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1667
+ function renderUploadedVideoPreview(container, thumbnailUrl, videoType, state) {
1413
1668
  const video = document.createElement("video");
1414
1669
  video.className = "w-full h-full object-contain";
1415
1670
  video.controls = true;
@@ -1420,7 +1675,7 @@ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1420
1675
  source.type = videoType;
1421
1676
  video.appendChild(source);
1422
1677
  video.appendChild(
1423
- document.createTextNode("Your browser does not support the video tag.")
1678
+ document.createTextNode(t("videoNotSupported", state))
1424
1679
  );
1425
1680
  container.appendChild(video);
1426
1681
  }
@@ -1439,7 +1694,7 @@ function renderDeleteButton(container, resourceId, state) {
1439
1694
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1440
1695
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1441
1696
  </svg>
1442
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1697
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1443
1698
  </div>
1444
1699
  `;
1445
1700
  });
@@ -1449,7 +1704,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1449
1704
  return;
1450
1705
  }
1451
1706
  if (meta.type && meta.type.startsWith("image/")) {
1452
- renderLocalImagePreview(container, meta.file, fileName);
1707
+ renderLocalImagePreview(container, meta.file, fileName, state);
1453
1708
  } else if (meta.type && meta.type.startsWith("video/")) {
1454
1709
  const newContainer = renderLocalVideoPreview(
1455
1710
  container,
@@ -1461,7 +1716,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1461
1716
  );
1462
1717
  container = newContainer;
1463
1718
  } else {
1464
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${fileName}</div></div>`;
1719
+ container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
1465
1720
  }
1466
1721
  if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
1467
1722
  renderDeleteButton(container, resourceId, state);
@@ -1477,11 +1732,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1477
1732
  if (thumbnailUrl) {
1478
1733
  clear(container);
1479
1734
  if (meta && meta.type && meta.type.startsWith("video/")) {
1480
- renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
1735
+ renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
1481
1736
  } else {
1482
1737
  const img = document.createElement("img");
1483
1738
  img.className = "w-full h-full object-contain";
1484
- img.alt = fileName || "Preview";
1739
+ img.alt = fileName || t("previewAlt", state);
1485
1740
  img.src = thumbnailUrl;
1486
1741
  container.appendChild(img);
1487
1742
  }
@@ -1495,7 +1750,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1495
1750
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1496
1751
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1497
1752
  </svg>
1498
- <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1753
+ <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
1499
1754
  </div>
1500
1755
  `;
1501
1756
  }
@@ -1552,16 +1807,16 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1552
1807
  try {
1553
1808
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
1554
1809
  if (thumbnailUrl) {
1555
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
1810
+ previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
1556
1811
  } else {
1557
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${actualFileName}</div></div></div>`;
1812
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1558
1813
  }
1559
1814
  } catch (error) {
1560
1815
  console.warn("getThumbnail failed for", resourceId, error);
1561
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${actualFileName}</div></div></div>`;
1816
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1562
1817
  }
1563
1818
  } else {
1564
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${actualFileName}</div></div></div>`;
1819
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1565
1820
  }
1566
1821
  } else if (isVideo) {
1567
1822
  if (state.config.getThumbnail) {
@@ -1572,7 +1827,7 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1572
1827
  <div class="relative group">
1573
1828
  <video class="w-full h-auto" controls preload="auto" muted>
1574
1829
  <source src="${videoUrl}" type="${(meta == null ? void 0 : meta.type) || "video/mp4"}">
1575
- \u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0434\u0435\u043E.
1830
+ ${escapeHtml(t("videoNotSupported", state))}
1576
1831
  </video>
1577
1832
  <div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
1578
1833
  <div class="bg-white bg-opacity-90 rounded-full p-3">
@@ -1584,14 +1839,14 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1584
1839
  </div>
1585
1840
  `;
1586
1841
  } else {
1587
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
1842
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1588
1843
  }
1589
1844
  } catch (error) {
1590
1845
  console.warn("getThumbnail failed for video", resourceId, error);
1591
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
1846
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1592
1847
  }
1593
1848
  } else {
1594
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
1849
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${escapeHtml(actualFileName)}</div></div></div>`;
1595
1850
  }
1596
1851
  } else {
1597
1852
  const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
@@ -1601,13 +1856,13 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1601
1856
  <div class="flex items-center space-x-3">
1602
1857
  <div class="text-3xl text-gray-400">${fileIcon}</div>
1603
1858
  <div class="flex-1 min-w-0">
1604
- <div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
1859
+ <div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
1605
1860
  <div class="text-xs text-gray-500">${fileDescription}</div>
1606
1861
  </div>
1607
1862
  </div>
1608
1863
  `;
1609
1864
  } else {
1610
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${actualFileName}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
1865
+ previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${fileIcon}</div><div class="text-sm">${escapeHtml(actualFileName)}</div><div class="text-xs text-gray-500 mt-1">${fileDescription}</div></div></div>`;
1611
1866
  }
1612
1867
  }
1613
1868
  const fileNameElement = document.createElement("p");
@@ -1630,12 +1885,18 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1630
1885
  fileResult.appendChild(downloadButton);
1631
1886
  return fileResult;
1632
1887
  }
1633
- function renderResourcePills(container, rids, state, onRemove) {
1888
+ function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
1634
1889
  clear(container);
1890
+ const buildHintLine = () => {
1891
+ const parts = [t("clickDragTextMultiple", state)];
1892
+ if (hint) parts.push(hint);
1893
+ if (countInfo) parts.push(countInfo);
1894
+ return parts.join(" \u2022 ");
1895
+ };
1635
1896
  const isInitialRender = !container.classList.contains("grid");
1636
1897
  if ((!rids || rids.length === 0) && isInitialRender) {
1637
- const gridContainer = document.createElement("div");
1638
- gridContainer.className = "grid grid-cols-4 gap-3 mb-3";
1898
+ const gridContainer2 = document.createElement("div");
1899
+ gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
1639
1900
  for (let i = 0; i < 4; i++) {
1640
1901
  const slot = document.createElement("div");
1641
1902
  slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
@@ -1666,36 +1927,17 @@ function renderResourcePills(container, rids, state, onRemove) {
1666
1927
  );
1667
1928
  if (fileInput) fileInput.click();
1668
1929
  };
1669
- gridContainer.appendChild(slot);
1670
- }
1671
- const textContainer = document.createElement("div");
1672
- textContainer.className = "text-center text-xs text-gray-600";
1673
- const uploadLink = document.createElement("span");
1674
- uploadLink.className = "underline cursor-pointer";
1675
- uploadLink.textContent = t("uploadText", state);
1676
- uploadLink.onclick = (e) => {
1677
- e.stopPropagation();
1678
- let filesWrapper = container.parentElement;
1679
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
1680
- filesWrapper = filesWrapper.parentElement;
1681
- }
1682
- if (!filesWrapper && container.classList.contains("space-y-2")) {
1683
- filesWrapper = container;
1684
- }
1685
- const fileInput = filesWrapper == null ? void 0 : filesWrapper.querySelector(
1686
- 'input[type="file"]'
1687
- );
1688
- if (fileInput) fileInput.click();
1689
- };
1690
- textContainer.appendChild(uploadLink);
1691
- textContainer.appendChild(
1692
- document.createTextNode(` ${t("dragDropText", state)}`)
1693
- );
1694
- container.appendChild(gridContainer);
1695
- container.appendChild(textContainer);
1930
+ gridContainer2.appendChild(slot);
1931
+ }
1932
+ const hintText2 = document.createElement("div");
1933
+ hintText2.className = "text-center text-xs text-gray-500 mt-2";
1934
+ hintText2.textContent = buildHintLine();
1935
+ container.appendChild(gridContainer2);
1936
+ container.appendChild(hintText2);
1696
1937
  return;
1697
1938
  }
1698
- container.className = "files-list grid grid-cols-4 gap-3 mt-2";
1939
+ const gridContainer = document.createElement("div");
1940
+ gridContainer.className = "files-list grid grid-cols-4 gap-3";
1699
1941
  const currentImagesCount = rids ? rids.length : 0;
1700
1942
  const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
1701
1943
  const slotsNeeded = rowsNeeded * 4;
@@ -1710,7 +1952,7 @@ function renderResourcePills(container, rids, state, onRemove) {
1710
1952
  console.error("Failed to render thumbnail:", err);
1711
1953
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1712
1954
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1713
- <div class="text-xs">Preview error</div>
1955
+ <div class="text-xs">${escapeHtml(t("previewError", state))}</div>
1714
1956
  </div>`;
1715
1957
  });
1716
1958
  if (onRemove) {
@@ -1743,15 +1985,20 @@ function renderResourcePills(container, rids, state, onRemove) {
1743
1985
  if (fileInput) fileInput.click();
1744
1986
  };
1745
1987
  }
1746
- container.appendChild(slot);
1988
+ gridContainer.appendChild(slot);
1747
1989
  }
1990
+ container.appendChild(gridContainer);
1991
+ const hintText = document.createElement("div");
1992
+ hintText.className = "text-center text-xs text-gray-500 mt-2";
1993
+ hintText.textContent = buildHintLine();
1994
+ container.appendChild(hintText);
1748
1995
  }
1749
- function renderThumbnailError(slot, iconSize = "w-12 h-12") {
1996
+ function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
1750
1997
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1751
- <svg class="${iconSize} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1998
+ <svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1752
1999
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
1753
2000
  </svg>
1754
- <div class="text-xs mt-1 text-red-600">Preview error</div>
2001
+ <div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
1755
2002
  </div>`;
1756
2003
  }
1757
2004
  async function renderThumbnailForResource(slot, rid, meta, state) {
@@ -1789,7 +2036,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1789
2036
  if (state.config.onThumbnailError) {
1790
2037
  state.config.onThumbnailError(err, rid);
1791
2038
  }
1792
- renderThumbnailError(slot);
2039
+ renderThumbnailError(slot, state);
1793
2040
  }
1794
2041
  } else {
1795
2042
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -1838,7 +2085,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1838
2085
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1839
2086
  <path d="M8 5v14l11-7z"/>
1840
2087
  </svg>
1841
- <div class="text-xs mt-1">${(meta == null ? void 0 : meta.name) || "Video"}</div>
2088
+ <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
1842
2089
  </div>`;
1843
2090
  }
1844
2091
  } catch (error) {
@@ -1846,30 +2093,32 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1846
2093
  if (state.config.onThumbnailError) {
1847
2094
  state.config.onThumbnailError(err, rid);
1848
2095
  }
1849
- renderThumbnailError(slot, "w-8 h-8");
2096
+ renderThumbnailError(slot, state, "w-8 h-8");
1850
2097
  }
1851
2098
  } else {
1852
2099
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1853
2100
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1854
2101
  <path d="M8 5v14l11-7z"/>
1855
2102
  </svg>
1856
- <div class="text-xs mt-1">${(meta == null ? void 0 : meta.name) || "Video"}</div>
2103
+ <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
1857
2104
  </div>`;
1858
2105
  }
1859
2106
  } else {
1860
2107
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1861
2108
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1862
- <div class="text-xs">${(meta == null ? void 0 : meta.name) || "File"}</div>
2109
+ <div class="text-xs">${escapeHtml((meta == null ? void 0 : meta.name) || "File")}</div>
1863
2110
  </div>`;
1864
2111
  }
1865
2112
  }
1866
- function setEmptyFileContainer(fileContainer, state) {
2113
+ function setEmptyFileContainer(fileContainer, state, hint) {
2114
+ const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
1867
2115
  fileContainer.innerHTML = `
1868
2116
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
1869
2117
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1870
2118
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
1871
2119
  </svg>
1872
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
2120
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2121
+ ${hintHtml}
1873
2122
  </div>
1874
2123
  `;
1875
2124
  }
@@ -2134,13 +2383,13 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2134
2383
  console.error("Failed to render file preview:", err);
2135
2384
  const emptyState = document.createElement("div");
2136
2385
  emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2137
- emptyState.innerHTML = `<div class="text-center">Preview unavailable</div>`;
2386
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
2138
2387
  wrapper.appendChild(emptyState);
2139
2388
  });
2140
2389
  } else {
2141
2390
  const emptyState = document.createElement("div");
2142
2391
  emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2143
- emptyState.innerHTML = `<div class="text-center">${t("noFileSelected", state)}</div>`;
2392
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
2144
2393
  wrapper.appendChild(emptyState);
2145
2394
  }
2146
2395
  } else {
@@ -2184,7 +2433,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2184
2433
  }
2185
2434
  );
2186
2435
  } else {
2187
- setEmptyFileContainer(fileContainer, state);
2436
+ const hint = makeFieldHint(element, state);
2437
+ setEmptyFileContainer(fileContainer, state, hint);
2188
2438
  }
2189
2439
  fileContainer.onclick = fileUploadHandler;
2190
2440
  setupDragAndDrop(fileContainer, dragHandler);
@@ -2203,18 +2453,6 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2203
2453
  };
2204
2454
  fileWrapper.appendChild(fileContainer);
2205
2455
  fileWrapper.appendChild(picker);
2206
- const uploadText = document.createElement("p");
2207
- uploadText.className = "text-xs text-gray-600 mt-2 text-center";
2208
- uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText", state)}</span> ${t("dragDropTextSingle", state)}`;
2209
- const uploadSpan = uploadText.querySelector("span");
2210
- if (uploadSpan) {
2211
- uploadSpan.onclick = () => picker.click();
2212
- }
2213
- fileWrapper.appendChild(uploadText);
2214
- const fileHint = document.createElement("p");
2215
- fileHint.className = "text-xs text-gray-500 mt-1 text-center";
2216
- fileHint.textContent = makeFieldHint(element);
2217
- fileWrapper.appendChild(fileHint);
2218
2456
  wrapper.appendChild(fileWrapper);
2219
2457
  }
2220
2458
  }
@@ -2234,18 +2472,24 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2234
2472
  });
2235
2473
  });
2236
2474
  } else {
2237
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected", state)}</div></div>`;
2475
+ resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
2238
2476
  }
2239
2477
  wrapper.appendChild(resultsWrapper);
2240
2478
  } else {
2241
2479
  let updateFilesList2 = function() {
2242
- renderResourcePills(list, initialFiles, state, (ridToRemove) => {
2243
- const index = initialFiles.indexOf(ridToRemove);
2244
- if (index > -1) {
2245
- initialFiles.splice(index, 1);
2246
- }
2247
- updateFilesList2();
2248
- });
2480
+ renderResourcePills(
2481
+ list,
2482
+ initialFiles,
2483
+ state,
2484
+ (ridToRemove) => {
2485
+ const index = initialFiles.indexOf(ridToRemove);
2486
+ if (index > -1) {
2487
+ initialFiles.splice(index, 1);
2488
+ }
2489
+ updateFilesList2();
2490
+ },
2491
+ filesFieldHint
2492
+ );
2249
2493
  };
2250
2494
  const filesWrapper = document.createElement("div");
2251
2495
  filesWrapper.className = "space-y-2";
@@ -2263,6 +2507,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2263
2507
  list.className = "files-list";
2264
2508
  const initialFiles = ctx.prefill[element.key] || [];
2265
2509
  addPrefillFilesToIndex(initialFiles, state);
2510
+ const filesFieldHint = makeFieldHint(element, state);
2266
2511
  updateFilesList2();
2267
2512
  setupFilesDropHandler(
2268
2513
  filesContainer,
@@ -2283,10 +2528,6 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2283
2528
  filesContainer.appendChild(list);
2284
2529
  filesWrapper.appendChild(filesContainer);
2285
2530
  filesWrapper.appendChild(filesPicker);
2286
- const filesHint = document.createElement("p");
2287
- filesHint.className = "text-xs text-gray-500 mt-1 text-center";
2288
- filesHint.textContent = makeFieldHint(element);
2289
- filesWrapper.appendChild(filesHint);
2290
2531
  wrapper.appendChild(filesWrapper);
2291
2532
  }
2292
2533
  }
@@ -2308,7 +2549,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2308
2549
  });
2309
2550
  });
2310
2551
  } else {
2311
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected", state)}</div></div>`;
2552
+ resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${escapeHtml(t("noFilesSelected", state))}</div></div>`;
2312
2553
  }
2313
2554
  wrapper.appendChild(resultsWrapper);
2314
2555
  } else {
@@ -2328,19 +2569,24 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2328
2569
  filesWrapper.appendChild(filesContainer);
2329
2570
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2330
2571
  addPrefillFilesToIndex(initialFiles, state);
2572
+ const multipleFilesHint = makeFieldHint(element, state);
2573
+ const buildCountInfo = () => {
2574
+ const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
2575
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
2576
+ return countText + minMaxText;
2577
+ };
2331
2578
  const updateFilesDisplay = () => {
2332
- renderResourcePills(filesContainer, initialFiles, state, (index) => {
2333
- initialFiles.splice(initialFiles.indexOf(index), 1);
2334
- updateFilesDisplay();
2335
- });
2336
- const countInfo = document.createElement("div");
2337
- countInfo.className = "text-xs text-gray-500 mt-2 file-count-info";
2338
- const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
2339
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` (${minFiles}-${maxFiles} allowed)` : "";
2340
- countInfo.textContent = countText + minMaxText;
2341
- const existingCount = filesWrapper.querySelector(".file-count-info");
2342
- if (existingCount) existingCount.remove();
2343
- filesWrapper.appendChild(countInfo);
2579
+ renderResourcePills(
2580
+ filesContainer,
2581
+ initialFiles,
2582
+ state,
2583
+ (index) => {
2584
+ initialFiles.splice(initialFiles.indexOf(index), 1);
2585
+ updateFilesDisplay();
2586
+ },
2587
+ multipleFilesHint,
2588
+ buildCountInfo()
2589
+ );
2344
2590
  };
2345
2591
  setupFilesDropHandler(
2346
2592
  filesContainer,
@@ -2370,16 +2616,17 @@ function validateFileElement(element, key, context) {
2370
2616
  const validateFileCount = (key2, resourceIds, element2) => {
2371
2617
  var _a2, _b;
2372
2618
  if (skipValidation) return;
2619
+ const { state } = context;
2373
2620
  const minFiles = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
2374
2621
  const maxFiles = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
2375
2622
  if (element2.required && resourceIds.length === 0) {
2376
- errors.push(`${key2}: required`);
2623
+ errors.push(`${key2}: ${t("required", state)}`);
2377
2624
  }
2378
2625
  if (resourceIds.length < minFiles) {
2379
- errors.push(`${key2}: minimum ${minFiles} files required`);
2626
+ errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
2380
2627
  }
2381
2628
  if (resourceIds.length > maxFiles) {
2382
- errors.push(`${key2}: maximum ${maxFiles} files allowed`);
2629
+ errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
2383
2630
  }
2384
2631
  };
2385
2632
  if (isMultipleField) {
@@ -2407,7 +2654,7 @@ function validateFileElement(element, key, context) {
2407
2654
  );
2408
2655
  const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2409
2656
  if (!skipValidation && element.required && rid === "") {
2410
- errors.push(`${key}: required`);
2657
+ errors.push(`${key}: ${t("required", context.state)}`);
2411
2658
  return { value: null, errors };
2412
2659
  }
2413
2660
  return { value: rid || null, errors };
@@ -2656,7 +2903,7 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
2656
2903
  font-size: var(--fb-font-size-small);
2657
2904
  color: var(--fb-text-secondary-color);
2658
2905
  `;
2659
- colourHint.textContent = makeFieldHint(element);
2906
+ colourHint.textContent = makeFieldHint(element, state);
2660
2907
  wrapper.appendChild(colourHint);
2661
2908
  }
2662
2909
  function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
@@ -2747,36 +2994,51 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2747
2994
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2748
2995
  });
2749
2996
  }
2997
+ let addRow = null;
2998
+ let countDisplay = null;
2999
+ if (!state.config.readonly) {
3000
+ addRow = document.createElement("div");
3001
+ addRow.className = "flex items-center gap-3 mt-2";
3002
+ const addBtn = document.createElement("button");
3003
+ addBtn.type = "button";
3004
+ addBtn.className = "add-colour-btn px-3 py-1 rounded";
3005
+ addBtn.style.cssText = `
3006
+ color: var(--fb-primary-color);
3007
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3008
+ background-color: transparent;
3009
+ font-size: var(--fb-font-size);
3010
+ transition: all var(--fb-transition-duration);
3011
+ `;
3012
+ addBtn.textContent = "+";
3013
+ addBtn.addEventListener("mouseenter", () => {
3014
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3015
+ });
3016
+ addBtn.addEventListener("mouseleave", () => {
3017
+ addBtn.style.backgroundColor = "transparent";
3018
+ });
3019
+ addBtn.onclick = () => {
3020
+ const defaultColour = element.default || "#000000";
3021
+ values.push(defaultColour);
3022
+ addColourItem(defaultColour);
3023
+ updateAddButton();
3024
+ updateRemoveButtons();
3025
+ };
3026
+ countDisplay = document.createElement("span");
3027
+ countDisplay.className = "text-sm text-gray-500";
3028
+ addRow.appendChild(addBtn);
3029
+ addRow.appendChild(countDisplay);
3030
+ wrapper.appendChild(addRow);
3031
+ }
2750
3032
  function updateAddButton() {
2751
- const existingAddBtn = wrapper.querySelector(".add-colour-btn");
2752
- if (existingAddBtn) existingAddBtn.remove();
2753
- if (!state.config.readonly && values.length < maxCount) {
2754
- const addBtn = document.createElement("button");
2755
- addBtn.type = "button";
2756
- addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
2757
- addBtn.style.cssText = `
2758
- color: var(--fb-primary-color);
2759
- border: var(--fb-border-width) solid var(--fb-primary-color);
2760
- background-color: transparent;
2761
- font-size: var(--fb-font-size);
2762
- transition: all var(--fb-transition-duration);
2763
- `;
2764
- addBtn.textContent = "+";
2765
- addBtn.addEventListener("mouseenter", () => {
2766
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2767
- });
2768
- addBtn.addEventListener("mouseleave", () => {
2769
- addBtn.style.backgroundColor = "transparent";
2770
- });
2771
- addBtn.onclick = () => {
2772
- const defaultColour = element.default || "#000000";
2773
- values.push(defaultColour);
2774
- addColourItem(defaultColour);
2775
- updateAddButton();
2776
- updateRemoveButtons();
2777
- };
2778
- wrapper.appendChild(addBtn);
3033
+ if (!addRow || !countDisplay) return;
3034
+ const addBtn = addRow.querySelector(".add-colour-btn");
3035
+ if (addBtn) {
3036
+ const disabled = values.length >= maxCount;
3037
+ addBtn.disabled = disabled;
3038
+ addBtn.style.opacity = disabled ? "0.5" : "1";
3039
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
2779
3040
  }
3041
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2780
3042
  }
2781
3043
  values.forEach((value) => addColourItem(value));
2782
3044
  updateAddButton();
@@ -2787,7 +3049,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2787
3049
  font-size: var(--fb-font-size-small);
2788
3050
  color: var(--fb-text-secondary-color);
2789
3051
  `;
2790
- hint.textContent = makeFieldHint(element);
3052
+ hint.textContent = makeFieldHint(element, state);
2791
3053
  wrapper.appendChild(hint);
2792
3054
  }
2793
3055
  function validateColourElement(element, key, context) {
@@ -2828,10 +3090,12 @@ function validateColourElement(element, key, context) {
2828
3090
  }
2829
3091
  };
2830
3092
  const validateColourValue = (input, val, fieldKey) => {
3093
+ const { state } = context;
2831
3094
  if (!val) {
2832
3095
  if (!skipValidation && element.required) {
2833
- errors.push(`${fieldKey}: required`);
2834
- markValidity(input, "required");
3096
+ const msg = t("required", state);
3097
+ errors.push(`${fieldKey}: ${msg}`);
3098
+ markValidity(input, msg);
2835
3099
  return "";
2836
3100
  }
2837
3101
  markValidity(input, null);
@@ -2839,8 +3103,9 @@ function validateColourElement(element, key, context) {
2839
3103
  }
2840
3104
  const normalized = normalizeColourValue(val);
2841
3105
  if (!skipValidation && !isValidHexColour(normalized)) {
2842
- errors.push(`${fieldKey}: invalid hex colour format`);
2843
- markValidity(input, "invalid hex colour format");
3106
+ const msg = t("invalidHexColour", state);
3107
+ errors.push(`${fieldKey}: ${msg}`);
3108
+ markValidity(input, msg);
2844
3109
  return val;
2845
3110
  }
2846
3111
  markValidity(input, null);
@@ -2856,17 +3121,18 @@ function validateColourElement(element, key, context) {
2856
3121
  values.push(validated);
2857
3122
  });
2858
3123
  if (!skipValidation) {
3124
+ const { state } = context;
2859
3125
  const minCount = (_a = element.minCount) != null ? _a : 1;
2860
3126
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
2861
3127
  const filteredValues = values.filter((v) => v !== "");
2862
3128
  if (element.required && filteredValues.length === 0) {
2863
- errors.push(`${key}: required`);
3129
+ errors.push(`${key}: ${t("required", state)}`);
2864
3130
  }
2865
3131
  if (filteredValues.length < minCount) {
2866
- errors.push(`${key}: minimum ${minCount} items required`);
3132
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
2867
3133
  }
2868
3134
  if (filteredValues.length > maxCount) {
2869
- errors.push(`${key}: maximum ${maxCount} items allowed`);
3135
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
2870
3136
  }
2871
3137
  }
2872
3138
  return { value: values, errors };
@@ -2876,8 +3142,9 @@ function validateColourElement(element, key, context) {
2876
3142
  );
2877
3143
  const val = (_c = hexInput == null ? void 0 : hexInput.value) != null ? _c : "";
2878
3144
  if (!skipValidation && element.required && val === "") {
2879
- errors.push(`${key}: required`);
2880
- markValidity(hexInput, "required");
3145
+ const msg = t("required", context.state);
3146
+ errors.push(`${key}: ${msg}`);
3147
+ markValidity(hexInput, msg);
2881
3148
  return { value: "", errors };
2882
3149
  }
2883
3150
  const validated = validateColourValue(hexInput, val, key);
@@ -2970,13 +3237,15 @@ function alignToStep(value, step) {
2970
3237
  function createSliderUI(value, pathKey, element, ctx, readonly) {
2971
3238
  var _a;
2972
3239
  const container = document.createElement("div");
2973
- container.className = "slider-container space-y-2";
2974
- const sliderRow = document.createElement("div");
2975
- sliderRow.className = "flex items-center gap-3";
3240
+ container.className = "slider-container";
3241
+ const mainRow = document.createElement("div");
3242
+ mainRow.className = "flex items-start gap-3";
3243
+ const sliderSection = document.createElement("div");
3244
+ sliderSection.className = "flex-1";
2976
3245
  const slider = document.createElement("input");
2977
3246
  slider.type = "range";
2978
3247
  slider.name = pathKey;
2979
- slider.className = "slider-input flex-1";
3248
+ slider.className = "slider-input w-full";
2980
3249
  slider.disabled = readonly;
2981
3250
  const scale = element.scale || "linear";
2982
3251
  const min = element.min;
@@ -3014,25 +3283,13 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
3014
3283
  cursor: ${readonly ? "not-allowed" : "pointer"};
3015
3284
  opacity: ${readonly ? "0.6" : "1"};
3016
3285
  `;
3017
- const valueDisplay = document.createElement("span");
3018
- valueDisplay.className = "slider-value";
3019
- valueDisplay.style.cssText = `
3020
- min-width: 60px;
3021
- text-align: right;
3022
- font-size: var(--fb-font-size);
3023
- color: var(--fb-text-color);
3024
- font-family: var(--fb-font-family-mono, monospace);
3025
- font-weight: 500;
3026
- `;
3027
- valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
3028
- sliderRow.appendChild(slider);
3029
- sliderRow.appendChild(valueDisplay);
3030
- container.appendChild(sliderRow);
3286
+ sliderSection.appendChild(slider);
3031
3287
  const labelsRow = document.createElement("div");
3032
3288
  labelsRow.className = "flex justify-between";
3033
3289
  labelsRow.style.cssText = `
3034
3290
  font-size: var(--fb-font-size-small);
3035
3291
  color: var(--fb-text-secondary-color);
3292
+ margin-top: 4px;
3036
3293
  `;
3037
3294
  const minLabel = document.createElement("span");
3038
3295
  minLabel.textContent = min.toString();
@@ -3040,7 +3297,22 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
3040
3297
  maxLabel.textContent = max.toString();
3041
3298
  labelsRow.appendChild(minLabel);
3042
3299
  labelsRow.appendChild(maxLabel);
3043
- container.appendChild(labelsRow);
3300
+ sliderSection.appendChild(labelsRow);
3301
+ const valueDisplay = document.createElement("span");
3302
+ valueDisplay.className = "slider-value";
3303
+ valueDisplay.style.cssText = `
3304
+ min-width: 60px;
3305
+ text-align: right;
3306
+ font-size: var(--fb-font-size);
3307
+ color: var(--fb-text-color);
3308
+ font-family: var(--fb-font-family-mono, monospace);
3309
+ font-weight: 500;
3310
+ padding-top: 2px;
3311
+ `;
3312
+ valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
3313
+ mainRow.appendChild(sliderSection);
3314
+ mainRow.appendChild(valueDisplay);
3315
+ container.appendChild(mainRow);
3044
3316
  if (!readonly) {
3045
3317
  const updateValue = () => {
3046
3318
  let displayValue;
@@ -3101,7 +3373,7 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
3101
3373
  font-size: var(--fb-font-size-small);
3102
3374
  color: var(--fb-text-secondary-color);
3103
3375
  `;
3104
- hint.textContent = makeFieldHint(element);
3376
+ hint.textContent = makeFieldHint(element, state);
3105
3377
  wrapper.appendChild(hint);
3106
3378
  }
3107
3379
  function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
@@ -3204,35 +3476,50 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3204
3476
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
3205
3477
  });
3206
3478
  }
3479
+ let addRow = null;
3480
+ let countDisplay = null;
3481
+ if (!state.config.readonly) {
3482
+ addRow = document.createElement("div");
3483
+ addRow.className = "flex items-center gap-3 mt-2";
3484
+ const addBtn = document.createElement("button");
3485
+ addBtn.type = "button";
3486
+ addBtn.className = "add-slider-btn px-3 py-1 rounded";
3487
+ addBtn.style.cssText = `
3488
+ color: var(--fb-primary-color);
3489
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3490
+ background-color: transparent;
3491
+ font-size: var(--fb-font-size);
3492
+ transition: all var(--fb-transition-duration);
3493
+ `;
3494
+ addBtn.textContent = "+";
3495
+ addBtn.addEventListener("mouseenter", () => {
3496
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3497
+ });
3498
+ addBtn.addEventListener("mouseleave", () => {
3499
+ addBtn.style.backgroundColor = "transparent";
3500
+ });
3501
+ addBtn.onclick = () => {
3502
+ values.push(defaultValue);
3503
+ addSliderItem(defaultValue);
3504
+ updateAddButton();
3505
+ updateRemoveButtons();
3506
+ };
3507
+ countDisplay = document.createElement("span");
3508
+ countDisplay.className = "text-sm text-gray-500";
3509
+ addRow.appendChild(addBtn);
3510
+ addRow.appendChild(countDisplay);
3511
+ wrapper.appendChild(addRow);
3512
+ }
3207
3513
  function updateAddButton() {
3208
- const existingAddBtn = wrapper.querySelector(".add-slider-btn");
3209
- if (existingAddBtn) existingAddBtn.remove();
3210
- if (!state.config.readonly && values.length < maxCount) {
3211
- const addBtn = document.createElement("button");
3212
- addBtn.type = "button";
3213
- addBtn.className = "add-slider-btn mt-2 px-3 py-1 rounded";
3214
- addBtn.style.cssText = `
3215
- color: var(--fb-primary-color);
3216
- border: var(--fb-border-width) solid var(--fb-primary-color);
3217
- background-color: transparent;
3218
- font-size: var(--fb-font-size);
3219
- transition: all var(--fb-transition-duration);
3220
- `;
3221
- addBtn.textContent = "+";
3222
- addBtn.addEventListener("mouseenter", () => {
3223
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3224
- });
3225
- addBtn.addEventListener("mouseleave", () => {
3226
- addBtn.style.backgroundColor = "transparent";
3227
- });
3228
- addBtn.onclick = () => {
3229
- values.push(defaultValue);
3230
- addSliderItem(defaultValue);
3231
- updateAddButton();
3232
- updateRemoveButtons();
3233
- };
3234
- wrapper.appendChild(addBtn);
3514
+ if (!addRow || !countDisplay) return;
3515
+ const addBtn = addRow.querySelector(".add-slider-btn");
3516
+ if (addBtn) {
3517
+ const disabled = values.length >= maxCount;
3518
+ addBtn.disabled = disabled;
3519
+ addBtn.style.opacity = disabled ? "0.5" : "1";
3520
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
3235
3521
  }
3522
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
3236
3523
  }
3237
3524
  values.forEach((value) => addSliderItem(value));
3238
3525
  updateAddButton();
@@ -3243,7 +3530,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3243
3530
  font-size: var(--fb-font-size-small);
3244
3531
  color: var(--fb-text-secondary-color);
3245
3532
  `;
3246
- hint.textContent = makeFieldHint(element);
3533
+ hint.textContent = makeFieldHint(element, state);
3247
3534
  wrapper.appendChild(hint);
3248
3535
  }
3249
3536
  function validateSliderElement(element, key, context) {
@@ -3302,11 +3589,13 @@ function validateSliderElement(element, key, context) {
3302
3589
  }
3303
3590
  };
3304
3591
  const validateSliderValue = (slider, fieldKey) => {
3592
+ const { state } = context;
3305
3593
  const rawValue = slider.value;
3306
3594
  if (!rawValue) {
3307
3595
  if (!skipValidation && element.required) {
3308
- errors.push(`${fieldKey}: required`);
3309
- markValidity(slider, "required");
3596
+ const msg = t("required", state);
3597
+ errors.push(`${fieldKey}: ${msg}`);
3598
+ markValidity(slider, msg);
3310
3599
  return null;
3311
3600
  }
3312
3601
  markValidity(slider, null);
@@ -3323,13 +3612,15 @@ function validateSliderElement(element, key, context) {
3323
3612
  }
3324
3613
  if (!skipValidation) {
3325
3614
  if (value < min) {
3326
- errors.push(`${fieldKey}: value ${value} < min ${min}`);
3327
- markValidity(slider, `value must be >= ${min}`);
3615
+ const msg = t("minValue", state, { min });
3616
+ errors.push(`${fieldKey}: ${msg}`);
3617
+ markValidity(slider, msg);
3328
3618
  return value;
3329
3619
  }
3330
3620
  if (value > max) {
3331
- errors.push(`${fieldKey}: value ${value} > max ${max}`);
3332
- markValidity(slider, `value must be <= ${max}`);
3621
+ const msg = t("maxValue", state, { max });
3622
+ errors.push(`${fieldKey}: ${msg}`);
3623
+ markValidity(slider, msg);
3333
3624
  return value;
3334
3625
  }
3335
3626
  }
@@ -3346,17 +3637,18 @@ function validateSliderElement(element, key, context) {
3346
3637
  values.push(value);
3347
3638
  });
3348
3639
  if (!skipValidation) {
3640
+ const { state } = context;
3349
3641
  const minCount = (_b = element.minCount) != null ? _b : 1;
3350
3642
  const maxCount = (_c = element.maxCount) != null ? _c : Infinity;
3351
3643
  const filteredValues = values.filter((v) => v !== null);
3352
3644
  if (element.required && filteredValues.length === 0) {
3353
- errors.push(`${key}: required`);
3645
+ errors.push(`${key}: ${t("required", state)}`);
3354
3646
  }
3355
3647
  if (filteredValues.length < minCount) {
3356
- errors.push(`${key}: minimum ${minCount} items required`);
3648
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
3357
3649
  }
3358
3650
  if (filteredValues.length > maxCount) {
3359
- errors.push(`${key}: maximum ${maxCount} items allowed`);
3651
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
3360
3652
  }
3361
3653
  }
3362
3654
  return { value: values, errors };
@@ -3366,7 +3658,7 @@ function validateSliderElement(element, key, context) {
3366
3658
  );
3367
3659
  if (!slider) {
3368
3660
  if (!skipValidation && element.required) {
3369
- errors.push(`${key}: required`);
3661
+ errors.push(`${key}: ${t("required", context.state)}`);
3370
3662
  }
3371
3663
  return { value: null, errors };
3372
3664
  }
@@ -3520,10 +3812,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3520
3812
  const containerWrap = document.createElement("div");
3521
3813
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3522
3814
  containerWrap.setAttribute("data-container", pathKey);
3523
- const header = document.createElement("div");
3524
- header.className = "flex justify-between items-center mb-4";
3525
- const left = document.createElement("div");
3526
- left.className = "flex-1";
3527
3815
  const itemsWrap = document.createElement("div");
3528
3816
  const columns = element.columns || 1;
3529
3817
  if (columns === 1) {
@@ -3531,8 +3819,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3531
3819
  } else {
3532
3820
  itemsWrap.className = `grid grid-cols-${columns} gap-4`;
3533
3821
  }
3534
- containerWrap.appendChild(header);
3535
- header.appendChild(left);
3536
3822
  if (!ctx.state.config.readonly) {
3537
3823
  const hintsElement = createPrefillHints(element, pathKey);
3538
3824
  if (hintsElement) {
@@ -3553,7 +3839,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3553
3839
  }
3554
3840
  });
3555
3841
  containerWrap.appendChild(itemsWrap);
3556
- left.innerHTML = `<span>${element.label || element.key}</span>`;
3557
3842
  wrapper.appendChild(containerWrap);
3558
3843
  }
3559
3844
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
@@ -3561,14 +3846,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3561
3846
  const state = ctx.state;
3562
3847
  const containerWrap = document.createElement("div");
3563
3848
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3564
- const header = document.createElement("div");
3565
- header.className = "flex justify-between items-center mb-4";
3566
- const left = document.createElement("div");
3567
- left.className = "flex-1";
3849
+ const countDisplay = document.createElement("span");
3850
+ countDisplay.className = "text-sm text-gray-500";
3568
3851
  const itemsWrap = document.createElement("div");
3569
3852
  itemsWrap.className = "space-y-4";
3570
- containerWrap.appendChild(header);
3571
- header.appendChild(left);
3572
3853
  if (!ctx.state.config.readonly) {
3573
3854
  const hintsElement = createPrefillHints(element, element.key);
3574
3855
  if (hintsElement) {
@@ -3582,7 +3863,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3582
3863
  const createAddButton = () => {
3583
3864
  const add = document.createElement("button");
3584
3865
  add.type = "button";
3585
- add.className = "add-container-btn mt-2 px-3 py-1 rounded";
3866
+ add.className = "add-container-btn px-3 py-1 rounded";
3586
3867
  add.style.cssText = `
3587
3868
  color: var(--fb-primary-color);
3588
3869
  border: var(--fb-border-width) solid var(--fb-primary-color);
@@ -3663,7 +3944,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3663
3944
  existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3664
3945
  existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
3665
3946
  }
3666
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "\u221E" : max})</span>`;
3947
+ countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
3667
3948
  };
3668
3949
  if (pre && Array.isArray(pre)) {
3669
3950
  pre.forEach((prefillObj, idx) => {
@@ -3771,7 +4052,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3771
4052
  }
3772
4053
  containerWrap.appendChild(itemsWrap);
3773
4054
  if (!state.config.readonly) {
3774
- containerWrap.appendChild(createAddButton());
4055
+ const addRow = document.createElement("div");
4056
+ addRow.className = "flex items-center gap-3 mt-2";
4057
+ addRow.appendChild(createAddButton());
4058
+ addRow.appendChild(countDisplay);
4059
+ containerWrap.appendChild(addRow);
3775
4060
  }
3776
4061
  updateAddButton();
3777
4062
  wrapper.appendChild(containerWrap);
@@ -3797,16 +4082,17 @@ function validateContainerElement(element, key, context) {
3797
4082
  const validateContainerCount = (key2, items, element2) => {
3798
4083
  var _a, _b;
3799
4084
  if (skipValidation) return;
4085
+ const { state } = context;
3800
4086
  const minItems = "minCount" in element2 ? (_a = element2.minCount) != null ? _a : 0 : 0;
3801
4087
  const maxItems = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
3802
4088
  if (element2.required && items.length === 0) {
3803
- errors.push(`${key2}: required`);
4089
+ errors.push(`${key2}: ${t("required", state)}`);
3804
4090
  }
3805
4091
  if (items.length < minItems) {
3806
- errors.push(`${key2}: minimum ${minItems} items required`);
4092
+ errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
3807
4093
  }
3808
4094
  if (items.length > maxItems) {
3809
- errors.push(`${key2}: maximum ${maxItems} items allowed`);
4095
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
3810
4096
  }
3811
4097
  };
3812
4098
  if ("multiple" in element && element.multiple) {
@@ -4360,7 +4646,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
4360
4646
  default: {
4361
4647
  const unsupported = document.createElement("div");
4362
4648
  unsupported.className = "text-red-500 text-sm";
4363
- unsupported.textContent = `Unsupported field type: ${element.type}`;
4649
+ unsupported.textContent = t("unsupportedFieldType", ctx.state, { type: element.type });
4364
4650
  wrapper.appendChild(unsupported);
4365
4651
  }
4366
4652
  }
@@ -4404,31 +4690,112 @@ var defaultConfig = {
4404
4690
  locale: "en",
4405
4691
  translations: {
4406
4692
  en: {
4407
- addElement: "Add Element",
4693
+ // UI texts
4408
4694
  removeElement: "Remove",
4409
- uploadText: "Upload",
4410
- dragDropText: "or drag and drop files",
4411
- dragDropTextSingle: "or drag and drop file",
4412
4695
  clickDragText: "Click or drag file",
4696
+ clickDragTextMultiple: "Click or drag files",
4413
4697
  noFileSelected: "No file selected",
4414
4698
  noFilesSelected: "No files selected",
4415
- downloadButton: "Download"
4699
+ downloadButton: "Download",
4700
+ changeButton: "Change",
4701
+ placeholderText: "Enter text",
4702
+ previewAlt: "Preview",
4703
+ previewUnavailable: "Preview unavailable",
4704
+ previewError: "Preview error",
4705
+ videoNotSupported: "Your browser does not support the video tag.",
4706
+ // Field hints
4707
+ hintLengthRange: "{min}-{max} chars",
4708
+ hintMaxLength: "\u2264{max} chars",
4709
+ hintMinLength: "\u2265{min} chars",
4710
+ hintValueRange: "{min}-{max}",
4711
+ hintMaxValue: "\u2264{max}",
4712
+ hintMinValue: "\u2265{min}",
4713
+ hintMaxSize: "\u2264{size}MB",
4714
+ hintFormats: "{formats}",
4715
+ hintRequired: "Required",
4716
+ hintOptional: "Optional",
4717
+ hintPattern: "Format: {pattern}",
4718
+ fileCountSingle: "{count} file",
4719
+ fileCountPlural: "{count} files",
4720
+ fileCountRange: "({min}-{max})",
4721
+ // Validation errors
4722
+ required: "Required",
4723
+ minItems: "Minimum {min} items required",
4724
+ maxItems: "Maximum {max} items allowed",
4725
+ minLength: "Minimum {min} characters",
4726
+ maxLength: "Maximum {max} characters",
4727
+ minValue: "Must be at least {min}",
4728
+ maxValue: "Must be at most {max}",
4729
+ patternMismatch: "Invalid format",
4730
+ invalidPattern: "Invalid pattern in schema",
4731
+ notANumber: "Must be a number",
4732
+ invalidHexColour: "Invalid hex color",
4733
+ minFiles: "Minimum {min} files required",
4734
+ maxFiles: "Maximum {max} files allowed",
4735
+ unsupportedFieldType: "Unsupported field type: {type}"
4416
4736
  },
4417
4737
  ru: {
4418
- addElement: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442",
4738
+ // UI texts
4419
4739
  removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
4420
- uploadText: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435",
4421
- dragDropText: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
4422
- dragDropTextSingle: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
4423
4740
  clickDragText: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
4741
+ clickDragTextMultiple: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
4424
4742
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
4425
4743
  noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
4426
- downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
4744
+ downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
4745
+ changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
4746
+ placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
4747
+ previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
4748
+ previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
4749
+ previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
4750
+ videoNotSupported: "\u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0434\u0435\u043E.",
4751
+ // Field hints
4752
+ hintLengthRange: "{min}-{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4753
+ hintMaxLength: "\u2264{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4754
+ hintMinLength: "\u2265{min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4755
+ hintValueRange: "{min}-{max}",
4756
+ hintMaxValue: "\u2264{max}",
4757
+ hintMinValue: "\u2265{min}",
4758
+ hintMaxSize: "\u2264{size}\u041C\u0411",
4759
+ hintFormats: "{formats}",
4760
+ hintRequired: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4761
+ hintOptional: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4762
+ hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
4763
+ fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
4764
+ fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
4765
+ fileCountRange: "({min}-{max})",
4766
+ // Validation errors
4767
+ required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
4768
+ minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4769
+ maxItems: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4770
+ minLength: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4771
+ maxLength: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4772
+ minValue: "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u0435\u0435 {min}",
4773
+ maxValue: "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u0435\u0435 {max}",
4774
+ patternMismatch: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442",
4775
+ invalidPattern: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u0442\u0442\u0435\u0440\u043D \u0432 \u0441\u0445\u0435\u043C\u0435",
4776
+ notANumber: "\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C",
4777
+ invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
4778
+ minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
4779
+ maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
4780
+ unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}"
4427
4781
  }
4428
4782
  },
4429
4783
  theme: {}
4430
4784
  };
4431
4785
  function createInstanceState(config) {
4786
+ const mergedTranslations = {
4787
+ ...defaultConfig.translations
4788
+ };
4789
+ if (config == null ? void 0 : config.translations) {
4790
+ for (const [locale, userTranslations] of Object.entries(
4791
+ config.translations
4792
+ )) {
4793
+ mergedTranslations[locale] = {
4794
+ ...defaultConfig.translations[locale] || {},
4795
+ ...userTranslations
4796
+ };
4797
+ }
4798
+ }
4432
4799
  return {
4433
4800
  schema: null,
4434
4801
  formRoot: null,
@@ -4437,7 +4804,8 @@ function createInstanceState(config) {
4437
4804
  version: "1.0.0",
4438
4805
  config: {
4439
4806
  ...defaultConfig,
4440
- ...config
4807
+ ...config,
4808
+ translations: mergedTranslations
4441
4809
  },
4442
4810
  debounceTimer: null
4443
4811
  };