@dmitryvim/form-builder 0.1.42 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +260 -22
  2. package/dist/browser/formbuilder.min.js +184 -0
  3. package/dist/browser/formbuilder.v0.2.1.min.js +184 -0
  4. package/dist/cjs/index.cjs +3652 -0
  5. package/dist/cjs/index.cjs.map +1 -0
  6. package/dist/esm/index.js +3603 -0
  7. package/dist/esm/index.js.map +1 -0
  8. package/dist/form-builder.js +154 -3369
  9. package/dist/types/components/container.d.ts +15 -0
  10. package/dist/types/components/file.d.ts +26 -0
  11. package/dist/types/components/group.d.ts +24 -0
  12. package/dist/types/components/index.d.ts +11 -0
  13. package/dist/types/components/number.d.ts +11 -0
  14. package/dist/types/components/registry.d.ts +15 -0
  15. package/dist/types/components/select.d.ts +11 -0
  16. package/dist/types/components/text.d.ts +11 -0
  17. package/dist/types/components/textarea.d.ts +11 -0
  18. package/dist/types/index.d.ts +33 -0
  19. package/dist/types/instance/FormBuilderInstance.d.ts +134 -0
  20. package/dist/types/instance/state.d.ts +13 -0
  21. package/dist/types/styles/theme.d.ts +63 -0
  22. package/dist/types/types/component-operations.d.ts +45 -0
  23. package/dist/types/types/config.d.ts +47 -0
  24. package/dist/types/types/index.d.ts +4 -0
  25. package/dist/types/types/schema.d.ts +115 -0
  26. package/dist/types/types/state.d.ts +11 -0
  27. package/dist/types/utils/helpers.d.ts +4 -0
  28. package/dist/types/utils/styles.d.ts +21 -0
  29. package/dist/types/utils/translation.d.ts +8 -0
  30. package/dist/types/utils/validation.d.ts +2 -0
  31. package/package.json +32 -15
  32. package/dist/demo.js +0 -861
  33. package/dist/elements.html +0 -1130
  34. package/dist/elements.js +0 -488
  35. package/dist/index.html +0 -315
