@dmitryvim/form-builder 0.2.11 → 0.2.13

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,79 @@
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 addPatternHint(element, parts, state) {
65
+ if (element.pattern) {
66
+ parts.push(t("hintPattern", state, { pattern: element.pattern }));
47
67
  }
48
68
  }
49
- function makeFieldHint(element) {
69
+ function makeFieldHint(element, state) {
50
70
  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);
71
+ addLengthHint(element, parts, state);
72
+ if (element.type !== "slider") {
73
+ addRangeHint(element, parts, state);
74
+ }
75
+ addFileSizeHint(element, parts, state);
76
+ addFormatHint(element, parts, state);
77
+ addPatternHint(element, parts, state);
57
78
  return parts.join(" \u2022 ");
58
79
  }
59
80
  function validateSchema(schema) {
@@ -191,6 +212,11 @@ function validateSchema(schema) {
191
212
  function isPlainObject(obj) {
192
213
  return obj && typeof obj === "object" && obj.constructor === Object;
193
214
  }
215
+ function escapeHtml(text) {
216
+ const div = document.createElement("div");
217
+ div.textContent = text;
218
+ return div.innerHTML;
219
+ }
194
220
  function pathJoin(base, key) {
195
221
  return base ? `${base}.${key}` : key;
196
222
  }
@@ -269,13 +295,62 @@ function deepEqual(a, b) {
269
295
  }
270
296
 
271
297
  // src/components/text.ts
298
+ function createCharCounter(element, input, isTextarea = false) {
299
+ const counter = document.createElement("span");
300
+ counter.className = "char-counter";
301
+ counter.style.cssText = `
302
+ position: absolute;
303
+ ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
304
+ right: 10px;
305
+ font-size: var(--fb-font-size-small);
306
+ color: var(--fb-text-secondary-color);
307
+ pointer-events: none;
308
+ background: var(--fb-background-color);
309
+ padding: 0 4px;
310
+ `;
311
+ const updateCounter = () => {
312
+ const len = input.value.length;
313
+ const min = element.minLength;
314
+ const max = element.maxLength;
315
+ if (min == null && max == null) {
316
+ counter.textContent = "";
317
+ return;
318
+ }
319
+ if (len === 0 || min != null && len < min) {
320
+ if (min != null && max != null) {
321
+ counter.textContent = `${min}-${max}`;
322
+ } else if (max != null) {
323
+ counter.textContent = `\u2264${max}`;
324
+ } else if (min != null) {
325
+ counter.textContent = `\u2265${min}`;
326
+ }
327
+ counter.style.color = "var(--fb-text-secondary-color)";
328
+ } else if (max != null && len > max) {
329
+ counter.textContent = `${len}/${max}`;
330
+ counter.style.color = "var(--fb-error-color)";
331
+ } else {
332
+ if (max != null) {
333
+ counter.textContent = `${len}/${max}`;
334
+ } else {
335
+ counter.textContent = `${len}`;
336
+ }
337
+ counter.style.color = "var(--fb-text-secondary-color)";
338
+ }
339
+ };
340
+ input.addEventListener("input", updateCounter);
341
+ updateCounter();
342
+ return counter;
343
+ }
272
344
  function renderTextElement(element, ctx, wrapper, pathKey) {
273
345
  const state = ctx.state;
346
+ const inputWrapper = document.createElement("div");
347
+ inputWrapper.style.cssText = "position: relative;";
274
348
  const textInput = document.createElement("input");
275
349
  textInput.type = "text";
276
350
  textInput.className = "w-full rounded-lg";
277
351
  textInput.style.cssText = `
278
352
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
353
+ padding-right: 60px;
279
354
  border: var(--fb-border-width) solid var(--fb-border-color);
280
355
  border-radius: var(--fb-border-radius);
281
356
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -283,6 +358,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
283
358
  font-size: var(--fb-font-size);
284
359
  font-family: var(--fb-font-family);
285
360
  transition: all var(--fb-transition-duration) ease-in-out;
361
+ width: 100%;
362
+ box-sizing: border-box;
286
363
  `;
287
364
  textInput.name = pathKey;
288
365
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
@@ -317,15 +394,12 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
317
394
  textInput.addEventListener("blur", handleChange);
318
395
  textInput.addEventListener("input", handleChange);
319
396
  }
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);
397
+ inputWrapper.appendChild(textInput);
398
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
399
+ const counter = createCharCounter(element, textInput, false);
400
+ inputWrapper.appendChild(counter);
401
+ }
402
+ wrapper.appendChild(inputWrapper);
329
403
  }
330
404
  function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
331
405
  var _a, _b;
@@ -352,11 +426,13 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
352
426
  function addTextItem(value = "", index = -1) {
353
427
  const itemWrapper = document.createElement("div");
354
428
  itemWrapper.className = "multiple-text-item flex items-center gap-2";
429
+ const inputContainer = document.createElement("div");
430
+ inputContainer.style.cssText = "position: relative; flex: 1;";
355
431
  const textInput = document.createElement("input");
356
432
  textInput.type = "text";
357
- textInput.className = "flex-1";
358
433
  textInput.style.cssText = `
359
434
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
435
+ padding-right: 60px;
360
436
  border: var(--fb-border-width) solid var(--fb-border-color);
361
437
  border-radius: var(--fb-border-radius);
362
438
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -364,8 +440,10 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
364
440
  font-size: var(--fb-font-size);
365
441
  font-family: var(--fb-font-family);
366
442
  transition: all var(--fb-transition-duration) ease-in-out;
443
+ width: 100%;
444
+ box-sizing: border-box;
367
445
  `;
368
- textInput.placeholder = element.placeholder || "Enter text";
446
+ textInput.placeholder = element.placeholder || t("placeholderText", state);
369
447
  textInput.value = value;
370
448
  textInput.readOnly = state.config.readonly;
371
449
  if (!state.config.readonly) {
@@ -397,7 +475,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
397
475
  textInput.addEventListener("blur", handleChange);
398
476
  textInput.addEventListener("input", handleChange);
399
477
  }
400
- itemWrapper.appendChild(textInput);
478
+ inputContainer.appendChild(textInput);
479
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
480
+ const counter = createCharCounter(element, textInput, false);
481
+ inputContainer.appendChild(counter);
482
+ }
483
+ itemWrapper.appendChild(inputContainer);
401
484
  if (index === -1) {
402
485
  container.appendChild(itemWrapper);
403
486
  } else {
@@ -450,47 +533,54 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
450
533
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
451
534
  });
452
535
  }
536
+ let addRow = null;
537
+ let countDisplay = null;
538
+ if (!state.config.readonly) {
539
+ addRow = document.createElement("div");
540
+ addRow.className = "flex items-center gap-3 mt-2";
541
+ const addBtn = document.createElement("button");
542
+ addBtn.type = "button";
543
+ addBtn.className = "add-text-btn px-3 py-1 rounded";
544
+ addBtn.style.cssText = `
545
+ color: var(--fb-primary-color);
546
+ border: var(--fb-border-width) solid var(--fb-primary-color);
547
+ background-color: transparent;
548
+ font-size: var(--fb-font-size);
549
+ transition: all var(--fb-transition-duration);
550
+ `;
551
+ addBtn.textContent = "+";
552
+ addBtn.addEventListener("mouseenter", () => {
553
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
554
+ });
555
+ addBtn.addEventListener("mouseleave", () => {
556
+ addBtn.style.backgroundColor = "transparent";
557
+ });
558
+ addBtn.onclick = () => {
559
+ values.push(element.default || "");
560
+ addTextItem(element.default || "");
561
+ updateAddButton();
562
+ updateRemoveButtons();
563
+ };
564
+ countDisplay = document.createElement("span");
565
+ countDisplay.className = "text-sm text-gray-500";
566
+ addRow.appendChild(addBtn);
567
+ addRow.appendChild(countDisplay);
568
+ wrapper.appendChild(addRow);
569
+ }
453
570
  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);
571
+ if (!addRow || !countDisplay) return;
572
+ const addBtn = addRow.querySelector(".add-text-btn");
573
+ if (addBtn) {
574
+ const disabled = values.length >= maxCount;
575
+ addBtn.disabled = disabled;
576
+ addBtn.style.opacity = disabled ? "0.5" : "1";
577
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
481
578
  }
579
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
482
580
  }
483
581
  values.forEach((value) => addTextItem(value));
484
582
  updateAddButton();
485
583
  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
584
  }
