@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.
package/dist/esm/index.js CHANGED
@@ -1,53 +1,75 @@
1
+ // src/utils/translation.ts
2
+ function t(key, state, params) {
3
+ const locale = state.config.locale || "en";
4
+ const localeTranslations = state.config.translations[locale];
5
+ const fallbackTranslations = state.config.translations.en;
6
+ let text = localeTranslations?.[key] || fallbackTranslations?.[key] || key;
7
+ if (params) {
8
+ for (const [paramKey, paramValue] of Object.entries(params)) {
9
+ text = text.replace(new RegExp(`\\{${paramKey}\\}`, "g"), String(paramValue));
10
+ }
11
+ }
12
+ return text;
13
+ }
14
+
1
15
  // src/utils/validation.ts
2
- function addLengthHint(element, parts) {
3
- if (element.minLength !== null || element.maxLength !== null) {
4
- if (element.minLength !== null && element.maxLength !== null) {
5
- parts.push(`length=${element.minLength}-${element.maxLength} characters`);
6
- } else if (element.maxLength !== null) {
7
- parts.push(`max=${element.maxLength} characters`);
8
- } else if (element.minLength !== null) {
9
- parts.push(`min=${element.minLength} characters`);
16
+ function addLengthHint(element, parts, state) {
17
+ if (element.minLength != null || element.maxLength != null) {
18
+ if (element.minLength != null && element.maxLength != null) {
19
+ parts.push(
20
+ t("hintLengthRange", state, {
21
+ min: element.minLength,
22
+ max: element.maxLength
23
+ })
24
+ );
25
+ } else if (element.maxLength != null) {
26
+ parts.push(t("hintMaxLength", state, { max: element.maxLength }));
27
+ } else if (element.minLength != null) {
28
+ parts.push(t("hintMinLength", state, { min: element.minLength }));
10
29
  }
11
30
  }
12
31
  }
13
- function addRangeHint(element, parts) {
14
- if (element.min !== null || element.max !== null) {
15
- if (element.min !== null && element.max !== null) {
16
- parts.push(`range=${element.min}-${element.max}`);
17
- } else if (element.max !== null) {
18
- parts.push(`max=${element.max}`);
19
- } else if (element.min !== null) {
20
- parts.push(`min=${element.min}`);
32
+ function addRangeHint(element, parts, state) {
33
+ if (element.min != null || element.max != null) {
34
+ if (element.min != null && element.max != null) {
35
+ parts.push(
36
+ t("hintValueRange", state, { min: element.min, max: element.max })
37
+ );
38
+ } else if (element.max != null) {
39
+ parts.push(t("hintMaxValue", state, { max: element.max }));
40
+ } else if (element.min != null) {
41
+ parts.push(t("hintMinValue", state, { min: element.min }));
21
42
  }
22
43
  }
23
44
  }
24
- function addFileSizeHint(element, parts) {
45
+ function addFileSizeHint(element, parts, state) {
25
46
  if (element.maxSizeMB) {
26
- parts.push(`max_size=${element.maxSizeMB}MB`);
47
+ parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
27
48
  }
28
49
  }
29
- function addFormatHint(element, parts) {
50
+ function addFormatHint(element, parts, state) {
30
51
  if (element.accept?.extensions) {
31
52
  parts.push(
32
- `formats=${element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")}`
53
+ t("hintFormats", state, {
54
+ formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
55
+ })
33
56
  );
34
57
  }
35
58
  }
36
- function addPatternHint(element, parts) {
37
- if (element.pattern && !element.pattern.includes("\u0410-\u042F")) {
38
- parts.push("plain text only");
39
- } else if (element.pattern?.includes("\u0410-\u042F")) {
40
- parts.push("text with punctuation");
59
+ function addPatternHint(element, parts, state) {
60
+ if (element.pattern) {
61
+ parts.push(t("hintPattern", state, { pattern: element.pattern }));
41
62
  }
42
63
  }
43
- function makeFieldHint(element) {
64
+ function makeFieldHint(element, state) {
44
65
  const parts = [];
45
- parts.push(element.required ? "required" : "optional");
46
- addLengthHint(element, parts);
47
- addRangeHint(element, parts);
48
- addFileSizeHint(element, parts);
49
- addFormatHint(element, parts);
50
- addPatternHint(element, parts);
66
+ addLengthHint(element, parts, state);
67
+ if (element.type !== "slider") {
68
+ addRangeHint(element, parts, state);
69
+ }
70
+ addFileSizeHint(element, parts, state);
71
+ addFormatHint(element, parts, state);
72
+ addPatternHint(element, parts, state);
51
73
  return parts.join(" \u2022 ");
52
74
  }
53
75
  function validateSchema(schema) {
@@ -185,6 +207,11 @@ function validateSchema(schema) {
185
207
  function isPlainObject(obj) {
186
208
  return obj && typeof obj === "object" && obj.constructor === Object;
187
209
  }
210
+ function escapeHtml(text) {
211
+ const div = document.createElement("div");
212
+ div.textContent = text;
213
+ return div.innerHTML;
214
+ }
188
215
  function pathJoin(base, key) {
189
216
  return base ? `${base}.${key}` : key;
190
217
  }
@@ -262,13 +289,62 @@ function deepEqual(a, b) {
262
289
  }
263
290
 
264
291
  // src/components/text.ts
292
+ function createCharCounter(element, input, isTextarea = false) {
293
+ const counter = document.createElement("span");
294
+ counter.className = "char-counter";
295
+ counter.style.cssText = `
296
+ position: absolute;
297
+ ${isTextarea ? "bottom: 8px" : "top: 50%; transform: translateY(-50%)"};
298
+ right: 10px;
299
+ font-size: var(--fb-font-size-small);
300
+ color: var(--fb-text-secondary-color);
301
+ pointer-events: none;
302
+ background: var(--fb-background-color);
303
+ padding: 0 4px;
304
+ `;
305
+ const updateCounter = () => {
306
+ const len = input.value.length;
307
+ const min = element.minLength;
308
+ const max = element.maxLength;
309
+ if (min == null && max == null) {
310
+ counter.textContent = "";
311
+ return;
312
+ }
313
+ if (len === 0 || min != null && len < min) {
314
+ if (min != null && max != null) {
315
+ counter.textContent = `${min}-${max}`;
316
+ } else if (max != null) {
317
+ counter.textContent = `\u2264${max}`;
318
+ } else if (min != null) {
319
+ counter.textContent = `\u2265${min}`;
320
+ }
321
+ counter.style.color = "var(--fb-text-secondary-color)";
322
+ } else if (max != null && len > max) {
323
+ counter.textContent = `${len}/${max}`;
324
+ counter.style.color = "var(--fb-error-color)";
325
+ } else {
326
+ if (max != null) {
327
+ counter.textContent = `${len}/${max}`;
328
+ } else {
329
+ counter.textContent = `${len}`;
330
+ }
331
+ counter.style.color = "var(--fb-text-secondary-color)";
332
+ }
333
+ };
334
+ input.addEventListener("input", updateCounter);
335
+ updateCounter();
336
+ return counter;
337
+ }
265
338
  function renderTextElement(element, ctx, wrapper, pathKey) {
266
339
  const state = ctx.state;
340
+ const inputWrapper = document.createElement("div");
341
+ inputWrapper.style.cssText = "position: relative;";
267
342
  const textInput = document.createElement("input");
268
343
  textInput.type = "text";
269
344
  textInput.className = "w-full rounded-lg";
270
345
  textInput.style.cssText = `
271
346
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
347
+ padding-right: 60px;
272
348
  border: var(--fb-border-width) solid var(--fb-border-color);
273
349
  border-radius: var(--fb-border-radius);
274
350
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -276,6 +352,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
276
352
  font-size: var(--fb-font-size);
277
353
  font-family: var(--fb-font-family);
278
354
  transition: all var(--fb-transition-duration) ease-in-out;
355
+ width: 100%;
356
+ box-sizing: border-box;
279
357
  `;
280
358
  textInput.name = pathKey;
281
359
  textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
@@ -310,15 +388,12 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
310
388
  textInput.addEventListener("blur", handleChange);
311
389
  textInput.addEventListener("input", handleChange);
312
390
  }
313
- wrapper.appendChild(textInput);
314
- const textHint = document.createElement("p");
315
- textHint.className = "mt-1";
316
- textHint.style.cssText = `
317
- font-size: var(--fb-font-size-small);
318
- color: var(--fb-text-secondary-color);
319
- `;
320
- textHint.textContent = makeFieldHint(element);
321
- wrapper.appendChild(textHint);
391
+ inputWrapper.appendChild(textInput);
392
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
393
+ const counter = createCharCounter(element, textInput, false);
394
+ inputWrapper.appendChild(counter);
395
+ }
396
+ wrapper.appendChild(inputWrapper);
322
397
  }
323
398
  function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
324
399
  const state = ctx.state;
@@ -344,11 +419,13 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
344
419
  function addTextItem(value = "", index = -1) {
345
420
  const itemWrapper = document.createElement("div");
346
421
  itemWrapper.className = "multiple-text-item flex items-center gap-2";
422
+ const inputContainer = document.createElement("div");
423
+ inputContainer.style.cssText = "position: relative; flex: 1;";
347
424
  const textInput = document.createElement("input");
348
425
  textInput.type = "text";
349
- textInput.className = "flex-1";
350
426
  textInput.style.cssText = `
351
427
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
428
+ padding-right: 60px;
352
429
  border: var(--fb-border-width) solid var(--fb-border-color);
353
430
  border-radius: var(--fb-border-radius);
354
431
  background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
@@ -356,8 +433,10 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
356
433
  font-size: var(--fb-font-size);
357
434
  font-family: var(--fb-font-family);
358
435
  transition: all var(--fb-transition-duration) ease-in-out;
436
+ width: 100%;
437
+ box-sizing: border-box;
359
438
  `;
360
- textInput.placeholder = element.placeholder || "Enter text";
439
+ textInput.placeholder = element.placeholder || t("placeholderText", state);
361
440
  textInput.value = value;
362
441
  textInput.readOnly = state.config.readonly;
363
442
  if (!state.config.readonly) {
@@ -389,7 +468,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
389
468
  textInput.addEventListener("blur", handleChange);
390
469
  textInput.addEventListener("input", handleChange);
391
470
  }
392
- itemWrapper.appendChild(textInput);
471
+ inputContainer.appendChild(textInput);
472
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
473
+ const counter = createCharCounter(element, textInput, false);
474
+ inputContainer.appendChild(counter);
475
+ }
476
+ itemWrapper.appendChild(inputContainer);
393
477
  if (index === -1) {
394
478
  container.appendChild(itemWrapper);
395
479
  } else {
@@ -442,47 +526,54 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
442
526
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
443
527
  });
444
528
  }
529
+ let addRow = null;
530
+ let countDisplay = null;
531
+ if (!state.config.readonly) {
532
+ addRow = document.createElement("div");
533
+ addRow.className = "flex items-center gap-3 mt-2";
534
+ const addBtn = document.createElement("button");
535
+ addBtn.type = "button";
536
+ addBtn.className = "add-text-btn px-3 py-1 rounded";
537
+ addBtn.style.cssText = `
538
+ color: var(--fb-primary-color);
539
+ border: var(--fb-border-width) solid var(--fb-primary-color);
540
+ background-color: transparent;
541
+ font-size: var(--fb-font-size);
542
+ transition: all var(--fb-transition-duration);
543
+ `;
544
+ addBtn.textContent = "+";
545
+ addBtn.addEventListener("mouseenter", () => {
546
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
547
+ });
548
+ addBtn.addEventListener("mouseleave", () => {
549
+ addBtn.style.backgroundColor = "transparent";
550
+ });
551
+ addBtn.onclick = () => {
552
+ values.push(element.default || "");
553
+ addTextItem(element.default || "");
554
+ updateAddButton();
555
+ updateRemoveButtons();
556
+ };
557
+ countDisplay = document.createElement("span");
558
+ countDisplay.className = "text-sm text-gray-500";
559
+ addRow.appendChild(addBtn);
560
+ addRow.appendChild(countDisplay);
561
+ wrapper.appendChild(addRow);
562
+ }
445
563
  function updateAddButton() {
446
- const existingAddBtn = wrapper.querySelector(".add-text-btn");
447
- if (existingAddBtn) existingAddBtn.remove();
448
- if (!state.config.readonly && values.length < maxCount) {
449
- const addBtn = document.createElement("button");
450
- addBtn.type = "button";
451
- addBtn.className = "add-text-btn mt-2 px-3 py-1 rounded";
452
- addBtn.style.cssText = `
453
- color: var(--fb-primary-color);
454
- border: var(--fb-border-width) solid var(--fb-primary-color);
455
- background-color: transparent;
456
- font-size: var(--fb-font-size);
457
- transition: all var(--fb-transition-duration);
458
- `;
459
- addBtn.textContent = "+";
460
- addBtn.addEventListener("mouseenter", () => {
461
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
462
- });
463
- addBtn.addEventListener("mouseleave", () => {
464
- addBtn.style.backgroundColor = "transparent";
465
- });
466
- addBtn.onclick = () => {
467
- values.push(element.default || "");
468
- addTextItem(element.default || "");
469
- updateAddButton();
470
- updateRemoveButtons();
471
- };
472
- wrapper.appendChild(addBtn);
564
+ if (!addRow || !countDisplay) return;
565
+ const addBtn = addRow.querySelector(".add-text-btn");
566
+ if (addBtn) {
567
+ const disabled = values.length >= maxCount;
568
+ addBtn.disabled = disabled;
569
+ addBtn.style.opacity = disabled ? "0.5" : "1";
570
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
473
571
  }
572
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
474
573
  }
475
574
  values.forEach((value) => addTextItem(value));
476
575
  updateAddButton();
477
576
  updateRemoveButtons();
478
- const hint = document.createElement("p");
479
- hint.className = "mt-1";
480
- hint.style.cssText = `
481
- font-size: var(--fb-font-size-small);
482
- color: var(--fb-text-secondary-color);
483
- `;
484
- hint.textContent = makeFieldHint(element);
485
- wrapper.appendChild(hint);
486
577
  }
487
578
  function validateTextElement(element, key, context) {
488
579
  const errors = [];
@@ -521,26 +612,31 @@ function validateTextElement(element, key, context) {
521
612
  };
522
613
  const validateTextInput = (input, val, fieldKey) => {
523
614
  let hasError = false;
615
+ const { state } = context;
524
616
  if (!skipValidation && val) {
525
617
  if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
526
- errors.push(`${fieldKey}: minLength=${element.minLength}`);
527
- markValidity(input, `minLength=${element.minLength}`);
618
+ const msg = t("minLength", state, { min: element.minLength });
619
+ errors.push(`${fieldKey}: ${msg}`);
620
+ markValidity(input, msg);
528
621
  hasError = true;
529
622
  } else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
530
- errors.push(`${fieldKey}: maxLength=${element.maxLength}`);
531
- markValidity(input, `maxLength=${element.maxLength}`);
623
+ const msg = t("maxLength", state, { max: element.maxLength });
624
+ errors.push(`${fieldKey}: ${msg}`);
625
+ markValidity(input, msg);
532
626
  hasError = true;
533
627
  } else if (element.pattern) {
534
628
  try {
535
629
  const re = new RegExp(element.pattern);
536
630
  if (!re.test(val)) {
537
- errors.push(`${fieldKey}: pattern mismatch`);
538
- markValidity(input, "pattern mismatch");
631
+ const msg = t("patternMismatch", state);
632
+ errors.push(`${fieldKey}: ${msg}`);
633
+ markValidity(input, msg);
539
634
  hasError = true;
540
635
  }
541
636
  } catch {
542
- errors.push(`${fieldKey}: invalid pattern`);
543
- markValidity(input, "invalid pattern");
637
+ const msg = t("invalidPattern", state);
638
+ errors.push(`${fieldKey}: ${msg}`);
639
+ markValidity(input, msg);
544
640
  hasError = true;
545
641
  }
546
642
  }
@@ -560,17 +656,18 @@ function validateTextElement(element, key, context) {
560
656
  validateTextInput(input, val, `${key}[${index}]`);
561
657
  });
562
658
  if (!skipValidation) {
659
+ const { state } = context;
563
660
  const minCount = element.minCount ?? 1;
564
661
  const maxCount = element.maxCount ?? Infinity;
565
662
  const filteredValues = rawValues.filter((v) => v.trim() !== "");
566
663
  if (element.required && filteredValues.length === 0) {
567
- errors.push(`${key}: required`);
664
+ errors.push(`${key}: ${t("required", state)}`);
568
665
  }
569
666
  if (filteredValues.length < minCount) {
570
- errors.push(`${key}: minimum ${minCount} items required`);
667
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
571
668
  }
572
669
  if (filteredValues.length > maxCount) {
573
- errors.push(`${key}: maximum ${maxCount} items allowed`);
670
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
574
671
  }
575
672
  }
576
673
  return { value: values, errors };
@@ -578,8 +675,9 @@ function validateTextElement(element, key, context) {
578
675
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
579
676
  const val = input?.value ?? "";
580
677
  if (!skipValidation && element.required && val === "") {
581
- errors.push(`${key}: required`);
582
- markValidity(input, "required");
678
+ const msg = t("required", context.state);
679
+ errors.push(`${key}: ${msg}`);
680
+ markValidity(input, msg);
583
681
  return { value: null, errors };
584
682
  }
585
683
  if (input) {
@@ -623,8 +721,11 @@ function updateTextField(element, fieldPath, value, context) {
623
721
  // src/components/textarea.ts
624
722
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
625
723
  const state = ctx.state;
724
+ const textareaWrapper = document.createElement("div");
725
+ textareaWrapper.style.cssText = "position: relative;";
626
726
  const textareaInput = document.createElement("textarea");
627
727
  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";
728
+ textareaInput.style.cssText = "padding-bottom: 24px;";
628
729
  textareaInput.name = pathKey;
629
730
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
630
731
  textareaInput.rows = element.rows || 4;
@@ -638,11 +739,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
638
739
  textareaInput.addEventListener("blur", handleChange);
639
740
  textareaInput.addEventListener("input", handleChange);
640
741
  }
641
- wrapper.appendChild(textareaInput);
642
- const textareaHint = document.createElement("p");
643
- textareaHint.className = "text-xs text-gray-500 mt-1";
644
- textareaHint.textContent = makeFieldHint(element);
645
- wrapper.appendChild(textareaHint);
742
+ textareaWrapper.appendChild(textareaInput);
743
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
744
+ const counter = createCharCounter(element, textareaInput, true);
745
+ textareaWrapper.appendChild(counter);
746
+ }
747
+ wrapper.appendChild(textareaWrapper);
646
748
  }
647
749
  function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
648
750
  const state = ctx.state;
@@ -668,9 +770,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
668
770
  function addTextareaItem(value = "", index = -1) {
669
771
  const itemWrapper = document.createElement("div");
670
772
  itemWrapper.className = "multiple-textarea-item";
773
+ const textareaContainer = document.createElement("div");
774
+ textareaContainer.style.cssText = "position: relative;";
671
775
  const textareaInput = document.createElement("textarea");
672
776
  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";
673
- textareaInput.placeholder = element.placeholder || "Enter text";
777
+ textareaInput.style.cssText = "padding-bottom: 24px;";
778
+ textareaInput.placeholder = element.placeholder || t("placeholderText", state);
674
779
  textareaInput.rows = element.rows || 4;
675
780
  textareaInput.value = value;
676
781
  textareaInput.readOnly = state.config.readonly;
@@ -682,7 +787,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
682
787
  textareaInput.addEventListener("blur", handleChange);
683
788
  textareaInput.addEventListener("input", handleChange);
684
789
  }
685
- itemWrapper.appendChild(textareaInput);
790
+ textareaContainer.appendChild(textareaInput);
791
+ if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
792
+ const counter = createCharCounter(element, textareaInput, true);
793
+ textareaContainer.appendChild(counter);
794
+ }
795
+ itemWrapper.appendChild(textareaContainer);
686
796
  if (index === -1) {
687
797
  container.appendChild(itemWrapper);
688
798
  } else {
@@ -724,30 +834,54 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
724
834
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
725
835
  });
726
836
  }
837
+ let addRow = null;
838
+ let countDisplay = null;
839
+ if (!state.config.readonly) {
840
+ addRow = document.createElement("div");
841
+ addRow.className = "flex items-center gap-3 mt-2";
842
+ const addBtn = document.createElement("button");
843
+ addBtn.type = "button";
844
+ addBtn.className = "add-textarea-btn px-3 py-1 rounded";
845
+ addBtn.style.cssText = `
846
+ color: var(--fb-primary-color);
847
+ border: var(--fb-border-width) solid var(--fb-primary-color);
848
+ background-color: transparent;
849
+ font-size: var(--fb-font-size);
850
+ transition: all var(--fb-transition-duration);
851
+ `;
852
+ addBtn.textContent = "+";
853
+ addBtn.addEventListener("mouseenter", () => {
854
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
855
+ });
856
+ addBtn.addEventListener("mouseleave", () => {
857
+ addBtn.style.backgroundColor = "transparent";
858
+ });
859
+ addBtn.onclick = () => {
860
+ values.push(element.default || "");
861
+ addTextareaItem(element.default || "");
862
+ updateAddButton();
863
+ updateRemoveButtons();
864
+ };
865
+ countDisplay = document.createElement("span");
866
+ countDisplay.className = "text-sm text-gray-500";
867
+ addRow.appendChild(addBtn);
868
+ addRow.appendChild(countDisplay);
869
+ wrapper.appendChild(addRow);
870
+ }
727
871
  function updateAddButton() {
728
- const existingAddBtn = wrapper.querySelector(".add-textarea-btn");
729
- if (existingAddBtn) existingAddBtn.remove();
730
- if (!state.config.readonly && values.length < maxCount) {
731
- const addBtn = document.createElement("button");
732
- addBtn.type = "button";
733
- 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";
734
- addBtn.textContent = "+";
735
- addBtn.onclick = () => {
736
- values.push(element.default || "");
737
- addTextareaItem(element.default || "");
738
- updateAddButton();
739
- updateRemoveButtons();
740
- };
741
- wrapper.appendChild(addBtn);
872
+ if (!addRow || !countDisplay) return;
873
+ const addBtn = addRow.querySelector(".add-textarea-btn");
874
+ if (addBtn) {
875
+ const disabled = values.length >= maxCount;
876
+ addBtn.disabled = disabled;
877
+ addBtn.style.opacity = disabled ? "0.5" : "1";
878
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
742
879
  }
880
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
743
881
  }
744
882
  values.forEach((value) => addTextareaItem(value));
745
883
  updateAddButton();
746
884
  updateRemoveButtons();
747
- const hint = document.createElement("p");
748
- hint.className = "text-xs text-gray-500 mt-1";
749
- hint.textContent = makeFieldHint(element);
750
- wrapper.appendChild(hint);
751
885
  }
752
886
  function validateTextareaElement(element, key, context) {
753
887
  return validateTextElement(element, key, context);
@@ -757,11 +891,61 @@ function updateTextareaField(element, fieldPath, value, context) {
757
891
  }
758
892
 
759
893
  // src/components/number.ts
894
+ function createNumberCounter(element, input) {
895
+ const counter = document.createElement("span");
896
+ counter.className = "number-counter";
897
+ counter.style.cssText = `
898
+ position: absolute;
899
+ top: 50%;
900
+ transform: translateY(-50%);
901
+ right: 10px;
902
+ font-size: var(--fb-font-size-small);
903
+ color: var(--fb-text-secondary-color);
904
+ pointer-events: none;
905
+ background: var(--fb-background-color);
906
+ padding: 0 4px;
907
+ `;
908
+ const updateCounter = () => {
909
+ const val = input.value ? parseFloat(input.value) : null;
910
+ const min = element.min;
911
+ const max = element.max;
912
+ if (min == null && max == null) {
913
+ counter.textContent = "";
914
+ return;
915
+ }
916
+ if (val == null || min != null && val < min) {
917
+ if (min != null && max != null) {
918
+ counter.textContent = `${min}-${max}`;
919
+ } else if (max != null) {
920
+ counter.textContent = `\u2264${max}`;
921
+ } else if (min != null) {
922
+ counter.textContent = `\u2265${min}`;
923
+ }
924
+ counter.style.color = "var(--fb-text-secondary-color)";
925
+ } else if (max != null && val > max) {
926
+ counter.textContent = `${val}/${max}`;
927
+ counter.style.color = "var(--fb-error-color)";
928
+ } else {
929
+ if (max != null) {
930
+ counter.textContent = `${val}/${max}`;
931
+ } else {
932
+ counter.textContent = `${val}`;
933
+ }
934
+ counter.style.color = "var(--fb-text-secondary-color)";
935
+ }
936
+ };
937
+ input.addEventListener("input", updateCounter);
938
+ updateCounter();
939
+ return counter;
940
+ }
760
941
  function renderNumberElement(element, ctx, wrapper, pathKey) {
761
942
  const state = ctx.state;
943
+ const inputWrapper = document.createElement("div");
944
+ inputWrapper.style.cssText = "position: relative;";
762
945
  const numberInput = document.createElement("input");
763
946
  numberInput.type = "number";
764
947
  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";
948
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
765
949
  numberInput.name = pathKey;
766
950
  numberInput.placeholder = element.placeholder || "0";
767
951
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -777,11 +961,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
777
961
  numberInput.addEventListener("blur", handleChange);
778
962
  numberInput.addEventListener("input", handleChange);
779
963
  }
780
- wrapper.appendChild(numberInput);
781
- const numberHint = document.createElement("p");
782
- numberHint.className = "text-xs text-gray-500 mt-1";
783
- numberHint.textContent = makeFieldHint(element);
784
- wrapper.appendChild(numberHint);
964
+ inputWrapper.appendChild(numberInput);
965
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
966
+ const counter = createNumberCounter(element, numberInput);
967
+ inputWrapper.appendChild(counter);
968
+ }
969
+ wrapper.appendChild(inputWrapper);
785
970
  }
786
971
  function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
787
972
  const state = ctx.state;
@@ -807,9 +992,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
807
992
  function addNumberItem(value = "", index = -1) {
808
993
  const itemWrapper = document.createElement("div");
809
994
  itemWrapper.className = "multiple-number-item flex items-center gap-2";
995
+ const inputContainer = document.createElement("div");
996
+ inputContainer.style.cssText = "position: relative; flex: 1;";
810
997
  const numberInput = document.createElement("input");
811
998
  numberInput.type = "number";
812
- 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";
999
+ 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";
1000
+ numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
813
1001
  numberInput.placeholder = element.placeholder || "0";
814
1002
  if (element.min !== void 0) numberInput.min = element.min.toString();
815
1003
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -824,7 +1012,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
824
1012
  numberInput.addEventListener("blur", handleChange);
825
1013
  numberInput.addEventListener("input", handleChange);
826
1014
  }
827
- itemWrapper.appendChild(numberInput);
1015
+ inputContainer.appendChild(numberInput);
1016
+ if (!state.config.readonly && (element.min != null || element.max != null)) {
1017
+ const counter = createNumberCounter(element, numberInput);
1018
+ inputContainer.appendChild(counter);
1019
+ }
1020
+ itemWrapper.appendChild(inputContainer);
828
1021
  if (index === -1) {
829
1022
  container.appendChild(itemWrapper);
830
1023
  } else {
@@ -866,30 +1059,54 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
866
1059
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
867
1060
  });
868
1061
  }
1062
+ let addRow = null;
1063
+ let countDisplay = null;
1064
+ if (!state.config.readonly) {
1065
+ addRow = document.createElement("div");
1066
+ addRow.className = "flex items-center gap-3 mt-2";
1067
+ const addBtn = document.createElement("button");
1068
+ addBtn.type = "button";
1069
+ addBtn.className = "add-number-btn px-3 py-1 rounded";
1070
+ addBtn.style.cssText = `
1071
+ color: var(--fb-primary-color);
1072
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1073
+ background-color: transparent;
1074
+ font-size: var(--fb-font-size);
1075
+ transition: all var(--fb-transition-duration);
1076
+ `;
1077
+ addBtn.textContent = "+";
1078
+ addBtn.addEventListener("mouseenter", () => {
1079
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1080
+ });
1081
+ addBtn.addEventListener("mouseleave", () => {
1082
+ addBtn.style.backgroundColor = "transparent";
1083
+ });
1084
+ addBtn.onclick = () => {
1085
+ values.push(element.default || "");
1086
+ addNumberItem(element.default || "");
1087
+ updateAddButton();
1088
+ updateRemoveButtons();
1089
+ };
1090
+ countDisplay = document.createElement("span");
1091
+ countDisplay.className = "text-sm text-gray-500";
1092
+ addRow.appendChild(addBtn);
1093
+ addRow.appendChild(countDisplay);
1094
+ wrapper.appendChild(addRow);
1095
+ }
869
1096
  function updateAddButton() {
870
- const existingAddBtn = wrapper.querySelector(".add-number-btn");
871
- if (existingAddBtn) existingAddBtn.remove();
872
- if (!state.config.readonly && values.length < maxCount) {
873
- const addBtn = document.createElement("button");
874
- addBtn.type = "button";
875
- 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";
876
- addBtn.textContent = "+";
877
- addBtn.onclick = () => {
878
- values.push(element.default || "");
879
- addNumberItem(element.default || "");
880
- updateAddButton();
881
- updateRemoveButtons();
882
- };
883
- wrapper.appendChild(addBtn);
1097
+ if (!addRow || !countDisplay) return;
1098
+ const addBtn = addRow.querySelector(".add-number-btn");
1099
+ if (addBtn) {
1100
+ const disabled = values.length >= maxCount;
1101
+ addBtn.disabled = disabled;
1102
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1103
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
884
1104
  }
1105
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
885
1106
  }
886
1107
  values.forEach((value) => addNumberItem(value));
887
1108
  updateAddButton();
888
1109
  updateRemoveButtons();
889
- const hint = document.createElement("p");
890
- hint.className = "text-xs text-gray-500 mt-1";
891
- hint.textContent = makeFieldHint(element);
892
- wrapper.appendChild(hint);
893
1110
  }
894
1111
  function validateNumberElement(element, key, context) {
895
1112
  const errors = [];
@@ -928,13 +1145,16 @@ function validateNumberElement(element, key, context) {
928
1145
  };
929
1146
  const validateNumberInput = (input, v, fieldKey) => {
930
1147
  let hasError = false;
1148
+ const { state } = context;
931
1149
  if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
932
- errors.push(`${fieldKey}: < min=${element.min}`);
933
- markValidity(input, `< min=${element.min}`);
1150
+ const msg = t("minValue", state, { min: element.min });
1151
+ errors.push(`${fieldKey}: ${msg}`);
1152
+ markValidity(input, msg);
934
1153
  hasError = true;
935
1154
  } else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
936
- errors.push(`${fieldKey}: > max=${element.max}`);
937
- markValidity(input, `> max=${element.max}`);
1155
+ const msg = t("maxValue", state, { max: element.max });
1156
+ errors.push(`${fieldKey}: ${msg}`);
1157
+ markValidity(input, msg);
938
1158
  hasError = true;
939
1159
  }
940
1160
  if (!hasError) {
@@ -955,8 +1175,9 @@ function validateNumberElement(element, key, context) {
955
1175
  }
956
1176
  const v = parseFloat(raw);
957
1177
  if (!skipValidation && !Number.isFinite(v)) {
958
- errors.push(`${key}[${index}]: not a number`);
959
- markValidity(input, "not a number");
1178
+ const msg = t("notANumber", context.state);
1179
+ errors.push(`${key}[${index}]: ${msg}`);
1180
+ markValidity(input, msg);
960
1181
  values.push(null);
961
1182
  return;
962
1183
  }
@@ -965,26 +1186,29 @@ function validateNumberElement(element, key, context) {
965
1186
  values.push(Number(v.toFixed(d)));
966
1187
  });
967
1188
  if (!skipValidation) {
1189
+ const { state } = context;
968
1190
  const minCount = element.minCount ?? 1;
969
1191
  const maxCount = element.maxCount ?? Infinity;
970
1192
  const filteredValues = values.filter((v) => v !== null);
971
1193
  if (element.required && filteredValues.length === 0) {
972
- errors.push(`${key}: required`);
1194
+ errors.push(`${key}: ${t("required", state)}`);
973
1195
  }
974
1196
  if (filteredValues.length < minCount) {
975
- errors.push(`${key}: minimum ${minCount} items required`);
1197
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
976
1198
  }
977
1199
  if (filteredValues.length > maxCount) {
978
- errors.push(`${key}: maximum ${maxCount} items allowed`);
1200
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
979
1201
  }
980
1202
  }
981
1203
  return { value: values, errors };
982
1204
  } else {
983
1205
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
984
1206
  const raw = input?.value ?? "";
1207
+ const { state } = context;
985
1208
  if (!skipValidation && element.required && raw === "") {
986
- errors.push(`${key}: required`);
987
- markValidity(input, "required");
1209
+ const msg = t("required", state);
1210
+ errors.push(`${key}: ${msg}`);
1211
+ markValidity(input, msg);
988
1212
  return { value: null, errors };
989
1213
  }
990
1214
  if (raw === "") {
@@ -993,8 +1217,9 @@ function validateNumberElement(element, key, context) {
993
1217
  }
994
1218
  const v = parseFloat(raw);
995
1219
  if (!skipValidation && !Number.isFinite(v)) {
996
- errors.push(`${key}: not a number`);
997
- markValidity(input, "not a number");
1220
+ const msg = t("notANumber", state);
1221
+ errors.push(`${key}: ${msg}`);
1222
+ markValidity(input, msg);
998
1223
  return { value: null, errors };
999
1224
  }
1000
1225
  validateNumberInput(input, v, key);
@@ -1061,10 +1286,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1061
1286
  selectInput.addEventListener("change", handleChange);
1062
1287
  }
1063
1288
  wrapper.appendChild(selectInput);
1064
- const selectHint = document.createElement("p");
1065
- selectHint.className = "text-xs text-gray-500 mt-1";
1066
- selectHint.textContent = makeFieldHint(element);
1067
- wrapper.appendChild(selectHint);
1289
+ if (!state.config.readonly) {
1290
+ const selectHint = document.createElement("p");
1291
+ selectHint.className = "text-xs text-gray-500 mt-1";
1292
+ selectHint.textContent = makeFieldHint(element, state);
1293
+ wrapper.appendChild(selectHint);
1294
+ }
1068
1295
  }
1069
1296
  function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1070
1297
  const state = ctx.state;
@@ -1148,31 +1375,61 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1148
1375
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1149
1376
  });