@@ -0,0 +1,3603 @@
1
+ // 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`);
10
+ }
11
+ }
12
+ }
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}`);
21
+ }
22
+ }
23
+ }
24
+ function addFileSizeHint(element, parts) {
25
+ if (element.maxSizeMB) {
26
+ parts.push(`max_size=${element.maxSizeMB}MB`);
27
+ }
28
+ }
29
+ function addFormatHint(element, parts) {
30
+ if (element.accept?.extensions) {
31
+ parts.push(
32
+ `formats=${element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")}`
33
+ );
34
+ }
35
+ }
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");
41
+ }
42
+ }
43
+ function makeFieldHint(element) {
44
+ 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);
51
+ return parts.join(" \u2022 ");
52
+ }
53
+ function validateSchema(schema) {
54
+ const errors = [];
55
+ if (!schema || typeof schema !== "object") {
56
+ errors.push("Schema must be an object");
57
+ return errors;
58
+ }
59
+ if (!schema.version) {
60
+ errors.push("Schema missing version");
61
+ }
62
+ if (!Array.isArray(schema.elements)) {
63
+ errors.push("Schema missing elements array");
64
+ return errors;
65
+ }
66
+ function validateElements(elements, path) {
67
+ elements.forEach((element, index) => {
68
+ const elementPath = `${path}[${index}]`;
69
+ if (!element.type) {
70
+ errors.push(`${elementPath}: missing type`);
71
+ }
72
+ if (!element.key) {
73
+ errors.push(`${elementPath}: missing key`);
74
+ }
75
+ if (element.type === "group" && "elements" in element && element.elements) {
76
+ validateElements(element.elements, `${elementPath}.elements`);
77
+ }
78
+ if (element.type === "container" && element.elements) {
79
+ validateElements(element.elements, `${elementPath}.elements`);
80
+ }
81
+ if (element.type === "select" && element.options) {
82
+ const defaultValue = element.default;
83
+ if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
84
+ const hasMatchingOption = element.options.some(
85
+ (opt) => opt.value === defaultValue
86
+ );
87
+ if (!hasMatchingOption) {
88
+ errors.push(
89
+ `${elementPath}: default "${defaultValue}" not in options`
90
+ );
91
+ }
92
+ }
93
+ }
94
+ });
95
+ }
96
+ if (Array.isArray(schema.elements))
97
+ validateElements(schema.elements, "elements");
98
+ return errors;
99
+ }
100
+
101
+ // src/utils/helpers.ts
102
+ function isPlainObject(obj) {
103
+ return obj && typeof obj === "object" && obj.constructor === Object;
104
+ }
105
+ function pathJoin(base, key) {
106
+ return base ? `${base}.${key}` : key;
107
+ }
108
+ function clear(node) {
109
+ while (node.firstChild) node.removeChild(node.firstChild);
110
+ }
111
+
112
+ // src/components/text.ts
113
+ function renderTextElement(element, ctx, wrapper, pathKey) {
114
+ const state = ctx.state;
115
+ const textInput = document.createElement("input");
116
+ textInput.type = "text";
117
+ textInput.className = "w-full rounded-lg";
118
+ textInput.style.cssText = `
119
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
120
+ border: var(--fb-border-width) solid var(--fb-border-color);
121
+ border-radius: var(--fb-border-radius);
122
+ background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
123
+ color: var(--fb-text-color);
124
+ font-size: var(--fb-font-size);
125
+ font-family: var(--fb-font-family);
126
+ transition: all var(--fb-transition-duration) ease-in-out;
127
+ `;
128
+ textInput.name = pathKey;
129
+ textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
130
+ textInput.value = ctx.prefill[element.key] || element.default || "";
131
+ textInput.readOnly = state.config.readonly;
132
+ if (!state.config.readonly) {
133
+ textInput.addEventListener("focus", () => {
134
+ textInput.style.borderColor = "var(--fb-border-focus-color)";
135
+ textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
136
+ textInput.style.outlineOffset = "0";
137
+ });
138
+ textInput.addEventListener("blur", () => {
139
+ textInput.style.borderColor = "var(--fb-border-color)";
140
+ textInput.style.outline = "none";
141
+ });
142
+ textInput.addEventListener("mouseenter", () => {
143
+ if (document.activeElement !== textInput) {
144
+ textInput.style.borderColor = "var(--fb-border-hover-color)";
145
+ }
146
+ });
147
+ textInput.addEventListener("mouseleave", () => {
148
+ if (document.activeElement !== textInput) {
149
+ textInput.style.borderColor = "var(--fb-border-color)";
150
+ }
151
+ });
152
+ }
153
+ if (!state.config.readonly && ctx.instance) {
154
+ const handleChange = () => {
155
+ ctx.instance.triggerOnChange(pathKey, textInput.value);
156
+ };
157
+ textInput.addEventListener("blur", handleChange);
158
+ textInput.addEventListener("input", handleChange);
159
+ }
160
+ wrapper.appendChild(textInput);
161
+ const textHint = document.createElement("p");
162
+ textHint.className = "mt-1";
163
+ textHint.style.cssText = `
164
+ font-size: var(--fb-font-size-small);
165
+ color: var(--fb-text-secondary-color);
166
+ `;
167
+ textHint.textContent = makeFieldHint(element);
168
+ wrapper.appendChild(textHint);
169
+ }
170
+ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
171
+ const state = ctx.state;
172
+ const prefillValues = ctx.prefill[element.key] || [];
173
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
174
+ const minCount = element.minCount ?? 1;
175
+ const maxCount = element.maxCount ?? Infinity;
176
+ while (values.length < minCount) {
177
+ values.push(element.default || "");
178
+ }
179
+ const container = document.createElement("div");
180
+ container.className = "space-y-2";
181
+ wrapper.appendChild(container);
182
+ function updateIndices() {
183
+ const items = container.querySelectorAll(".multiple-text-item");
184
+ items.forEach((item, index) => {
185
+ const input = item.querySelector("input");
186
+ if (input) {
187
+ input.name = `${pathKey}[${index}]`;
188
+ }
189
+ });
190
+ }
191
+ function addTextItem(value = "", index = -1) {
192
+ const itemWrapper = document.createElement("div");
193
+ itemWrapper.className = "multiple-text-item flex items-center gap-2";
194
+ const textInput = document.createElement("input");
195
+ textInput.type = "text";
196
+ textInput.className = "flex-1";
197
+ textInput.style.cssText = `
198
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
199
+ border: var(--fb-border-width) solid var(--fb-border-color);
200
+ border-radius: var(--fb-border-radius);
201
+ background-color: ${state.config.readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
202
+ color: var(--fb-text-color);
203
+ font-size: var(--fb-font-size);
204
+ font-family: var(--fb-font-family);
205
+ transition: all var(--fb-transition-duration) ease-in-out;
206
+ `;
207
+ textInput.placeholder = element.placeholder || "Enter text";
208
+ textInput.value = value;
209
+ textInput.readOnly = state.config.readonly;
210
+ if (!state.config.readonly) {
211
+ textInput.addEventListener("focus", () => {
212
+ textInput.style.borderColor = "var(--fb-border-focus-color)";
213
+ textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
214
+ textInput.style.outlineOffset = "0";
215
+ });
216
+ textInput.addEventListener("blur", () => {
217
+ textInput.style.borderColor = "var(--fb-border-color)";
218
+ textInput.style.outline = "none";
219
+ });
220
+ textInput.addEventListener("mouseenter", () => {
221
+ if (document.activeElement !== textInput) {
222
+ textInput.style.borderColor = "var(--fb-border-hover-color)";
223
+ }
224
+ });
225
+ textInput.addEventListener("mouseleave", () => {
226
+ if (document.activeElement !== textInput) {
227
+ textInput.style.borderColor = "var(--fb-border-color)";
228
+ }
229
+ });
230
+ }
231
+ if (!state.config.readonly && ctx.instance) {
232
+ const handleChange = () => {
233
+ ctx.instance.triggerOnChange(textInput.name, textInput.value);
234
+ };
235
+ textInput.addEventListener("blur", handleChange);
236
+ textInput.addEventListener("input", handleChange);
237
+ }
238
+ itemWrapper.appendChild(textInput);
239
+ if (index === -1) {
240
+ container.appendChild(itemWrapper);
241
+ } else {
242
+ container.insertBefore(itemWrapper, container.children[index]);
243
+ }
244
+ updateIndices();
245
+ return itemWrapper;
246
+ }
247
+ function updateRemoveButtons() {
248
+ if (state.config.readonly) return;
249
+ const items = container.querySelectorAll(".multiple-text-item");
250
+ const currentCount = items.length;
251
+ items.forEach((item) => {
252
+ let removeBtn = item.querySelector(
253
+ ".remove-item-btn"
254
+ );
255
+ if (!removeBtn) {
256
+ removeBtn = document.createElement("button");
257
+ removeBtn.type = "button";
258
+ removeBtn.className = "remove-item-btn px-2 py-1 rounded";
259
+ removeBtn.style.cssText = `
260
+ color: var(--fb-error-color);
261
+ background-color: transparent;
262
+ transition: background-color var(--fb-transition-duration);
263
+ `;
264
+ removeBtn.innerHTML = "\u2715";
265
+ removeBtn.addEventListener("mouseenter", () => {
266
+ removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
267
+ });
268
+ removeBtn.addEventListener("mouseleave", () => {
269
+ removeBtn.style.backgroundColor = "transparent";
270
+ });
271
+ removeBtn.onclick = () => {
272
+ const currentIndex = Array.from(container.children).indexOf(
273
+ item
274
+ );
275
+ if (container.children.length > minCount) {
276
+ values.splice(currentIndex, 1);
277
+ item.remove();
278
+ updateIndices();
279
+ updateAddButton();
280
+ updateRemoveButtons();
281
+ }
282
+ };
283
+ item.appendChild(removeBtn);
284
+ }
285
+ const disabled = currentCount <= minCount;
286
+ removeBtn.disabled = disabled;
287
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
288
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
289
+ });
290
+ }
291
+ function updateAddButton() {
292
+ const existingAddBtn = wrapper.querySelector(".add-text-btn");
293
+ if (existingAddBtn) existingAddBtn.remove();
294
+ if (!state.config.readonly && values.length < maxCount) {
295
+ const addBtn = document.createElement("button");
296
+ addBtn.type = "button";
297
+ addBtn.className = "add-text-btn mt-2 px-3 py-1 rounded";
298
+ addBtn.style.cssText = `
299
+ color: var(--fb-primary-color);
300
+ border: var(--fb-border-width) solid var(--fb-primary-color);
301
+ background-color: transparent;
302
+ font-size: var(--fb-font-size);
303
+ transition: all var(--fb-transition-duration);
304
+ `;
305
+ addBtn.textContent = `+ Add ${element.label || "Text"}`;
306
+ addBtn.addEventListener("mouseenter", () => {
307
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
308
+ });
309
+ addBtn.addEventListener("mouseleave", () => {
310
+ addBtn.style.backgroundColor = "transparent";
311
+ });
312
+ addBtn.onclick = () => {
313
+ values.push(element.default || "");
314
+ addTextItem(element.default || "");
315
+ updateAddButton();
316
+ updateRemoveButtons();
317
+ };
318
+ wrapper.appendChild(addBtn);
319
+ }
320
+ }
321
+ values.forEach((value) => addTextItem(value));
322
+ updateAddButton();
323
+ updateRemoveButtons();
324
+ const hint = document.createElement("p");
325
+ hint.className = "mt-1";
326
+ hint.style.cssText = `
327
+ font-size: var(--fb-font-size-small);
328
+ color: var(--fb-text-secondary-color);
329
+ `;
330
+ hint.textContent = makeFieldHint(element);
331
+ wrapper.appendChild(hint);
332
+ }
333
+ function validateTextElement(element, key, context) {
334
+ const errors = [];
335
+ const { scopeRoot, skipValidation } = context;
336
+ const markValidity = (input, errorMessage) => {
337
+ if (!input) return;
338
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
339
+ let errorElement = document.getElementById(errorId);
340
+ if (errorMessage) {
341
+ input.classList.add("invalid");
342
+ input.title = errorMessage;
343
+ if (!errorElement) {
344
+ errorElement = document.createElement("div");
345
+ errorElement.id = errorId;
346
+ errorElement.className = "error-message";
347
+ errorElement.style.cssText = `
348
+ color: var(--fb-error-color);
349
+ font-size: var(--fb-font-size-small);
350
+ margin-top: 0.25rem;
351
+ `;
352
+ if (input.nextSibling) {
353
+ input.parentNode?.insertBefore(errorElement, input.nextSibling);
354
+ } else {
355
+ input.parentNode?.appendChild(errorElement);
356
+ }
357
+ }
358
+ errorElement.textContent = errorMessage;
359
+ errorElement.style.display = "block";
360
+ } else {
361
+ input.classList.remove("invalid");
362
+ input.title = "";
363
+ if (errorElement) {
364
+ errorElement.remove();
365
+ }
366
+ }
367
+ };
368
+ const validateTextInput = (input, val, fieldKey) => {
369
+ let hasError = false;
370
+ if (!skipValidation && val) {
371
+ if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
372
+ errors.push(`${fieldKey}: minLength=${element.minLength}`);
373
+ markValidity(input, `minLength=${element.minLength}`);
374
+ hasError = true;
375
+ } else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
376
+ errors.push(`${fieldKey}: maxLength=${element.maxLength}`);
377
+ markValidity(input, `maxLength=${element.maxLength}`);
378
+ hasError = true;
379
+ } else if (element.pattern) {
380
+ try {
381
+ const re = new RegExp(element.pattern);
382
+ if (!re.test(val)) {
383
+ errors.push(`${fieldKey}: pattern mismatch`);
384
+ markValidity(input, "pattern mismatch");
385
+ hasError = true;
386
+ }
387
+ } catch {
388
+ errors.push(`${fieldKey}: invalid pattern`);
389
+ markValidity(input, "invalid pattern");
390
+ hasError = true;
391
+ }
392
+ }
393
+ }
394
+ if (!hasError) {
395
+ markValidity(input, null);
396
+ }
397
+ };
398
+ if (element.multiple) {
399
+ const inputs = scopeRoot.querySelectorAll(
400
+ `[name^="${key}["]`
401
+ );
402
+ const values = [];
403
+ inputs.forEach((input, index) => {
404
+ const val = input?.value ?? "";
405
+ values.push(val);
406
+ validateTextInput(input, val, `${key}[${index}]`);
407
+ });
408
+ if (!skipValidation) {
409
+ const minCount = element.minCount ?? 1;
410
+ const maxCount = element.maxCount ?? Infinity;
411
+ const filteredValues = values.filter((v) => v.trim() !== "");
412
+ if (element.required && filteredValues.length === 0) {
413
+ errors.push(`${key}: required`);
414
+ }
415
+ if (filteredValues.length < minCount) {
416
+ errors.push(`${key}: minimum ${minCount} items required`);
417
+ }
418
+ if (filteredValues.length > maxCount) {
419
+ errors.push(`${key}: maximum ${maxCount} items allowed`);
420
+ }
421
+ }
422
+ return { value: values, errors };
423
+ } else {
424
+ const input = scopeRoot.querySelector(
425
+ `[name$="${key}"]`
426
+ );
427
+ const val = input?.value ?? "";
428
+ if (!skipValidation && element.required && val === "") {
429
+ errors.push(`${key}: required`);
430
+ markValidity(input, "required");
431
+ return { value: "", errors };
432
+ }
433
+ validateTextInput(input, val, key);
434
+ return { value: val, errors };
435
+ }
436
+ }
437
+ function updateTextField(element, fieldPath, value, context) {
438
+ const { scopeRoot } = context;
439
+ if (element.multiple) {
440
+ if (!Array.isArray(value)) {
441
+ console.warn(
442
+ `updateTextField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
443
+ );
444
+ return;
445
+ }
446
+ const inputs = scopeRoot.querySelectorAll(
447
+ `[name^="${fieldPath}["]`
448
+ );
449
+ inputs.forEach((input, index) => {
450
+ if (index < value.length) {
451
+ input.value = value[index] != null ? String(value[index]) : "";
452
+ input.classList.remove("invalid");
453
+ input.title = "";
454
+ }
455
+ });
456
+ if (value.length !== inputs.length) {
457
+ console.warn(
458
+ `updateTextField: Multiple field "${fieldPath}" has ${inputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
459
+ );
460
+ }
461
+ } else {
462
+ const input = scopeRoot.querySelector(
463
+ `[name="${fieldPath}"]`
464
+ );
465
+ if (input) {
466
+ input.value = value != null ? String(value) : "";
467
+ input.classList.remove("invalid");
468
+ input.title = "";
469
+ }
470
+ }
471
+ }
472
+
473
+ // src/components/textarea.ts
474
+ function renderTextareaElement(element, ctx, wrapper, pathKey) {
475
+ const state = ctx.state;
476
+ const textareaInput = document.createElement("textarea");
477
+ 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";
478
+ textareaInput.name = pathKey;
479
+ textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
480
+ textareaInput.rows = element.rows || 4;
481
+ textareaInput.value = ctx.prefill[element.key] || element.default || "";
482
+ textareaInput.readOnly = state.config.readonly;
483
+ if (!state.config.readonly && ctx.instance) {
484
+ const handleChange = () => {
485
+ ctx.instance.triggerOnChange(pathKey, textareaInput.value);
486
+ };
487
+ textareaInput.addEventListener("blur", handleChange);
488
+ textareaInput.addEventListener("input", handleChange);
489
+ }
490
+ wrapper.appendChild(textareaInput);
491
+ const textareaHint = document.createElement("p");
492
+ textareaHint.className = "text-xs text-gray-500 mt-1";
493
+ textareaHint.textContent = makeFieldHint(element);
494
+ wrapper.appendChild(textareaHint);
495
+ }
496
+ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
497
+ const state = ctx.state;
498
+ const prefillValues = ctx.prefill[element.key] || [];
499
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
500
+ const minCount = element.minCount ?? 1;
501
+ const maxCount = element.maxCount ?? Infinity;
502
+ while (values.length < minCount) {
503
+ values.push(element.default || "");
504
+ }
505
+ const container = document.createElement("div");
506
+ container.className = "space-y-2";
507
+ wrapper.appendChild(container);
508
+ function updateIndices() {
509
+ const items = container.querySelectorAll(".multiple-textarea-item");
510
+ items.forEach((item, index) => {
511
+ const textarea = item.querySelector("textarea");
512
+ if (textarea) {
513
+ textarea.name = `${pathKey}[${index}]`;
514
+ }
515
+ });
516
+ }
517
+ function addTextareaItem(value = "", index = -1) {
518
+ const itemWrapper = document.createElement("div");
519
+ itemWrapper.className = "multiple-textarea-item";
520
+ const textareaInput = document.createElement("textarea");
521
+ 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";
522
+ textareaInput.placeholder = element.placeholder || "Enter text";
523
+ textareaInput.rows = element.rows || 4;
524
+ textareaInput.value = value;
525
+ textareaInput.readOnly = state.config.readonly;
526
+ if (!state.config.readonly && ctx.instance) {
527
+ const handleChange = () => {
528
+ ctx.instance.triggerOnChange(textareaInput.name, textareaInput.value);
529
+ };
530
+ textareaInput.addEventListener("blur", handleChange);
531
+ textareaInput.addEventListener("input", handleChange);
532
+ }
533
+ itemWrapper.appendChild(textareaInput);
534
+ if (index === -1) {
535
+ container.appendChild(itemWrapper);
536
+ } else {
537
+ container.insertBefore(itemWrapper, container.children[index]);
538
+ }
539
+ updateIndices();
540
+ return itemWrapper;
541
+ }
542
+ function updateRemoveButtons() {
543
+ if (state.config.readonly) return;
544
+ const items = container.querySelectorAll(".multiple-textarea-item");
545
+ const currentCount = items.length;
546
+ items.forEach((item) => {
547
+ let removeBtn = item.querySelector(
548
+ ".remove-item-btn"
549
+ );
550
+ if (!removeBtn) {
551
+ removeBtn = document.createElement("button");
552
+ removeBtn.type = "button";
553
+ removeBtn.className = "remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
554
+ removeBtn.innerHTML = "\u2715 Remove";
555
+ removeBtn.onclick = () => {
556
+ const currentIndex = Array.from(container.children).indexOf(
557
+ item
558
+ );
559
+ if (container.children.length > minCount) {
560
+ values.splice(currentIndex, 1);
561
+ item.remove();
562
+ updateIndices();
563
+ updateAddButton();
564
+ updateRemoveButtons();
565
+ }
566
+ };
567
+ item.appendChild(removeBtn);
568
+ }
569
+ const disabled = currentCount <= minCount;
570
+ removeBtn.disabled = disabled;
571
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
572
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
573
+ });
574
+ }
575
+ function updateAddButton() {
576
+ const existingAddBtn = wrapper.querySelector(".add-textarea-btn");
577
+ if (existingAddBtn) existingAddBtn.remove();
578
+ if (!state.config.readonly && values.length < maxCount) {
579
+ const addBtn = document.createElement("button");
580
+ addBtn.type = "button";
581
+ 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";
582
+ addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
583
+ addBtn.onclick = () => {
584
+ values.push(element.default || "");
585
+ addTextareaItem(element.default || "");
586
+ updateAddButton();
587
+ updateRemoveButtons();
588
+ };
589
+ wrapper.appendChild(addBtn);
590
+ }
591
+ }
592
+ values.forEach((value) => addTextareaItem(value));
593
+ updateAddButton();
594
+ updateRemoveButtons();
595
+ const hint = document.createElement("p");
596
+ hint.className = "text-xs text-gray-500 mt-1";
597
+ hint.textContent = makeFieldHint(element);
598
+ wrapper.appendChild(hint);
599
+ }
600
+ function validateTextareaElement(element, key, context) {
601
+ return validateTextElement(element, key, context);
602
+ }
603
+ function updateTextareaField(element, fieldPath, value, context) {
604
+ updateTextField(element, fieldPath, value, context);
605
+ }
606
+
607
+ // src/components/number.ts
608
+ function renderNumberElement(element, ctx, wrapper, pathKey) {
609
+ const state = ctx.state;
610
+ const numberInput = document.createElement("input");
611
+ numberInput.type = "number";
612
+ 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";
613
+ numberInput.name = pathKey;
614
+ numberInput.placeholder = element.placeholder || "0";
615
+ if (element.min !== void 0) numberInput.min = element.min.toString();
616
+ if (element.max !== void 0) numberInput.max = element.max.toString();
617
+ if (element.step !== void 0) numberInput.step = element.step.toString();
618
+ numberInput.value = ctx.prefill[element.key] || element.default || "";
619
+ numberInput.readOnly = state.config.readonly;
620
+ if (!state.config.readonly && ctx.instance) {
621
+ const handleChange = () => {
622
+ const value = numberInput.value ? parseFloat(numberInput.value) : null;
623
+ ctx.instance.triggerOnChange(pathKey, value);
624
+ };
625
+ numberInput.addEventListener("blur", handleChange);
626
+ numberInput.addEventListener("input", handleChange);
627
+ }
628
+ wrapper.appendChild(numberInput);
629
+ const numberHint = document.createElement("p");
630
+ numberHint.className = "text-xs text-gray-500 mt-1";
631
+ numberHint.textContent = makeFieldHint(element);
632
+ wrapper.appendChild(numberHint);
633
+ }
634
+ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
635
+ const state = ctx.state;
636
+ const prefillValues = ctx.prefill[element.key] || [];
637
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
638
+ const minCount = element.minCount ?? 1;
639
+ const maxCount = element.maxCount ?? Infinity;
640
+ while (values.length < minCount) {
641
+ values.push(element.default || "");
642
+ }
643
+ const container = document.createElement("div");
644
+ container.className = "space-y-2";
645
+ wrapper.appendChild(container);
646
+ function updateIndices() {
647
+ const items = container.querySelectorAll(".multiple-number-item");
648
+ items.forEach((item, index) => {
649
+ const input = item.querySelector("input");
650
+ if (input) {
651
+ input.name = `${pathKey}[${index}]`;
652
+ }
653
+ });
654
+ }
655
+ function addNumberItem(value = "", index = -1) {
656
+ const itemWrapper = document.createElement("div");
657
+ itemWrapper.className = "multiple-number-item flex items-center gap-2";
658
+ const numberInput = document.createElement("input");
659
+ numberInput.type = "number";
660
+ 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";
661
+ numberInput.placeholder = element.placeholder || "0";
662
+ if (element.min !== void 0) numberInput.min = element.min.toString();
663
+ if (element.max !== void 0) numberInput.max = element.max.toString();
664
+ if (element.step !== void 0) numberInput.step = element.step.toString();
665
+ numberInput.value = value.toString();
666
+ numberInput.readOnly = state.config.readonly;
667
+ if (!state.config.readonly && ctx.instance) {
668
+ const handleChange = () => {
669
+ const val = numberInput.value ? parseFloat(numberInput.value) : null;
670
+ ctx.instance.triggerOnChange(numberInput.name, val);
671
+ };
672
+ numberInput.addEventListener("blur", handleChange);
673
+ numberInput.addEventListener("input", handleChange);
674
+ }
675
+ itemWrapper.appendChild(numberInput);
676
+ if (index === -1) {
677
+ container.appendChild(itemWrapper);
678
+ } else {
679
+ container.insertBefore(itemWrapper, container.children[index]);
680
+ }
681
+ updateIndices();
682
+ return itemWrapper;
683
+ }
684
+ function updateRemoveButtons() {
685
+ if (state.config.readonly) return;
686
+ const items = container.querySelectorAll(".multiple-number-item");
687
+ const currentCount = items.length;
688
+ items.forEach((item) => {
689
+ let removeBtn = item.querySelector(
690
+ ".remove-item-btn"
691
+ );
692
+ if (!removeBtn) {
693
+ removeBtn = document.createElement("button");
694
+ removeBtn.type = "button";
695
+ removeBtn.className = "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
696
+ removeBtn.innerHTML = "\u2715";
697
+ removeBtn.onclick = () => {
698
+ const currentIndex = Array.from(container.children).indexOf(
699
+ item
700
+ );
701
+ if (container.children.length > minCount) {
702
+ values.splice(currentIndex, 1);
703
+ item.remove();
704
+ updateIndices();
705
+ updateAddButton();
706
+ updateRemoveButtons();
707
+ }
708
+ };
709
+ item.appendChild(removeBtn);
710
+ }
711
+ const disabled = currentCount <= minCount;
712
+ removeBtn.disabled = disabled;
713
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
714
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
715
+ });
716
+ }
717
+ function updateAddButton() {
718
+ const existingAddBtn = wrapper.querySelector(".add-number-btn");
719
+ if (existingAddBtn) existingAddBtn.remove();
720
+ if (!state.config.readonly && values.length < maxCount) {
721
+ const addBtn = document.createElement("button");
722
+ addBtn.type = "button";
723
+ 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";
724
+ addBtn.textContent = `+ Add ${element.label || "Number"}`;
725
+ addBtn.onclick = () => {
726
+ values.push(element.default || "");
727
+ addNumberItem(element.default || "");
728
+ updateAddButton();
729
+ updateRemoveButtons();
730
+ };
731
+ wrapper.appendChild(addBtn);
732
+ }
733
+ }
734
+ values.forEach((value) => addNumberItem(value));
735
+ updateAddButton();
736
+ updateRemoveButtons();
737
+ const hint = document.createElement("p");
738
+ hint.className = "text-xs text-gray-500 mt-1";
739
+ hint.textContent = makeFieldHint(element);
740
+ wrapper.appendChild(hint);
741
+ }
742
+ function validateNumberElement(element, key, context) {
743
+ const errors = [];
744
+ const { scopeRoot, skipValidation } = context;
745
+ const markValidity = (input, errorMessage) => {
746
+ if (!input) return;
747
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
748
+ let errorElement = document.getElementById(errorId);
749
+ if (errorMessage) {
750
+ input.classList.add("invalid");
751
+ input.title = errorMessage;
752
+ if (!errorElement) {
753
+ errorElement = document.createElement("div");
754
+ errorElement.id = errorId;
755
+ errorElement.className = "error-message";
756
+ errorElement.style.cssText = `
757
+ color: var(--fb-error-color);
758
+ font-size: var(--fb-font-size-small);
759
+ margin-top: 0.25rem;
760
+ `;
761
+ if (input.nextSibling) {
762
+ input.parentNode?.insertBefore(errorElement, input.nextSibling);
763
+ } else {
764
+ input.parentNode?.appendChild(errorElement);
765
+ }
766
+ }
767
+ errorElement.textContent = errorMessage;
768
+ errorElement.style.display = "block";
769
+ } else {
770
+ input.classList.remove("invalid");
771
+ input.title = "";
772
+ if (errorElement) {
773
+ errorElement.remove();
774
+ }
775
+ }
776
+ };
777
+ const validateNumberInput = (input, v, fieldKey) => {
778
+ let hasError = false;
779
+ if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
780
+ errors.push(`${fieldKey}: < min=${element.min}`);
781
+ markValidity(input, `< min=${element.min}`);
782
+ hasError = true;
783
+ } else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
784
+ errors.push(`${fieldKey}: > max=${element.max}`);
785
+ markValidity(input, `> max=${element.max}`);
786
+ hasError = true;
787
+ }
788
+ if (!hasError) {
789
+ markValidity(input, null);
790
+ }
791
+ };
792
+ if (element.multiple) {
793
+ const inputs = scopeRoot.querySelectorAll(
794
+ `[name^="${key}["]`
795
+ );
796
+ const values = [];
797
+ inputs.forEach((input, index) => {
798
+ const raw = input?.value ?? "";
799
+ if (raw === "") {
800
+ values.push(null);
801
+ markValidity(input, null);
802
+ return;
803
+ }
804
+ const v = parseFloat(raw);
805
+ if (!skipValidation && !Number.isFinite(v)) {
806
+ errors.push(`${key}[${index}]: not a number`);
807
+ markValidity(input, "not a number");
808
+ values.push(null);
809
+ return;
810
+ }
811
+ validateNumberInput(input, v, `${key}[${index}]`);
812
+ const d = Number.isInteger(element.decimals ?? 0) ? element.decimals ?? 0 : 0;
813
+ values.push(Number(v.toFixed(d)));
814
+ });
815
+ if (!skipValidation) {
816
+ const minCount = element.minCount ?? 1;
817
+ const maxCount = element.maxCount ?? Infinity;
818
+ const filteredValues = values.filter((v) => v !== null);
819
+ if (element.required && filteredValues.length === 0) {
820
+ errors.push(`${key}: required`);
821
+ }
822
+ if (filteredValues.length < minCount) {
823
+ errors.push(`${key}: minimum ${minCount} items required`);
824
+ }
825
+ if (filteredValues.length > maxCount) {
826
+ errors.push(`${key}: maximum ${maxCount} items allowed`);
827
+ }
828
+ }
829
+ return { value: values, errors };
830
+ } else {
831
+ const input = scopeRoot.querySelector(`[name$="${key}"]`);
832
+ const raw = input?.value ?? "";
833
+ if (!skipValidation && element.required && raw === "") {
834
+ errors.push(`${key}: required`);
835
+ markValidity(input, "required");
836
+ return { value: null, errors };
837
+ }
838
+ if (raw === "") {
839
+ markValidity(input, null);
840
+ return { value: null, errors };
841
+ }
842
+ const v = parseFloat(raw);
843
+ if (!skipValidation && !Number.isFinite(v)) {
844
+ errors.push(`${key}: not a number`);
845
+ markValidity(input, "not a number");
846
+ return { value: null, errors };
847
+ }
848
+ validateNumberInput(input, v, key);
849
+ const d = Number.isInteger(element.decimals ?? 0) ? element.decimals ?? 0 : 0;
850
+ return { value: Number(v.toFixed(d)), errors };
851
+ }
852
+ }
853
+ function updateNumberField(element, fieldPath, value, context) {
854
+ const { scopeRoot } = context;
855
+ if (element.multiple) {
856
+ if (!Array.isArray(value)) {
857
+ console.warn(
858
+ `updateNumberField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
859
+ );
860
+ return;
861
+ }
862
+ const inputs = scopeRoot.querySelectorAll(
863
+ `[name^="${fieldPath}["]`
864
+ );
865
+ inputs.forEach((input, index) => {
866
+ if (index < value.length) {
867
+ input.value = value[index] != null ? String(value[index]) : "";
868
+ input.classList.remove("invalid");
869
+ input.title = "";
870
+ }
871
+ });
872
+ if (value.length !== inputs.length) {
873
+ console.warn(
874
+ `updateNumberField: Multiple field "${fieldPath}" has ${inputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
875
+ );
876
+ }
877
+ } else {
878
+ const input = scopeRoot.querySelector(
879
+ `[name="${fieldPath}"]`
880
+ );
881
+ if (input) {
882
+ input.value = value != null ? String(value) : "";
883
+ input.classList.remove("invalid");
884
+ input.title = "";
885
+ }
886
+ }
887
+ }
888
+
889
+ // src/components/select.ts
890
+ function renderSelectElement(element, ctx, wrapper, pathKey) {
891
+ const state = ctx.state;
892
+ const selectInput = document.createElement("select");
893
+ selectInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
894
+ selectInput.name = pathKey;
895
+ selectInput.disabled = state.config.readonly;
896
+ (element.options || []).forEach((option) => {
897
+ const optionEl = document.createElement("option");
898
+ optionEl.value = option.value;
899
+ optionEl.textContent = option.label;
900
+ if ((ctx.prefill[element.key] || element.default) === option.value) {
901
+ optionEl.selected = true;
902
+ }
903
+ selectInput.appendChild(optionEl);
904
+ });
905
+ if (!state.config.readonly && ctx.instance) {
906
+ const handleChange = () => {
907
+ ctx.instance.triggerOnChange(pathKey, selectInput.value);
908
+ };
909
+ selectInput.addEventListener("change", handleChange);
910
+ }
911
+ wrapper.appendChild(selectInput);
912
+ const selectHint = document.createElement("p");
913
+ selectHint.className = "text-xs text-gray-500 mt-1";
914
+ selectHint.textContent = makeFieldHint(element);
915
+ wrapper.appendChild(selectHint);
916
+ }
917
+ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
918
+ const state = ctx.state;
919
+ const prefillValues = ctx.prefill[element.key] || [];
920
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
921
+ const minCount = element.minCount ?? 1;
922
+ const maxCount = element.maxCount ?? Infinity;
923
+ while (values.length < minCount) {
924
+ values.push(element.default || element.options?.[0]?.value || "");
925
+ }
926
+ const container = document.createElement("div");
927
+ container.className = "space-y-2";
928
+ wrapper.appendChild(container);
929
+ function updateIndices() {
930
+ const items = container.querySelectorAll(".multiple-select-item");
931
+ items.forEach((item, index) => {
932
+ const select = item.querySelector("select");
933
+ if (select) {
934
+ select.name = `${pathKey}[${index}]`;
935
+ }
936
+ });
937
+ }
938
+ function addSelectItem(value = "", index = -1) {
939
+ const itemWrapper = document.createElement("div");
940
+ itemWrapper.className = "multiple-select-item flex items-center gap-2";
941
+ const selectInput = document.createElement("select");
942
+ selectInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
943
+ selectInput.disabled = state.config.readonly;
944
+ (element.options || []).forEach((option) => {
945
+ const optionElement = document.createElement("option");
946
+ optionElement.value = option.value;
947
+ optionElement.textContent = option.label;
948
+ if (value === option.value) {
949
+ optionElement.selected = true;
950
+ }
951
+ selectInput.appendChild(optionElement);
952
+ });
953
+ if (!state.config.readonly && ctx.instance) {
954
+ const handleChange = () => {
955
+ ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
956
+ };
957
+ selectInput.addEventListener("change", handleChange);
958
+ }
959
+ itemWrapper.appendChild(selectInput);
960
+ if (index === -1) {
961
+ container.appendChild(itemWrapper);
962
+ } else {
963
+ container.insertBefore(itemWrapper, container.children[index]);
964
+ }
965
+ updateIndices();
966
+ return itemWrapper;
967
+ }
968
+ function updateRemoveButtons() {
969
+ if (state.config.readonly) return;
970
+ const items = container.querySelectorAll(".multiple-select-item");
971
+ const currentCount = items.length;
972
+ items.forEach((item) => {
973
+ let removeBtn = item.querySelector(
974
+ ".remove-item-btn"
975
+ );
976
+ if (!removeBtn) {
977
+ removeBtn = document.createElement("button");
978
+ removeBtn.type = "button";
979
+ removeBtn.className = "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
980
+ removeBtn.innerHTML = "\u2715";
981
+ removeBtn.onclick = () => {
982
+ const currentIndex = Array.from(container.children).indexOf(item);
983
+ if (container.children.length > minCount) {
984
+ values.splice(currentIndex, 1);
985
+ item.remove();
986
+ updateIndices();
987
+ updateAddButton();
988
+ updateRemoveButtons();
989
+ }
990
+ };
991
+ item.appendChild(removeBtn);
992
+ }
993
+ const disabled = currentCount <= minCount;
994
+ removeBtn.disabled = disabled;
995
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
996
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
997
+ });
998
+ }
999
+ function updateAddButton() {
1000
+ const existingAddBtn = wrapper.querySelector(".add-select-btn");
1001
+ if (existingAddBtn) existingAddBtn.remove();
1002
+ if (!state.config.readonly && values.length < maxCount) {
1003
+ const addBtn = document.createElement("button");
1004
+ addBtn.type = "button";
1005
+ 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";
1006
+ addBtn.textContent = `+ Add ${element.label || "Selection"}`;
1007
+ addBtn.onclick = () => {
1008
+ const defaultValue = element.default || element.options?.[0]?.value || "";
1009
+ values.push(defaultValue);
1010
+ addSelectItem(defaultValue);
1011
+ updateAddButton();
1012
+ updateRemoveButtons();
1013
+ };
1014
+ wrapper.appendChild(addBtn);
1015
+ }
1016
+ }
1017
+ values.forEach((value) => addSelectItem(value));
1018
+ updateAddButton();
1019
+ updateRemoveButtons();
1020
+ const hint = document.createElement("p");
1021
+ hint.className = "text-xs text-gray-500 mt-1";
1022
+ hint.textContent = makeFieldHint(element);
1023
+ wrapper.appendChild(hint);
1024
+ }
1025
+ function validateSelectElement(element, key, context) {
1026
+ const errors = [];
1027
+ const { scopeRoot, skipValidation } = context;
1028
+ const markValidity = (input, errorMessage) => {
1029
+ if (!input) return;
1030
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
1031
+ let errorElement = document.getElementById(errorId);
1032
+ if (errorMessage) {
1033
+ input.classList.add("invalid");
1034
+ input.title = errorMessage;
1035
+ if (!errorElement) {
1036
+ errorElement = document.createElement("div");
1037
+ errorElement.id = errorId;
1038
+ errorElement.className = "error-message";
1039
+ errorElement.style.cssText = `
1040
+ color: var(--fb-error-color);
1041
+ font-size: var(--fb-font-size-small);
1042
+ margin-top: 0.25rem;
1043
+ `;
1044
+ if (input.nextSibling) {
1045
+ input.parentNode?.insertBefore(errorElement, input.nextSibling);
1046
+ } else {
1047
+ input.parentNode?.appendChild(errorElement);
1048
+ }
1049
+ }
1050
+ errorElement.textContent = errorMessage;
1051
+ errorElement.style.display = "block";
1052
+ } else {
1053
+ input.classList.remove("invalid");
1054
+ input.title = "";
1055
+ if (errorElement) {
1056
+ errorElement.remove();
1057
+ }
1058
+ }
1059
+ };
1060
+ const validateMultipleCount = (key2, values, element2, filterFn) => {
1061
+ if (skipValidation) return;
1062
+ const filteredValues = values.filter(filterFn);
1063
+ const minCount = "minCount" in element2 ? element2.minCount ?? 1 : 1;
1064
+ const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
1065
+ if (element2.required && filteredValues.length === 0) {
1066
+ errors.push(`${key2}: required`);
1067
+ }
1068
+ if (filteredValues.length < minCount) {
1069
+ errors.push(`${key2}: minimum ${minCount} items required`);
1070
+ }
1071
+ if (filteredValues.length > maxCount) {
1072
+ errors.push(`${key2}: maximum ${maxCount} items allowed`);
1073
+ }
1074
+ };
1075
+ if ("multiple" in element && element.multiple) {
1076
+ const inputs = scopeRoot.querySelectorAll(
1077
+ `[name^="${key}["]`
1078
+ );
1079
+ const values = [];
1080
+ inputs.forEach((input) => {
1081
+ const val = input?.value ?? "";
1082
+ values.push(val);
1083
+ markValidity(input, null);
1084
+ });
1085
+ validateMultipleCount(key, values, element, (v) => v !== "");
1086
+ return { value: values, errors };
1087
+ } else {
1088
+ const input = scopeRoot.querySelector(
1089
+ `[name$="${key}"]`
1090
+ );
1091
+ const val = input?.value ?? "";
1092
+ if (!skipValidation && element.required && val === "") {
1093
+ errors.push(`${key}: required`);
1094
+ markValidity(input, "required");
1095
+ return { value: null, errors };
1096
+ } else {
1097
+ markValidity(input, null);
1098
+ }
1099
+ return { value: val === "" ? null : val, errors };
1100
+ }
1101
+ }
1102
+ function updateSelectField(element, fieldPath, value, context) {
1103
+ const { scopeRoot } = context;
1104
+ if ("multiple" in element && element.multiple) {
1105
+ if (!Array.isArray(value)) {
1106
+ console.warn(
1107
+ `updateSelectField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
1108
+ );
1109
+ return;
1110
+ }
1111
+ const selects = scopeRoot.querySelectorAll(
1112
+ `[name^="${fieldPath}["]`
1113
+ );
1114
+ selects.forEach((select, index) => {
1115
+ if (index < value.length) {
1116
+ select.value = value[index] != null ? String(value[index]) : "";
1117
+ const options = select.querySelectorAll("option");
1118
+ options.forEach((option) => {
1119
+ option.selected = option.value === String(value[index]);
1120
+ });
1121
+ select.classList.remove("invalid");
1122
+ select.title = "";
1123
+ }
1124
+ });
1125
+ if (value.length !== selects.length) {
1126
+ console.warn(
1127
+ `updateSelectField: Multiple field "${fieldPath}" has ${selects.length} selects but received ${value.length} values. Consider re-rendering for add/remove.`
1128
+ );
1129
+ }
1130
+ } else {
1131
+ const select = scopeRoot.querySelector(
1132
+ `[name="${fieldPath}"]`
1133
+ );
1134
+ if (select) {
1135
+ select.value = value != null ? String(value) : "";
1136
+ const options = select.querySelectorAll("option");
1137
+ options.forEach((option) => {
1138
+ option.selected = option.value === String(value);
1139
+ });
1140
+ select.classList.remove("invalid");
1141
+ select.title = "";
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ // src/utils/translation.ts
1147
+ function t(key, state) {
1148
+ const locale = state.config.locale || "en";
1149
+ const translations = state.config.translations[locale] || state.config.translations.en;
1150
+ return translations[key] || key;
1151
+ }
1152
+
1153
+ // src/components/file.ts
1154
+ async function renderFilePreview(container, resourceId, state, options = {}) {
1155
+ const { fileName = "", isReadonly = false, deps = null } = options;
1156
+ if (!isReadonly && deps && (!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)) {
1157
+ throw new Error(
1158
+ "renderFilePreview: missing deps {picker, fileUploadHandler, dragHandler}"
1159
+ );
1160
+ }
1161
+ clear(container);
1162
+ if (isReadonly) {
1163
+ container.classList.add("cursor-pointer");
1164
+ }
1165
+ const img = document.createElement("img");
1166
+ img.className = "w-full h-full object-contain";
1167
+ img.alt = fileName || "Preview";
1168
+ const meta = state.resourceIndex.get(resourceId);
1169
+ if (meta && meta.file && meta.file instanceof File) {
1170
+ if (meta.type && meta.type.startsWith("image/")) {
1171
+ const reader = new FileReader();
1172
+ reader.onload = (e) => {
1173
+ img.src = e.target?.result || "";
1174
+ };
1175
+ reader.readAsDataURL(meta.file);
1176
+ container.appendChild(img);
1177
+ } else if (meta.type && meta.type.startsWith("video/")) {
1178
+ const videoUrl = URL.createObjectURL(meta.file);
1179
+ container.onclick = null;
1180
+ const newContainer = container.cloneNode(false);
1181
+ if (container.parentNode) {
1182
+ container.parentNode.replaceChild(newContainer, container);
1183
+ }
1184
+ container = newContainer;
1185
+ container.innerHTML = `
1186
+ <div class="relative group h-full">
1187
+ <video class="w-full h-full object-contain" controls preload="auto" muted>
1188
+ <source src="${videoUrl}" type="${meta.type}">
1189
+ Your browser does not support the video tag.
1190
+ </video>
1191
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1192
+ <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1193
+ ${t("removeElement", state)}
1194
+ </button>
1195
+ <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1196
+ Change
1197
+ </button>
1198
+ </div>
1199
+ </div>
1200
+ `;
1201
+ const changeBtn = container.querySelector(
1202
+ ".change-file-btn"
1203
+ );
1204
+ if (changeBtn) {
1205
+ changeBtn.onclick = (e) => {
1206
+ e.stopPropagation();
1207
+ if (deps?.picker) {
1208
+ deps.picker.click();
1209
+ }
1210
+ };
1211
+ }
1212
+ const deleteBtn = container.querySelector(
1213
+ ".delete-file-btn"
1214
+ );
1215
+ if (deleteBtn) {
1216
+ deleteBtn.onclick = (e) => {
1217
+ e.stopPropagation();
1218
+ state.resourceIndex.delete(resourceId);
1219
+ const hiddenInput = container.parentElement?.querySelector(
1220
+ 'input[type="hidden"]'
1221
+ );
1222
+ if (hiddenInput) {
1223
+ hiddenInput.value = "";
1224
+ }
1225
+ if (deps?.fileUploadHandler) {
1226
+ container.onclick = deps.fileUploadHandler;
1227
+ }
1228
+ if (deps?.dragHandler) {
1229
+ setupDragAndDrop(container, deps.dragHandler);
1230
+ }
1231
+ container.innerHTML = `
1232
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1233
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1234
+ <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"/>
1235
+ </svg>
1236
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1237
+ </div>
1238
+ `;
1239
+ };
1240
+ }
1241
+ } else {
1242
+ 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>`;
1243
+ }
1244
+ if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
1245
+ addDeleteButton(container, state, () => {
1246
+ state.resourceIndex.delete(resourceId);
1247
+ const hiddenInput = container.parentElement?.querySelector(
1248
+ 'input[type="hidden"]'
1249
+ );
1250
+ if (hiddenInput) {
1251
+ hiddenInput.value = "";
1252
+ }
1253
+ container.innerHTML = `
1254
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1255
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1256
+ <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"/>
1257
+ </svg>
1258
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1259
+ </div>
1260
+ `;
1261
+ });
1262
+ }
1263
+ } else if (state.config.getThumbnail) {
1264
+ try {
1265
+ const thumbnailUrl = await state.config.getThumbnail(resourceId);
1266
+ if (thumbnailUrl) {
1267
+ clear(container);
1268
+ img.src = thumbnailUrl;
1269
+ container.appendChild(img);
1270
+ } else {
1271
+ setEmptyFileContainer(container, state);
1272
+ }
1273
+ } catch (error) {
1274
+ console.error("Failed to get thumbnail:", error);
1275
+ container.innerHTML = `
1276
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1277
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1278
+ <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"/>
1279
+ </svg>
1280
+ <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1281
+ </div>
1282
+ `;
1283
+ }
1284
+ } else {
1285
+ setEmptyFileContainer(container, state);
1286
+ }
1287
+ }
1288
+ async function renderFilePreviewReadonly(resourceId, state, fileName) {
1289
+ const meta = state.resourceIndex.get(resourceId);
1290
+ const actualFileName = meta?.name || resourceId.split("/").pop() || "file";
1291
+ const isPSD = actualFileName.toLowerCase().match(/\.psd$/);
1292
+ const fileResult = document.createElement("div");
1293
+ fileResult.className = isPSD ? "space-y-2" : "space-y-3";
1294
+ const previewContainer = document.createElement("div");
1295
+ if (isPSD) {
1296
+ previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity flex items-center p-3 max-w-sm";
1297
+ } else {
1298
+ previewContainer.className = "bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity";
1299
+ }
1300
+ const isImage = !isPSD && (meta?.type?.startsWith("image/") || actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/));
1301
+ const isVideo = meta?.type?.startsWith("video/") || actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/);
1302
+ if (isImage) {
1303
+ if (state.config.getThumbnail) {
1304
+ try {
1305
+ const thumbnailUrl = await state.config.getThumbnail(resourceId);
1306
+ if (thumbnailUrl) {
1307
+ previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
1308
+ } else {
1309
+ 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>`;
1310
+ }
1311
+ } catch (error) {
1312
+ console.warn("getThumbnail failed for", resourceId, error);
1313
+ 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>`;
1314
+ }
1315
+ } else {
1316
+ 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>`;
1317
+ }
1318
+ } else if (isVideo) {
1319
+ if (state.config.getThumbnail) {
1320
+ try {
1321
+ const videoUrl = await state.config.getThumbnail(resourceId);
1322
+ if (videoUrl) {
1323
+ previewContainer.innerHTML = `
1324
+ <div class="relative group">
1325
+ <video class="w-full h-auto" controls preload="auto" muted>
1326
+ <source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
1327
+ \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.
1328
+ </video>
1329
+ <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">
1330
+ <div class="bg-white bg-opacity-90 rounded-full p-3">
1331
+ <svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
1332
+ <path d="M8 5v14l11-7z"/>
1333
+ </svg>
1334
+ </div>
1335
+ </div>
1336
+ </div>
1337
+ `;
1338
+ } else {
1339
+ 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>`;
1340
+ }
1341
+ } catch (error) {
1342
+ console.warn("getThumbnail failed for video", resourceId, error);
1343
+ 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>`;
1344
+ }
1345
+ } else {
1346
+ 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>`;
1347
+ }
1348
+ } else {
1349
+ const fileIcon = isPSD ? "\u{1F3A8}" : "\u{1F4C1}";
1350
+ const fileDescription = isPSD ? "PSD File" : "Document";
1351
+ if (isPSD) {
1352
+ previewContainer.innerHTML = `
1353
+ <div class="flex items-center space-x-3">
1354
+ <div class="text-3xl text-gray-400">${fileIcon}</div>
1355
+ <div class="flex-1 min-w-0">
1356
+ <div class="text-sm font-medium text-gray-900 truncate">${actualFileName}</div>
1357
+ <div class="text-xs text-gray-500">${fileDescription}</div>
1358
+ </div>
1359
+ </div>
1360
+ `;
1361
+ } else {
1362
+ 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>`;
1363
+ }
1364
+ }
1365
+ const fileNameElement = document.createElement("p");
1366
+ fileNameElement.className = isPSD ? "hidden" : "text-sm font-medium text-gray-900 text-center";
1367
+ fileNameElement.textContent = actualFileName;
1368
+ const downloadButton = document.createElement("button");
1369
+ downloadButton.className = "w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors";
1370
+ downloadButton.textContent = t("downloadButton", state);
1371
+ downloadButton.onclick = (e) => {
1372
+ e.preventDefault();
1373
+ e.stopPropagation();
1374
+ if (state.config.downloadFile) {
1375
+ state.config.downloadFile(resourceId, actualFileName);
1376
+ } else {
1377
+ forceDownload(resourceId, actualFileName, state);
1378
+ }
1379
+ };
1380
+ fileResult.appendChild(previewContainer);
1381
+ fileResult.appendChild(fileNameElement);
1382
+ fileResult.appendChild(downloadButton);
1383
+ return fileResult;
1384
+ }
1385
+ function renderResourcePills(container, rids, state, onRemove) {
1386
+ clear(container);
1387
+ const isInitialRender = !container.classList.contains("grid");
1388
+ if ((!rids || rids.length === 0) && isInitialRender) {
1389
+ const gridContainer = document.createElement("div");
1390
+ gridContainer.className = "grid grid-cols-4 gap-3 mb-3";
1391
+ for (let i = 0; i < 4; i++) {
1392
+ const slot = document.createElement("div");
1393
+ 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";
1394
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1395
+ svg.setAttribute("class", "w-12 h-12 text-gray-400");
1396
+ svg.setAttribute("fill", "currentColor");
1397
+ svg.setAttribute("viewBox", "0 0 24 24");
1398
+ const path = document.createElementNS(
1399
+ "http://www.w3.org/2000/svg",
1400
+ "path"
1401
+ );
1402
+ path.setAttribute(
1403
+ "d",
1404
+ "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"
1405
+ );
1406
+ svg.appendChild(path);
1407
+ slot.appendChild(svg);
1408
+ slot.onclick = () => {
1409
+ let filesWrapper = container.parentElement;
1410
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
1411
+ filesWrapper = filesWrapper.parentElement;
1412
+ }
1413
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
1414
+ filesWrapper = container;
1415
+ }
1416
+ const fileInput = filesWrapper?.querySelector(
1417
+ 'input[type="file"]'
1418
+ );
1419
+ if (fileInput) fileInput.click();
1420
+ };
1421
+ gridContainer.appendChild(slot);
1422
+ }
1423
+ const textContainer = document.createElement("div");
1424
+ textContainer.className = "text-center text-xs text-gray-600";
1425
+ const uploadLink = document.createElement("span");
1426
+ uploadLink.className = "underline cursor-pointer";
1427
+ uploadLink.textContent = t("uploadText", state);
1428
+ uploadLink.onclick = (e) => {
1429
+ e.stopPropagation();
1430
+ let filesWrapper = container.parentElement;
1431
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
1432
+ filesWrapper = filesWrapper.parentElement;
1433
+ }
1434
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
1435
+ filesWrapper = container;
1436
+ }
1437
+ const fileInput = filesWrapper?.querySelector(
1438
+ 'input[type="file"]'
1439
+ );
1440
+ if (fileInput) fileInput.click();
1441
+ };
1442
+ textContainer.appendChild(uploadLink);
1443
+ textContainer.appendChild(document.createTextNode(` ${t("dragDropText", state)}`));
1444
+ container.appendChild(gridContainer);
1445
+ container.appendChild(textContainer);
1446
+ return;
1447
+ }
1448
+ container.className = "files-list grid grid-cols-4 gap-3 mt-2";
1449
+ const currentImagesCount = rids ? rids.length : 0;
1450
+ const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
1451
+ const slotsNeeded = rowsNeeded * 4;
1452
+ for (let i = 0; i < slotsNeeded; i++) {
1453
+ const slot = document.createElement("div");
1454
+ if (rids && i < rids.length) {
1455
+ const rid = rids[i];
1456
+ const meta = state.resourceIndex.get(rid);
1457
+ slot.className = "resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300";
1458
+ slot.dataset.resourceId = rid;
1459
+ renderThumbnailForResource(slot, rid, meta, state).catch((err) => {
1460
+ console.error("Failed to render thumbnail:", err);
1461
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1462
+ <div class="text-2xl mb-1">\u{1F4C1}</div>
1463
+ <div class="text-xs">Preview error</div>
1464
+ </div>`;
1465
+ });
1466
+ if (onRemove) {
1467
+ const overlay = document.createElement("div");
1468
+ overlay.className = "absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
1469
+ const removeBtn = document.createElement("button");
1470
+ removeBtn.className = "bg-red-600 text-white px-2 py-1 rounded text-xs";
1471
+ removeBtn.textContent = t("removeElement", state);
1472
+ removeBtn.onclick = (e) => {
1473
+ e.stopPropagation();
1474
+ onRemove(rid);
1475
+ };
1476
+ overlay.appendChild(removeBtn);
1477
+ slot.appendChild(overlay);
1478
+ }
1479
+ } else {
1480
+ slot.className = "aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
1481
+ slot.innerHTML = '<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>';
1482
+ slot.onclick = () => {
1483
+ let filesWrapper = container.parentElement;
1484
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
1485
+ filesWrapper = filesWrapper.parentElement;
1486
+ }
1487
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
1488
+ filesWrapper = container;
1489
+ }
1490
+ const fileInput = filesWrapper?.querySelector(
1491
+ 'input[type="file"]'
1492
+ );
1493
+ if (fileInput) fileInput.click();
1494
+ };
1495
+ }
1496
+ container.appendChild(slot);
1497
+ }
1498
+ }
1499
+ function renderThumbnailError(slot, iconSize = "w-12 h-12") {
1500
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1501
+ <svg class="${iconSize} text-red-400" fill="currentColor" viewBox="0 0 24 24">
1502
+ <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"/>
1503
+ </svg>
1504
+ <div class="text-xs mt-1 text-red-600">Preview error</div>
1505
+ </div>`;
1506
+ }
1507
+ async function renderThumbnailForResource(slot, rid, meta, state) {
1508
+ if (meta && meta.type?.startsWith("image/")) {
1509
+ if (meta.file && meta.file instanceof File) {
1510
+ const img = document.createElement("img");
1511
+ img.className = "w-full h-full object-contain";
1512
+ img.alt = meta.name;
1513
+ const reader = new FileReader();
1514
+ reader.onload = (e) => {
1515
+ img.src = e.target?.result || "";
1516
+ };
1517
+ reader.readAsDataURL(meta.file);
1518
+ slot.appendChild(img);
1519
+ } else if (state.config.getThumbnail) {
1520
+ try {
1521
+ const url = await state.config.getThumbnail(rid);
1522
+ if (url) {
1523
+ const img = document.createElement("img");
1524
+ img.className = "w-full h-full object-contain";
1525
+ img.alt = meta.name;
1526
+ img.src = url;
1527
+ slot.appendChild(img);
1528
+ } else {
1529
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1530
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
1531
+ <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"/>
1532
+ </svg>
1533
+ </div>`;
1534
+ }
1535
+ } catch (error) {
1536
+ const err = error instanceof Error ? error : new Error(String(error));
1537
+ if (state.config.onThumbnailError) {
1538
+ state.config.onThumbnailError(err, rid);
1539
+ }
1540
+ renderThumbnailError(slot);
1541
+ }
1542
+ } else {
1543
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1544
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
1545
+ <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"/>
1546
+ </svg>
1547
+ </div>`;
1548
+ }
1549
+ } else if (meta && meta.type?.startsWith("video/")) {
1550
+ if (meta.file && meta.file instanceof File) {
1551
+ const videoUrl = URL.createObjectURL(meta.file);
1552
+ slot.innerHTML = `
1553
+ <div class="relative group h-full w-full">
1554
+ <video class="w-full h-full object-contain" preload="metadata" muted>
1555
+ <source src="${videoUrl}" type="${meta.type}">
1556
+ </video>
1557
+ <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
1558
+ <div class="bg-white bg-opacity-90 rounded-full p-1">
1559
+ <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
1560
+ <path d="M8 5v14l11-7z"/>
1561
+ </svg>
1562
+ </div>
1563
+ </div>
1564
+ </div>
1565
+ `;
1566
+ } else if (state.config.getThumbnail) {
1567
+ try {
1568
+ const videoUrl = await state.config.getThumbnail(rid);
1569
+ if (videoUrl) {
1570
+ slot.innerHTML = `
1571
+ <div class="relative group h-full w-full">
1572
+ <video class="w-full h-full object-contain" preload="metadata" muted>
1573
+ <source src="${videoUrl}" type="${meta.type}">
1574
+ </video>
1575
+ <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
1576
+ <div class="bg-white bg-opacity-90 rounded-full p-1">
1577
+ <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
1578
+ <path d="M8 5v14l11-7z"/>
1579
+ </svg>
1580
+ </div>
1581
+ </div>
1582
+ </div>
1583
+ `;
1584
+ } else {
1585
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1586
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1587
+ <path d="M8 5v14l11-7z"/>
1588
+ </svg>
1589
+ <div class="text-xs mt-1">${meta?.name || "Video"}</div>
1590
+ </div>`;
1591
+ }
1592
+ } catch (error) {
1593
+ const err = error instanceof Error ? error : new Error(String(error));
1594
+ if (state.config.onThumbnailError) {
1595
+ state.config.onThumbnailError(err, rid);
1596
+ }
1597
+ renderThumbnailError(slot, "w-8 h-8");
1598
+ }
1599
+ } else {
1600
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1601
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
1602
+ <path d="M8 5v14l11-7z"/>
1603
+ </svg>
1604
+ <div class="text-xs mt-1">${meta?.name || "Video"}</div>
1605
+ </div>`;
1606
+ }
1607
+ } else {
1608
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
1609
+ <div class="text-2xl mb-1">\u{1F4C1}</div>
1610
+ <div class="text-xs">${meta?.name || "File"}</div>
1611
+ </div>`;
1612
+ }
1613
+ }
1614
+ function setEmptyFileContainer(fileContainer, state) {
1615
+ fileContainer.innerHTML = `
1616
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1617
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1618
+ <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"/>
1619
+ </svg>
1620
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1621
+ </div>
1622
+ `;
1623
+ }
1624
+ async function handleFileSelect(file, container, fieldName, state, deps = null, instance) {
1625
+ let rid;
1626
+ if (state.config.uploadFile) {
1627
+ try {
1628
+ rid = await state.config.uploadFile(file);
1629
+ if (typeof rid !== "string") {
1630
+ throw new Error("Upload handler must return a string resource ID");
1631
+ }
1632
+ } catch (error) {
1633
+ const err = error instanceof Error ? error : new Error(String(error));
1634
+ if (state.config.onUploadError) {
1635
+ state.config.onUploadError(err, file);
1636
+ }
1637
+ throw new Error(`File upload failed: ${err.message}`);
1638
+ }
1639
+ } else {
1640
+ throw new Error(
1641
+ "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
1642
+ );
1643
+ }
1644
+ state.resourceIndex.set(rid, {
1645
+ name: file.name,
1646
+ type: file.type,
1647
+ size: file.size,
1648
+ uploadedAt: /* @__PURE__ */ new Date(),
1649
+ file
1650
+ // Store the file object for local preview
1651
+ });
1652
+ let hiddenInput = container.parentElement?.querySelector(
1653
+ 'input[type="hidden"]'
1654
+ );
1655
+ if (!hiddenInput) {
1656
+ hiddenInput = document.createElement("input");
1657
+ hiddenInput.type = "hidden";
1658
+ hiddenInput.name = fieldName;
1659
+ container.parentElement?.appendChild(hiddenInput);
1660
+ }
1661
+ hiddenInput.value = rid;
1662
+ renderFilePreview(container, rid, state, {
1663
+ fileName: file.name,
1664
+ isReadonly: false,
1665
+ deps
1666
+ }).catch(console.error);
1667
+ if (instance && !state.config.readonly) {
1668
+ instance.triggerOnChange(fieldName, rid);
1669
+ }
1670
+ }
1671
+ function setupDragAndDrop(element, dropHandler) {
1672
+ element.addEventListener("dragover", (e) => {
1673
+ e.preventDefault();
1674
+ element.classList.add("border-blue-500", "bg-blue-50");
1675
+ });
1676
+ element.addEventListener("dragleave", (e) => {
1677
+ e.preventDefault();
1678
+ element.classList.remove("border-blue-500", "bg-blue-50");
1679
+ });
1680
+ element.addEventListener("drop", (e) => {
1681
+ e.preventDefault();
1682
+ element.classList.remove("border-blue-500", "bg-blue-50");
1683
+ if (e.dataTransfer?.files) {
1684
+ dropHandler(e.dataTransfer.files);
1685
+ }
1686
+ });
1687
+ }
1688
+ function addDeleteButton(container, state, onDelete) {
1689
+ const existingOverlay = container.querySelector(".delete-overlay");
1690
+ if (existingOverlay) {
1691
+ existingOverlay.remove();
1692
+ }
1693
+ const overlay = document.createElement("div");
1694
+ overlay.className = "delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
1695
+ const deleteBtn = document.createElement("button");
1696
+ deleteBtn.className = "bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
1697
+ deleteBtn.textContent = t("removeElement", state);
1698
+ deleteBtn.onclick = (e) => {
1699
+ e.stopPropagation();
1700
+ onDelete();
1701
+ };
1702
+ overlay.appendChild(deleteBtn);
1703
+ container.appendChild(overlay);
1704
+ }
1705
+ async function uploadSingleFile(file, state) {
1706
+ if (state.config.uploadFile) {
1707
+ try {
1708
+ const rid = await state.config.uploadFile(file);
1709
+ if (typeof rid !== "string") {
1710
+ throw new Error("Upload handler must return a string resource ID");
1711
+ }
1712
+ return rid;
1713
+ } catch (error) {
1714
+ const err = error instanceof Error ? error : new Error(String(error));
1715
+ if (state.config.onUploadError) {
1716
+ state.config.onUploadError(err, file);
1717
+ }
1718
+ throw new Error(`File upload failed: ${err.message}`);
1719
+ }
1720
+ } else {
1721
+ throw new Error(
1722
+ "No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()"
1723
+ );
1724
+ }
1725
+ }
1726
+ async function forceDownload(resourceId, fileName, state) {
1727
+ try {
1728
+ let fileUrl = null;
1729
+ if (state.config.getDownloadUrl) {
1730
+ fileUrl = state.config.getDownloadUrl(resourceId);
1731
+ } else if (state.config.getThumbnail) {
1732
+ fileUrl = await state.config.getThumbnail(resourceId);
1733
+ }
1734
+ if (fileUrl) {
1735
+ const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
1736
+ const response = await fetch(finalUrl);
1737
+ if (!response.ok) {
1738
+ throw new Error(`HTTP error! status: ${response.status}`);
1739
+ }
1740
+ const blob = await response.blob();
1741
+ downloadBlob(blob, fileName);
1742
+ } else {
1743
+ throw new Error("No download URL available for resource");
1744
+ }
1745
+ } catch (error) {
1746
+ const err = error instanceof Error ? error : new Error(String(error));
1747
+ if (state.config.onDownloadError) {
1748
+ state.config.onDownloadError(err, resourceId, fileName);
1749
+ }
1750
+ console.error(`File download failed for ${fileName}:`, err);
1751
+ throw err;
1752
+ }
1753
+ }
1754
+ function downloadBlob(blob, fileName) {
1755
+ try {
1756
+ const blobUrl = URL.createObjectURL(blob);
1757
+ const link = document.createElement("a");
1758
+ link.href = blobUrl;
1759
+ link.download = fileName;
1760
+ link.style.display = "none";
1761
+ document.body.appendChild(link);
1762
+ link.click();
1763
+ document.body.removeChild(link);
1764
+ setTimeout(() => {
1765
+ URL.revokeObjectURL(blobUrl);
1766
+ }, 100);
1767
+ } catch (error) {
1768
+ throw new Error(`Blob download failed: ${error.message}`);
1769
+ }
1770
+ }
1771
+ function addPrefillFilesToIndex(initialFiles, state) {
1772
+ if (initialFiles.length > 0) {
1773
+ initialFiles.forEach((resourceId) => {
1774
+ if (!state.resourceIndex.has(resourceId)) {
1775
+ const filename = resourceId.split("/").pop() || "file";
1776
+ const extension = filename.split(".").pop()?.toLowerCase();
1777
+ let fileType = "application/octet-stream";
1778
+ if (extension) {
1779
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
1780
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
1781
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
1782
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
1783
+ }
1784
+ }
1785
+ state.resourceIndex.set(resourceId, {
1786
+ name: filename,
1787
+ type: fileType,
1788
+ size: 0,
1789
+ uploadedAt: /* @__PURE__ */ new Date(),
1790
+ file: void 0
1791
+ });
1792
+ }
1793
+ });
1794
+ }
1795
+ }
1796
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
1797
+ if (!state.resourceIndex.has(initial)) {
1798
+ const filename = initial.split("/").pop() || "file";
1799
+ const extension = filename.split(".").pop()?.toLowerCase();
1800
+ let fileType = "application/octet-stream";
1801
+ if (extension) {
1802
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
1803
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
1804
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
1805
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
1806
+ }
1807
+ }
1808
+ state.resourceIndex.set(initial, {
1809
+ name: filename,
1810
+ type: fileType,
1811
+ size: 0,
1812
+ uploadedAt: /* @__PURE__ */ new Date(),
1813
+ file: void 0
1814
+ });
1815
+ }
1816
+ renderFilePreview(fileContainer, initial, state, {
1817
+ fileName: initial,
1818
+ isReadonly: false,
1819
+ deps
1820
+ }).catch(console.error);
1821
+ const hiddenInput = document.createElement("input");
1822
+ hiddenInput.type = "hidden";
1823
+ hiddenInput.name = pathKey;
1824
+ hiddenInput.value = initial;
1825
+ fileWrapper.appendChild(hiddenInput);
1826
+ }
1827
+ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, pathKey, instance) {
1828
+ setupDragAndDrop(filesContainer, async (files) => {
1829
+ const arr = Array.from(files);
1830
+ for (const file of arr) {
1831
+ const rid = await uploadSingleFile(file, state);
1832
+ state.resourceIndex.set(rid, {
1833
+ name: file.name,
1834
+ type: file.type,
1835
+ size: file.size,
1836
+ uploadedAt: /* @__PURE__ */ new Date(),
1837
+ file: void 0
1838
+ });
1839
+ initialFiles.push(rid);
1840
+ }
1841
+ updateCallback();
1842
+ if (instance && pathKey && !state.config.readonly) {
1843
+ instance.triggerOnChange(pathKey, initialFiles);
1844
+ }
1845
+ });
1846
+ }
1847
+ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, pathKey, instance) {
1848
+ filesPicker.onchange = async () => {
1849
+ if (filesPicker.files) {
1850
+ for (const file of Array.from(filesPicker.files)) {
1851
+ const rid = await uploadSingleFile(file, state);
1852
+ state.resourceIndex.set(rid, {
1853
+ name: file.name,
1854
+ type: file.type,
1855
+ size: file.size,
1856
+ uploadedAt: /* @__PURE__ */ new Date(),
1857
+ file: void 0
1858
+ });
1859
+ initialFiles.push(rid);
1860
+ }
1861
+ }
1862
+ updateCallback();
1863
+ filesPicker.value = "";
1864
+ if (instance && pathKey && !state.config.readonly) {
1865
+ instance.triggerOnChange(pathKey, initialFiles);
1866
+ }
1867
+ };
1868
+ }
1869
+ function renderFileElement(element, ctx, wrapper, pathKey) {
1870
+ const state = ctx.state;
1871
+ if (state.config.readonly) {
1872
+ const initial = ctx.prefill[element.key];
1873
+ if (initial) {
1874
+ renderFilePreviewReadonly(initial, state).then((filePreview) => {
1875
+ wrapper.appendChild(filePreview);
1876
+ }).catch((err) => {
1877
+ console.error("Failed to render file preview:", err);
1878
+ const emptyState = document.createElement("div");
1879
+ emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
1880
+ emptyState.innerHTML = `<div class="text-center">Preview unavailable</div>`;
1881
+ wrapper.appendChild(emptyState);
1882
+ });
1883
+ } else {
1884
+ const emptyState = document.createElement("div");
1885
+ emptyState.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
1886
+ emptyState.innerHTML = `<div class="text-center">${t("noFileSelected", state)}</div>`;
1887
+ wrapper.appendChild(emptyState);
1888
+ }
1889
+ } else {
1890
+ const fileWrapper = document.createElement("div");
1891
+ fileWrapper.className = "space-y-2";
1892
+ const picker = document.createElement("input");
1893
+ picker.type = "file";
1894
+ picker.name = pathKey;
1895
+ picker.style.display = "none";
1896
+ if (element.accept) {
1897
+ picker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
1898
+ }
1899
+ const fileContainer = document.createElement("div");
1900
+ fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
1901
+ const initial = ctx.prefill[element.key];
1902
+ const fileUploadHandler = () => picker.click();
1903
+ const dragHandler = (files) => {
1904
+ if (files.length > 0) {
1905
+ const deps = { picker, fileUploadHandler, dragHandler };
1906
+ handleFileSelect(files[0], fileContainer, pathKey, state, deps, ctx.instance);
1907
+ }
1908
+ };
1909
+ if (initial) {
1910
+ handleInitialFileData(
1911
+ initial,
1912
+ fileContainer,
1913
+ pathKey,
1914
+ fileWrapper,
1915
+ state,
1916
+ {
1917
+ picker,
1918
+ fileUploadHandler,
1919
+ dragHandler
1920
+ }
1921
+ );
1922
+ } else {
1923
+ setEmptyFileContainer(fileContainer, state);
1924
+ }
1925
+ fileContainer.onclick = fileUploadHandler;
1926
+ setupDragAndDrop(fileContainer, dragHandler);
1927
+ picker.onchange = () => {
1928
+ if (picker.files && picker.files.length > 0) {
1929
+ const deps = { picker, fileUploadHandler, dragHandler };
1930
+ handleFileSelect(picker.files[0], fileContainer, pathKey, state, deps, ctx.instance);
1931
+ }
1932
+ };
1933
+ fileWrapper.appendChild(fileContainer);
1934
+ fileWrapper.appendChild(picker);
1935
+ const uploadText = document.createElement("p");
1936
+ uploadText.className = "text-xs text-gray-600 mt-2 text-center";
1937
+ uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText", state)}</span> ${t("dragDropTextSingle", state)}`;
1938
+ const uploadSpan = uploadText.querySelector("span");
1939
+ if (uploadSpan) {
1940
+ uploadSpan.onclick = () => picker.click();
1941
+ }
1942
+ fileWrapper.appendChild(uploadText);
1943
+ const fileHint = document.createElement("p");
1944
+ fileHint.className = "text-xs text-gray-500 mt-1 text-center";
1945
+ fileHint.textContent = makeFieldHint(element);
1946
+ fileWrapper.appendChild(fileHint);
1947
+ wrapper.appendChild(fileWrapper);
1948
+ }
1949
+ }
1950
+ function renderFilesElement(element, ctx, wrapper, pathKey) {
1951
+ const state = ctx.state;
1952
+ if (state.config.readonly) {
1953
+ const resultsWrapper = document.createElement("div");
1954
+ resultsWrapper.className = "space-y-4";
1955
+ const initialFiles = ctx.prefill[element.key] || [];
1956
+ if (initialFiles.length > 0) {
1957
+ initialFiles.forEach((resourceId) => {
1958
+ renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
1959
+ resultsWrapper.appendChild(filePreview);
1960
+ }).catch((err) => {
1961
+ console.error("Failed to render file preview:", err);
1962
+ });
1963
+ });
1964
+ } else {
1965
+ 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>`;
1966
+ }
1967
+ wrapper.appendChild(resultsWrapper);
1968
+ } else {
1969
+ let updateFilesList2 = function() {
1970
+ renderResourcePills(list, initialFiles, state, (ridToRemove) => {
1971
+ const index = initialFiles.indexOf(ridToRemove);
1972
+ if (index > -1) {
1973
+ initialFiles.splice(index, 1);
1974
+ }
1975
+ updateFilesList2();
1976
+ });
1977
+ };
1978
+ const filesWrapper = document.createElement("div");
1979
+ filesWrapper.className = "space-y-2";
1980
+ const filesPicker = document.createElement("input");
1981
+ filesPicker.type = "file";
1982
+ filesPicker.name = pathKey;
1983
+ filesPicker.multiple = true;
1984
+ filesPicker.style.display = "none";
1985
+ if (element.accept) {
1986
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
1987
+ }
1988
+ const filesContainer = document.createElement("div");
1989
+ filesContainer.className = "border-2 border-dashed border-gray-300 rounded-lg p-3 hover:border-gray-400 transition-colors";
1990
+ const list = document.createElement("div");
1991
+ list.className = "files-list";
1992
+ const initialFiles = ctx.prefill[element.key] || [];
1993
+ addPrefillFilesToIndex(initialFiles, state);
1994
+ updateFilesList2();
1995
+ setupFilesDropHandler(filesContainer, initialFiles, state, updateFilesList2, pathKey, ctx.instance);
1996
+ setupFilesPickerHandler(filesPicker, initialFiles, state, updateFilesList2, pathKey, ctx.instance);
1997
+ filesContainer.appendChild(list);
1998
+ filesWrapper.appendChild(filesContainer);
1999
+ filesWrapper.appendChild(filesPicker);
2000
+ const filesHint = document.createElement("p");
2001
+ filesHint.className = "text-xs text-gray-500 mt-1 text-center";
2002
+ filesHint.textContent = makeFieldHint(element);
2003
+ filesWrapper.appendChild(filesHint);
2004
+ wrapper.appendChild(filesWrapper);
2005
+ }
2006
+ }
2007
+ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2008
+ const state = ctx.state;
2009
+ const minFiles = element.minCount ?? 0;
2010
+ const maxFiles = element.maxCount ?? Infinity;
2011
+ if (state.config.readonly) {
2012
+ const resultsWrapper = document.createElement("div");
2013
+ resultsWrapper.className = "space-y-4";
2014
+ const initialFiles = ctx.prefill[element.key] || [];
2015
+ if (initialFiles.length > 0) {
2016
+ initialFiles.forEach((resourceId) => {
2017
+ renderFilePreviewReadonly(resourceId, state).then((filePreview) => {
2018
+ resultsWrapper.appendChild(filePreview);
2019
+ }).catch((err) => {
2020
+ console.error("Failed to render file preview:", err);
2021
+ });
2022
+ });
2023
+ } else {
2024
+ 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>`;
2025
+ }
2026
+ wrapper.appendChild(resultsWrapper);
2027
+ } else {
2028
+ const filesWrapper = document.createElement("div");
2029
+ filesWrapper.className = "space-y-2";
2030
+ const filesPicker = document.createElement("input");
2031
+ filesPicker.type = "file";
2032
+ filesPicker.name = pathKey;
2033
+ filesPicker.multiple = true;
2034
+ filesPicker.style.display = "none";
2035
+ if (element.accept) {
2036
+ filesPicker.accept = typeof element.accept === "string" ? element.accept : element.accept.extensions?.map((ext) => `.${ext}`).join(",") || "";
2037
+ }
2038
+ const filesContainer = document.createElement("div");
2039
+ filesContainer.className = "files-list space-y-2";
2040
+ filesWrapper.appendChild(filesPicker);
2041
+ filesWrapper.appendChild(filesContainer);
2042
+ const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2043
+ addPrefillFilesToIndex(initialFiles, state);
2044
+ const updateFilesDisplay = () => {
2045
+ renderResourcePills(filesContainer, initialFiles, state, (index) => {
2046
+ initialFiles.splice(initialFiles.indexOf(index), 1);
2047
+ updateFilesDisplay();
2048
+ });
2049
+ const countInfo = document.createElement("div");
2050
+ countInfo.className = "text-xs text-gray-500 mt-2 file-count-info";
2051
+ const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
2052
+ const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` (${minFiles}-${maxFiles} allowed)` : "";
2053
+ countInfo.textContent = countText + minMaxText;
2054
+ const existingCount = filesWrapper.querySelector(".file-count-info");
2055
+ if (existingCount) existingCount.remove();
2056
+ filesWrapper.appendChild(countInfo);
2057
+ };
2058
+ setupFilesDropHandler(filesContainer, initialFiles, state, updateFilesDisplay, pathKey, ctx.instance);
2059
+ setupFilesPickerHandler(filesPicker, initialFiles, state, updateFilesDisplay, pathKey, ctx.instance);
2060
+ updateFilesDisplay();
2061
+ wrapper.appendChild(filesWrapper);
2062
+ }
2063
+ }
2064
+ function validateFileElement(element, key, context) {
2065
+ const errors = [];
2066
+ const { scopeRoot, skipValidation, path } = context;
2067
+ const validateFileCount = (key2, resourceIds, element2) => {
2068
+ if (skipValidation) return;
2069
+ const minFiles = "minCount" in element2 ? element2.minCount ?? 0 : 0;
2070
+ const maxFiles = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
2071
+ if (element2.required && resourceIds.length === 0) {
2072
+ errors.push(`${key2}: required`);
2073
+ }
2074
+ if (resourceIds.length < minFiles) {
2075
+ errors.push(`${key2}: minimum ${minFiles} files required`);
2076
+ }
2077
+ if (resourceIds.length > maxFiles) {
2078
+ errors.push(`${key2}: maximum ${maxFiles} files allowed`);
2079
+ }
2080
+ };
2081
+ if ("multiple" in element && element.multiple) {
2082
+ const fullKey = pathJoin(path, key);
2083
+ const pickerInput = scopeRoot.querySelector(
2084
+ `input[type="file"][name="${fullKey}"]`
2085
+ );
2086
+ const filesWrapper = pickerInput?.closest(".space-y-2");
2087
+ const container = filesWrapper?.querySelector(".files-list") || null;
2088
+ const resourceIds = [];
2089
+ if (container) {
2090
+ const pills = container.querySelectorAll(".resource-pill");
2091
+ pills.forEach((pill) => {
2092
+ const resourceId = pill.dataset.resourceId;
2093
+ if (resourceId) {
2094
+ resourceIds.push(resourceId);
2095
+ }
2096
+ });
2097
+ }
2098
+ validateFileCount(key, resourceIds, element);
2099
+ return { value: resourceIds, errors };
2100
+ } else {
2101
+ const input = scopeRoot.querySelector(
2102
+ `input[name$="${key}"][type="hidden"]`
2103
+ );
2104
+ const rid = input?.value ?? "";
2105
+ if (!skipValidation && element.required && rid === "") {
2106
+ errors.push(`${key}: required`);
2107
+ return { value: null, errors };
2108
+ }
2109
+ return { value: rid || null, errors };
2110
+ }
2111
+ }
2112
+ function updateFileField(element, fieldPath, value, context) {
2113
+ const { scopeRoot, state } = context;
2114
+ if ("multiple" in element && element.multiple) {
2115
+ if (!Array.isArray(value)) {
2116
+ console.warn(
2117
+ `updateFileField: Expected array for multiple file field "${fieldPath}", got ${typeof value}`
2118
+ );
2119
+ return;
2120
+ }
2121
+ value.forEach((resourceId) => {
2122
+ if (resourceId && typeof resourceId === "string") {
2123
+ if (!state.resourceIndex.has(resourceId)) {
2124
+ const filename = resourceId.split("/").pop() || "file";
2125
+ const extension = filename.split(".").pop()?.toLowerCase();
2126
+ let fileType = "application/octet-stream";
2127
+ if (extension) {
2128
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2129
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2130
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2131
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2132
+ }
2133
+ }
2134
+ state.resourceIndex.set(resourceId, {
2135
+ name: filename,
2136
+ type: fileType,
2137
+ size: 0,
2138
+ uploadedAt: /* @__PURE__ */ new Date(),
2139
+ file: void 0
2140
+ });
2141
+ }
2142
+ }
2143
+ });
2144
+ console.info(
2145
+ `updateFileField: Multiple file field "${fieldPath}" updated. Preview update requires re-render.`
2146
+ );
2147
+ } else {
2148
+ const hiddenInput = scopeRoot.querySelector(
2149
+ `input[name="${fieldPath}"][type="hidden"]`
2150
+ );
2151
+ if (!hiddenInput) {
2152
+ console.warn(
2153
+ `updateFileField: Hidden input not found for file field "${fieldPath}"`
2154
+ );
2155
+ return;
2156
+ }
2157
+ hiddenInput.value = value != null ? String(value) : "";
2158
+ if (value && typeof value === "string") {
2159
+ if (!state.resourceIndex.has(value)) {
2160
+ const filename = value.split("/").pop() || "file";
2161
+ const extension = filename.split(".").pop()?.toLowerCase();
2162
+ let fileType = "application/octet-stream";
2163
+ if (extension) {
2164
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
2165
+ fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
2166
+ } else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
2167
+ fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
2168
+ }
2169
+ }
2170
+ state.resourceIndex.set(value, {
2171
+ name: filename,
2172
+ type: fileType,
2173
+ size: 0,
2174
+ uploadedAt: /* @__PURE__ */ new Date(),
2175
+ file: void 0
2176
+ });
2177
+ }
2178
+ console.info(
2179
+ `updateFileField: File field "${fieldPath}" updated. Preview update requires re-render.`
2180
+ );
2181
+ }
2182
+ }
2183
+ }
2184
+
2185
+ // src/components/container.ts
2186
+ var renderElementFunc = null;
2187
+ function setRenderElement(fn) {
2188
+ renderElementFunc = fn;
2189
+ }
2190
+ function renderElement(element, ctx) {
2191
+ if (!renderElementFunc) {
2192
+ throw new Error(
2193
+ "renderElement not initialized. Import from components/index.ts"
2194
+ );
2195
+ }
2196
+ return renderElementFunc(element, ctx);
2197
+ }
2198
+ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2199
+ const containerWrap = document.createElement("div");
2200
+ containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
2201
+ containerWrap.setAttribute("data-container", pathKey);
2202
+ const header = document.createElement("div");
2203
+ header.className = "flex justify-between items-center mb-4";
2204
+ const left = document.createElement("div");
2205
+ left.className = "flex-1";
2206
+ const itemsWrap = document.createElement("div");
2207
+ itemsWrap.className = "space-y-4";
2208
+ containerWrap.appendChild(header);
2209
+ header.appendChild(left);
2210
+ const subCtx = {
2211
+ path: pathJoin(ctx.path, element.key),
2212
+ prefill: ctx.prefill?.[element.key] || {},
2213
+ state: ctx.state
2214
+ };
2215
+ element.elements.forEach((child) => {
2216
+ if (!child.hidden) {
2217
+ itemsWrap.appendChild(renderElement(child, subCtx));
2218
+ }
2219
+ });
2220
+ containerWrap.appendChild(itemsWrap);
2221
+ left.innerHTML = `<span>${element.label || element.key}</span>`;
2222
+ wrapper.appendChild(containerWrap);
2223
+ }
2224
+ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2225
+ const state = ctx.state;
2226
+ const containerWrap = document.createElement("div");
2227
+ containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
2228
+ const header = document.createElement("div");
2229
+ header.className = "flex justify-between items-center mb-4";
2230
+ const left = document.createElement("div");
2231
+ left.className = "flex-1";
2232
+ const right = document.createElement("div");
2233
+ right.className = "flex gap-2";
2234
+ const itemsWrap = document.createElement("div");
2235
+ itemsWrap.className = "space-y-4";
2236
+ containerWrap.appendChild(header);
2237
+ header.appendChild(left);
2238
+ if (!state.config.readonly) {
2239
+ header.appendChild(right);
2240
+ }
2241
+ const min = element.minCount ?? 0;
2242
+ const max = element.maxCount ?? Infinity;
2243
+ const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
2244
+ const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
2245
+ const createAddButton = () => {
2246
+ const add = document.createElement("button");
2247
+ add.type = "button";
2248
+ add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
2249
+ add.textContent = t("addElement", state);
2250
+ add.onclick = () => {
2251
+ if (countItems() < max) {
2252
+ const idx = countItems();
2253
+ const subCtx = {
2254
+ state: ctx.state,
2255
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2256
+ prefill: {}
2257
+ };
2258
+ const item = document.createElement("div");
2259
+ item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2260
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2261
+ element.elements.forEach((child) => {
2262
+ if (!child.hidden) {
2263
+ item.appendChild(renderElement(child, subCtx));
2264
+ }
2265
+ });
2266
+ if (!state.config.readonly) {
2267
+ const rem = document.createElement("button");
2268
+ rem.type = "button";
2269
+ rem.className = "absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
2270
+ rem.textContent = "\xD7";
2271
+ rem.onclick = () => {
2272
+ item.remove();
2273
+ updateAddButton();
2274
+ };
2275
+ item.style.position = "relative";
2276
+ item.appendChild(rem);
2277
+ }
2278
+ itemsWrap.appendChild(item);
2279
+ updateAddButton();
2280
+ }
2281
+ };
2282
+ return add;
2283
+ };
2284
+ const updateAddButton = () => {
2285
+ const currentCount = countItems();
2286
+ const addBtn = right.querySelector("button");
2287
+ if (addBtn) {
2288
+ addBtn.disabled = currentCount >= max;
2289
+ addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
2290
+ }
2291
+ left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "\u221E" : max})</span>`;
2292
+ };
2293
+ if (!state.config.readonly) {
2294
+ right.appendChild(createAddButton());
2295
+ }
2296
+ if (pre && Array.isArray(pre)) {
2297
+ pre.forEach((prefillObj, idx) => {
2298
+ const subCtx = {
2299
+ state: ctx.state,
2300
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2301
+ prefill: prefillObj || {}
2302
+ };
2303
+ const item = document.createElement("div");
2304
+ item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2305
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2306
+ element.elements.forEach((child) => {
2307
+ if (!child.hidden) {
2308
+ item.appendChild(renderElement(child, subCtx));
2309
+ }
2310
+ });
2311
+ if (!state.config.readonly) {
2312
+ const rem = document.createElement("button");
2313
+ rem.type = "button";
2314
+ rem.className = "absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
2315
+ rem.textContent = "\xD7";
2316
+ rem.onclick = () => {
2317
+ item.remove();
2318
+ updateAddButton();
2319
+ };
2320
+ item.style.position = "relative";
2321
+ item.appendChild(rem);
2322
+ }
2323
+ itemsWrap.appendChild(item);
2324
+ });
2325
+ }
2326
+ if (!state.config.readonly) {
2327
+ while (countItems() < min) {
2328
+ const idx = countItems();
2329
+ const subCtx = {
2330
+ state: ctx.state,
2331
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2332
+ prefill: {}
2333
+ };
2334
+ const item = document.createElement("div");
2335
+ item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2336
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2337
+ element.elements.forEach((child) => {
2338
+ if (!child.hidden) {
2339
+ item.appendChild(renderElement(child, subCtx));
2340
+ }
2341
+ });
2342
+ const rem = document.createElement("button");
2343
+ rem.type = "button";
2344
+ rem.className = "absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
2345
+ rem.textContent = "\xD7";
2346
+ rem.onclick = () => {
2347
+ if (countItems() > min) {
2348
+ item.remove();
2349
+ updateAddButton();
2350
+ }
2351
+ };
2352
+ item.style.position = "relative";
2353
+ item.appendChild(rem);
2354
+ itemsWrap.appendChild(item);
2355
+ }
2356
+ }
2357
+ containerWrap.appendChild(itemsWrap);
2358
+ updateAddButton();
2359
+ wrapper.appendChild(containerWrap);
2360
+ }
2361
+ var validateElementFunc = null;
2362
+ function setValidateElement(fn) {
2363
+ validateElementFunc = fn;
2364
+ }
2365
+ function validateElement(element, ctx, customScopeRoot) {
2366
+ if (!validateElementFunc) {
2367
+ throw new Error(
2368
+ "validateElement not initialized. Should be set from FormBuilderInstance"
2369
+ );
2370
+ }
2371
+ return validateElementFunc(element, ctx, customScopeRoot);
2372
+ }
2373
+ function validateContainerElement(element, key, context) {
2374
+ const errors = [];
2375
+ const { scopeRoot, skipValidation, path } = context;
2376
+ if (!("elements" in element)) {
2377
+ return { value: null, errors };
2378
+ }
2379
+ const validateContainerCount = (key2, items, element2) => {
2380
+ if (skipValidation) return;
2381
+ const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
2382
+ const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
2383
+ if (element2.required && items.length === 0) {
2384
+ errors.push(`${key2}: required`);
2385
+ }
2386
+ if (items.length < minItems) {
2387
+ errors.push(`${key2}: minimum ${minItems} items required`);
2388
+ }
2389
+ if (items.length > maxItems) {
2390
+ errors.push(`${key2}: maximum ${maxItems} items allowed`);
2391
+ }
2392
+ };
2393
+ if ("multiple" in element && element.multiple) {
2394
+ const items = [];
2395
+ const allContainerWrappers = scopeRoot.querySelectorAll(
2396
+ "[data-container-item]"
2397
+ );
2398
+ const containerWrappers = Array.from(allContainerWrappers).filter((el) => {
2399
+ const attr = el.getAttribute("data-container-item");
2400
+ return attr?.startsWith(`${key}[`);
2401
+ });
2402
+ const itemCount = containerWrappers.length;
2403
+ for (let i = 0; i < itemCount; i++) {
2404
+ const itemData = {};
2405
+ const itemContainer = scopeRoot.querySelector(
2406
+ `[data-container-item="${key}[${i}]"]`
2407
+ ) || scopeRoot;
2408
+ element.elements.forEach((child) => {
2409
+ if (child.hidden || child.type === "hidden") {
2410
+ itemData[child.key] = child.default !== void 0 ? child.default : null;
2411
+ } else {
2412
+ const childKey = `${key}[${i}].${child.key}`;
2413
+ itemData[child.key] = validateElement(
2414
+ { ...child, key: childKey },
2415
+ { path },
2416
+ itemContainer
2417
+ );
2418
+ }
2419
+ });
2420
+ items.push(itemData);
2421
+ }
2422
+ validateContainerCount(key, items, element);
2423
+ return { value: items, errors };
2424
+ } else {
2425
+ const containerData = {};
2426
+ const containerContainer = scopeRoot.querySelector(`[data-container="${key}"]`) || scopeRoot;
2427
+ element.elements.forEach((child) => {
2428
+ if (child.hidden || child.type === "hidden") {
2429
+ containerData[child.key] = child.default !== void 0 ? child.default : null;
2430
+ } else {
2431
+ const childKey = `${key}.${child.key}`;
2432
+ containerData[child.key] = validateElement(
2433
+ { ...child, key: childKey },
2434
+ { path },
2435
+ containerContainer
2436
+ );
2437
+ }
2438
+ });
2439
+ return { value: containerData, errors };
2440
+ }
2441
+ }
2442
+ function updateContainerField(element, fieldPath, value, context) {
2443
+ const { instance, scopeRoot } = context;
2444
+ if (!("elements" in element)) {
2445
+ return;
2446
+ }
2447
+ if ("multiple" in element && element.multiple) {
2448
+ if (!Array.isArray(value)) {
2449
+ console.warn(
2450
+ `updateContainerField: Expected array for multiple container field "${fieldPath}", got ${typeof value}`
2451
+ );
2452
+ return;
2453
+ }
2454
+ value.forEach((itemValue, index) => {
2455
+ if (isPlainObject(itemValue)) {
2456
+ element.elements.forEach((childElement) => {
2457
+ const childKey = childElement.key;
2458
+ const childPath = `${fieldPath}[${index}].${childKey}`;
2459
+ const childValue = itemValue[childKey];
2460
+ if (childValue !== void 0) {
2461
+ instance.updateField(childPath, childValue);
2462
+ }
2463
+ });
2464
+ }
2465
+ });
2466
+ const existingContainers = scopeRoot.querySelectorAll(
2467
+ `[data-container-item^="${fieldPath}["]`
2468
+ );
2469
+ if (value.length !== existingContainers.length) {
2470
+ console.warn(
2471
+ `updateContainerField: Multiple container field "${fieldPath}" item count mismatch. Consider re-rendering for add/remove.`
2472
+ );
2473
+ }
2474
+ } else {
2475
+ if (!isPlainObject(value)) {
2476
+ console.warn(
2477
+ `updateContainerField: Expected object for container field "${fieldPath}", got ${typeof value}`
2478
+ );
2479
+ return;
2480
+ }
2481
+ element.elements.forEach((childElement) => {
2482
+ const childKey = childElement.key;
2483
+ const childPath = `${fieldPath}.${childKey}`;
2484
+ const childValue = value[childKey];
2485
+ if (childValue !== void 0) {
2486
+ instance.updateField(childPath, childValue);
2487
+ }
2488
+ });
2489
+ }
2490
+ }
2491
+ function renderGroupElement(element, ctx, wrapper, pathKey) {
2492
+ if (typeof console !== "undefined" && console.warn) {
2493
+ console.warn(
2494
+ `[Form Builder] The "group" field type is deprecated and will be removed in a future version. Please use type: "container" with multiple: true instead. Field key: "${element.key}"`
2495
+ );
2496
+ }
2497
+ const containerElement = {
2498
+ key: element.key,
2499
+ label: element.label,
2500
+ description: element.description,
2501
+ hint: element.hint,
2502
+ required: element.required,
2503
+ hidden: element.hidden,
2504
+ default: element.default,
2505
+ actions: element.actions,
2506
+ elements: element.elements,
2507
+ // Translate repeat pattern to multiple pattern
2508
+ multiple: !!(element.repeat && isPlainObject(element.repeat)),
2509
+ minCount: element.repeat?.min,
2510
+ maxCount: element.repeat?.max
2511
+ };
2512
+ if (containerElement.multiple) {
2513
+ renderMultipleContainerElement(containerElement, ctx, wrapper);
2514
+ } else {
2515
+ renderSingleContainerElement(containerElement, ctx, wrapper, pathKey);
2516
+ }
2517
+ }
2518
+ function translateGroupToContainer(element) {
2519
+ const groupElement = element;
2520
+ return {
2521
+ type: "container",
2522
+ key: groupElement.key,
2523
+ label: groupElement.label,
2524
+ description: groupElement.description,
2525
+ hint: groupElement.hint,
2526
+ required: groupElement.required,
2527
+ hidden: groupElement.hidden,
2528
+ default: groupElement.default,
2529
+ actions: groupElement.actions,
2530
+ elements: groupElement.elements,
2531
+ // Translate repeat pattern to multiple pattern
2532
+ multiple: !!(groupElement.repeat && isPlainObject(groupElement.repeat)),
2533
+ minCount: groupElement.repeat?.min,
2534
+ maxCount: groupElement.repeat?.max
2535
+ };
2536
+ }
2537
+ function validateGroupElement(element, key, context) {
2538
+ if (typeof console !== "undefined" && console.warn) {
2539
+ console.warn(
2540
+ `[Form Builder] The "group" field type is deprecated. Please use type: "container" instead. Field key: "${key}"`
2541
+ );
2542
+ }
2543
+ const containerElement = translateGroupToContainer(element);
2544
+ return validateContainerElement(containerElement, key, context);
2545
+ }
2546
+ function updateGroupField(element, fieldPath, value, context) {
2547
+ if (typeof console !== "undefined" && console.warn) {
2548
+ console.warn(
2549
+ `[Form Builder] The "group" field type is deprecated. Please use type: "container" instead. Field path: "${fieldPath}"`
2550
+ );
2551
+ }
2552
+ const containerElement = translateGroupToContainer(element);
2553
+ return updateContainerField(containerElement, fieldPath, value, context);
2554
+ }
2555
+
2556
+ // src/components/index.ts
2557
+ function showTooltip(tooltipId, button) {
2558
+ const tooltip = document.getElementById(tooltipId);
2559
+ if (!tooltip) return;
2560
+ const isCurrentlyVisible = !tooltip.classList.contains("hidden");
2561
+ document.querySelectorAll('[id^="tooltip-"]').forEach((t2) => {
2562
+ t2.classList.add("hidden");
2563
+ });
2564
+ if (isCurrentlyVisible) {
2565
+ return;
2566
+ }
2567
+ const rect = button.getBoundingClientRect();
2568
+ const viewportWidth = window.innerWidth;
2569
+ const viewportHeight = window.innerHeight;
2570
+ if (tooltip && tooltip.parentElement !== document.body) {
2571
+ document.body.appendChild(tooltip);
2572
+ }
2573
+ tooltip.style.visibility = "hidden";
2574
+ tooltip.style.position = "fixed";
2575
+ tooltip.classList.remove("hidden");
2576
+ const tooltipRect = tooltip.getBoundingClientRect();
2577
+ tooltip.classList.add("hidden");
2578
+ tooltip.style.visibility = "visible";
2579
+ let left = rect.left;
2580
+ let top = rect.bottom + 5;
2581
+ if (left + tooltipRect.width > viewportWidth) {
2582
+ left = rect.right - tooltipRect.width;
2583
+ }
2584
+ if (top + tooltipRect.height > viewportHeight) {
2585
+ top = rect.top - tooltipRect.height - 5;
2586
+ }
2587
+ if (left < 10) {
2588
+ left = 10;
2589
+ }
2590
+ if (top < 10) {
2591
+ top = rect.bottom + 5;
2592
+ }
2593
+ tooltip.style.left = `${left}px`;
2594
+ tooltip.style.top = `${top}px`;
2595
+ tooltip.classList.remove("hidden");
2596
+ setTimeout(() => {
2597
+ tooltip.classList.add("hidden");
2598
+ }, 25e3);
2599
+ }
2600
+ if (typeof document !== "undefined") {
2601
+ document.addEventListener("click", (e) => {
2602
+ const target = e.target;
2603
+ const isInfoButton = target.closest("button") && target.closest("button").onclick;
2604
+ const isTooltip = target.closest('[id^="tooltip-"]');
2605
+ if (!isInfoButton && !isTooltip) {
2606
+ document.querySelectorAll('[id^="tooltip-"]').forEach((tooltip) => {
2607
+ tooltip.classList.add("hidden");
2608
+ });
2609
+ }
2610
+ });
2611
+ }
2612
+ function renderElement2(element, ctx) {
2613
+ const wrapper = document.createElement("div");
2614
+ wrapper.className = "mb-6 fb-field-wrapper";
2615
+ const label = document.createElement("div");
2616
+ label.className = "flex items-center mb-2";
2617
+ const title = document.createElement("label");
2618
+ title.className = "text-sm font-medium text-gray-900";
2619
+ title.textContent = element.label || element.key;
2620
+ if (element.required) {
2621
+ const req = document.createElement("span");
2622
+ req.className = "text-red-500 ml-1";
2623
+ req.textContent = "*";
2624
+ title.appendChild(req);
2625
+ }
2626
+ label.appendChild(title);
2627
+ if (element.description || element.hint) {
2628
+ const infoBtn = document.createElement("button");
2629
+ infoBtn.type = "button";
2630
+ infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
2631
+ infoBtn.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><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-6h2v6zm0-8h-2V7h2v2z"/></svg>';
2632
+ const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
2633
+ const tooltip = document.createElement("div");
2634
+ tooltip.id = tooltipId;
2635
+ tooltip.className = "hidden absolute z-50 bg-gray-200 text-gray-900 text-sm rounded-lg p-3 max-w-sm border border-gray-300 shadow-lg";
2636
+ tooltip.style.position = "fixed";
2637
+ tooltip.textContent = element.description || element.hint || "Field information";
2638
+ document.body.appendChild(tooltip);
2639
+ infoBtn.onclick = (e) => {
2640
+ e.preventDefault();
2641
+ e.stopPropagation();
2642
+ showTooltip(tooltipId, infoBtn);
2643
+ };
2644
+ label.appendChild(infoBtn);
2645
+ }
2646
+ wrapper.appendChild(label);
2647
+ const pathKey = pathJoin(ctx.path, element.key);
2648
+ switch (element.type) {
2649
+ case "text":
2650
+ if ("multiple" in element && element.multiple) {
2651
+ renderMultipleTextElement(element, ctx, wrapper, pathKey);
2652
+ } else {
2653
+ renderTextElement(element, ctx, wrapper, pathKey);
2654
+ }
2655
+ break;
2656
+ case "textarea":
2657
+ if ("multiple" in element && element.multiple) {
2658
+ renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
2659
+ } else {
2660
+ renderTextareaElement(element, ctx, wrapper, pathKey);
2661
+ }
2662
+ break;
2663
+ case "number":
2664
+ if ("multiple" in element && element.multiple) {
2665
+ renderMultipleNumberElement(element, ctx, wrapper, pathKey);
2666
+ } else {
2667
+ renderNumberElement(element, ctx, wrapper, pathKey);
2668
+ }
2669
+ break;
2670
+ case "select":
2671
+ if ("multiple" in element && element.multiple) {
2672
+ renderMultipleSelectElement(element, ctx, wrapper, pathKey);
2673
+ } else {
2674
+ renderSelectElement(element, ctx, wrapper, pathKey);
2675
+ }
2676
+ break;
2677
+ case "file":
2678
+ if ("multiple" in element && element.multiple) {
2679
+ renderMultipleFileElement(element, ctx, wrapper, pathKey);
2680
+ } else {
2681
+ renderFileElement(element, ctx, wrapper, pathKey);
2682
+ }
2683
+ break;
2684
+ case "files":
2685
+ renderFilesElement(element, ctx, wrapper, pathKey);
2686
+ break;
2687
+ case "group":
2688
+ renderGroupElement(element, ctx, wrapper, pathKey);
2689
+ break;
2690
+ case "container":
2691
+ if ("multiple" in element && element.multiple) {
2692
+ renderMultipleContainerElement(element, ctx, wrapper);
2693
+ } else {
2694
+ renderSingleContainerElement(element, ctx, wrapper, pathKey);
2695
+ }
2696
+ break;
2697
+ default: {
2698
+ const unsupported = document.createElement("div");
2699
+ unsupported.className = "text-red-500 text-sm";
2700
+ unsupported.textContent = `Unsupported field type: ${element.type}`;
2701
+ wrapper.appendChild(unsupported);
2702
+ }
2703
+ }
2704
+ return wrapper;
2705
+ }
2706
+ setRenderElement(renderElement2);
2707
+
2708
+ // src/instance/state.ts
2709
+ var defaultConfig = {
2710
+ uploadFile: null,
2711
+ downloadFile: null,
2712
+ getThumbnail: null,
2713
+ getDownloadUrl: null,
2714
+ actionHandler: null,
2715
+ onChange: null,
2716
+ onFieldChange: null,
2717
+ onThumbnailError: null,
2718
+ onUploadError: null,
2719
+ onDownloadError: null,
2720
+ debounceMs: 300,
2721
+ enableFilePreview: true,
2722
+ maxPreviewSize: "200px",
2723
+ readonly: false,
2724
+ locale: "en",
2725
+ translations: {
2726
+ en: {
2727
+ addElement: "Add Element",
2728
+ removeElement: "Remove",
2729
+ uploadText: "Upload",
2730
+ dragDropText: "or drag and drop files",
2731
+ dragDropTextSingle: "or drag and drop file",
2732
+ clickDragText: "Click or drag file",
2733
+ noFileSelected: "No file selected",
2734
+ noFilesSelected: "No files selected",
2735
+ downloadButton: "Download"
2736
+ },
2737
+ ru: {
2738
+ addElement: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442",
2739
+ removeElement: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
2740
+ uploadText: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435",
2741
+ dragDropText: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",
2742
+ dragDropTextSingle: "\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",
2743
+ 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",
2744
+ noFileSelected: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",
2745
+ noFilesSelected: "\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",
2746
+ downloadButton: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
2747
+ }
2748
+ },
2749
+ theme: {}
2750
+ };
2751
+ function createInstanceState(config) {
2752
+ return {
2753
+ schema: null,
2754
+ formRoot: null,
2755
+ resourceIndex: /* @__PURE__ */ new Map(),
2756
+ externalActions: null,
2757
+ version: "1.0.0",
2758
+ config: {
2759
+ ...defaultConfig,
2760
+ ...config
2761
+ },
2762
+ debounceTimer: null
2763
+ };
2764
+ }
2765
+ function generateInstanceId() {
2766
+ const timestamp = Date.now().toString(36);
2767
+ const random = Math.random().toString(36).substring(2, 9);
2768
+ return `inst-${timestamp}-${random}`;
2769
+ }
2770
+
2771
+ // src/styles/theme.ts
2772
+ var defaultTheme = {
2773
+ // Colors - matching Tailwind defaults
2774
+ primaryColor: "#3b82f6",
2775
+ // blue-500
2776
+ primaryHoverColor: "#2563eb",
2777
+ // blue-600
2778
+ errorColor: "#ef4444",
2779
+ // red-500
2780
+ errorHoverColor: "#dc2626",
2781
+ // red-600
2782
+ successColor: "#10b981",
2783
+ // green-500
2784
+ borderColor: "#d1d5db",
2785
+ // gray-300
2786
+ borderHoverColor: "#9ca3af",
2787
+ // gray-400
2788
+ borderFocusColor: "#3b82f6",
2789
+ // blue-500
2790
+ backgroundColor: "#ffffff",
2791
+ // white
2792
+ backgroundHoverColor: "#f9fafb",
2793
+ // gray-50
2794
+ backgroundReadonlyColor: "#f3f4f6",
2795
+ // gray-100
2796
+ textColor: "#1f2937",
2797
+ // gray-800
2798
+ textSecondaryColor: "#6b7280",
2799
+ // gray-500
2800
+ textPlaceholderColor: "#9ca3af",
2801
+ // gray-400
2802
+ textDisabledColor: "#d1d5db",
2803
+ // gray-300
2804
+ // Button colors
2805
+ buttonBgColor: "#3b82f6",
2806
+ // blue-500
2807
+ buttonTextColor: "#ffffff",
2808
+ // white
2809
+ buttonBorderColor: "#2563eb",
2810
+ // blue-600
2811
+ buttonHoverBgColor: "#2563eb",
2812
+ // blue-600
2813
+ buttonHoverBorderColor: "#1d4ed8",
2814
+ // blue-700
2815
+ // Action button colors
2816
+ actionBgColor: "#ffffff",
2817
+ // white
2818
+ actionTextColor: "#374151",
2819
+ // gray-700
2820
+ actionBorderColor: "#e5e7eb",
2821
+ // gray-200
2822
+ actionHoverBgColor: "#f9fafb",
2823
+ // gray-50
2824
+ actionHoverBorderColor: "#d1d5db",
2825
+ // gray-300
2826
+ // File upload colors
2827
+ fileUploadBgColor: "#f3f4f6",
2828
+ // gray-100
2829
+ fileUploadBorderColor: "#d1d5db",
2830
+ // gray-300
2831
+ fileUploadTextColor: "#9ca3af",
2832
+ // gray-400
2833
+ fileUploadHoverBorderColor: "#3b82f6",
2834
+ // blue-500
2835
+ // Spacing
2836
+ inputPaddingX: "0.75rem",
2837
+ // 3 (12px)
2838
+ inputPaddingY: "0.5rem",
2839
+ // 2 (8px)
2840
+ borderRadius: "0.5rem",
2841
+ // rounded-lg (8px)
2842
+ borderWidth: "1px",
2843
+ // Typography
2844
+ fontSize: "0.875rem",
2845
+ // text-sm (14px)
2846
+ fontSizeSmall: "0.75rem",
2847
+ // text-xs (12px)
2848
+ fontSizeExtraSmall: "0.625rem",
2849
+ // 10px
2850
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
2851
+ fontWeightNormal: "400",
2852
+ fontWeightMedium: "500",
2853
+ // Focus ring
2854
+ focusRingWidth: "2px",
2855
+ focusRingColor: "#3b82f6",
2856
+ // blue-500
2857
+ focusRingOpacity: "0.5",
2858
+ // Transitions
2859
+ transitionDuration: "200ms"
2860
+ };
2861
+ function generateCSSVariables(theme) {
2862
+ const mergedTheme = { ...defaultTheme, ...theme };
2863
+ const cssVars = [];
2864
+ Object.entries(mergedTheme).forEach(([key, value]) => {
2865
+ const kebabKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
2866
+ cssVars.push(` --fb-${kebabKey}: ${value};`);
2867
+ });
2868
+ return cssVars.join("\n");
2869
+ }
2870
+ function injectThemeVariables(container, theme) {
2871
+ const cssVariables = generateCSSVariables(theme);
2872
+ let styleTag = container.querySelector(
2873
+ "style[data-fb-theme]"
2874
+ );
2875
+ if (!styleTag) {
2876
+ styleTag = document.createElement("style");
2877
+ styleTag.setAttribute("data-fb-theme", "true");
2878
+ container.appendChild(styleTag);
2879
+ }
2880
+ styleTag.textContent = `
2881
+ [data-fb-root="true"] {
2882
+ ${cssVariables}
2883
+ }
2884
+ `;
2885
+ }
2886
+ var exampleThemes = {
2887
+ default: defaultTheme,
2888
+ dark: {
2889
+ ...defaultTheme,
2890
+ primaryColor: "#60a5fa",
2891
+ // blue-400
2892
+ primaryHoverColor: "#3b82f6",
2893
+ // blue-500
2894
+ borderColor: "#4b5563",
2895
+ // gray-600
2896
+ borderHoverColor: "#6b7280",
2897
+ // gray-500
2898
+ borderFocusColor: "#60a5fa",
2899
+ // blue-400
2900
+ backgroundColor: "#1f2937",
2901
+ // gray-800
2902
+ backgroundHoverColor: "#374151",
2903
+ // gray-700
2904
+ backgroundReadonlyColor: "#111827",
2905
+ // gray-900
2906
+ textColor: "#f9fafb",
2907
+ // gray-50
2908
+ textSecondaryColor: "#9ca3af",
2909
+ // gray-400
2910
+ textPlaceholderColor: "#6b7280",
2911
+ // gray-500
2912
+ fileUploadBgColor: "#374151",
2913
+ // gray-700
2914
+ fileUploadBorderColor: "#4b5563",
2915
+ // gray-600
2916
+ fileUploadTextColor: "#9ca3af"
2917
+ // gray-400
2918
+ },
2919
+ klein: {
2920
+ ...defaultTheme,
2921
+ primaryColor: "#0066cc",
2922
+ primaryHoverColor: "#0052a3",
2923
+ errorColor: "#d32f2f",
2924
+ errorHoverColor: "#c62828",
2925
+ successColor: "#388e3c",
2926
+ borderColor: "#e0e0e0",
2927
+ borderHoverColor: "#bdbdbd",
2928
+ borderFocusColor: "#0066cc",
2929
+ borderRadius: "4px",
2930
+ fontSize: "16px",
2931
+ fontSizeSmall: "14px",
2932
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
2933
+ }
2934
+ };
2935
+
2936
+ // src/utils/styles.ts
2937
+ function applyActionButtonStyles(button, isFormLevel = false) {
2938
+ button.style.cssText = `
2939
+ background-color: var(--fb-action-bg-color);
2940
+ color: var(--fb-action-text-color);
2941
+ border: var(--fb-border-width) solid var(--fb-action-border-color);
2942
+ padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
2943
+ font-size: var(--fb-font-size);
2944
+ font-weight: var(--fb-font-weight-medium);
2945
+ border-radius: var(--fb-border-radius);
2946
+ transition: all var(--fb-transition-duration);
2947
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
2948
+ `;
2949
+ button.addEventListener("mouseenter", () => {
2950
+ button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
2951
+ button.style.borderColor = "var(--fb-action-hover-border-color)";
2952
+ });
2953
+ button.addEventListener("mouseleave", () => {
2954
+ button.style.backgroundColor = "var(--fb-action-bg-color)";
2955
+ button.style.borderColor = "var(--fb-action-border-color)";
2956
+ });
2957
+ }
2958
+
2959
+ // src/components/registry.ts
2960
+ var componentRegistry = {
2961
+ text: {
2962
+ validate: validateTextElement,
2963
+ update: updateTextField
2964
+ },
2965
+ textarea: {
2966
+ validate: validateTextareaElement,
2967
+ update: updateTextareaField
2968
+ },
2969
+ number: {
2970
+ validate: validateNumberElement,
2971
+ update: updateNumberField
2972
+ },
2973
+ select: {
2974
+ validate: validateSelectElement,
2975
+ update: updateSelectField
2976
+ },
2977
+ file: {
2978
+ validate: validateFileElement,
2979
+ update: updateFileField
2980
+ },
2981
+ files: {
2982
+ // Legacy type - delegates to file
2983
+ validate: validateFileElement,
2984
+ update: updateFileField
2985
+ },
2986
+ container: {
2987
+ validate: validateContainerElement,
2988
+ update: updateContainerField
2989
+ },
2990
+ group: {
2991
+ // Deprecated type - delegates to container
2992
+ validate: validateGroupElement,
2993
+ update: updateGroupField
2994
+ }
2995
+ };
2996
+ function getComponentOperations(elementType) {
2997
+ return componentRegistry[elementType] || null;
2998
+ }
2999
+ function validateElementWithComponent(element, key, context) {
3000
+ const ops = getComponentOperations(element.type);
3001
+ if (ops && ops.validate) {
3002
+ return ops.validate(element, key, context);
3003
+ }
3004
+ return null;
3005
+ }
3006
+ function updateElementWithComponent(element, fieldPath, value, context) {
3007
+ const ops = getComponentOperations(element.type);
3008
+ if (ops && ops.update) {
3009
+ ops.update(element, fieldPath, value, context);
3010
+ return true;
3011
+ }
3012
+ return false;
3013
+ }
3014
+
3015
+ // src/instance/FormBuilderInstance.ts
3016
+ var FormBuilderInstance = class {
3017
+ constructor(config) {
3018
+ this.instanceId = generateInstanceId();
3019
+ this.state = createInstanceState(config);
3020
+ if (process.env.NODE_ENV === "development") {
3021
+ if (config?.getThumbnail) {
3022
+ const testResult = config.getThumbnail("test-id");
3023
+ if (!(testResult instanceof Promise)) {
3024
+ console.warn(
3025
+ "[form-builder] getThumbnail must be async (return Promise). Wrap your function: async (id) => { ... }"
3026
+ );
3027
+ }
3028
+ }
3029
+ if (!globalThis.__formBuilderInstances) {
3030
+ globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
3031
+ }
3032
+ globalThis.__formBuilderInstances.add(this.instanceId);
3033
+ if (globalThis.__formBuilderInstances.size > 10) {
3034
+ console.warn(
3035
+ `[form-builder] ${globalThis.__formBuilderInstances.size} instances active. Possible memory leak - ensure you call destroy() when done.`
3036
+ );
3037
+ }
3038
+ }
3039
+ }
3040
+ /**
3041
+ * Get instance ID (useful for debugging and resource prefixing)
3042
+ */
3043
+ getInstanceId() {
3044
+ return this.instanceId;
3045
+ }
3046
+ /**
3047
+ * Get current state (for advanced use cases)
3048
+ */
3049
+ getState() {
3050
+ return this.state;
3051
+ }
3052
+ /**
3053
+ * Set the form root element
3054
+ */
3055
+ setFormRoot(element) {
3056
+ this.state.formRoot = element;
3057
+ }
3058
+ /**
3059
+ * Configure the form builder
3060
+ */
3061
+ configure(config) {
3062
+ Object.assign(this.state.config, config);
3063
+ }
3064
+ /**
3065
+ * Set file upload handler
3066
+ */
3067
+ setUploadHandler(uploadFn) {
3068
+ this.state.config.uploadFile = uploadFn;
3069
+ }
3070
+ /**
3071
+ * Set file download handler
3072
+ */
3073
+ setDownloadHandler(downloadFn) {
3074
+ this.state.config.downloadFile = downloadFn;
3075
+ }
3076
+ /**
3077
+ * Set thumbnail generation handler
3078
+ */
3079
+ setThumbnailHandler(thumbnailFn) {
3080
+ this.state.config.getThumbnail = thumbnailFn;
3081
+ }
3082
+ /**
3083
+ * Set action handler
3084
+ */
3085
+ setActionHandler(actionFn) {
3086
+ this.state.config.actionHandler = actionFn;
3087
+ }
3088
+ /**
3089
+ * Set mode (edit or readonly)
3090
+ */
3091
+ setMode(mode) {
3092
+ this.state.config.readonly = mode === "readonly";
3093
+ }
3094
+ /**
3095
+ * Set locale
3096
+ */
3097
+ setLocale(locale) {
3098
+ if (this.state.config.translations[locale]) {
3099
+ this.state.config.locale = locale;
3100
+ }
3101
+ }
3102
+ /**
3103
+ * Trigger onChange callbacks with debouncing
3104
+ * @param fieldPath - Optional field path for field-specific change events
3105
+ * @param fieldValue - Optional field value for field-specific change events
3106
+ */
3107
+ triggerOnChange(fieldPath, fieldValue) {
3108
+ if (this.state.config.readonly) return;
3109
+ if (this.state.debounceTimer !== null) {
3110
+ clearTimeout(this.state.debounceTimer);
3111
+ }
3112
+ this.state.debounceTimer = setTimeout(() => {
3113
+ const formData = this.validateForm(true);
3114
+ if (this.state.config.onChange) {
3115
+ this.state.config.onChange(formData);
3116
+ }
3117
+ if (this.state.config.onFieldChange && fieldPath !== void 0 && fieldValue !== void 0) {
3118
+ this.state.config.onFieldChange(fieldPath, fieldValue, formData);
3119
+ }
3120
+ this.state.debounceTimer = null;
3121
+ }, this.state.config.debounceMs);
3122
+ }
3123
+ /**
3124
+ * Register an external action that will be displayed as a button
3125
+ * External actions can be form-level (no related_field) or field-level (with related_field)
3126
+ * @param action - External action definition
3127
+ */
3128
+ registerAction(action) {
3129
+ if (!action || !action.value) {
3130
+ throw new Error("Action must have a value property");
3131
+ }
3132
+ if (!this.state.externalActions) {
3133
+ this.state.externalActions = [];
3134
+ }
3135
+ const existingIndex = this.state.externalActions.findIndex(
3136
+ (a) => a.value === action.value && a.related_field === action.related_field
3137
+ );
3138
+ if (existingIndex >= 0) {
3139
+ this.state.externalActions[existingIndex] = action;
3140
+ } else {
3141
+ this.state.externalActions.push(action);
3142
+ }
3143
+ }
3144
+ /**
3145
+ * Find the DOM element corresponding to a field path (instance-scoped)
3146
+ */
3147
+ findFormElementByFieldPath(fieldPath) {
3148
+ if (!this.state.formRoot) return null;
3149
+ if (!this.state.config.readonly) {
3150
+ let element = this.state.formRoot.querySelector(
3151
+ `[name="${fieldPath}"]`
3152
+ );
3153
+ if (element) return element;
3154
+ const variations = [
3155
+ fieldPath,
3156
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
3157
+ fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
3158
+ ];
3159
+ for (const variation of variations) {
3160
+ element = this.state.formRoot.querySelector(
3161
+ `[name="${variation}"]`
3162
+ );
3163
+ if (element) return element;
3164
+ }
3165
+ }
3166
+ const schemaElement = this.findSchemaElement(fieldPath);
3167
+ if (!schemaElement) return null;
3168
+ const fieldWrappers = this.state.formRoot.querySelectorAll(".fb-field-wrapper");
3169
+ for (const wrapper of fieldWrappers) {
3170
+ const labelText = schemaElement.label || schemaElement.key;
3171
+ const labelElement = wrapper.querySelector("label");
3172
+ if (labelElement && (labelElement.textContent === labelText || labelElement.textContent === `${labelText}*`)) {
3173
+ let fieldElement = wrapper.querySelector(".field-placeholder");
3174
+ if (!fieldElement) {
3175
+ fieldElement = document.createElement("div");
3176
+ fieldElement.className = "field-placeholder";
3177
+ fieldElement.style.display = "none";
3178
+ wrapper.appendChild(fieldElement);
3179
+ }
3180
+ return fieldElement;
3181
+ }
3182
+ }
3183
+ return null;
3184
+ }
3185
+ /**
3186
+ * Find schema element by field path
3187
+ */
3188
+ findSchemaElement(fieldPath) {
3189
+ if (!this.state.schema || !this.state.schema.elements) return null;
3190
+ let currentElements = this.state.schema.elements;
3191
+ let foundElement = null;
3192
+ const keys = fieldPath.replace(/\[\d+\]/g, "").split(".").filter(Boolean);
3193
+ for (const key of keys) {
3194
+ foundElement = currentElements.find((el) => el.key === key) || null;
3195
+ if (!foundElement) {
3196
+ return null;
3197
+ }
3198
+ if ("elements" in foundElement && foundElement.elements) {
3199
+ currentElements = foundElement.elements;
3200
+ }
3201
+ }
3202
+ return foundElement;
3203
+ }
3204
+ /**
3205
+ * Resolve action label from schema or external action
3206
+ */
3207
+ resolveActionLabel(actionKey, externalLabel, schemaElement, isTrueFormLevelAction = false) {
3208
+ if (schemaElement && "actions" in schemaElement && schemaElement.actions) {
3209
+ const predefinedAction = schemaElement.actions.find(
3210
+ (a) => a.key === actionKey
3211
+ );
3212
+ if (predefinedAction && predefinedAction.label) {
3213
+ return predefinedAction.label;
3214
+ }
3215
+ }
3216
+ if (isTrueFormLevelAction && this.state.schema && "actions" in this.state.schema && this.state.schema.actions) {
3217
+ const rootAction = this.state.schema.actions.find(
3218
+ (a) => a.key === actionKey
3219
+ );
3220
+ if (rootAction && rootAction.label) {
3221
+ return rootAction.label;
3222
+ }
3223
+ }
3224
+ if (externalLabel) {
3225
+ return externalLabel;
3226
+ }
3227
+ return actionKey;
3228
+ }
3229
+ /**
3230
+ * Render form-level action buttons at the bottom of the form
3231
+ */
3232
+ renderFormLevelActions(actions, trueFormLevelActions = []) {
3233
+ if (!this.state.formRoot) return;
3234
+ const existingContainer = this.state.formRoot.querySelector(
3235
+ ".form-level-actions-container"
3236
+ );
3237
+ if (existingContainer) {
3238
+ existingContainer.remove();
3239
+ }
3240
+ const actionsContainer = document.createElement("div");
3241
+ actionsContainer.className = "form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center";
3242
+ actionsContainer.style.cssText = `
3243
+ border-top: var(--fb-border-width) solid var(--fb-border-color);
3244
+ `;
3245
+ actions.forEach((action) => {
3246
+ const actionBtn = document.createElement("button");
3247
+ actionBtn.type = "button";
3248
+ applyActionButtonStyles(actionBtn, true);
3249
+ const isTrueFormLevelAction = trueFormLevelActions.includes(action);
3250
+ const resolvedLabel = this.resolveActionLabel(
3251
+ action.key,
3252
+ action.label,
3253
+ null,
3254
+ isTrueFormLevelAction
3255
+ );
3256
+ actionBtn.textContent = resolvedLabel;
3257
+ actionBtn.addEventListener("click", (e) => {
3258
+ e.preventDefault();
3259
+ e.stopPropagation();
3260
+ if (this.state.config.actionHandler && typeof this.state.config.actionHandler === "function") {
3261
+ this.state.config.actionHandler(action.value, action.key, null);
3262
+ }
3263
+ });
3264
+ actionsContainer.appendChild(actionBtn);
3265
+ });
3266
+ this.state.formRoot.appendChild(actionsContainer);
3267
+ }
3268
+ /**
3269
+ * Render external action buttons for fields
3270
+ */
3271
+ renderExternalActions() {
3272
+ if (!this.state.externalActions || !Array.isArray(this.state.externalActions))
3273
+ return;
3274
+ const actionsByField = /* @__PURE__ */ new Map();
3275
+ const trueFormLevelActions = [];
3276
+ const movedFormLevelActions = [];
3277
+ this.state.externalActions.forEach((action) => {
3278
+ if (!action.key || !action.value) return;
3279
+ if (!action.related_field) {
3280
+ trueFormLevelActions.push(action);
3281
+ } else {
3282
+ if (!actionsByField.has(action.related_field)) {
3283
+ actionsByField.set(action.related_field, []);
3284
+ }
3285
+ actionsByField.get(action.related_field).push(action);
3286
+ }
3287
+ });
3288
+ actionsByField.forEach((actions, fieldPath) => {
3289
+ const fieldElement = this.findFormElementByFieldPath(fieldPath);
3290
+ if (!fieldElement) {
3291
+ console.warn(
3292
+ `External action: Could not find form element for field "${fieldPath}", treating as form-level actions`
3293
+ );
3294
+ movedFormLevelActions.push(...actions);
3295
+ return;
3296
+ }
3297
+ let wrapper = fieldElement.closest(".fb-field-wrapper");
3298
+ if (!wrapper) {
3299
+ wrapper = fieldElement.parentElement;
3300
+ }
3301
+ if (!wrapper) {
3302
+ console.warn(
3303
+ `External action: Could not find wrapper for field "${fieldPath}"`
3304
+ );
3305
+ return;
3306
+ }
3307
+ const existingContainer = wrapper.querySelector(
3308
+ ".external-actions-container"
3309
+ );
3310
+ if (existingContainer) {
3311
+ existingContainer.remove();
3312
+ }
3313
+ const actionsContainer = document.createElement("div");
3314
+ actionsContainer.className = "external-actions-container mt-3 flex flex-wrap gap-2";
3315
+ const schemaElement = this.findSchemaElement(fieldPath);
3316
+ actions.forEach((action) => {
3317
+ const actionBtn = document.createElement("button");
3318
+ actionBtn.type = "button";
3319
+ applyActionButtonStyles(actionBtn, false);
3320
+ const resolvedLabel = this.resolveActionLabel(
3321
+ action.key,
3322
+ action.label,
3323
+ schemaElement
3324
+ );
3325
+ actionBtn.textContent = resolvedLabel;
3326
+ actionBtn.addEventListener("click", (e) => {
3327
+ e.preventDefault();
3328
+ e.stopPropagation();
3329
+ if (this.state.config.actionHandler && typeof this.state.config.actionHandler === "function") {
3330
+ this.state.config.actionHandler(
3331
+ action.value,
3332
+ action.key,
3333
+ action.related_field
3334
+ );
3335
+ }
3336
+ });
3337
+ actionsContainer.appendChild(actionBtn);
3338
+ });
3339
+ wrapper.appendChild(actionsContainer);
3340
+ });
3341
+ const allFormLevelActions = [
3342
+ ...trueFormLevelActions,
3343
+ ...movedFormLevelActions
3344
+ ];
3345
+ if (allFormLevelActions.length > 0) {
3346
+ this.renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
3347
+ }
3348
+ }
3349
+ /**
3350
+ * Render form from schema
3351
+ */
3352
+ renderForm(root, schema, prefill, actions) {
3353
+ const errors = validateSchema(schema);
3354
+ if (errors.length > 0) {
3355
+ console.error("Schema validation errors:", errors);
3356
+ return;
3357
+ }
3358
+ this.state.formRoot = root;
3359
+ this.state.schema = schema;
3360
+ this.state.externalActions = actions || null;
3361
+ clear(root);
3362
+ root.setAttribute("data-fb-root", "true");
3363
+ injectThemeVariables(root, this.state.config.theme);
3364
+ const formEl = document.createElement("div");
3365
+ formEl.className = "space-y-6";
3366
+ schema.elements.forEach((element) => {
3367
+ if (element.hidden) {
3368
+ return;
3369
+ }
3370
+ const block = renderElement2(element, {
3371
+ path: "",
3372
+ prefill: prefill || {},
3373
+ state: this.state,
3374
+ instance: this
3375
+ });
3376
+ formEl.appendChild(block);
3377
+ });
3378
+ root.appendChild(formEl);
3379
+ if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
3380
+ this.renderExternalActions();
3381
+ }
3382
+ }
3383
+ /**
3384
+ * Validate form and extract data
3385
+ * This is a complete copy of the validateForm logic from form-builder.ts
3386
+ * but uses instance state instead of global state
3387
+ */
3388
+ validateForm(skipValidation = false) {
3389
+ if (!this.state.schema || !this.state.formRoot)
3390
+ return { valid: true, errors: [], data: {} };
3391
+ const errors = [];
3392
+ const data = {};
3393
+ const validateElement2 = (element, ctx, customScopeRoot = null) => {
3394
+ const key = element.key;
3395
+ const scopeRoot = customScopeRoot || this.state.formRoot;
3396
+ const componentContext = {
3397
+ scopeRoot,
3398
+ state: this.state,
3399
+ instance: this,
3400
+ path: ctx.path,
3401
+ skipValidation
3402
+ };
3403
+ const componentResult = validateElementWithComponent(
3404
+ element,
3405
+ key,
3406
+ componentContext
3407
+ );
3408
+ if (componentResult !== null) {
3409
+ errors.push(...componentResult.errors);
3410
+ return componentResult.value;
3411
+ }
3412
+ console.warn(`Unknown field type "${element.type}" for key "${key}"`);
3413
+ return null;
3414
+ };
3415
+ setValidateElement(validateElement2);
3416
+ this.state.schema.elements.forEach((element) => {
3417
+ if (element.hidden) {
3418
+ data[element.key] = element.default !== void 0 ? element.default : null;
3419
+ } else {
3420
+ data[element.key] = validateElement2(element, { path: "" });
3421
+ }
3422
+ });
3423
+ return {
3424
+ valid: errors.length === 0,
3425
+ errors,
3426
+ data
3427
+ };
3428
+ }
3429
+ /**
3430
+ * Get form data
3431
+ */
3432
+ getFormData() {
3433
+ return this.validateForm(false);
3434
+ }
3435
+ /**
3436
+ * Submit form with validation
3437
+ */
3438
+ submitForm() {
3439
+ const result = this.validateForm(false);
3440
+ if (result.valid) {
3441
+ if (typeof window !== "undefined" && window.parent) {
3442
+ window.parent.postMessage(
3443
+ {
3444
+ type: "formSubmit",
3445
+ data: result.data,
3446
+ schema: this.state.schema
3447
+ },
3448
+ "*"
3449
+ );
3450
+ }
3451
+ }
3452
+ return result;
3453
+ }
3454
+ /**
3455
+ * Save draft without validation
3456
+ */
3457
+ saveDraft() {
3458
+ const result = this.validateForm(true);
3459
+ if (typeof window !== "undefined" && window.parent) {
3460
+ window.parent.postMessage(
3461
+ {
3462
+ type: "formDraft",
3463
+ data: result.data,
3464
+ schema: this.state.schema
3465
+ },
3466
+ "*"
3467
+ );
3468
+ }
3469
+ return result;
3470
+ }
3471
+ /**
3472
+ * Clear the form - reset all field values to empty while preserving form structure
3473
+ * This is done by re-rendering the form with empty data
3474
+ */
3475
+ clearForm() {
3476
+ if (!this.state.schema || !this.state.formRoot) {
3477
+ console.warn("clearForm: Form not initialized. Call renderForm() first.");
3478
+ return;
3479
+ }
3480
+ const schema = this.state.schema;
3481
+ const formRoot = this.state.formRoot;
3482
+ const emptyPrefill = this.buildHiddenFieldsData(schema.elements);
3483
+ this.renderForm(formRoot, schema, emptyPrefill);
3484
+ }
3485
+ /**
3486
+ * Build prefill data for hidden fields only
3487
+ * Hidden fields should retain their values when clearing
3488
+ */
3489
+ buildHiddenFieldsData(elements, parentPath = "") {
3490
+ const data = {};
3491
+ for (const element of elements) {
3492
+ const key = element.key;
3493
+ const fieldPath = parentPath ? `${parentPath}.${key}` : key;
3494
+ if (element.hidden && element.default !== void 0) {
3495
+ data[fieldPath] = element.default;
3496
+ }
3497
+ if (element.type === "container" || element.type === "group") {
3498
+ const containerElement = element;
3499
+ const nestedData = this.buildHiddenFieldsData(
3500
+ containerElement.elements,
3501
+ fieldPath
3502
+ );
3503
+ Object.assign(data, nestedData);
3504
+ }
3505
+ }
3506
+ return data;
3507
+ }
3508
+ /**
3509
+ * Set form data - update multiple fields without full re-render
3510
+ * @param data - Object with field paths and their values
3511
+ */
3512
+ setFormData(data) {
3513
+ if (!this.state.schema || !this.state.formRoot) {
3514
+ console.warn("setFormData: Form not initialized. Call renderForm() first.");
3515
+ return;
3516
+ }
3517
+ for (const fieldPath in data) {
3518
+ this.updateField(fieldPath, data[fieldPath]);
3519
+ }
3520
+ }
3521
+ /**
3522
+ * Update a single field by path without full re-render
3523
+ * @param fieldPath - Field path (e.g., "email", "address.city", "items[0].name")
3524
+ * @param value - New value for the field
3525
+ */
3526
+ updateField(fieldPath, value) {
3527
+ if (!this.state.schema || !this.state.formRoot) {
3528
+ console.warn("updateField: Form not initialized. Call renderForm() first.");
3529
+ return;
3530
+ }
3531
+ const schemaElement = this.findSchemaElement(fieldPath);
3532
+ if (!schemaElement) {
3533
+ console.warn(`updateField: Schema element not found for path "${fieldPath}"`);
3534
+ return;
3535
+ }
3536
+ const domElement = this.findFormElementByFieldPath(fieldPath);
3537
+ if (!domElement) {
3538
+ console.warn(`updateField: DOM element not found for path "${fieldPath}"`);
3539
+ return;
3540
+ }
3541
+ this.updateFieldValue(domElement, schemaElement, fieldPath, value);
3542
+ if (this.state.config.onChange || this.state.config.onFieldChange) {
3543
+ this.triggerOnChange(fieldPath, value);
3544
+ }
3545
+ }
3546
+ /**
3547
+ * Update field value in DOM based on field type
3548
+ * Delegates to component-specific updaters via registry
3549
+ */
3550
+ updateFieldValue(domElement, schemaElement, fieldPath, value) {
3551
+ const componentContext = {
3552
+ scopeRoot: this.state.formRoot,
3553
+ state: this.state,
3554
+ instance: this,
3555
+ path: ""
3556
+ };
3557
+ const handled = updateElementWithComponent(
3558
+ schemaElement,
3559
+ fieldPath,
3560
+ value,
3561
+ componentContext
3562
+ );
3563
+ if (!handled) {
3564
+ console.warn(
3565
+ `updateField: No updater found for field type "${schemaElement.type}" at path "${fieldPath}"`
3566
+ );
3567
+ }
3568
+ }
3569
+ /**
3570
+ * Destroy instance and clean up resources
3571
+ */
3572
+ destroy() {
3573
+ if (this.state.debounceTimer !== null) {
3574
+ clearTimeout(this.state.debounceTimer);
3575
+ this.state.debounceTimer = null;
3576
+ }
3577
+ this.state.resourceIndex.clear();
3578
+ if (this.state.formRoot) {
3579
+ clear(this.state.formRoot);
3580
+ }
3581
+ this.state.formRoot = null;
3582
+ this.state.schema = null;
3583
+ this.state.externalActions = null;
3584
+ if (process.env.NODE_ENV === "development") {
3585
+ globalThis.__formBuilderInstances?.delete(this.instanceId);
3586
+ }
3587
+ }
3588
+ };
3589
+
3590
+ // src/index.ts
3591
+ function createFormBuilder(config) {
3592
+ return new FormBuilderInstance(config);
3593
+ }
3594
+ var index_default = FormBuilderInstance;
3595
+ if (typeof window !== "undefined") {
3596
+ window.FormBuilder = FormBuilderInstance;
3597
+ window.createFormBuilder = createFormBuilder;
3598
+ window.validateSchema = validateSchema;
3599
+ }
3600
+
3601
+ export { FormBuilderInstance, createFormBuilder, index_default as default, defaultTheme, exampleThemes, validateSchema };
3602
+ //# sourceMappingURL=index.js.map
3603
+ //# sourceMappingURL=index.js.map