495
585
  function validateTextElement(element, key, context) {
496
586
  var _a, _b, _c;
@@ -531,26 +621,31 @@ function validateTextElement(element, key, context) {
531
621
  };
532
622
  const validateTextInput = (input, val, fieldKey) => {
533
623
  let hasError = false;
624
+ const { state } = context;
534
625
  if (!skipValidation && val) {
535
626
  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}`);
627
+ const msg = t("minLength", state, { min: element.minLength });
628
+ errors.push(`${fieldKey}: ${msg}`);
629
+ markValidity(input, msg);
538
630
  hasError = true;
539
631
  } 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}`);
632
+ const msg = t("maxLength", state, { max: element.maxLength });
633
+ errors.push(`${fieldKey}: ${msg}`);
634
+ markValidity(input, msg);
542
635
  hasError = true;
543
636
  } else if (element.pattern) {
544
637
  try {
545
638
  const re = new RegExp(element.pattern);
546
639
  if (!re.test(val)) {
547
- errors.push(`${fieldKey}: pattern mismatch`);
548
- markValidity(input, "pattern mismatch");
640
+ const msg = t("patternMismatch", state);
641
+ errors.push(`${fieldKey}: ${msg}`);
642
+ markValidity(input, msg);
549
643
  hasError = true;
550
644
  }
551
645
  } catch {
552
- errors.push(`${fieldKey}: invalid pattern`);
553
- markValidity(input, "invalid pattern");
646
+ const msg = t("invalidPattern", state);
647
+ errors.push(`${fieldKey}: ${msg}`);
648
+ markValidity(input, msg);
554
649
  hasError = true;
555
650
  }
556
651
  }
@@ -571,17 +666,18 @@ function validateTextElement(element, key, context) {
571
666
  validateTextInput(input, val, `${key}[${index}]`);
572
667
  });
573
668
  if (!skipValidation) {
669
+ const { state } = context;
574
670
  const minCount = (_a = element.minCount) != null ? _a : 1;
575
671
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
576
672
  const filteredValues = rawValues.filter((v) => v.trim() !== "");
577
673
  if (element.required && filteredValues.length === 0) {
578
- errors.push(`${key}: required`);
674
+ errors.push(`${key}: ${t("required", state)}`);
579
675
  }
580
676
  if (filteredValues.length < minCount) {
581
- errors.push(`${key}: minimum ${minCount} items required`);
677
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
582
678
  }
583
679
  if (filteredValues.length > maxCount) {
584
- errors.push(`${key}: maximum ${maxCount} items allowed`);
680
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
585
681
  }
586
682
  }
587
683
  return { value: values, errors };
@@ -589,8 +685,9 @@ function validateTextElement(element, key, context) {
589
685
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
590
686
  const val = (_c = input == null ? void 0 : input.value) != null ? _c : "";
591
687
  if (!skipValidation && element.required && val === "") {
592
- errors.push(`${key}: required`);
593
- markValidity(input, "required");
688
+ const msg = t("required", context.state);
689
+ errors.push(`${key}: ${msg}`);
690
+ markValidity(input, msg);
594
691
  return { value: null, errors };
595
692
  }
596
693
  if (input) {
@@ -634,8 +731,11 @@ function updateTextField(element, fieldPath, value, context) {
634
731
  // src/components/textarea.ts
635
732
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
636
733
  const state = ctx.state;
734
+ const textareaWrapper = document.createElement("div");
735
+ textareaWrapper.style.cssText = "position: relative;";
637
736
  const textareaInput = document.createElement("textarea");
638
737
  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";
738
+ textareaInput.style.cssText = "padding-bottom: 24px;";
639
739
  textareaInput.name = pathKey;
640
740
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
641
741
  textareaInput.rows = element.rows || 4;
@@ -649,11 +749,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
649
749
  textareaInput.addEventListener("blur", handleChange);
650
750
  textareaInput.addEventListener("input", handleChange);
651
751
  }
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);
752
+ textareaWrapper.appendChild(textareaInput);
753
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
754
+ const counter = createCharCounter(element, textareaInput, true);
755
+ textareaWrapper.appendChild(counter);
756
+ }
757
+ wrapper.appendChild(textareaWrapper);
657
758
  }
658
759
  function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
659
760
  var _a, _b;
@@ -680,9 +781,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
680
781
  function addTextareaItem(value = "", index = -1) {
681
782
  const itemWrapper = document.createElement("div");
682
783
  itemWrapper.className = "multiple-textarea-item";
784
+ const textareaContainer = document.createElement("div");
785
+ textareaContainer.style.cssText = "position: relative;";
683
786
  const textareaInput = document.createElement("textarea");
684
787
  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";
788
+ textareaInput.style.cssText = "padding-bottom: 24px;";
789
+ textareaInput.placeholder = element.placeholder || t("placeholderText", state);
686
790
  textareaInput.rows = element.rows || 4;
687
791
  textareaInput.value = value;
688
792
  textareaInput.readOnly = state.config.readonly;
@@ -694,7 +798,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
694
798
  textareaInput.addEventListener("blur", handleChange);
695
799
  textareaInput.addEventListener("input", handleChange);
696
800
  }
697
- itemWrapper.appendChild(textareaInput);
801
+ textareaContainer.appendChild(textareaInput);
802
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
803
+ const counter = createCharCounter(element, textareaInput, true);
804
+ textareaContainer.appendChild(counter);
805
+ }
806
+ itemWrapper.appendChild(textareaContainer);
698
807
  if (index === -1) {
699
808
  container.appendChild(itemWrapper);
700
809
  } else {
@@ -736,30 +845,54 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
736
845
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
737
846
  });
738
847
  }
848
+ let addRow = null;
849
+ let countDisplay = null;
850
+ if (!state.config.readonly) {
851
+ addRow = document.createElement("div");
852
+ addRow.className = "flex items-center gap-3 mt-2";
853
+ const addBtn = document.createElement("button");
854
+ addBtn.type = "button";
855
+ addBtn.className = "add-textarea-btn px-3 py-1 rounded";
856
+ addBtn.style.cssText = `
857
+ color: var(--fb-primary-color);
858
+ border: var(--fb-border-width) solid var(--fb-primary-color);
859
+ background-color: transparent;
860
+ font-size: var(--fb-font-size);
861
+ transition: all var(--fb-transition-duration);
862
+ `;
863
+ addBtn.textContent = "+";
864
+ addBtn.addEventListener("mouseenter", () => {
865
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
866
+ });
867
+ addBtn.addEventListener("mouseleave", () => {
868
+ addBtn.style.backgroundColor = "transparent";
869
+ });
870
+ addBtn.onclick = () => {
871
+ values.push(element.default || "");
872
+ addTextareaItem(element.default || "");
873
+ updateAddButton();
874
+ updateRemoveButtons();
875
+ };
876
+ countDisplay = document.createElement("span");
877
+ countDisplay.className = "text-sm text-gray-500";
878
+ addRow.appendChild(addBtn);
879
+ addRow.appendChild(countDisplay);
880
+ wrapper.appendChild(addRow);
881
+ }
739
882
  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);
883
+ if (!addRow || !countDisplay) return;
884
+ const addBtn = addRow.querySelector(".add-textarea-btn");
885
+ if (addBtn) {
886
+ const disabled = values.length >= maxCount;
887
+ addBtn.disabled = disabled;
888
+ addBtn.style.opacity = disabled ? "0.5" : "1";
889
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
754
890
  }
891
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
755
892
  }
756
893
  values.forEach((value) => addTextareaItem(value));
757
894
  updateAddButton();
758
895
  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
896
  }