1150
1377
  }
1378
+ let addRow = null;
1379
+ let countDisplay = null;
1380
+ if (!state.config.readonly) {
1381
+ addRow = document.createElement("div");
1382
+ addRow.className = "flex items-center gap-3 mt-2";
1383
+ const addBtn = document.createElement("button");
1384
+ addBtn.type = "button";
1385
+ addBtn.className = "add-select-btn px-3 py-1 rounded";
1386
+ addBtn.style.cssText = `
1387
+ color: var(--fb-primary-color);
1388
+ border: var(--fb-border-width) solid var(--fb-primary-color);
1389
+ background-color: transparent;
1390
+ font-size: var(--fb-font-size);
1391
+ transition: all var(--fb-transition-duration);
1392
+ `;
1393
+ addBtn.textContent = "+";
1394
+ addBtn.addEventListener("mouseenter", () => {
1395
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
1396
+ });
1397
+ addBtn.addEventListener("mouseleave", () => {
1398
+ addBtn.style.backgroundColor = "transparent";
1399
+ });
1400
+ addBtn.onclick = () => {
1401
+ const defaultValue = element.default || element.options?.[0]?.value || "";
1402
+ values.push(defaultValue);
1403
+ addSelectItem(defaultValue);
1404
+ updateAddButton();
1405
+ updateRemoveButtons();
1406
+ };
1407
+ countDisplay = document.createElement("span");
1408
+ countDisplay.className = "text-sm text-gray-500";
1409
+ addRow.appendChild(addBtn);
1410
+ addRow.appendChild(countDisplay);
1411
+ wrapper.appendChild(addRow);
1412
+ }
1151
1413
  function updateAddButton() {
1152
- const existingAddBtn = wrapper.querySelector(".add-select-btn");
1153
- if (existingAddBtn) existingAddBtn.remove();
1154
- if (!state.config.readonly && values.length < maxCount) {
1155
- const addBtn = document.createElement("button");
1156
- addBtn.type = "button";
1157
- 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";
1158
- addBtn.textContent = "+";
1159
- addBtn.onclick = () => {
1160
- const defaultValue = element.default || element.options?.[0]?.value || "";
1161
- values.push(defaultValue);
1162
- addSelectItem(defaultValue);
1163
- updateAddButton();
1164
- updateRemoveButtons();
1165
- };
1166
- wrapper.appendChild(addBtn);
1414
+ if (!addRow || !countDisplay) return;
1415
+ const addBtn = addRow.querySelector(".add-select-btn");
1416
+ if (addBtn) {
1417
+ const disabled = values.length >= maxCount;
1418
+ addBtn.disabled = disabled;
1419
+ addBtn.style.opacity = disabled ? "0.5" : "1";
1420
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
1167
1421
  }
1422
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
1168
1423
  }
1169
1424
  values.forEach((value) => addSelectItem(value));
1170
1425
  updateAddButton();
1171
1426
  updateRemoveButtons();
1172
- const hint = document.createElement("p");
1173
- hint.className = "text-xs text-gray-500 mt-1";
1174
- hint.textContent = makeFieldHint(element);
1175
- wrapper.appendChild(hint);
1427
+ if (!state.config.readonly) {
1428
+ const hint = document.createElement("p");
1429
+ hint.className = "text-xs text-gray-500 mt-1";
1430
+ hint.textContent = makeFieldHint(element, state);
1431
+ wrapper.appendChild(hint);
1432
+ }
1176
1433
  }
1177
1434
  function validateSelectElement(element, key, context) {
1178
1435
  const errors = [];
@@ -1211,17 +1468,18 @@ function validateSelectElement(element, key, context) {
1211
1468
  };
1212
1469
  const validateMultipleCount = (key2, values, element2, filterFn) => {
1213
1470
  if (skipValidation) return;
1471
+ const { state } = context;
1214
1472
  const filteredValues = values.filter(filterFn);
1215
1473
  const minCount = "minCount" in element2 ? element2.minCount ?? 1 : 1;
1216
1474
  const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
1217
1475
  if (element2.required && filteredValues.length === 0) {
1218
- errors.push(`${key2}: required`);
1476
+ errors.push(`${key2}: ${t("required", state)}`);
1219
1477
  }
1220
1478
  if (filteredValues.length < minCount) {
1221
- errors.push(`${key2}: minimum ${minCount} items required`);
1479
+ errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
1222
1480
  }
1223
1481
  if (filteredValues.length > maxCount) {
1224
- errors.push(`${key2}: maximum ${maxCount} items allowed`);
1482
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
1225
1483
  }
1226
1484
  };
1227
1485
  if ("multiple" in element && element.multiple) {
@@ -1242,8 +1500,9 @@ function validateSelectElement(element, key, context) {
1242
1500
  );
1243
1501
  const val = input?.value ?? "";
1244
1502
  if (!skipValidation && element.required && val === "") {
1245
- errors.push(`${key}: required`);
1246
- markValidity(input, "required");
1503
+ const msg = t("required", context.state);
1504
+ errors.push(`${key}: ${msg}`);
1505
+ markValidity(input, msg);
1247
1506
  return { value: null, errors };
1248
1507
  } else {
1249
1508
  markValidity(input, null);
@@ -1295,18 +1554,11 @@ function updateSelectField(element, fieldPath, value, context) {
1295
1554
  }
1296
1555
  }
1297
1556
 
1298
- // src/utils/translation.ts
1299
- function t(key, state) {
1300
- const locale = state.config.locale || "en";
1301
- const translations = state.config.translations[locale] || state.config.translations.en;
1302
- return translations[key] || key;
1303
- }
1304
-
1305
1557
  // src/components/file.ts
1306
- function renderLocalImagePreview(container, file, fileName) {
1558
+ function renderLocalImagePreview(container, file, fileName, state) {
1307
1559
  const img = document.createElement("img");
1308
1560
  img.className = "w-full h-full object-contain";
1309
- img.alt = fileName || "Preview";
1561
+ img.alt = fileName || t("previewAlt", state);
1310
1562
  const reader = new FileReader();
1311
1563
  reader.onload = (e) => {
1312
1564
  img.src = e.target?.result || "";
@@ -1325,14 +1577,14 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
1325
1577
  <div class="relative group h-full">
1326
1578
  <video class="w-full h-full object-contain" controls preload="auto" muted>
1327
1579
  <source src="${videoUrl}" type="${videoType}">
1328
- Your browser does not support the video tag.
1580
+ ${escapeHtml(t("videoNotSupported", state))}
1329
1581
  </video>
1330
1582
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1331
1583
  <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1332
- ${t("removeElement", state)}
1584
+ ${escapeHtml(t("removeElement", state))}
1333
1585
  </button>
1334
1586
  <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1335
- Change
1587
+ ${escapeHtml(t("changeButton", state))}
1336
1588
  </button>
1337
1589
  </div>
1338
1590
  </div>
@@ -1381,11 +1633,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
1381
1633
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1382
1634
  <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"/>
1383
1635
  </svg>
1384
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1636
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1385
1637
  </div>
1386
1638
  `;
1387
1639
  }
1388
- function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1640
+ function renderUploadedVideoPreview(container, thumbnailUrl, videoType, state) {
1389
1641
  const video = document.createElement("video");
1390
1642
  video.className = "w-full h-full object-contain";
1391
1643
  video.controls = true;
@@ -1396,7 +1648,7 @@ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1396
1648
  source.type = videoType;
1397
1649
  video.appendChild(source);
1398
1650
  video.appendChild(
1399
- document.createTextNode("Your browser does not support the video tag.")
1651
+ document.createTextNode(t("videoNotSupported", state))
1400
1652
  );
1401
1653
  container.appendChild(video);
1402
1654
  }
@@ -1414,7 +1666,7 @@ function renderDeleteButton(container, resourceId, state) {
1414
1666
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1415
1667
  <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"/>
1416
1668
  </svg>
1417
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1669
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
1418
1670
  </div>
1419
1671
  `;
1420
1672
  });
@@ -1424,7 +1676,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1424
1676
  return;
1425
1677
  }
1426
1678
  if (meta.type && meta.type.startsWith("image/")) {
1427
- renderLocalImagePreview(container, meta.file, fileName);
1679
+ renderLocalImagePreview(container, meta.file, fileName, state);
1428
1680
  } else if (meta.type && meta.type.startsWith("video/")) {
1429
1681
  const newContainer = renderLocalVideoPreview(
1430
1682
  container,
@@ -1436,7 +1688,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
1436
1688
  );
1437
1689
  container = newContainer;
1438
1690
  } else {
1439
- 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>`;
1691
+ 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>`;
1440
1692
  }
1441
1693
  if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
1442
1694
  renderDeleteButton(container, resourceId, state);
@@ -1452,11 +1704,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1452
1704
  if (thumbnailUrl) {
1453
1705
  clear(container);
1454
1706
  if (meta && meta.type && meta.type.startsWith("video/")) {
1455
- renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
1707
+ renderUploadedVideoPreview(container, thumbnailUrl, meta.type, state);
1456
1708
  } else {
1457
1709
  const img = document.createElement("img");
1458
1710
  img.className = "w-full h-full object-contain";
1459
- img.alt = fileName || "Preview";
1711
+ img.alt = fileName || t("previewAlt", state);
1460
1712
  img.src = thumbnailUrl;
1461
1713
  container.appendChild(img);
1462
1714
  }
@@ -1470,7 +1722,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
1470
1722
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1471
1723
  <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"/>
1472
1724
  </svg>
1473
- <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1725
+ <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
1474
1726
  </div>
1475
1727
  `;
1476
1728
  }
@@ -1526,16 +1778,16 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1526
1778
  try {
1527
1779
  const thumbnailUrl = await state.config.getThumbnail(resourceId);
1528
1780
  if (thumbnailUrl) {
1529
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
1781
+ previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${escapeHtml(actualFileName)}" class="w-full h-auto">`;
1530
1782
  } else {
1531
- 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>`;
1783
+ 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>`;
1532
1784
  }
1533
1785
  } catch (error) {
1534
1786
  console.warn("getThumbnail failed for", resourceId, error);
1535
- 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>`;
1787
+ 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>`;
1536
1788
  }
1537
1789
  } else {
1538
- 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>`;
1790
+ 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>`;
1539
1791
  }