764
897
  function validateTextareaElement(element, key, context) {
765
898
  return validateTextElement(element, key, context);
@@ -769,11 +902,61 @@ function updateTextareaField(element, fieldPath, value, context) {
769
902
  }
770
903
 
771
904
  // src/components/number.ts
905
+ function createNumberCounter(element, input) {
906
+ const counter = document.createElement("span");
907
+ counter.className = "number-counter";
908
+ counter.style.cssText = `
909
+ position: absolute;
910
+ top: 50%;
911
+ transform: translateY(-50%);
912
+ right: 10px;
913
+ font-size: var(--fb-font-size-small);
914
+ color: var(--fb-text-secondary-color);
915
+ pointer-events: none;
916
+ background: var(--fb-background-color);
917
+ padding: 0 4px;
918
+ `;
919
+ const updateCounter = () => {
920
+ const val = input.value ? parseFloat(input.value) : null;
921
+ const min = element.min;
922
+ const max = element.max;
923
+ if (min == null && max == null) {
924
+ counter.textContent = "";
925
+ return;
926
+ }
927
+ if (val == null || min != null && val < min) {
928
+ if (min != null && max != null) {
929
+ counter.textContent = `${min}-${max}`;
930
+ } else if (max != null) {
931
+ counter.textContent = `\u2264${max}`;
932
+ } else if (min != null) {
933
+ counter.textContent = `\u2265${min}`;
934
+ }
935
+ counter.style.color = "var(--fb-text-secondary-color)";
936
+ } else if (max != null && val > max) {
937
+ counter.textContent = `${val}/${max}`;
938
+ counter.style.color = "var(--fb-error-color)";
939
+ } else {
940
+ if (max != null) {
941
+ counter.textContent = `${val}/${max}`;
942
+ } else {
943
+ counter.textContent = `${val}`;
944
+ }
945
+ counter.style.color = "var(--fb-text-secondary-color)";
946
+ }
947
+ };
948
+ input.addEventListener("input", updateCounter);
949
+ updateCounter();
950
+ return counter;
951
+ }
772
952
  function renderNumberElement(element, ctx, wrapper, pathKey) {
773
953
  const state = ctx.state;
954
+ const inputWrapper = document.createElement("div");
955
+ inputWrapper.style.cssText = "position: relative;";
774
956
  const numberInput = document.createElement("input");
775
957
  numberInput.type = "number";
776
958
  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";
959
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
777
960
  numberInput.name = pathKey;
778
961
  numberInput.placeholder = element.placeholder || "0";
779
962
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -789,11 +972,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
789
972
  numberInput.addEventListener("blur", handleChange);
790
973
  numberInput.addEventListener("input", handleChange);
791
974
  }
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);
975
+ inputWrapper.appendChild(numberInput);
976
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
977
+ const counter = createNumberCounter(element, numberInput);
978
+ inputWrapper.appendChild(counter);
979
+ }
980
+ wrapper.appendChild(inputWrapper);
797
981
  }
798
982
  function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
799
983
  var _a, _b;
@@ -820,9 +1004,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
820
1004
  function addNumberItem(value = "", index = -1) {
821
1005
  const itemWrapper = document.createElement("div");
822
1006
  itemWrapper.className = "multiple-number-item flex items-center gap-2";
1007
+ const inputContainer = document.createElement("div");
1008
+ inputContainer.style.cssText = "position: relative; flex: 1;";
823
1009
  const numberInput = document.createElement("input");
824
1010
  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";
1011
+ 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";
1012
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
826
1013
  numberInput.placeholder = element.placeholder || "0";
827
1014
  if (element.min !== void 0) numberInput.min = element.min.toString();
828
1015
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -837,7 +1024,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
837
1024
  numberInput.addEventListener("blur", handleChange);
838
1025
  numberInput.addEventListener("input", handleChange);
839
1026
  }
840
- itemWrapper.appendChild(numberInput);
1027
+ inputContainer.appendChild(numberInput);
1028
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
1029
+ const counter = createNumberCounter(element, numberInput);
1030
+ inputContainer.appendChild(counter);
1031
+ }
1032
+ itemWrapper.appendChild(inputContainer);
841
1033
  if (index === -1) {
842
1034
  container.appendChild(itemWrapper);
843
1035
  } else {
@@ -879,30 +1071,54 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
879
1071
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
880
1072
  });
881
1073
  }
1074
+ let addRow = null;
1075
+ let countDisplay = null;
1076
+ if (!state.config.readonly) {
1077
+ addRow = document.createElement("div");
1078
+ addRow.className = "flex items-center gap-3 mt-2";
1079
+ const addBtn = document.createElement("button");
1080
+ addBtn.type = "button";
1081
+ addBtn.className = "add-number-btn px-3 py-1 rounded";
1082
+ addBtn.style.cssText = `
1083
+ color: var(--fb-primary-color);
1084
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1085
+ background-color: transparent;
1086
+ font-size: var(--fb-font-size);
1087
+ transition: all var(--fb-transition-duration);
1088
+ `;
1089
+ addBtn.textContent = "+";
1090
+ addBtn.addEventListener("mouseenter", () => {
1091
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1092
+ });
1093
+ addBtn.addEventListener("mouseleave", () => {
1094
+ addBtn.style.backgroundColor = "transparent";
1095
+ });
1096
+ addBtn.onclick = () => {
1097
+ values.push(element.default || "");
1098
+ addNumberItem(element.default || "");
1099
+ updateAddButton();
1100
+ updateRemoveButtons();
1101
+ };
1102
+ countDisplay = document.createElement("span");
1103
+ countDisplay.className = "text-sm text-gray-500";
1104
+ addRow.appendChild(addBtn);
1105
+ addRow.appendChild(countDisplay);
1106
+ wrapper.appendChild(addRow);
1107
+ }
882
1108
  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);
1109
+ if (!addRow || !countDisplay) return;
1110
+ const addBtn = addRow.querySelector(".add-number-btn");
1111
+ if (addBtn) {
1112
+ const disabled = values.length >= maxCount;
1113
+ addBtn.disabled = disabled;
1114
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1115
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
897
1116
  }
1117
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
898
1118
  }
899
1119
  values.forEach((value) => addNumberItem(value));
900
1120
  updateAddButton();
901
1121
  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
1122
  }
907
1123
  function validateNumberElement(element, key, context) {
908
1124
  var _a, _b, _c, _d, _e;
@@ -943,13 +1159,16 @@ function validateNumberElement(element, key, context) {
943
1159
  };
944
1160
  const validateNumberInput = (input, v, fieldKey) => {
945
1161
  let hasError = false;
1162
+ const { state } = context;
946
1163
  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}`);
1164
+ const msg = t("minValue", state, { min: element.min });
1165
+ errors.push(`${fieldKey}: ${msg}`);
1166
+ markValidity(input, msg);
949
1167
  hasError = true;
950
1168
  } 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}`);
1169
+ const msg = t("maxValue", state, { max: element.max });
1170
+ errors.push(`${fieldKey}: ${msg}`);
1171
+ markValidity(input, msg);
953
1172
  hasError = true;
954
1173
  }
955
1174
  if (!hasError) {
@@ -971,8 +1190,9 @@ function validateNumberElement(element, key, context) {
971
1190
  }
972
1191
  const v = parseFloat(raw);
973
1192
  if (!skipValidation && !Number.isFinite(v)) {
974
- errors.push(`${key}[${index}]: not a number`);
975
- markValidity(input, "not a number");
1193
+ const msg = t("notANumber", context.state);
1194
+ errors.push(`${key}[${index}]: ${msg}`);
1195
+ markValidity(input, msg);
976
1196
  values.push(null);
977
1197
  return;
978
1198
  }
@@ -981,26 +1201,29 @@ function validateNumberElement(element, key, context) {
981
1201
  values.push(Number(v.toFixed(d)));
982
1202
  });
983
1203
  if (!skipValidation) {
1204
+ const { state } = context;
984
1205
  const minCount = (_a = element.minCount) != null ? _a : 1;
985
1206
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
986
1207
  const filteredValues = values.filter((v) => v !== null);
987
1208
  if (element.required && filteredValues.length === 0) {
988
- errors.push(`${key}: required`);
1209
+ errors.push(`${key}: ${t("required", state)}`);
989
1210
  }
990
1211
  if (filteredValues.length < minCount) {
991
- errors.push(`${key}: minimum ${minCount} items required`);
1212
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
992
1213
  }
993
1214
  if (filteredValues.length > maxCount) {
994
- errors.push(`${key}: maximum ${maxCount} items allowed`);
1215
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
995
1216
  }
996
1217
  }
997
1218
  return { value: values, errors };
998
1219
  } else {
999
1220
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
1000
1221
  const raw = (_c = input == null ? void 0 : input.value) != null ? _c : "";
1222
+ const { state } = context;
1001
1223
  if (!skipValidation && element.required && raw === "") {
1002
- errors.push(`${key}: required`);
1003
- markValidity(input, "required");
1224
+ const msg = t("required", state);
1225
+ errors.push(`${key}: ${msg}`);
1226
+ markValidity(input, msg);
1004
1227
  return { value: null, errors };
1005
1228
  }
1006
1229
  if (raw === "") {
@@ -1009,8 +1232,9 @@ function validateNumberElement(element, key, context) {
1009
1232
  }
1010
1233
  const v = parseFloat(raw);
1011
1234
  if (!skipValidation && !Number.isFinite(v)) {
1012
- errors.push(`${key}: not a number`);
1013
- markValidity(input, "not a number");
1235
+ const msg = t("notANumber", state);
1236
+ errors.push(`${key}: ${msg}`);
1237
+ markValidity(input, msg);
1014
1238
  return { value: null, errors };
1015
1239
  }
1016
1240
  validateNumberInput(input, v, key);
@@ -1077,10 +1301,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1077
1301
  selectInput.addEventListener("change", handleChange);
1078
1302
  }
1079
1303
  wrapper.appendChild(selectInput);
1080
- const selectHint = document.createElement("p");
1081
- selectHint.className = "text-xs text-gray-500 mt-1";
1082
- selectHint.textContent = makeFieldHint(element);
1083
- wrapper.appendChild(selectHint);
1304
+ if (!state.config.readonly) {
1305
+ const selectHint = document.createElement("p");
1306
+ selectHint.className = "text-xs text-gray-500 mt-1";
1307
+ selectHint.textContent = makeFieldHint(element, state);
1308
+ wrapper.appendChild(selectHint);
1309
+ }
1084
1310
  }
1085
1311
  function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1086
1312
  var _a, _b, _c, _d;
@@ -1165,32 +1391,62 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1165
1391
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1166
1392
  });
1167
1393
  }
1394
+ let addRow = null;
1395
+ let countDisplay = null;
1396
+ if (!state.config.readonly) {
1397
+ addRow = document.createElement("div");
1398
+ addRow.className = "flex items-center gap-3 mt-2";
1399
+ const addBtn = document.createElement("button");
1400
+ addBtn.type = "button";
1401
+ addBtn.className = "add-select-btn px-3 py-1 rounded";
1402
+ addBtn.style.cssText = `
1403
+ color: var(--fb-primary-color);
1404
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1405
+ background-color: transparent;
1406
+ font-size: var(--fb-font-size);
1407
+ transition: all var(--fb-transition-duration);
1408
+ `;
1409
+ addBtn.textContent = "+";
1410
+ addBtn.addEventListener("mouseenter", () => {
1411
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1412
+ });
1413
+ addBtn.addEventListener("mouseleave", () => {
1414
+ addBtn.style.backgroundColor = "transparent";
1415
+ });
1416
+ addBtn.onclick = () => {
1417
+ var _a2, _b2;
1418
+ const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
1419
+ values.push(defaultValue);
1420
+ addSelectItem(defaultValue);
1421
+ updateAddButton();
1422
+ updateRemoveButtons();
1423
+ };
1424
+ countDisplay = document.createElement("span");
1425
+ countDisplay.className = "text-sm text-gray-500";
1426
+ addRow.appendChild(addBtn);
1427
+ addRow.appendChild(countDisplay);
1428
+ wrapper.appendChild(addRow);
1429
+ }
1168
1430
  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);
1431
+ if (!addRow || !countDisplay) return;
1432
+ const addBtn = addRow.querySelector(".add-select-btn");
1433
+ if (addBtn) {
1434
+ const disabled = values.length >= maxCount;
1435
+ addBtn.disabled = disabled;
1436
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1437
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
1185
1438
  }
1439
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1186
1440
  }
1187
1441
  values.forEach((value) => addSelectItem(value));
1188
1442
  updateAddButton();
1189
1443
  updateRemoveButtons();
1190
- const hint = document.createElement("p");
1191
- hint.className = "text-xs text-gray-500 mt-1";
1192
- hint.textContent = makeFieldHint(element);
1193
- wrapper.appendChild(hint);
1444
+ if (!state.config.readonly) {
1445
+ const hint = document.createElement("p");
1446
+ hint.className = "text-xs text-gray-500 mt-1";
1447
+ hint.textContent = makeFieldHint(element, state);
1448
+ wrapper.appendChild(hint);
1449
+ }
1194
1450
  }
1195
1451
  function validateSelectElement(element, key, context) {
1196
1452
  var _a;
@@ -1232,17 +1488,18 @@ function validateSelectElement(element, key, context) {
1232
1488
  const validateMultipleCount = (key2, values, element2, filterFn) => {
1233
1489
  var _a2, _b;
1234
1490
  if (skipValidation) return;
1491
+ const { state } = context;
1235
1492
  const filteredValues = values.filter(filterFn);
1236
1493
  const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 1 : 1;
1237
1494
  const maxCount = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
1238
1495
  if (element2.required && filteredValues.length === 0) {
1239
- errors.push(`${key2}: required`);
1496
+ errors.push(`${key2}: ${t("required", state)}`);
1240
1497
  }
1241
1498
  if (filteredValues.length < minCount) {
1242
- errors.push(`${key2}: minimum ${minCount} items required`);
1499
+ errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
1243
1500
  }
1244
1501
  if (filteredValues.length > maxCount) {
1245
- errors.push(`${key2}: maximum ${maxCount} items allowed`);
1502
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
1246
1503
  }
1247
1504
  };
1248
1505
  if ("multiple" in element && element.multiple) {
@@ -1264,8 +1521,9 @@ function validateSelectElement(element, key, context) {
1264
1521
  );
1265
1522
  const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
1266
1523
  if (!skipValidation && element.required && val === "") {
1267
- errors.push(`${key}: required`);
1268
- markValidity(input, "required");
1524
+ const msg = t("required", context.state);
1525
+ errors.push(`${key}: ${msg}`);
1526
+ markValidity(input, msg);
1269
1527
  return { value: null, errors };
1270
1528
  } else {
1271
1529
  markValidity(input, null);
@@ -1317,18 +1575,11 @@ function updateSelectField(element, fieldPath, value, context) {
1317
1575
  }
1318
1576
  }
1319
1577
 
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
1578
  // src/components/file.ts
1328
- function renderLocalImagePreview(container, file, fileName) {
1579
+ function renderLocalImagePreview(container, file, fileName, state) {
1329
1580
  const img = document.createElement("img");
1330
1581
  img.className = "w-full h-full object-contain";
1331
- img.alt = fileName || "Preview";
1582
+ img.alt = fileName || t("previewAlt", state);
1332
1583
  const reader = new FileReader();
1333
1584
  reader.onload = (e) => {
1334
1585
  var _a;
@@ -1348,14 +1599,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
1348
1599
  <div class="relative group h-full">
1349
1600
  <video class="w-full h-full object-contain" controls preload="auto" muted>
1350
1601
  <source src="${videoUrl}" type="${videoType}">
1351
- Your browser does not support the video tag.
1602
+ ${escapeHtml(t("videoNotSupported", state))}
1352
1603
  </video>
1353
1604
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1354
1605
  <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)}
1606
+ ${escapeHtml(t("removeElement", state))}
1356
1607
  </button>
1357
1608
  <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1358
- Change
1609
+ ${escapeHtml(t("changeButton", state))}
1359
1610
  </button>
1360
1611
  </div>
1361
1612
  </div>
@@ -1405,11 +1656,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
1405
1656
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1406
1657
  <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
1658
  </svg>
1408
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1659
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1409
1660
  </div>
1410
1661
  `;
1411
1662
  }
1412
- function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1663
+ function renderUploadedVideoPreview(container, thumbnailUrl, videoType, state) {
1413
1664
  const video = document.createElement("video");
1414
1665
  video.className = "w-full h-full object-contain";
1415
1666
  video.controls = true;
@@ -1420,7 +1671,7 @@ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1420
1671
  source.type = videoType;
1421
1672
  video.appendChild(source);
1422
1673
  video.appendChild(
1423
- document.createTextNode("Your browser does not support the video tag.")
1674
+ document.createTextNode(t("videoNotSupported", state))
1424
1675
  );
1425
1676
  container.appendChild(video);
1426
1677
  }
@@ -1439,7 +1690,7 @@ function renderDeleteButton(container, resourceId, state) {
1439
1690
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1440
1691
  <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
1692
  </svg>
1442
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1693
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1443
1694
  </div>
1444
1695
  `;
1445
1696
  });
@@ -1449,7 +1700,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1449
1700
  return;
1450
1701
  }
1451
1702
  if (meta.type && meta.type.startsWith("image/")) {
1452
- renderLocalImagePreview(container, meta.file, fileName);
1703
+ renderLocalImagePreview(container, meta.file, fileName, state);
1453
1704
  } else if (meta.type && meta.type.startsWith("video/")) {
1454
1705
  const newContainer = renderLocalVideoPreview(
1455
1706
  container,
@@ -1461,7 +1712,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1461
1712
  );
1462
1713
  container = newContainer;
1463
1714
  } 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>`;
1715
+ 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
1716
  }
1466
1717
  if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
1467
1718
  renderDeleteButton(container, resourceId, state);
@@ -1477,11 +1728,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1477
1728
  if (thumbnailUrl) {
1478
1729
  clear(container);
1479
1730
  if (meta && meta.type && meta.type.startsWith("video/")) {
1480
- renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
1731
+ renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
1481
1732
  } else {
1482
1733
  const img = document.createElement("img");
1483
1734
  img.className = "w-full h-full object-contain";
1484
- img.alt = fileName || "Preview";
1735
+ img.alt = fileName || t("previewAlt", state);
1485
1736
  img.src = thumbnailUrl;
1486
1737
  container.appendChild(img);
1487
1738
  }
@@ -1495,7 +1746,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1495
1746
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1496
1747
  <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
1748
  </svg>
1498
- <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1749
+ <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
1499
1750
  </div>
1500
1751
  `;
1501
1752
  }