1540
1792
  } else if (isVideo) {
1541
1793
  if (state.config.getThumbnail) {
@@ -1546,7 +1798,7 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1546
1798
  <div class="relative group">
1547
1799
  <video class="w-full h-auto" controls preload="auto" muted>
1548
1800
  <source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
1549
- \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.
1801
+ ${escapeHtml(t("videoNotSupported", state))}
1550
1802
  </video>
1551
1803
  <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">
1552
1804
  <div class="bg-white bg-opacity-90 rounded-full p-3">
@@ -1558,14 +1810,14 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1558
1810
  </div>
1559
1811
  `;
1560
1812
  } else {
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{1F3A5}</div><div class="text-sm">${actualFileName}</div></div></div>`;
1813
+ 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>`;
1562
1814
  }
1563
1815
  } catch (error) {
1564
1816
  console.warn("getThumbnail failed for video", resourceId, error);
1565
- 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>`;
1817
+ 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>`;
1566
1818
  }
1567
1819
  } else {
1568
- 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>`;
1820
+ 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>`;
1569
1821
  }
1570
1822
  } else {
1571
1823
  const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
@@ -1575,13 +1827,13 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1575
1827
  <div class="flex items-center space-x-3">
1576
1828
  <div class="text-3xl text-gray-400">${fileIcon}</div>
1577
1829
  <div class="flex-1 min-w-0">
1578
- <div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
1830
+ <div class="text-sm font-medium text-gray-900 truncate">${escapeHtml(actualFileName)}</div>
1579
1831
  <div class="text-xs text-gray-500">${fileDescription}</div>
1580
1832
  </div>
1581
1833
  </div>
1582
1834
  `;
1583
1835
  } else {
1584
- 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>`;
1836
+ 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>`;
1585
1837
  }
1586
1838
  }
1587
1839
  const fileNameElement = document.createElement("p");
@@ -1604,12 +1856,18 @@ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1604
1856
  fileResult.appendChild(downloadButton);
1605
1857
  return fileResult;
1606
1858
  }
1607
- function renderResourcePills(container, rids, state, onRemove) {
1859
+ function renderResourcePills(container, rids, state, onRemove, hint, countInfo) {
1608
1860
  clear(container);
1861
+ const buildHintLine = () => {
1862
+ const parts = [t("clickDragTextMultiple", state)];
1863
+ if (hint) parts.push(hint);
1864
+ if (countInfo) parts.push(countInfo);
1865
+ return parts.join(" \u2022 ");
1866
+ };
1609
1867
  const isInitialRender = !container.classList.contains("grid");
1610
1868
  if ((!rids || rids.length === 0) && isInitialRender) {
1611
- const gridContainer = document.createElement("div");
1612
- gridContainer.className = "grid grid-cols-4 gap-3 mb-3";
1869
+ const gridContainer2 = document.createElement("div");
1870
+ gridContainer2.className = "grid grid-cols-4 gap-3 mb-3";
1613
1871
  for (let i = 0; i < 4; i++) {
1614
1872
  const slot = document.createElement("div");
1615
1873
  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";
@@ -1640,36 +1898,17 @@ function renderResourcePills(container, rids, state, onRemove) {
1640
1898
  );
1641
1899
  if (fileInput) fileInput.click();
1642
1900
  };
1643
- gridContainer.appendChild(slot);
1644
- }
1645
- const textContainer = document.createElement("div");
1646
- textContainer.className = "text-center text-xs text-gray-600";
1647
- const uploadLink = document.createElement("span");
1648
- uploadLink.className = "underline cursor-pointer";
1649
- uploadLink.textContent = t("uploadText", state);
1650
- uploadLink.onclick = (e) => {
1651
- e.stopPropagation();
1652
- let filesWrapper = container.parentElement;
1653
- while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
1654
- filesWrapper = filesWrapper.parentElement;
1655
- }
1656
- if (!filesWrapper && container.classList.contains("space-y-2")) {
1657
- filesWrapper = container;
1658
- }
1659
- const fileInput = filesWrapper?.querySelector(
1660
- 'input[type="file"]'
1661
- );
1662
- if (fileInput) fileInput.click();
1663
- };
1664
- textContainer.appendChild(uploadLink);
1665
- textContainer.appendChild(
1666
- document.createTextNode(` ${t("dragDropText", state)}`)
1667
- );
1668
- container.appendChild(gridContainer);
1669
- container.appendChild(textContainer);
1901
+ gridContainer2.appendChild(slot);
1902
+ }
1903
+ const hintText2 = document.createElement("div");
1904
+ hintText2.className = "text-center text-xs text-gray-500 mt-2";
1905
+ hintText2.textContent = buildHintLine();
1906
+ container.appendChild(gridContainer2);
1907
+ container.appendChild(hintText2);
1670
1908
  return;
1671
1909
  }
1672
- container.className = "files-list grid grid-cols-4 gap-3 mt-2";
1910
+ const gridContainer = document.createElement("div");
1911
+ gridContainer.className = "files-list grid grid-cols-4 gap-3";
1673
1912
  const currentImagesCount = rids ? rids.length : 0;
1674
1913
  const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
1675
1914
  const slotsNeeded = rowsNeeded * 4;
@@ -1684,7 +1923,7 @@ function renderResourcePills(container, rids, state, onRemove) {
1684
1923
  console.error("Failed to render thumbnail:", err);
1685
1924
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1686
1925
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1687
- <div class="text-xs">Preview error</div>
1926
+ <div class="text-xs">${escapeHtml(t("previewError", state))}</div>
1688
1927
  </div>`;
1689
1928
  });
1690
1929
  if (onRemove) {
@@ -1717,15 +1956,20 @@ function renderResourcePills(container, rids, state, onRemove) {
1717
1956
  if (fileInput) fileInput.click();
1718
1957
  };
1719
1958
  }
1720
- container.appendChild(slot);
1959
+ gridContainer.appendChild(slot);
1721
1960
  }
1961
+ container.appendChild(gridContainer);
1962
+ const hintText = document.createElement("div");
1963
+ hintText.className = "text-center text-xs text-gray-500 mt-2";
1964
+ hintText.textContent = buildHintLine();
1965
+ container.appendChild(hintText);
1722
1966
  }
1723
- function renderThumbnailError(slot, iconSize = "w-12 h-12") {
1967
+ function renderThumbnailError(slot, state, iconSize = "w-12 h-12") {
1724
1968
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1725
- <svg class="${iconSize} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1969
+ <svg class="${escapeHtml(iconSize)} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1726
1970
  <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"/>
1727
1971
  </svg>
1728
- <div class="text-xs mt-1 text-red-600">Preview error</div>
1972
+ <div class="text-xs mt-1 text-red-600">${escapeHtml(t("previewError", state))}</div>
1729
1973
  </div>`;
1730
1974
  }
1731
1975
  async function renderThumbnailForResource(slot, rid, meta, state) {
@@ -1761,7 +2005,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1761
2005
  if (state.config.onThumbnailError) {
1762
2006
  state.config.onThumbnailError(err, rid);
1763
2007
  }
1764
- renderThumbnailError(slot);
2008
+ renderThumbnailError(slot, state);
1765
2009
  }
1766
2010
  } else {
1767
2011
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -1810,7 +2054,7 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1810
2054
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1811
2055
  <path d="M8 5v14l11-7z"/>
1812
2056
  </svg>
1813
- <div class="text-xs mt-1">${meta?.name || "Video"}</div>
2057
+ <div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
1814
2058
  </div>`;
1815
2059
  }
1816
2060
  } catch (error) {
@@ -1818,30 +2062,32 @@ async function renderThumbnailForResource(slot, rid, meta, state) {
1818
2062
  if (state.config.onThumbnailError) {
1819
2063
  state.config.onThumbnailError(err, rid);
1820
2064
  }
1821
- renderThumbnailError(slot, "w-8 h-8");
2065
+ renderThumbnailError(slot, state, "w-8 h-8");
1822
2066
  }
1823
2067
  } else {
1824
2068
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1825
2069
  <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1826
2070
  <path d="M8 5v14l11-7z"/>
1827
2071
  </svg>
1828
- <div class="text-xs mt-1">${meta?.name || "Video"}</div>
2072
+ <div class="text-xs mt-1">${escapeHtml(meta?.name || "Video")}</div>
1829
2073
  </div>`;
1830
2074
  }
1831
2075
  } else {
1832
2076
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1833
2077
  <div class="text-2xl mb-1">\u{1F4C1}</div>
1834
- <div class="text-xs">${meta?.name || "File"}</div>
2078
+ <div class="text-xs">${escapeHtml(meta?.name || "File")}</div>
1835
2079
  </div>`;
1836
2080
  }
1837
2081
  }
1838
- function setEmptyFileContainer(fileContainer, state) {
2082
+ function setEmptyFileContainer(fileContainer, state, hint) {
2083
+ const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
1839
2084
  fileContainer.innerHTML = `
1840
2085
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
1841
2086
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1842
2087
  <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"/>
1843
2088
  </svg>
1844
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
2089
+ <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
2090
+ ${hintHtml}
1845
2091
  </div>
1846
2092
  `;
1847
2093
  }
@@ -2101,13 +2347,13 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2101
2347
  console.error("Failed to render file preview:", err);
2102
2348
  const emptyState = document.createElement("div");
2103
2349
  emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2104
- emptyState.innerHTML = `<div class="text-center">Preview unavailable</div>`;
2350
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("previewUnavailable", state))}</div>`;
2105
2351
  wrapper.appendChild(emptyState);
2106
2352
  });
2107
2353
  } else {
2108
2354
  const emptyState = document.createElement("div");
2109
2355
  emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2110
- emptyState.innerHTML = `<div class="text-center">${t("noFileSelected", state)}</div>`;
2356
+ emptyState.innerHTML = `<div class="text-center">${escapeHtml(t("noFileSelected", state))}</div>`;
2111
2357
  wrapper.appendChild(emptyState);
2112
2358
  }
2113
2359
  } else {
@@ -2151,7 +2397,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2151
2397
  }
2152
2398
  );
2153
2399
  } else {
2154
- setEmptyFileContainer(fileContainer, state);
2400
+ const hint = makeFieldHint(element, state);
2401
+ setEmptyFileContainer(fileContainer, state, hint);
2155
2402
  }
2156
2403
  fileContainer.onclick = fileUploadHandler;
2157
2404
  setupDragAndDrop(fileContainer, dragHandler);
@@ -2170,18 +2417,6 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2170
2417
  };
2171
2418
  fileWrapper.appendChild(fileContainer);
2172
2419
  fileWrapper.appendChild(picker);
2173
- const uploadText = document.createElement("p");
2174
- uploadText.className = "text-xs text-gray-600 mt-2 text-center";
2175
- uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText", state)}</span> ${t("dragDropTextSingle", state)}`;
2176
- const uploadSpan = uploadText.querySelector("span");
2177
- if (uploadSpan) {
2178
- uploadSpan.onclick = () => picker.click();
2179
- }
2180
- fileWrapper.appendChild(uploadText);
2181
- const fileHint = document.createElement("p");
2182
- fileHint.className = "text-xs text-gray-500 mt-1 text-center";
2183
- fileHint.textContent = makeFieldHint(element);
2184
- fileWrapper.appendChild(fileHint);
2185
2420
  wrapper.appendChild(fileWrapper);
2186
2421
  }
2187
2422
  }
@@ -2200,18 +2435,24 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2200
2435
  });
2201
2436
  });
2202
2437
  } else {
2203
- 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>`;
2438
+ 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>`;
2204
2439
  }
2205
2440
  wrapper.appendChild(resultsWrapper);
2206
2441
  } else {
2207
2442
  let updateFilesList2 = function() {
2208
- renderResourcePills(list, initialFiles, state, (ridToRemove) => {
2209
- const index = initialFiles.indexOf(ridToRemove);
2210
- if (index > -1) {
2211
- initialFiles.splice(index, 1);
2212
- }
2213
- updateFilesList2();
2214
- });
2443
+ renderResourcePills(
2444
+ list,
2445
+ initialFiles,
2446
+ state,
2447
+ (ridToRemove) => {
2448
+ const index = initialFiles.indexOf(ridToRemove);
2449
+ if (index > -1) {
2450
+ initialFiles.splice(index, 1);
2451
+ }
2452
+ updateFilesList2();
2453
+ },
2454
+ filesFieldHint
2455
+ );
2215
2456
  };
2216
2457
  const filesWrapper = document.createElement("div");
2217
2458
  filesWrapper.className = "space-y-2";
@@ -2229,6 +2470,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2229
2470
  list.className = "files-list";
2230
2471
  const initialFiles = ctx.prefill[element.key] || [];
2231
2472
  addPrefillFilesToIndex(initialFiles, state);
2473
+ const filesFieldHint = makeFieldHint(element, state);
2232
2474
  updateFilesList2();
2233
2475
  setupFilesDropHandler(
2234
2476
  filesContainer,
@@ -2249,10 +2491,6 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2249
2491
  filesContainer.appendChild(list);
2250
2492
  filesWrapper.appendChild(filesContainer);
2251
2493
  filesWrapper.appendChild(filesPicker);
2252
- const filesHint = document.createElement("p");
2253
- filesHint.className = "text-xs text-gray-500 mt-1 text-center";
2254
- filesHint.textContent = makeFieldHint(element);
2255
- filesWrapper.appendChild(filesHint);
2256
2494
  wrapper.appendChild(filesWrapper);
2257
2495
  }
2258
2496
  }