@@ -1552,16 +1803,16 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1552
1803
  try {
1553
1804
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
1554
1805
  if (thumbnailUrl) {
1555
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
1806
+ previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
1556
1807
  } 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>`;
1808
+ 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
1809
  }
1559
1810
  } catch (error) {
1560
1811
  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>`;
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>`;
1562
1813
  }
1563
1814
  } 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>`;
1815
+ 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
1816
  }
1566
1817
  } else if (isVideo) {
1567
1818
  if (state.config.getThumbnail) {
@@ -1572,7 +1823,7 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1572
1823
  <div class="relative group">
1573
1824
  <video class="w-full h-auto" controls preload="auto" muted>
1574
1825
  <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.
1826
+ ${escapeHtml(t("videoNotSupported", state))}
1576
1827
  </video>
1577
1828
  <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
1829
  <div class="bg-white bg-opacity-90 rounded-full p-3">
@@ -1584,14 +1835,14 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1584
1835
  </div>
1585
1836
  `;
1586
1837
  } 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>`;
1838
+ 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
1839
  }
1589
1840
  } catch (error) {
1590
1841
  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>`;
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>`;
1592
1843
  }
1593
1844
  } 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>`;
1845
+ 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
1846
  }
1596
1847
  } else {
1597
1848
  const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
@@ -1601,13 +1852,13 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1601
1852
  <div class="flex items-center space-x-3">
1602
1853
  <div class="text-3xl text-gray-400">${fileIcon}</div>
1603
1854
  <div class="flex-1 min-w-0">
1604
- <div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
1855
+ <div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
1605
1856
  <div class="text-xs text-gray-500">${fileDescription}</div>
1606
1857
  </div>
1607
1858
  </div>
1608
1859
  `;
1609
1860
  } 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>`;
1861
+ 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
1862
  }
1612
1863
  }
1613
1864
  const fileNameElement = document.createElement("p");
@@ -1630,12 +1881,18 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1630
1881
  fileResult.appendChild(downloadButton);
1631
1882
  return fileResult;
1632
1883
  }
1633
- function renderResourcePills(container, rids, state, onRemove) {
1884
+ function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
1634
1885
  clear(container);
1886
+ const buildHintLine = () => {
1887
+ const parts = [t("clickDragTextMultiple", state)];
1888
+ if (hint) parts.push(hint);
1889
+ if (countInfo) parts.push(countInfo);
1890
+ return parts.join(" \u2022 ");
1891
+ };
1635
1892
  const isInitialRender = !container.classList.contains("grid");
1636
1893
  if ((!rids || rids.length === 0) && isInitialRender) {
1637
- const gridContainer = document.createElement("div");
1638
- gridContainer.className = "grid grid-cols-4 gap-3 mb-3";
1894
+ const gridContainer2 = document.createElement("div");
1895
+ gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
1639
1896
  for (let i = 0; i < 4; i++) {
1640
1897
  const slot = document.createElement("div");
1641
1898
  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 +1923,17 @@ function renderResourcePills(container, rids, state, onRemove) {
1666
1923
  );
1667
1924
  if (fileInput) fileInput.click();
1668
1925
  };
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);
1926
+ gridContainer2.appendChild(slot);
1927
+ }
1928
+ const hintText2 = document.createElement("div");
1929
+ hintText2.className = "text-center text-xs text-gray-500 mt-2";
1930
+ hintText2.textContent = buildHintLine();
1931
+ container.appendChild(gridContainer2);
1932
+ container.appendChild(hintText2);
1696
1933
  return;
1697
1934
  }
1698
- container.className = "files-list grid grid-cols-4 gap-3 mt-2";
1935
+ const gridContainer = document.createElement("div");
1936
+ gridContainer.className = "files-list grid grid-cols-4 gap-3";
1699
1937
  const currentImagesCount = rids ? rids.length : 0;
1700
1938
  const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
1701
1939
  const slotsNeeded = rowsNeeded * 4;
@@ -1710,7 +1948,7 @@ function renderResourcePills(container, rids, state, onRemove) {
1710
1948
  console.error("Failed to render thumbnail:", err);
1711
1949
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1712
1950
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1713
- <div class="text-xs">Preview error</div>
1951
+ <div class="text-xs">${escapeHtml(t("previewError", state))}</div>
1714
1952
  </div>`;
1715
1953
  });
1716
1954
  if (onRemove) {
@@ -1743,15 +1981,20 @@ function renderResourcePills(container, rids, state, onRemove) {
1743
1981
  if (fileInput) fileInput.click();
1744
1982
  };
1745
1983
  }
1746
- container.appendChild(slot);
1984
+ gridContainer.appendChild(slot);
1747
1985
  }
1986
+ container.appendChild(gridContainer);
1987
+ const hintText = document.createElement("div");
1988
+ hintText.className = "text-center text-xs text-gray-500 mt-2";
1989
+ hintText.textContent = buildHintLine();
1990
+ container.appendChild(hintText);
1748
1991
  }
1749
- function renderThumbnailError(slot, iconSize = "w-12 h-12") {
1992
+ function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
1750
1993
  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">
1994
+ <svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1752
1995
  <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
1996
  </svg>
1754
- <div class="text-xs mt-1 text-red-600">Preview error</div>
1997
+ <div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
1755
1998
  </div>`;
1756
1999
  }
1757
2000
  async function renderThumbnailForResource(slot, rid, meta, state) {
@@ -1789,7 +2032,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1789
2032
  if (state.config.onThumbnailError) {
1790
2033
  state.config.onThumbnailError(err, rid);
1791
2034
  }
1792
- renderThumbnailError(slot);
2035
+ renderThumbnailError(slot, state);
1793
2036
  }
1794
2037
  } else {
1795
2038
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -1838,7 +2081,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1838
2081
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1839
2082
  <path d="M8 5v14l11-7z"/>
1840
2083
  </svg>
1841
- <div class="text-xs mt-1">${(meta == null ? void 0 : meta.name) || "Video"}</div>
2084
+ <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
1842
2085
  </div>`;
1843
2086
  }
1844
2087
  } catch (error) {
@@ -1846,30 +2089,32 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1846
2089
  if (state.config.onThumbnailError) {
1847
2090
  state.config.onThumbnailError(err, rid);
1848
2091
  }
1849
- renderThumbnailError(slot, "w-8 h-8");
2092
+ renderThumbnailError(slot, state, "w-8 h-8");
1850
2093
  }
1851
2094
  } else {
1852
2095
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1853
2096
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1854
2097
  <path d="M8 5v14l11-7z"/>
1855
2098
  </svg>
1856
- <div class="text-xs mt-1">${(meta == null ? void 0 : meta.name) || "Video"}</div>
2099
+ <div class="text-xs mt-1">${escapeHtml((meta == null ? void 0 : meta.name) || "Video")}</div>
1857
2100
  </div>`;
1858
2101
  }
1859
2102
  } else {
1860
2103
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1861
2104
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1862
- <div class="text-xs">${(meta == null ? void 0 : meta.name) || "File"}</div>
2105
+ <div class="text-xs">${escapeHtml((meta == null ? void 0 : meta.name) || "File")}</div>
1863
2106
  </div>`;
1864
2107
  }
1865
2108
  }
1866
- function setEmptyFileContainer(fileContainer, state) {
2109
+ function setEmptyFileContainer(fileContainer, state, hint) {
2110
+ const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
1867
2111
  fileContainer.innerHTML = `
1868
2112
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
1869
2113
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1870
2114
  <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
2115
  </svg>
1872
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
2116
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2117
+ ${hintHtml}
1873
2118
  </div>
1874
2119
  `;
1875
2120
  }
@@ -2134,13 +2379,13 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2134
2379
  console.error("Failed to render file preview:", err);
2135
2380
  const emptyState = document.createElement("div");
2136
2381
  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>`;
2382
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
2138
2383
  wrapper.appendChild(emptyState);
2139
2384
  });
2140
2385
  } else {
2141
2386
  const emptyState = document.createElement("div");
2142
2387
  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>`;
2388
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
2144
2389
  wrapper.appendChild(emptyState);
2145
2390
  }
2146
2391
  } else {
@@ -2184,7 +2429,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2184
2429
  }