@@ -2273,7 +2511,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2273
2511
  });
2274
2512
  });
2275
2513
  } else {
2276
- 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>`;
2514
+ 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>`;
2277
2515
  }
2278
2516
  wrapper.appendChild(resultsWrapper);
2279
2517
  } else {
@@ -2293,19 +2531,24 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2293
2531
  filesWrapper.appendChild(filesContainer);
2294
2532
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2295
2533
  addPrefillFilesToIndex(initialFiles, state);
2534
+ const multipleFilesHint = makeFieldHint(element, state);
2535
+ const buildCountInfo = () => {
2536
+ const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
2537
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
2538
+ return countText + minMaxText;
2539
+ };
2296
2540
  const updateFilesDisplay = () => {
2297
- renderResourcePills(filesContainer, initialFiles, state, (index) => {
2298
- initialFiles.splice(initialFiles.indexOf(index), 1);
2299
- updateFilesDisplay();
2300
- });
2301
- const countInfo = document.createElement("div");
2302
- countInfo.className = "text-xs text-gray-500 mt-2 file-count-info";
2303
- const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
2304
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` (${minFiles}-${maxFiles} allowed)` : "";
2305
- countInfo.textContent = countText + minMaxText;
2306
- const existingCount = filesWrapper.querySelector(".file-count-info");
2307
- if (existingCount) existingCount.remove();
2308
- filesWrapper.appendChild(countInfo);
2541
+ renderResourcePills(
2542
+ filesContainer,
2543
+ initialFiles,
2544
+ state,
2545
+ (index) => {
2546
+ initialFiles.splice(initialFiles.indexOf(index), 1);
2547
+ updateFilesDisplay();
2548
+ },
2549
+ multipleFilesHint,
2550
+ buildCountInfo()
2551
+ );
2309
2552
  };
2310
2553
  setupFilesDropHandler(
2311
2554
  filesContainer,
@@ -2333,16 +2576,17 @@ function validateFileElement(element, key, context) {
2333
2576
  const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
2334
2577
  const validateFileCount = (key2, resourceIds, element2) => {
2335
2578
  if (skipValidation) return;
2579
+ const { state } = context;
2336
2580
  const minFiles = "minCount" in element2 ? element2.minCount ?? 0 : 0;
2337
2581
  const maxFiles = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
2338
2582
  if (element2.required && resourceIds.length === 0) {
2339
- errors.push(`${key2}: required`);
2583
+ errors.push(`${key2}: ${t("required", state)}`);
2340
2584
  }
2341
2585
  if (resourceIds.length < minFiles) {
2342
- errors.push(`${key2}: minimum ${minFiles} files required`);
2586
+ errors.push(`${key2}: ${t("minFiles", state, { min: minFiles })}`);
2343
2587
  }
2344
2588
  if (resourceIds.length > maxFiles) {
2345
- errors.push(`${key2}: maximum ${maxFiles} files allowed`);
2589
+ errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
2346
2590
  }
2347
2591
  };
2348
2592
  if (isMultipleField) {
@@ -2370,7 +2614,7 @@ function validateFileElement(element, key, context) {
2370
2614
  );
2371
2615
  const rid = input?.value ?? "";
2372
2616
  if (!skipValidation && element.required && rid === "") {
2373
- errors.push(`${key}: required`);
2617
+ errors.push(`${key}: ${t("required", context.state)}`);
2374
2618
  return { value: null, errors };
2375
2619
  }
2376
2620
  return { value: rid || null, errors };
@@ -2611,14 +2855,16 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
2611
2855
  const editUI = createEditColourUI(initialValue, pathKey, ctx);
2612
2856
  wrapper.appendChild(editUI);
2613
2857
  }
2614
- const colourHint = document.createElement("p");
2615
- colourHint.className = "mt-1";
2616
- colourHint.style.cssText = `
2617
- font-size: var(--fb-font-size-small);
2618
- color: var(--fb-text-secondary-color);
2619
- `;
2620
- colourHint.textContent = makeFieldHint(element);
2621
- wrapper.appendChild(colourHint);
2858
+ if (!state.config.readonly) {
2859
+ const colourHint = document.createElement("p");
2860
+ colourHint.className = "mt-1";
2861
+ colourHint.style.cssText = `
2862
+ font-size: var(--fb-font-size-small);
2863
+ color: var(--fb-text-secondary-color);
2864
+ `;
2865
+ colourHint.textContent = makeFieldHint(element, state);
2866
+ wrapper.appendChild(colourHint);
2867
+ }
2622
2868
  }
2623
2869
  function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2624
2870
  const state = ctx.state;
@@ -2707,48 +2953,65 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2707
2953
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2708
2954
  });
2709
2955
  }
2956
+ let addRow = null;
2957
+ let countDisplay = null;
2958
+ if (!state.config.readonly) {
2959
+ addRow = document.createElement("div");
2960
+ addRow.className = "flex items-center gap-3 mt-2";
2961
+ const addBtn = document.createElement("button");
2962
+ addBtn.type = "button";
2963
+ addBtn.className = "add-colour-btn px-3 py-1 rounded";
2964
+ addBtn.style.cssText = `
2965
+ color: var(--fb-primary-color);
2966
+ border: var(--fb-border-width) solid var(--fb-primary-color);
2967
+ background-color: transparent;
2968
+ font-size: var(--fb-font-size);
2969
+ transition: all var(--fb-transition-duration);
2970
+ `;
2971
+ addBtn.textContent = "+";
2972
+ addBtn.addEventListener("mouseenter", () => {
2973
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2974
+ });
2975
+ addBtn.addEventListener("mouseleave", () => {
2976
+ addBtn.style.backgroundColor = "transparent";
2977
+ });
2978
+ addBtn.onclick = () => {
2979
+ const defaultColour = element.default || "#000000";
2980
+ values.push(defaultColour);
2981
+ addColourItem(defaultColour);
2982
+ updateAddButton();
2983
+ updateRemoveButtons();
2984
+ };
2985
+ countDisplay = document.createElement("span");
2986
+ countDisplay.className = "text-sm text-gray-500";
2987
+ addRow.appendChild(addBtn);
2988
+ addRow.appendChild(countDisplay);
2989
+ wrapper.appendChild(addRow);
2990
+ }
2710
2991
  function updateAddButton() {
2711
- const existingAddBtn = wrapper.querySelector(".add-colour-btn");
2712
- if (existingAddBtn) existingAddBtn.remove();
2713
- if (!state.config.readonly && values.length < maxCount) {
2714
- const addBtn = document.createElement("button");
2715
- addBtn.type = "button";
2716
- addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
2717
- addBtn.style.cssText = `
2718
- color: var(--fb-primary-color);
2719
- border: var(--fb-border-width) solid var(--fb-primary-color);
2720
- background-color: transparent;
2721
- font-size: var(--fb-font-size);
2722
- transition: all var(--fb-transition-duration);
2723
- `;
2724
- addBtn.textContent = "+";
2725
- addBtn.addEventListener("mouseenter", () => {
2726
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2727
- });
2728
- addBtn.addEventListener("mouseleave", () => {
2729
- addBtn.style.backgroundColor = "transparent";
2730
- });
2731
- addBtn.onclick = () => {
2732
- const defaultColour = element.default || "#000000";
2733
- values.push(defaultColour);
2734
- addColourItem(defaultColour);
2735
- updateAddButton();
2736
- updateRemoveButtons();
2737
- };
2738
- wrapper.appendChild(addBtn);
2992
+ if (!addRow || !countDisplay) return;
2993
+ const addBtn = addRow.querySelector(".add-colour-btn");
2994
+ if (addBtn) {
2995
+ const disabled = values.length >= maxCount;
2996
+ addBtn.disabled = disabled;
2997
+ addBtn.style.opacity = disabled ? "0.5" : "1";
2998
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
2739
2999
  }
3000
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
2740
3001
  }
2741
3002
  values.forEach((value) => addColourItem(value));
2742
3003
  updateAddButton();
2743
3004
  updateRemoveButtons();
2744
- const hint = document.createElement("p");
2745
- hint.className = "mt-1";
2746
- hint.style.cssText = `
2747
- font-size: var(--fb-font-size-small);
2748
- color: var(--fb-text-secondary-color);
2749
- `;
2750
- hint.textContent = makeFieldHint(element);
2751
- wrapper.appendChild(hint);
3005
+ if (!state.config.readonly) {
3006
+ const hint = document.createElement("p");
3007
+ hint.className = "mt-1";
3008
+ hint.style.cssText = `
3009
+ font-size: var(--fb-font-size-small);
3010
+ color: var(--fb-text-secondary-color);
3011
+ `;
3012
+ hint.textContent = makeFieldHint(element, state);
3013
+ wrapper.appendChild(hint);
3014
+ }
2752
3015
  }
2753
3016
  function validateColourElement(element, key, context) {
2754
3017
  const errors = [];
@@ -2786,10 +3049,12 @@ function validateColourElement(element, key, context) {
2786
3049
  }
2787
3050
  };