2185
2430
  );
2186
2431
  } else {
2187
- setEmptyFileContainer(fileContainer, state);
2432
+ const hint = makeFieldHint(element, state);
2433
+ setEmptyFileContainer(fileContainer, state, hint);
2188
2434
  }
2189
2435
  fileContainer.onclick = fileUploadHandler;
2190
2436
  setupDragAndDrop(fileContainer, dragHandler);
@@ -2203,18 +2449,6 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2203
2449
  };
2204
2450
  fileWrapper.appendChild(fileContainer);
2205
2451
  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
2452
  wrapper.appendChild(fileWrapper);
2219
2453
  }
2220
2454
  }
@@ -2234,18 +2468,24 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2234
2468
  });
2235
2469
  });
2236
2470
  } 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>`;
2471
+ 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
2472
  }
2239
2473
  wrapper.appendChild(resultsWrapper);
2240
2474
  } else {
2241
2475
  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
- });
2476
+ renderResourcePills(
2477
+ list,
2478
+ initialFiles,
2479
+ state,
2480
+ (ridToRemove) => {
2481
+ const index = initialFiles.indexOf(ridToRemove);
2482
+ if (index > -1) {
2483
+ initialFiles.splice(index, 1);
2484
+ }
2485
+ updateFilesList2();
2486
+ },
2487
+ filesFieldHint
2488
+ );
2249
2489
  };
2250
2490
  const filesWrapper = document.createElement("div");
2251
2491
  filesWrapper.className = "space-y-2";
@@ -2263,6 +2503,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2263
2503
  list.className = "files-list";
2264
2504
  const initialFiles = ctx.prefill[element.key] || [];
2265
2505
  addPrefillFilesToIndex(initialFiles, state);
2506
+ const filesFieldHint = makeFieldHint(element, state);
2266
2507
  updateFilesList2();
2267
2508
  setupFilesDropHandler(
2268
2509
  filesContainer,
@@ -2283,10 +2524,6 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2283
2524
  filesContainer.appendChild(list);
2284
2525
  filesWrapper.appendChild(filesContainer);
2285
2526
  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
2527
  wrapper.appendChild(filesWrapper);
2291
2528
  }
2292
2529
  }
@@ -2308,7 +2545,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2308
2545
  });
2309
2546
  });
2310
2547
  } 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>`;
2548
+ 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
2549
  }
2313
2550
  wrapper.appendChild(resultsWrapper);
2314
2551
  } else {
@@ -2328,19 +2565,24 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2328
2565
  filesWrapper.appendChild(filesContainer);
2329
2566
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2330
2567
  addPrefillFilesToIndex(initialFiles, state);
2568
+ const multipleFilesHint = makeFieldHint(element, state);
2569
+ const buildCountInfo = () => {
2570
+ const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
2571
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
2572
+ return countText + minMaxText;
2573
+ };
2331
2574
  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);
2575
+ renderResourcePills(
2576
+ filesContainer,
2577
+ initialFiles,
2578
+ state,
2579
+ (index) => {
2580
+ initialFiles.splice(initialFiles.indexOf(index), 1);
2581
+ updateFilesDisplay();
2582
+ },
2583
+ multipleFilesHint,
2584
+ buildCountInfo()
2585
+ );
2344
2586
  };
2345
2587
  setupFilesDropHandler(
2346
2588
  filesContainer,
@@ -2370,16 +2612,17 @@ function validateFileElement(element, key, context) {
2370
2612
  const validateFileCount = (key2, resourceIds, element2) => {
2371
2613
  var _a2, _b;
2372
2614
  if (skipValidation) return;
2615
+ const { state } = context;
2373
2616
  const minFiles = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
2374
2617
  const maxFiles = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
2375
2618
  if (element2.required && resourceIds.length === 0) {
2376
- errors.push(`${key2}: required`);
2619
+ errors.push(`${key2}: ${t("required", state)}`);
2377
2620
  }
2378
2621
  if (resourceIds.length < minFiles) {
2379
- errors.push(`${key2}: minimum ${minFiles} files required`);
2622
+ errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
2380
2623
  }
2381
2624
  if (resourceIds.length > maxFiles) {
2382
- errors.push(`${key2}: maximum ${maxFiles} files allowed`);
2625
+ errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
2383
2626
  }
2384
2627
  };
2385
2628
  if (isMultipleField) {
@@ -2407,7 +2650,7 @@ function validateFileElement(element, key, context) {
2407
2650
  );
2408
2651
  const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2409
2652
  if (!skipValidation && element.required && rid === "") {
2410
- errors.push(`${key}: required`);
2653
+ errors.push(`${key}: ${t("required", context.state)}`);
2411
2654
  return { value: null, errors };
2412
2655
  }
2413
2656
  return { value: rid || null, errors };
@@ -2650,14 +2893,16 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
2650
2893
  const editUI = createEditColourUI(initialValue, pathKey, ctx);
2651
2894
  wrapper.appendChild(editUI);
2652
2895
  }
2653
- const colourHint = document.createElement("p");
2654
- colourHint.className = "mt-1";
2655
- colourHint.style.cssText = `
2656
- font-size: var(--fb-font-size-small);
2657
- color: var(--fb-text-secondary-color);
2658
- `;
2659
- colourHint.textContent = makeFieldHint(element);
2660
- wrapper.appendChild(colourHint);
2896
+ if (!state.config.readonly) {
2897
+ const colourHint = document.createElement("p");
2898
+ colourHint.className = "mt-1";
2899
+ colourHint.style.cssText = `
2900
+ font-size: var(--fb-font-size-small);
2901
+ color: var(--fb-text-secondary-color);
2902
+ `;
2903
+ colourHint.textContent = makeFieldHint(element, state);
2904
+ wrapper.appendChild(colourHint);
2905
+ }
2661
2906
  }
2662
2907
  function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2663
2908
  var _a, _b;
@@ -2747,48 +2992,65 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2747
2992
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2748
2993
  });
2749
2994
  }
2995
+ let addRow = null;
2996
+ let countDisplay = null;
2997
+ if (!state.config.readonly) {
2998
+ addRow = document.createElement("div");
2999
+ addRow.className = "flex items-center gap-3 mt-2";
3000
+ const addBtn = document.createElement("button");
3001
+ addBtn.type = "button";
3002
+ addBtn.className = "add-colour-btn px-3 py-1 rounded";
3003
+ addBtn.style.cssText = `
3004
+ color: var(--fb-primary-color);
3005
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3006
+ background-color: transparent;
3007
+ font-size: var(--fb-font-size);
3008
+ transition: all var(--fb-transition-duration);
3009
+ `;
3010
+ addBtn.textContent = "+";
3011
+ addBtn.addEventListener("mouseenter", () => {
3012
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3013
+ });
3014
+ addBtn.addEventListener("mouseleave", () => {
3015
+ addBtn.style.backgroundColor = "transparent";
3016
+ });
3017
+ addBtn.onclick = () => {
3018
+ const defaultColour = element.default || "#000000";
3019
+ values.push(defaultColour);
3020
+ addColourItem(defaultColour);
3021
+ updateAddButton();
3022
+ updateRemoveButtons();
3023
+ };
3024
+ countDisplay = document.createElement("span");
3025
+ countDisplay.className = "text-sm text-gray-500";
3026
+ addRow.appendChild(addBtn);
3027
+ addRow.appendChild(countDisplay);
3028
+ wrapper.appendChild(addRow);
3029
+ }
2750
3030
  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);
3031
+ if (!addRow || !countDisplay) return;
3032
+ const addBtn = addRow.querySelector(".add-colour-btn");
3033
+ if (addBtn) {
3034
+ const disabled = values.length >= maxCount;
3035
+ addBtn.disabled = disabled;
3036
+ addBtn.style.opacity = disabled ? "0.5" : "1";
3037
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
2779
3038
  }
3039
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2780
3040
  }
2781
3041
  values.forEach((value) => addColourItem(value));
2782
3042
  updateAddButton();
2783
3043
  updateRemoveButtons();
2784
- const hint = document.createElement("p");
2785
- hint.className = "mt-1";
2786
- hint.style.cssText = `
2787
- font-size: var(--fb-font-size-small);
2788
- color: var(--fb-text-secondary-color);
2789
- `;
2790
- hint.textContent = makeFieldHint(element);
2791
- wrapper.appendChild(hint);
3044
+ if (!state.config.readonly) {
3045
+ const hint = document.createElement("p");
3046
+ hint.className = "mt-1";
3047
+ hint.style.cssText = `
3048
+ font-size: var(--fb-font-size-small);
3049
+ color: var(--fb-text-secondary-color);
3050
+ `;
3051
+ hint.textContent = makeFieldHint(element, state);
3052
+ wrapper.appendChild(hint);
3053
+ }
2792
3054
  }