2788
3051
  const validateColourValue = (input, val, fieldKey) => {
3052
+ const { state } = context;
2789
3053
  if (!val) {
2790
3054
  if (!skipValidation && element.required) {
2791
- errors.push(`${fieldKey}: required`);
2792
- markValidity(input, "required");
3055
+ const msg = t("required", state);
3056
+ errors.push(`${fieldKey}: ${msg}`);
3057
+ markValidity(input, msg);
2793
3058
  return "";
2794
3059
  }
2795
3060
  markValidity(input, null);
@@ -2797,8 +3062,9 @@ function validateColourElement(element, key, context) {
2797
3062
  }
2798
3063
  const normalized = normalizeColourValue(val);
2799
3064
  if (!skipValidation && !isValidHexColour(normalized)) {
2800
- errors.push(`${fieldKey}: invalid hex colour format`);
2801
- markValidity(input, "invalid hex colour format");
3065
+ const msg = t("invalidHexColour", state);
3066
+ errors.push(`${fieldKey}: ${msg}`);
3067
+ markValidity(input, msg);
2802
3068
  return val;
2803
3069
  }
2804
3070
  markValidity(input, null);
@@ -2813,17 +3079,18 @@ function validateColourElement(element, key, context) {
2813
3079
  values.push(validated);
2814
3080
  });
2815
3081
  if (!skipValidation) {
3082
+ const { state } = context;
2816
3083
  const minCount = element.minCount ?? 1;
2817
3084
  const maxCount = element.maxCount ?? Infinity;
2818
3085
  const filteredValues = values.filter((v) => v !== "");
2819
3086
  if (element.required && filteredValues.length === 0) {
2820
- errors.push(`${key}: required`);
3087
+ errors.push(`${key}: ${t("required", state)}`);
2821
3088
  }
2822
3089
  if (filteredValues.length < minCount) {
2823
- errors.push(`${key}: minimum ${minCount} items required`);
3090
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
2824
3091
  }
2825
3092
  if (filteredValues.length > maxCount) {
2826
- errors.push(`${key}: maximum ${maxCount} items allowed`);
3093
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
2827
3094
  }
2828
3095
  }
2829
3096
  return { value: values, errors };
@@ -2833,8 +3100,9 @@ function validateColourElement(element, key, context) {
2833
3100
  );
2834
3101
  const val = hexInput?.value ?? "";
2835
3102
  if (!skipValidation && element.required && val === "") {
2836
- errors.push(`${key}: required`);
2837
- markValidity(hexInput, "required");
3103
+ const msg = t("required", context.state);
3104
+ errors.push(`${key}: ${msg}`);
3105
+ markValidity(hexInput, msg);
2838
3106
  return { value: "", errors };
2839
3107
  }
2840
3108
  const validated = validateColourValue(hexInput, val, key);
@@ -2926,13 +3194,15 @@ function alignToStep(value, step) {
2926
3194
  }
2927
3195
  function createSliderUI(value, pathKey, element, ctx, readonly) {
2928
3196
  const container = document.createElement("div");
2929
- container.className = "slider-container space-y-2";
2930
- const sliderRow = document.createElement("div");
2931
- sliderRow.className = "flex items-center gap-3";
3197
+ container.className = "slider-container";
3198
+ const mainRow = document.createElement("div");
3199
+ mainRow.className = "flex items-start gap-3";
3200
+ const sliderSection = document.createElement("div");
3201
+ sliderSection.className = "flex-1";
2932
3202
  const slider = document.createElement("input");
2933
3203
  slider.type = "range";
2934
3204
  slider.name = pathKey;
2935
- slider.className = "slider-input flex-1";
3205
+ slider.className = "slider-input w-full";
2936
3206
  slider.disabled = readonly;
2937
3207
  const scale = element.scale || "linear";
2938
3208
  const min = element.min;
@@ -2970,25 +3240,13 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
2970
3240
  cursor: ${readonly ? "not-allowed" : "pointer"};
2971
3241
  opacity: ${readonly ? "0.6" : "1"};
2972
3242
  `;
2973
- const valueDisplay = document.createElement("span");
2974
- valueDisplay.className = "slider-value";
2975
- valueDisplay.style.cssText = `
2976
- min-width: 60px;
2977
- text-align: right;
2978
- font-size: var(--fb-font-size);
2979
- color: var(--fb-text-color);
2980
- font-family: var(--fb-font-family-mono, monospace);
2981
- font-weight: 500;
2982
- `;
2983
- valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
2984
- sliderRow.appendChild(slider);
2985
- sliderRow.appendChild(valueDisplay);
2986
- container.appendChild(sliderRow);
3243
+ sliderSection.appendChild(slider);
2987
3244
  const labelsRow = document.createElement("div");
2988
3245
  labelsRow.className = "flex justify-between";
2989
3246
  labelsRow.style.cssText = `
2990
3247
  font-size: var(--fb-font-size-small);
2991
3248
  color: var(--fb-text-secondary-color);
3249
+ margin-top: 4px;
2992
3250
  `;
2993
3251
  const minLabel = document.createElement("span");
2994
3252
  minLabel.textContent = min.toString();
@@ -2996,7 +3254,22 @@ function createSliderUI(value, pathKey, element, ctx, readonly) {
2996
3254
  maxLabel.textContent = max.toString();
2997
3255
  labelsRow.appendChild(minLabel);
2998
3256
  labelsRow.appendChild(maxLabel);
2999
- container.appendChild(labelsRow);
3257
+ sliderSection.appendChild(labelsRow);
3258
+ const valueDisplay = document.createElement("span");
3259
+ valueDisplay.className = "slider-value";
3260
+ valueDisplay.style.cssText = `
3261
+ min-width: 60px;
3262
+ text-align: right;
3263
+ font-size: var(--fb-font-size);
3264
+ color: var(--fb-text-color);
3265
+ font-family: var(--fb-font-family-mono, monospace);
3266
+ font-weight: 500;
3267
+ padding-top: 2px;
3268
+ `;
3269
+ valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
3270
+ mainRow.appendChild(sliderSection);
3271
+ mainRow.appendChild(valueDisplay);
3272
+ container.appendChild(mainRow);
3000
3273
  if (!readonly) {
3001
3274
  const updateValue = () => {
3002
3275
  let displayValue;
@@ -3050,14 +3323,16 @@ function renderSliderElement(element, ctx, wrapper, pathKey) {
3050
3323
  state.config.readonly
3051
3324
  );
3052
3325
  wrapper.appendChild(sliderUI);
3053
- const hint = document.createElement("p");
3054
- hint.className = "mt-1";
3055
- hint.style.cssText = `
3056
- font-size: var(--fb-font-size-small);
3057
- color: var(--fb-text-secondary-color);
3058
- `;
3059
- hint.textContent = makeFieldHint(element);
3060
- wrapper.appendChild(hint);
3326
+ if (!state.config.readonly) {
3327
+ const hint = document.createElement("p");
3328
+ hint.className = "mt-1";
3329
+ hint.style.cssText = `
3330
+ font-size: var(--fb-font-size-small);
3331
+ color: var(--fb-text-secondary-color);
3332
+ `;
3333
+ hint.textContent = makeFieldHint(element, state);
3334
+ wrapper.appendChild(hint);
3335
+ }
3061
3336
  }
3062
3337
  function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3063
3338
  if (element.min === void 0 || element.min === null) {
@@ -3158,47 +3433,64 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
3158
3433
  removeBtn.style.pointerEvents = disabled ? "none" : "auto";
3159
3434
  });
3160
3435
  }
3436
+ let addRow = null;
3437
+ let countDisplay = null;
3438
+ if (!state.config.readonly) {
3439
+ addRow = document.createElement("div");
3440
+ addRow.className = "flex items-center gap-3 mt-2";
3441
+ const addBtn = document.createElement("button");
3442
+ addBtn.type = "button";
3443
+ addBtn.className = "add-slider-btn px-3 py-1 rounded";
3444
+ addBtn.style.cssText = `
3445
+ color: var(--fb-primary-color);
3446
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3447
+ background-color: transparent;
3448
+ font-size: var(--fb-font-size);
3449
+ transition: all var(--fb-transition-duration);
3450
+ `;
3451
+ addBtn.textContent = "+";
3452
+ addBtn.addEventListener("mouseenter", () => {
3453
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3454
+ });
3455
+ addBtn.addEventListener("mouseleave", () => {
3456
+ addBtn.style.backgroundColor = "transparent";
3457
+ });
3458
+ addBtn.onclick = () => {
3459
+ values.push(defaultValue);
3460
+ addSliderItem(defaultValue);
3461
+ updateAddButton();
3462
+ updateRemoveButtons();
3463
+ };
3464
+ countDisplay = document.createElement("span");
3465
+ countDisplay.className = "text-sm text-gray-500";
3466
+ addRow.appendChild(addBtn);
3467
+ addRow.appendChild(countDisplay);
3468
+ wrapper.appendChild(addRow);
3469
+ }
3161
3470
  function updateAddButton() {
3162
- const existingAddBtn = wrapper.querySelector(".add-slider-btn");
3163
- if (existingAddBtn) existingAddBtn.remove();
3164
- if (!state.config.readonly && values.length < maxCount) {
3165
- const addBtn = document.createElement("button");
3166
- addBtn.type = "button";
3167
- addBtn.className = "add-slider-btn mt-2 px-3 py-1 rounded";
3168
- addBtn.style.cssText = `
3169
- color: var(--fb-primary-color);
3170
- border: var(--fb-border-width) solid var(--fb-primary-color);
3171
- background-color: transparent;
3172
- font-size: var(--fb-font-size);
3173
- transition: all var(--fb-transition-duration);
3174
- `;
3175
- addBtn.textContent = "+";
3176
- addBtn.addEventListener("mouseenter", () => {
3177
- addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3178
- });
3179
- addBtn.addEventListener("mouseleave", () => {
3180
- addBtn.style.backgroundColor = "transparent";
3181
- });
3182
- addBtn.onclick = () => {
3183
- values.push(defaultValue);
3184
- addSliderItem(defaultValue);
3185
- updateAddButton();
3186
- updateRemoveButtons();
3187
- };
3188
- wrapper.appendChild(addBtn);
3471
+ if (!addRow || !countDisplay) return;
3472
+ const addBtn = addRow.querySelector(".add-slider-btn");
3473
+ if (addBtn) {
3474
+ const disabled = values.length >= maxCount;
3475
+ addBtn.disabled = disabled;
3476
+ addBtn.style.opacity = disabled ? "0.5" : "1";
3477
+ addBtn.style.pointerEvents = disabled ? "none" : "auto";
3189
3478
  }
3479
+ countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
3190
3480
  }
3191
3481
  values.forEach((value) => addSliderItem(value));
3192
3482
  updateAddButton();
3193
3483
  updateRemoveButtons();
3194
- const hint = document.createElement("p");
3195
- hint.className = "mt-1";
3196
- hint.style.cssText = `
3197
- font-size: var(--fb-font-size-small);
3198
- color: var(--fb-text-secondary-color);
3199
- `;
3200
- hint.textContent = makeFieldHint(element);
3201
- wrapper.appendChild(hint);
3484
+ if (!state.config.readonly) {
3485
+ const hint = document.createElement("p");
3486
+ hint.className = "mt-1";
3487
+ hint.style.cssText = `
3488
+ font-size: var(--fb-font-size-small);
3489
+ color: var(--fb-text-secondary-color);
3490
+ `;
3491
+ hint.textContent = makeFieldHint(element, state);
3492
+ wrapper.appendChild(hint);
3493
+ }
3202
3494
  }
3203
3495
  function validateSliderElement(element, key, context) {
3204
3496
  const errors = [];
@@ -3254,11 +3546,13 @@ function validateSliderElement(element, key, context) {
3254
3546
  }
3255
3547
  };
3256
3548
  const validateSliderValue = (slider, fieldKey) => {
3549
+ const { state } = context;
3257
3550
  const rawValue = slider.value;
3258
3551
  if (!rawValue) {
3259
3552
  if (!skipValidation && element.required) {
3260
- errors.push(`${fieldKey}: required`);
3261
- markValidity(slider, "required");
3553
+ const msg = t("required", state);
3554
+ errors.push(`${fieldKey}: ${msg}`);
3555
+ markValidity(slider, msg);
3262
3556
  return null;
3263
3557
  }
3264
3558
  markValidity(slider, null);
@@ -3275,13 +3569,15 @@ function validateSliderElement(element, key, context) {
3275
3569
  }
3276
3570
  if (!skipValidation) {
3277
3571
  if (value < min) {
3278
- errors.push(`${fieldKey}: value ${value} < min ${min}`);
3279
- markValidity(slider, `value must be >= ${min}`);
3572
+ const msg = t("minValue", state, { min });
3573
+ errors.push(`${fieldKey}: ${msg}`);
3574
+ markValidity(slider, msg);
3280
3575
  return value;
3281
3576
  }
3282
3577
  if (value > max) {
3283
- errors.push(`${fieldKey}: value ${value} > max ${max}`);
3284
- markValidity(slider, `value must be <= ${max}`);
3578
+ const msg = t("maxValue", state, { max });
3579
+ errors.push(`${fieldKey}: ${msg}`);
3580
+ markValidity(slider, msg);
3285
3581
  return value;
3286
3582
  }
3287
3583
  }
@@ -3298,17 +3594,18 @@ function validateSliderElement(element, key, context) {
3298
3594
  values.push(value);
3299
3595
  });
3300
3596
  if (!skipValidation) {
3597
+ const { state } = context;
3301
3598
  const minCount = element.minCount ?? 1;
3302
3599
  const maxCount = element.maxCount ?? Infinity;
3303
3600
  const filteredValues = values.filter((v) => v !== null);
3304
3601
  if (element.required && filteredValues.length === 0) {
3305
- errors.push(`${key}: required`);
3602
+ errors.push(`${key}: ${t("required", state)}`);
3306
3603
  }
3307
3604
  if (filteredValues.length < minCount) {
3308
- errors.push(`${key}: minimum ${minCount} items required`);
3605
+ errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
3309
3606
  }
3310
3607
  if (filteredValues.length > maxCount) {
3311
- errors.push(`${key}: maximum ${maxCount} items allowed`);
3608
+ errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
3312
3609
  }
3313
3610
  }
3314
3611
  return { value: values, errors };
@@ -3318,7 +3615,7 @@ function validateSliderElement(element, key, context) {
3318
3615
  );
3319
3616
  if (!slider) {
3320
3617
  if (!skipValidation && element.required) {
3321
- errors.push(`${key}: required`);
3618
+ errors.push(`${key}: ${t("required", context.state)}`);
3322
3619
  }
3323
3620
  return { value: null, errors };
3324
3621
  }
@@ -3470,10 +3767,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3470
3767
  const containerWrap = document.createElement("div");
3471
3768
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3472
3769
  containerWrap.setAttribute("data-container", pathKey);
3473
- const header = document.createElement("div");
3474
- header.className = "flex justify-between items-center mb-4";
3475
- const left = document.createElement("div");
3476
- left.className = "flex-1";
3477
3770
  const itemsWrap = document.createElement("div");
3478
3771
  const columns = element.columns || 1;
3479
3772
  if (columns === 1) {
@@ -3481,8 +3774,6 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3481
3774
  } else {
3482
3775
  itemsWrap.className = `grid grid-cols-${columns} gap-4`;
3483
3776
  }
3484
- containerWrap.appendChild(header);
3485
- header.appendChild(left);
3486
3777
  if (!ctx.state.config.readonly) {
3487
3778
  const hintsElement = createPrefillHints(element, pathKey);
3488
3779
  if (hintsElement) {
@@ -3503,21 +3794,16 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3503
3794
  }
3504
3795
  });
3505
3796
  containerWrap.appendChild(itemsWrap);
3506
- left.innerHTML = `<span>${element.label || element.key}</span>`;
3507
3797
  wrapper.appendChild(containerWrap);
3508
3798
  }
3509
3799
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3510
3800
  const state = ctx.state;
3511
3801
  const containerWrap = document.createElement("div");
3512
3802
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3513
- const header = document.createElement("div");
3514
- header.className = "flex justify-between items-center mb-4";
3515
- const left = document.createElement("div");
3516
- left.className = "flex-1";
3803
+ const countDisplay = document.createElement("span");
3804
+ countDisplay.className = "text-sm text-gray-500";
3517
3805
  const itemsWrap = document.createElement("div");
3518
3806
  itemsWrap.className = "space-y-4";
3519
- containerWrap.appendChild(header);
3520
- header.appendChild(left);
3521
3807
  if (!ctx.state.config.readonly) {
3522
3808
  const hintsElement = createPrefillHints(element, element.key);
3523
3809
  if (hintsElement) {
@@ -3531,7 +3817,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3531
3817
  const createAddButton = () => {
3532
3818
  const add = document.createElement("button");
3533
3819
  add.type = "button";
3534
- add.className = "add-container-btn mt-2 px-3 py-1 rounded";
3820
+ add.className = "add-container-btn px-3 py-1 rounded";
3535
3821
  add.style.cssText = `
3536
3822
  color: var(--fb-primary-color);
3537
3823
  border: var(--fb-border-width) solid var(--fb-primary-color);
@@ -3612,7 +3898,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3612
3898
  existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3613
3899
  existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
3614
3900
  }
3615
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "\u221E" : max})</span>`;
3901
+ countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
3616
3902
  };
3617
3903
  if (pre && Array.isArray(pre)) {
3618
3904
  pre.forEach((prefillObj, idx) => {
@@ -3719,7 +4005,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3719
4005
  }
3720
4006
  containerWrap.appendChild(itemsWrap);
3721
4007
  if (!state.config.readonly) {
3722
- containerWrap.appendChild(createAddButton());
4008
+ const addRow = document.createElement("div");
4009
+ addRow.className = "flex items-center gap-3 mt-2";
4010
+ addRow.appendChild(createAddButton());
4011
+ addRow.appendChild(countDisplay);
4012
+ containerWrap.appendChild(addRow);
3723
4013
  }
3724
4014
  updateAddButton();
3725
4015
  wrapper.appendChild(containerWrap);
@@ -3744,16 +4034,17 @@ function validateContainerElement(element, key, context) {
3744
4034
  }
3745
4035
  const validateContainerCount = (key2, items, element2) => {
3746
4036
  if (skipValidation) return;
4037
+ const { state } = context;
3747
4038
  const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
3748
4039
  const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
3749
4040
  if (element2.required && items.length === 0) {
3750
- errors.push(`${key2}: required`);
4041
+ errors.push(`${key2}: ${t("required", state)}`);
3751
4042
  }
3752
4043
  if (items.length < minItems) {
3753
- errors.push(`${key2}: minimum ${minItems} items required`);
4044
+ errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
3754
4045
  }
3755
4046
  if (items.length > maxItems) {
3756
- errors.push(`${key2}: maximum ${maxItems} items allowed`);
4047
+ errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
3757
4048
  }
3758
4049
  };
3759
4050
  if ("multiple" in element && element.multiple) {
@@ -4300,7 +4591,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
4300
4591
  default: {
4301
4592
  const unsupported = document.createElement("div");
4302
4593
  unsupported.className = "text-red-500 text-sm";
4303
- unsupported.textContent = `Unsupported field type: ${element.type}`;
4594
+ unsupported.textContent = t("unsupportedFieldType", ctx.state, { type: element.type });
4304
4595
  wrapper.appendChild(unsupported);
4305
4596
  }
4306
4597
  }
@@ -4344,31 +4635,112 @@ var defaultConfig = {
4344
4635
  locale: "en",
4345
4636
  translations: {
4346
4637
  en: {
4347
- addElement: "Add Element",
4638
+ // UI texts
4348
4639
  removeElement: "Remove",
4349
- uploadText: "Upload",
4350
- dragDropText: "or drag and drop files",
4351
- dragDropTextSingle: "or drag and drop file",
4352
4640
  clickDragText: "Click or drag file",
4641
+ clickDragTextMultiple: "Click or drag files",
4353
4642
  noFileSelected: "No file selected",
4354
4643
  noFilesSelected: "No files selected",
4355
- downloadButton: "Download"
4644
+ downloadButton: "Download",
4645
+ changeButton: "Change",
4646
+ placeholderText: "Enter text",
4647
+ previewAlt: "Preview",
4648
+ previewUnavailable: "Preview unavailable",
4649
+ previewError: "Preview error",
4650
+ videoNotSupported: "Your browser does not support the video tag.",
4651
+ // Field hints
4652
+ hintLengthRange: "{min}-{max} chars",
4653
+ hintMaxLength: "\u2264{max} chars",
4654
+ hintMinLength: "\u2265{min} chars",
4655
+ hintValueRange: "{min}-{max}",
4656
+ hintMaxValue: "\u2264{max}",
4657
+ hintMinValue: "\u2265{min}",
4658
+ hintMaxSize: "\u2264{size}MB",
4659
+ hintFormats: "{formats}",
4660
+ hintRequired: "Required",
4661
+ hintOptional: "Optional",
4662
+ hintPattern: "Format: {pattern}",
4663
+ fileCountSingle: "{count} file",
4664
+ fileCountPlural: "{count} files",
4665
+ fileCountRange: "({min}-{max})",
4666
+ // Validation errors
4667
+ required: "Required",
4668
+ minItems: "Minimum {min} items required",
4669
+ maxItems: "Maximum {max} items allowed",
4670
+ minLength: "Minimum {min} characters",
4671
+ maxLength: "Maximum {max} characters",
4672
+ minValue: "Must be at least {min}",
4673
+ maxValue: "Must be at most {max}",
4674
+ patternMismatch: "Invalid format",
4675
+ invalidPattern: "Invalid pattern in schema",
4676
+ notANumber: "Must be a number",
4677
+ invalidHexColour: "Invalid hex color",
4678
+ minFiles: "Minimum {min} files required",
4679
+ maxFiles: "Maximum {max} files allowed",
4680
+ unsupportedFieldType: "Unsupported field type: {type}"
4356
4681
  },
4357
4682
  ru: {
4358
- addElement: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442",
4683
+ // UI texts
4359
4684
  removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
4360
- uploadText: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435",
4361
- dragDropText: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
4362
- dragDropTextSingle: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
4363
4685
  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",
4686
+ 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",
4364
4687
  noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
4365
4688
  noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
4366
- downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
4689
+ downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
4690
+ changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
4691
+ placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
4692
+ previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
4693
+ previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
4694
+ previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
4695
+ 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.",
4696
+ // Field hints
4697
+ hintLengthRange: "{min}-{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4698
+ hintMaxLength: "\u2264{max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4699
+ hintMinLength: "\u2265{min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4700
+ hintValueRange: "{min}-{max}",
4701
+ hintMaxValue: "\u2264{max}",
4702
+ hintMinValue: "\u2265{min}",
4703
+ hintMaxSize: "\u2264{size}\u041C\u0411",
4704
+ hintFormats: "{formats}",
4705
+ hintRequired: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4706
+ hintOptional: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435",
4707
+ hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
4708
+ fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
4709
+ fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
4710
+ fileCountRange: "({min}-{max})",
4711
+ // Validation errors
4712
+ required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
4713
+ minItems: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4714
+ maxItems: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432",
4715
+ minLength: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4716
+ maxLength: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432",
4717
+ 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}",
4718
+ 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}",
4719
+ patternMismatch: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442",
4720
+ invalidPattern: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u0442\u0442\u0435\u0440\u043D \u0432 \u0441\u0445\u0435\u043C\u0435",
4721
+ notANumber: "\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C",
4722
+ invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
4723
+ minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
4724
+ maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
4725
+ 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}"
4367
4726
  }
4368
4727
  },
4369
4728
  theme: {}
4370
4729
  };
4371
4730
  function createInstanceState(config) {
4731
+ const mergedTranslations = {
4732
+ ...defaultConfig.translations
4733
+ };
4734
+ if (config?.translations) {
4735
+ for (const [locale, userTranslations] of Object.entries(
4736
+ config.translations
4737
+ )) {
4738
+ mergedTranslations[locale] = {
4739
+ ...defaultConfig.translations[locale] || {},
4740
+ ...userTranslations
4741
+ };
4742
+ }
4743
+ }
4372
4744
  return {
4373
4745
  schema: null,
4374
4746
  formRoot: null,
@@ -4377,7 +4749,8 @@ function createInstanceState(config) {
4377
4749
  version: "1.0.0",
4378
4750
  config: {
4379
4751
  ...defaultConfig,
4380
- ...config
4752
+ ...config,
4753
+ translations: mergedTranslations
4381
4754
  },
4382
4755
  debounceTimer: null
4383
4756
  };