2793
3055
  function validateColourElement(element, key, context) {
2794
3056
  var _a, _b, _c;
@@ -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;
@@ -3095,14 +3367,16 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
3095
3367
  state.config.readonly
3096
3368
  );
3097
3369
  wrapper.appendChild(sliderUI);
3098
- const hint = document.createElement("p");
3099
- hint.className = "mt-1";
3100
- hint.style.cssText = `
3101
- font-size: var(--fb-font-size-small);
3102
- color: var(--fb-text-secondary-color);
3103
- `;
3104
- hint.textContent = makeFieldHint(element);
3105
- wrapper.appendChild(hint);
3370
+ if (!state.config.readonly) {
3371
+ const hint = document.createElement("p");
3372
+ hint.className = "mt-1";
3373
+ hint.style.cssText = `
3374
+ font-size: var(--fb-font-size-small);
3375
+ color: var(--fb-text-secondary-color);
3376
+ `;
3377
+ hint.textContent = makeFieldHint(element, state);
3378
+ wrapper.appendChild(hint);
3379
+ }
3106
3380
  }
3107
3381
  function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3108
3382
  var _a, _b;
@@ -3204,47 +3478,64 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3204
3478
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
3205
3479
  });
3206
3480
  }
3481
+ let addRow = null;
3482
+ let countDisplay = null;
3483
+ if (!state.config.readonly) {
3484
+ addRow = document.createElement("div");
3485
+ addRow.className = "flex items-center gap-3 mt-2";
3486
+ const addBtn = document.createElement("button");
3487
+ addBtn.type = "button";
3488
+ addBtn.className = "add-slider-btn px-3 py-1 rounded";
3489
+ addBtn.style.cssText = `
3490
+ color: var(--fb-primary-color);
3491
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3492
+ background-color: transparent;
3493
+ font-size: var(--fb-font-size);
3494
+ transition: all var(--fb-transition-duration);
3495
+ `;
3496
+ addBtn.textContent = "+";
3497
+ addBtn.addEventListener("mouseenter", () => {
3498
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3499
+ });
3500
+ addBtn.addEventListener("mouseleave", () => {
3501
+ addBtn.style.backgroundColor = "transparent";
3502
+ });
3503
+ addBtn.onclick = () => {
3504
+ values.push(defaultValue);
3505
+ addSliderItem(defaultValue);
3506
+ updateAddButton();
3507
+ updateRemoveButtons();
3508
+ };
3509
+ countDisplay = document.createElement("span");
3510
+ countDisplay.className = "text-sm text-gray-500";
3511
+ addRow.appendChild(addBtn);
3512
+ addRow.appendChild(countDisplay);
3513
+ wrapper.appendChild(addRow);
3514
+ }
3207
3515
  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);
3516
+ if (!addRow || !countDisplay) return;
3517
+ const addBtn = addRow.querySelector(".add-slider-btn");
3518
+ if (addBtn) {
3519
+ const disabled = values.length >= maxCount;
3520
+ addBtn.disabled = disabled;
3521
+ addBtn.style.opacity = disabled ? "0.5" : "1";
3522
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
3235
3523
  }
3524
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
3236
3525
  }
3237
3526
  values.forEach((value) => addSliderItem(value));
3238
3527
  updateAddButton();
3239
3528
  updateRemoveButtons();
3240
- const hint = document.createElement("p");
3241
- hint.className = "mt-1";
3242
- hint.style.cssText = `
3243
- font-size: var(--fb-font-size-small);
3244
- color: var(--fb-text-secondary-color);
3245
- `;
3246
- hint.textContent = makeFieldHint(element);
3247
- wrapper.appendChild(hint);
3529
+ if (!state.config.readonly) {
3530
+ const hint = document.createElement("p");
3531
+ hint.className = "mt-1";
3532
+ hint.style.cssText = `
3533
+ font-size: var(--fb-font-size-small);
3534
+ color: var(--fb-text-secondary-color);
3535
+ `;
3536
+ hint.textContent = makeFieldHint(element, state);
3537
+ wrapper.appendChild(hint);
3538
+ }
3248
3539
  }
3249
3540
  function validateSliderElement(element, key, context) {
3250
3541
  var _a, _b, _c;
@@ -3302,11 +3593,13 @@ function validateSliderElement(element, key, context) {
3302
3593
  }
3303
3594
  };
3304
3595
  const validateSliderValue = (slider, fieldKey) => {
3596
+ const { state } = context;
3305
3597
  const rawValue = slider.value;
3306
3598
  if (!rawValue) {
3307
3599
  if (!skipValidation && element.required) {
3308
- errors.push(`${fieldKey}: required`);
3309
- markValidity(slider, "required");
3600
+ const msg = t("required", state);
3601
+ errors.push(`${fieldKey}: ${msg}`);
3602
+ markValidity(slider, msg);
3310
3603
  return null;
3311
3604
  }
3312
3605
  markValidity(slider, null);
@@ -3323,13 +3616,15 @@ function validateSliderElement(element, key, context) {
3323
3616
  }
3324
3617
  if (!skipValidation) {
3325
3618
  if (value < min) {
3326
- errors.push(`${fieldKey}: value ${value} < min ${min}`);
3327
- markValidity(slider, `value must be >= ${min}`);
3619
+ const msg = t("minValue", state, { min });
3620
+ errors.push(`${fieldKey}: ${msg}`);
3621
+ markValidity(slider, msg);
3328
3622
  return value;
3329
3623
  }
3330
3624
  if (value > max) {
3331
- errors.push(`${fieldKey}: value ${value} > max ${max}`);
3332
- markValidity(slider, `value must be <= ${max}`);
3625
+ const msg = t("maxValue", state, { max });
3626
+ errors.push(`${fieldKey}: ${msg}`);
3627
+ markValidity(slider, msg);
3333
3628
  return value;
3334
3629
  }
3335
3630
  }
@@ -3346,17 +3641,18 @@ function validateSliderElement(element, key, context) {
3346
3641
  values.push(value);
3347
3642
  });
3348
3643
  if (!skipValidation) {
3644
+ const { state } = context;
3349
3645
  const minCount = (_b = element.minCount) != null ? _b : 1;
3350
3646
  const maxCount = (_c = element.maxCount) != null ? _c : Infinity;
3351
3647
  const filteredValues = values.filter((v) => v !== null);
3352
3648
  if (element.required && filteredValues.length === 0) {
3353
- errors.push(`${key}: required`);
3649
+ errors.push(`${key}: ${t("required", state)}`);
3354
3650
  }
3355
3651
  if (filteredValues.length < minCount) {
3356
- errors.push(`${key}: minimum ${minCount} items required`);
3652
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
3357
3653
  }
3358
3654
  if (filteredValues.length > maxCount) {
3359
- errors.push(`${key}: maximum ${maxCount} items allowed`);
3655
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
3360
3656
  }
3361
3657
  }
3362
3658
  return { value: values, errors };
@@ -3366,7 +3662,7 @@ function validateSliderElement(element, key, context) {
3366
3662
  );
3367
3663
  if (!slider) {
3368
3664
  if (!skipValidation && element.required) {
3369
- errors.push(`${key}: required`);
3665
+ errors.push(`${key}: ${t("required", context.state)}`);
3370
3666
  }
3371
3667
  return { value: null, errors };
3372
3668
  }
@@ -3520,10 +3816,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3520
3816
  const containerWrap = document.createElement("div");
3521
3817
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3522
3818
  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
3819
  const itemsWrap = document.createElement("div");
3528
3820
  const columns = element.columns || 1;
3529
3821
  if (columns === 1) {
@@ -3531,8 +3823,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3531
3823
  } else {
3532
3824
  itemsWrap.className = `grid grid-cols-${columns} gap-4`;
3533
3825
  }
3534
- containerWrap.appendChild(header);
3535
- header.appendChild(left);
3536
3826
  if (!ctx.state.config.readonly) {
3537
3827
  const hintsElement = createPrefillHints(element, pathKey);
3538
3828
  if (hintsElement) {
@@ -3553,7 +3843,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3553
3843
  }
3554
3844
  });
3555
3845
  containerWrap.appendChild(itemsWrap);
3556
- left.innerHTML = `<span>${element.label || element.key}</span>`;
3557
3846
  wrapper.appendChild(containerWrap);
3558
3847
  }
3559
3848
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
@@ -3561,14 +3850,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3561
3850
  const state = ctx.state;
3562
3851
  const containerWrap = document.createElement("div");
3563
3852
  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";
3853
+ const countDisplay = document.createElement("span");
3854
+ countDisplay.className = "text-sm text-gray-500";
3568
3855
  const itemsWrap = document.createElement("div");
3569
3856
  itemsWrap.className = "space-y-4";
3570
- containerWrap.appendChild(header);
3571
- header.appendChild(left);
3572
3857
  if (!ctx.state.config.readonly) {
3573
3858
  const hintsElement = createPrefillHints(element, element.key);
3574
3859
  if (hintsElement) {
@@ -3582,7 +3867,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3582
3867
  const createAddButton = () => {
3583
3868
  const add = document.createElement("button");
3584
3869
  add.type = "button";
3585
- add.className = "add-container-btn mt-2 px-3 py-1 rounded";
3870
+ add.className = "add-container-btn px-3 py-1 rounded";
3586
3871
  add.style.cssText = `
3587
3872
  color: var(--fb-primary-color);
3588
3873
  border: var(--fb-border-width) solid var(--fb-primary-color);
@@ -3663,7 +3948,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3663
3948
  existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3664
3949
  existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
3665
3950
  }
3666
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "\u221E" : max})</span>`;
3951
+ countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
3667
3952
  };
3668
3953
  if (pre && Array.isArray(pre)) {
3669
3954
  pre.forEach((prefillObj, idx) => {
@@ -3771,7 +4056,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3771
4056
  }
3772
4057
  containerWrap.appendChild(itemsWrap);
3773
4058
  if (!state.config.readonly) {
3774
- containerWrap.appendChild(createAddButton());
4059
+ const addRow = document.createElement("div");
4060
+ addRow.className = "flex items-center gap-3 mt-2";
4061
+ addRow.appendChild(createAddButton());
4062
+ addRow.appendChild(countDisplay);
4063
+ containerWrap.appendChild(addRow);
3775
4064
  }
3776
4065
  updateAddButton();
3777
4066
  wrapper.appendChild(containerWrap);
@@ -3797,16 +4086,17 @@ function validateContainerElement(element, key, context) {
3797
4086
  const validateContainerCount = (key2, items, element2) => {
3798
4087
  var _a, _b;
3799
4088
  if (skipValidation) return;
4089
+ const { state } = context;
3800
4090
  const minItems = "minCount" in element2 ? (_a = element2.minCount) != null ? _a : 0 : 0;
3801
4091
  const maxItems = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
3802
4092
  if (element2.required && items.length === 0) {
3803
- errors.push(`${key2}: required`);
4093
+ errors.push(`${key2}: ${t("required", state)}`);
3804
4094
  }
3805
4095
  if (items.length < minItems) {
3806
- errors.push(`${key2}: minimum ${minItems} items required`);
4096
+ errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
3807
4097
  }
3808
4098
  if (items.length > maxItems) {
3809
- errors.push(`${key2}: maximum ${maxItems} items allowed`);
4099
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
3810
4100
  }
3811
4101
  };
3812
4102
  if ("multiple" in element && element.multiple) {
@@ -4360,7 +4650,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
4360
4650
  default: {
4361
4651
  const unsupported = document.createElement("div");
4362
4652
  unsupported.className = "text-red-500 text-sm";
4363
- unsupported.textContent = `Unsupported field type: ${element.type}`;
4653
+ unsupported.textContent = t("unsupportedFieldType", ctx.state, { type: element.type });
4364
4654
  wrapper.appendChild(unsupported);
4365
4655
  }
4366
4656
  }
@@ -4404,31 +4694,112 @@ var defaultConfig = {
4404
4694
  locale: "en",
4405
4695
  translations: {
4406
4696
  en: {
4407
- addElement: "Add Element",
4697
+ // UI texts
4408
4698
  removeElement: "Remove",
4409
- uploadText: "Upload",
4410
- dragDropText: "or drag and drop files",
4411
- dragDropTextSingle: "or drag and drop file",
4412
4699
  clickDragText: "Click or drag file",
4700
+ clickDragTextMultiple: "Click or drag files",
4413
4701
  noFileSelected: "No file selected",
4414
4702
  noFilesSelected: "No files selected",
4415
- downloadButton: "Download"
4703
+ downloadButton: "Download",
4704
+ changeButton: "Change",
4705
+ placeholderText: "Enter text",
4706
+ previewAlt: "Preview",
4707
+ previewUnavailable: "Preview unavailable",
4708
+ previewError: "Preview error",
4709
+ videoNotSupported: "Your browser does not support the video tag.",
4710
+ // Field hints
4711
+ hintLengthRange: "{min}-{max} chars",
4712
+ hintMaxLength: "\u2264{max} chars",
4713
+ hintMinLength: "\u2265{min} chars",
4714
+ hintValueRange: "{min}-{max}",
4715
+ hintMaxValue: "\u2264{max}",
4716
+ hintMinValue: "\u2265{min}",
4717
+ hintMaxSize: "\u2264{size}MB",
4718
+ hintFormats: "{formats}",
4719
+ hintRequired: "Required",
4720
+ hintOptional: "Optional",
4721
+ hintPattern: "Format: {pattern}",
4722
+ fileCountSingle: "{count} file",
4723
+ fileCountPlural: "{count} files",
4724
+ fileCountRange: "({min}-{max})",
4725
+ // Validation errors
4726
+ required: "Required",
4727
+ minItems: "Minimum {min} items required",
4728
+ maxItems: "Maximum {max} items allowed",
4729
+ minLength: "Minimum {min} characters",
4730
+ maxLength: "Maximum {max} characters",
4731
+ minValue: "Must be at least {min}",
4732
+ maxValue: "Must be at most {max}",
4733
+ patternMismatch: "Invalid format",
4734
+ invalidPattern: "Invalid pattern in schema",
4735
+ notANumber: "Must be a number",
4736
+ invalidHexColour: "Invalid hex color",
4737
+ minFiles: "Minimum {min} files required",
4738
+ maxFiles: "Maximum {max} files allowed",
4739
+ unsupportedFieldType: "Unsupported field type: {type}"
4416
4740
  },
4417
4741
  ru: {
4418
- addElement: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442",
4742
+ // UI texts
4419
4743
  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
4744
  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",
4745
+ 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
4746
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
4425
4747
  noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
4426
- downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
4748
+ downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
4749
+ changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
4750
+ placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
4751
+ previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
4752
+ previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
4753
+ previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
4754
+ 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.",
4755
+ // Field hints
4756
+ hintLengthRange: "{min}-{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4757
+ hintMaxLength: "\u2264{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4758
+ hintMinLength: "\u2265{min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4759
+ hintValueRange: "{min}-{max}",
4760
+ hintMaxValue: "\u2264{max}",
4761
+ hintMinValue: "\u2265{min}",
4762
+ hintMaxSize: "\u2264{size}\u041C\u0411",
4763
+ hintFormats: "{formats}",
4764
+ hintRequired: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4765
+ hintOptional: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4766
+ hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
4767
+ fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
4768
+ fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
4769
+ fileCountRange: "({min}-{max})",
4770
+ // Validation errors
4771
+ required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
4772
+ minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4773
+ maxItems: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4774
+ minLength: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4775
+ maxLength: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4776
+ 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}",
4777
+ 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}",
4778
+ patternMismatch: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442",
4779
+ invalidPattern: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u0442\u0442\u0435\u0440\u043D \u0432 \u0441\u0445\u0435\u043C\u0435",
4780
+ notANumber: "\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C",
4781
+ invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
4782
+ minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
4783
+ maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
4784
+ 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
4785
  }
4428
4786
  },
4429
4787
  theme: {}
4430
4788
  };
4431
4789
  function createInstanceState(config) {
4790
+ const mergedTranslations = {
4791
+ ...defaultConfig.translations
4792
+ };
4793
+ if (config == null ? void 0 : config.translations) {
4794
+ for (const [locale, userTranslations] of Object.entries(
4795
+ config.translations
4796
+ )) {
4797
+ mergedTranslations[locale] = {
4798
+ ...defaultConfig.translations[locale] || {},
4799
+ ...userTranslations
4800
+ };
4801
+ }
4802
+ }
4432
4803
  return {
4433
4804
  schema: null,
4434
4805
  formRoot: null,
@@ -4437,7 +4808,8 @@ function createInstanceState(config) {
4437
4808
  version: "1.0.0",
4438
4809
  config: {
4439
4810
  ...defaultConfig,
4440
- ...config
4811
+ ...config,
4812
+ translations: mergedTranslations
4441
4813
  },
4442
4814
  debounceTimer: null
4443
4815
  };