@dmitryvim/form-builder 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -87,6 +87,43 @@ function validateSchema(schema) {
87
87
  validateElements(element.elements, `${elementPath}.elements`);
88
88
  }
89
89
  if (element.type === "container" && element.elements) {
90
+ if ("columns" in element && element.columns !== void 0) {
91
+ const columns = element.columns;
92
+ const validColumns = [1, 2, 3, 4];
93
+ if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
94
+ errors.push(
95
+ `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
96
+ );
97
+ }
98
+ }
99
+ if ("prefillHints" in element && element.prefillHints) {
100
+ const prefillHints = element.prefillHints;
101
+ if (Array.isArray(prefillHints)) {
102
+ prefillHints.forEach((hint, hintIndex) => {
103
+ if (!hint.label || typeof hint.label !== "string") {
104
+ errors.push(
105
+ `${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
106
+ );
107
+ }
108
+ if (!hint.values || typeof hint.values !== "object") {
109
+ errors.push(
110
+ `${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
111
+ );
112
+ } else {
113
+ for (const fieldKey in hint.values) {
114
+ const fieldExists = element.elements.some(
115
+ (childElement) => childElement.key === fieldKey
116
+ );
117
+ if (!fieldExists) {
118
+ errors.push(
119
+ `container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
120
+ );
121
+ }
122
+ }
123
+ }
124
+ });
125
+ }
126
+ }
90
127
  validateElements(element.elements, `${elementPath}.elements`);
91
128
  }
92
129
  if (element.type === "select" && element.options) {
@@ -1222,6 +1259,172 @@ function t(key, state) {
1222
1259
  }
1223
1260
 
1224
1261
  // src/components/file.ts
1262
+ function renderLocalImagePreview(container, file, fileName) {
1263
+ const img = document.createElement("img");
1264
+ img.className = "w-full h-full object-contain";
1265
+ img.alt = fileName || "Preview";
1266
+ const reader = new FileReader();
1267
+ reader.onload = (e) => {
1268
+ img.src = e.target?.result || "";
1269
+ };
1270
+ reader.readAsDataURL(file);
1271
+ container.appendChild(img);
1272
+ }
1273
+ function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
1274
+ const videoUrl = URL.createObjectURL(file);
1275
+ container.onclick = null;
1276
+ const newContainer = container.cloneNode(false);
1277
+ if (container.parentNode) {
1278
+ container.parentNode.replaceChild(newContainer, container);
1279
+ }
1280
+ newContainer.innerHTML = `
1281
+ <div class="relative group h-full">
1282
+ <video class="w-full h-full object-contain" controls preload="auto" muted>
1283
+ <source src="${videoUrl}" type="${videoType}">
1284
+ Your browser does not support the video tag.
1285
+ </video>
1286
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1287
+ <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1288
+ ${t("removeElement", state)}
1289
+ </button>
1290
+ <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1291
+ Change
1292
+ </button>
1293
+ </div>
1294
+ </div>
1295
+ `;
1296
+ attachVideoButtonHandlers(newContainer, resourceId, state, deps);
1297
+ return newContainer;
1298
+ }
1299
+ function attachVideoButtonHandlers(container, resourceId, state, deps) {
1300
+ const changeBtn = container.querySelector(".change-file-btn");
1301
+ if (changeBtn) {
1302
+ changeBtn.onclick = (e) => {
1303
+ e.stopPropagation();
1304
+ if (deps?.picker) {
1305
+ deps.picker.click();
1306
+ }
1307
+ };
1308
+ }
1309
+ const deleteBtn = container.querySelector(".delete-file-btn");
1310
+ if (deleteBtn) {
1311
+ deleteBtn.onclick = (e) => {
1312
+ e.stopPropagation();
1313
+ handleVideoDelete(container, resourceId, state, deps);
1314
+ };
1315
+ }
1316
+ }
1317
+ function handleVideoDelete(container, resourceId, state, deps) {
1318
+ state.resourceIndex.delete(resourceId);
1319
+ const hiddenInput = container.parentElement?.querySelector(
1320
+ 'input[type="hidden"]'
1321
+ );
1322
+ if (hiddenInput) {
1323
+ hiddenInput.value = "";
1324
+ }
1325
+ if (deps?.fileUploadHandler) {
1326
+ container.onclick = deps.fileUploadHandler;
1327
+ }
1328
+ if (deps?.dragHandler) {
1329
+ setupDragAndDrop(container, deps.dragHandler);
1330
+ }
1331
+ container.innerHTML = `
1332
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1333
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1334
+ <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"/>
1335
+ </svg>
1336
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1337
+ </div>
1338
+ `;
1339
+ }
1340
+ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1341
+ const video = document.createElement("video");
1342
+ video.className = "w-full h-full object-contain";
1343
+ video.controls = true;
1344
+ video.preload = "metadata";
1345
+ video.muted = true;
1346
+ const source = document.createElement("source");
1347
+ source.src = thumbnailUrl;
1348
+ source.type = videoType;
1349
+ video.appendChild(source);
1350
+ video.appendChild(document.createTextNode("Your browser does not support the video tag."));
1351
+ container.appendChild(video);
1352
+ }
1353
+ function renderDeleteButton(container, resourceId, state) {
1354
+ addDeleteButton(container, state, () => {
1355
+ state.resourceIndex.delete(resourceId);
1356
+ const hiddenInput = container.parentElement?.querySelector(
1357
+ 'input[type="hidden"]'
1358
+ );
1359
+ if (hiddenInput) {
1360
+ hiddenInput.value = "";
1361
+ }
1362
+ container.innerHTML = `
1363
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1364
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1365
+ <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"/>
1366
+ </svg>
1367
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1368
+ </div>
1369
+ `;
1370
+ });
1371
+ }
1372
+ async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
1373
+ if (!meta.file || !(meta.file instanceof File)) {
1374
+ return;
1375
+ }
1376
+ if (meta.type && meta.type.startsWith("image/")) {
1377
+ renderLocalImagePreview(container, meta.file, fileName);
1378
+ } else if (meta.type && meta.type.startsWith("video/")) {
1379
+ const newContainer = renderLocalVideoPreview(
1380
+ container,
1381
+ meta.file,
1382
+ meta.type,
1383
+ resourceId,
1384
+ state,
1385
+ deps
1386
+ );
1387
+ container = newContainer;
1388
+ } else {
1389
+ 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>`;
1390
+ }
1391
+ if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
1392
+ renderDeleteButton(container, resourceId, state);
1393
+ }
1394
+ }
1395
+ async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
1396
+ if (!state.config.getThumbnail) {
1397
+ setEmptyFileContainer(container, state);
1398
+ return;
1399
+ }
1400
+ try {
1401
+ const thumbnailUrl = await state.config.getThumbnail(resourceId);
1402
+ if (thumbnailUrl) {
1403
+ clear(container);
1404
+ if (meta && meta.type && meta.type.startsWith("video/")) {
1405
+ renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
1406
+ } else {
1407
+ const img = document.createElement("img");
1408
+ img.className = "w-full h-full object-contain";
1409
+ img.alt = fileName || "Preview";
1410
+ img.src = thumbnailUrl;
1411
+ container.appendChild(img);
1412
+ }
1413
+ } else {
1414
+ setEmptyFileContainer(container, state);
1415
+ }
1416
+ } catch (error) {
1417
+ console.error("Failed to get thumbnail:", error);
1418
+ container.innerHTML = `
1419
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1420
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1421
+ <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"/>
1422
+ </svg>
1423
+ <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1424
+ </div>
1425
+ `;
1426
+ }
1427
+ }
1225
1428
  async function renderFilePreview(container, resourceId, state, options = {}) {
1226
1429
  const { fileName = "", isReadonly = false, deps = null } = options;
1227
1430
  if (!isReadonly && deps && (!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)) {
@@ -1233,141 +1436,19 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
1233
1436
  if (isReadonly) {
1234
1437
  container.classList.add("cursor-pointer");
1235
1438
  }
1236
- const img = document.createElement("img");
1237
- img.className = "w-full h-full object-contain";
1238
- img.alt = fileName || "Preview";
1239
1439
  const meta = state.resourceIndex.get(resourceId);
1240
1440
  if (meta && meta.file && meta.file instanceof File) {
1241
- if (meta.type && meta.type.startsWith("image/")) {
1242
- const reader = new FileReader();
1243
- reader.onload = (e) => {
1244
- img.src = e.target?.result || "";
1245
- };
1246
- reader.readAsDataURL(meta.file);
1247
- container.appendChild(img);
1248
- } else if (meta.type && meta.type.startsWith("video/")) {
1249
- const videoUrl = URL.createObjectURL(meta.file);
1250
- container.onclick = null;
1251
- const newContainer = container.cloneNode(false);
1252
- if (container.parentNode) {
1253
- container.parentNode.replaceChild(newContainer, container);
1254
- }
1255
- container = newContainer;
1256
- container.innerHTML = `
1257
- <div class="relative group h-full">
1258
- <video class="w-full h-full object-contain" controls preload="auto" muted>
1259
- <source src="${videoUrl}" type="${meta.type}">
1260
- Your browser does not support the video tag.
1261
- </video>
1262
- <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1263
- <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1264
- ${t("removeElement", state)}
1265
- </button>
1266
- <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1267
- Change
1268
- </button>
1269
- </div>
1270
- </div>
1271
- `;
1272
- const changeBtn = container.querySelector(
1273
- ".change-file-btn"
1274
- );
1275
- if (changeBtn) {
1276
- changeBtn.onclick = (e) => {
1277
- e.stopPropagation();
1278
- if (deps?.picker) {
1279
- deps.picker.click();
1280
- }
1281
- };
1282
- }
1283
- const deleteBtn = container.querySelector(
1284
- ".delete-file-btn"
1285
- );
1286
- if (deleteBtn) {
1287
- deleteBtn.onclick = (e) => {
1288
- e.stopPropagation();
1289
- state.resourceIndex.delete(resourceId);
1290
- const hiddenInput = container.parentElement?.querySelector(
1291
- 'input[type="hidden"]'
1292
- );
1293
- if (hiddenInput) {
1294
- hiddenInput.value = "";
1295
- }
1296
- if (deps?.fileUploadHandler) {
1297
- container.onclick = deps.fileUploadHandler;
1298
- }
1299
- if (deps?.dragHandler) {
1300
- setupDragAndDrop(container, deps.dragHandler);
1301
- }
1302
- container.innerHTML = `
1303
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
1304
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1305
- <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"/>
1306
- </svg>
1307
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1308
- </div>
1309
- `;
1310
- };
1311
- }
1312
- } else {
1313
- 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>`;
1314
- }
1315
- if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
1316
- addDeleteButton(container, state, () => {
1317
- state.resourceIndex.delete(resourceId);
1318
- const hiddenInput = container.parentElement?.querySelector(
1319
- 'input[type="hidden"]'
1320
- );
1321
- if (hiddenInput) {
1322
- hiddenInput.value = "";
1323
- }
1324
- container.innerHTML = `
1325
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
1326
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1327
- <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"/>
1328
- </svg>
1329
- <div class="text-sm text-center">${t("clickDragText", state)}</div>
1330
- </div>
1331
- `;
1332
- });
1333
- }
1334
- } else if (state.config.getThumbnail) {
1335
- try {
1336
- const thumbnailUrl = await state.config.getThumbnail(resourceId);
1337
- if (thumbnailUrl) {
1338
- clear(container);
1339
- if (meta && meta.type && meta.type.startsWith("video/")) {
1340
- const video = document.createElement("video");
1341
- video.className = "w-full h-full object-contain";
1342
- video.controls = true;
1343
- video.preload = "metadata";
1344
- video.muted = true;
1345
- const source = document.createElement("source");
1346
- source.src = thumbnailUrl;
1347
- source.type = meta.type;
1348
- video.appendChild(source);
1349
- video.appendChild(document.createTextNode("Your browser does not support the video tag."));
1350
- container.appendChild(video);
1351
- } else {
1352
- img.src = thumbnailUrl;
1353
- container.appendChild(img);
1354
- }
1355
- } else {
1356
- setEmptyFileContainer(container, state);
1357
- }
1358
- } catch (error) {
1359
- console.error("Failed to get thumbnail:", error);
1360
- container.innerHTML = `
1361
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
1362
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1363
- <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"/>
1364
- </svg>
1365
- <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1366
- </div>
1367
- `;
1368
- }
1441
+ await renderLocalFilePreview(
1442
+ container,
1443
+ meta,
1444
+ fileName,
1445
+ resourceId,
1446
+ isReadonly,
1447
+ state,
1448
+ deps
1449
+ );
1369
1450
  } else {
1370
- setEmptyFileContainer(container, state);
1451
+ await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
1371
1452
  }
1372
1453
  }
1373
1454
  async function renderFilePreviewReadonly(resourceId, state, fileName) {
@@ -2268,114 +2349,1121 @@ function updateFileField(element, fieldPath, value, context) {
2268
2349
  }
2269
2350
  }
2270
2351
 
2271
- // src/components/container.ts
2272
- var renderElementFunc = null;
2273
- function setRenderElement(fn) {
2274
- renderElementFunc = fn;
2352
+ // src/components/colour.ts
2353
+ function normalizeColourValue(value) {
2354
+ if (!value) return "#000000";
2355
+ return value.toUpperCase();
2275
2356
  }
2276
- function renderElement(element, ctx) {
2277
- if (!renderElementFunc) {
2278
- throw new Error(
2279
- "renderElement not initialized. Import from components/index.ts"
2280
- );
2357
+ function isValidHexColour(value) {
2358
+ return /^#[0-9A-F]{6}$/i.test(value) || /^#[0-9A-F]{3}$/i.test(value);
2359
+ }
2360
+ function expandHexColour(value) {
2361
+ if (/^#[0-9A-F]{3}$/i.test(value)) {
2362
+ const r = value[1];
2363
+ const g = value[2];
2364
+ const b = value[3];
2365
+ return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
2281
2366
  }
2282
- return renderElementFunc(element, ctx);
2367
+ return value.toUpperCase();
2283
2368
  }
2284
- function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2285
- const containerWrap = document.createElement("div");
2286
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
2287
- containerWrap.setAttribute("data-container", pathKey);
2288
- const header = document.createElement("div");
2289
- header.className = "flex justify-between items-center mb-4";
2290
- const left = document.createElement("div");
2291
- left.className = "flex-1";
2292
- const itemsWrap = document.createElement("div");
2293
- itemsWrap.className = "space-y-4";
2294
- containerWrap.appendChild(header);
2295
- header.appendChild(left);
2296
- const subCtx = {
2297
- path: pathJoin(ctx.path, element.key),
2298
- prefill: ctx.prefill?.[element.key] || {},
2299
- // Sliced data for value population
2300
- formData: ctx.formData ?? ctx.prefill,
2301
- // Complete root data for displayIf evaluation
2302
- state: ctx.state
2303
- };
2304
- element.elements.forEach((child) => {
2305
- if (!child.hidden) {
2306
- itemsWrap.appendChild(renderElement(child, subCtx));
2369
+ function createReadonlyColourUI(value) {
2370
+ const container = document.createElement("div");
2371
+ container.className = "flex items-center gap-2";
2372
+ const normalizedValue = normalizeColourValue(value);
2373
+ const swatch = document.createElement("div");
2374
+ swatch.style.cssText = `
2375
+ width: 32px;
2376
+ height: 32px;
2377
+ border-radius: var(--fb-border-radius);
2378
+ border: var(--fb-border-width) solid var(--fb-border-color);
2379
+ background-color: ${normalizedValue};
2380
+ `;
2381
+ const hexText = document.createElement("span");
2382
+ hexText.style.cssText = `
2383
+ font-size: var(--fb-font-size);
2384
+ color: var(--fb-text-color);
2385
+ font-family: var(--fb-font-family-mono, monospace);
2386
+ `;
2387
+ hexText.textContent = normalizedValue;
2388
+ container.appendChild(swatch);
2389
+ container.appendChild(hexText);
2390
+ return container;
2391
+ }
2392
+ function createEditColourUI(value, pathKey, ctx) {
2393
+ const normalizedValue = normalizeColourValue(value);
2394
+ const pickerWrapper = document.createElement("div");
2395
+ pickerWrapper.className = "colour-picker-wrapper";
2396
+ pickerWrapper.style.cssText = `
2397
+ display: flex;
2398
+ align-items: center;
2399
+ gap: 8px;
2400
+ `;
2401
+ const swatch = document.createElement("div");
2402
+ swatch.className = "colour-swatch";
2403
+ swatch.style.cssText = `
2404
+ width: 40px;
2405
+ height: 40px;
2406
+ border-radius: var(--fb-border-radius);
2407
+ border: var(--fb-border-width) solid var(--fb-border-color);
2408
+ background-color: ${normalizedValue};
2409
+ cursor: pointer;
2410
+ transition: border-color var(--fb-transition-duration) ease-in-out;
2411
+ flex-shrink: 0;
2412
+ `;
2413
+ const hexInput = document.createElement("input");
2414
+ hexInput.type = "text";
2415
+ hexInput.className = "colour-hex-input";
2416
+ hexInput.name = pathKey;
2417
+ hexInput.value = normalizedValue;
2418
+ hexInput.placeholder = "#000000";
2419
+ hexInput.style.cssText = `
2420
+ width: 100px;
2421
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2422
+ border: var(--fb-border-width) solid var(--fb-border-color);
2423
+ border-radius: var(--fb-border-radius);
2424
+ background-color: var(--fb-background-color);
2425
+ color: var(--fb-text-color);
2426
+ font-size: var(--fb-font-size);
2427
+ font-family: var(--fb-font-family-mono, monospace);
2428
+ transition: all var(--fb-transition-duration) ease-in-out;
2429
+ `;
2430
+ const colourInput = document.createElement("input");
2431
+ colourInput.type = "color";
2432
+ colourInput.className = "colour-picker-hidden";
2433
+ colourInput.value = normalizedValue.toLowerCase();
2434
+ colourInput.style.cssText = `
2435
+ position: absolute;
2436
+ opacity: 0;
2437
+ pointer-events: none;
2438
+ `;
2439
+ hexInput.addEventListener("input", () => {
2440
+ const inputValue = hexInput.value.trim();
2441
+ if (isValidHexColour(inputValue)) {
2442
+ const expanded = expandHexColour(inputValue);
2443
+ swatch.style.backgroundColor = expanded;
2444
+ colourInput.value = expanded.toLowerCase();
2445
+ hexInput.classList.remove("invalid");
2446
+ if (ctx.instance) {
2447
+ ctx.instance.triggerOnChange(pathKey, expanded);
2448
+ }
2449
+ } else {
2450
+ hexInput.classList.add("invalid");
2307
2451
  }
2308
2452
  });
2309
- containerWrap.appendChild(itemsWrap);
2310
- left.innerHTML = `<span>${element.label || element.key}</span>`;
2311
- wrapper.appendChild(containerWrap);
2453
+ hexInput.addEventListener("blur", () => {
2454
+ const inputValue = hexInput.value.trim();
2455
+ if (isValidHexColour(inputValue)) {
2456
+ const expanded = expandHexColour(inputValue);
2457
+ hexInput.value = expanded;
2458
+ swatch.style.backgroundColor = expanded;
2459
+ colourInput.value = expanded.toLowerCase();
2460
+ hexInput.classList.remove("invalid");
2461
+ }
2462
+ });
2463
+ colourInput.addEventListener("change", () => {
2464
+ const normalized = normalizeColourValue(colourInput.value);
2465
+ hexInput.value = normalized;
2466
+ swatch.style.backgroundColor = normalized;
2467
+ if (ctx.instance) {
2468
+ ctx.instance.triggerOnChange(pathKey, normalized);
2469
+ }
2470
+ });
2471
+ swatch.addEventListener("click", () => {
2472
+ colourInput.click();
2473
+ });
2474
+ swatch.addEventListener("mouseenter", () => {
2475
+ swatch.style.borderColor = "var(--fb-border-hover-color)";
2476
+ });
2477
+ swatch.addEventListener("mouseleave", () => {
2478
+ swatch.style.borderColor = "var(--fb-border-color)";
2479
+ });
2480
+ hexInput.addEventListener("focus", () => {
2481
+ hexInput.style.borderColor = "var(--fb-border-focus-color)";
2482
+ hexInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
2483
+ hexInput.style.outlineOffset = "0";
2484
+ });
2485
+ hexInput.addEventListener("blur", () => {
2486
+ hexInput.style.borderColor = "var(--fb-border-color)";
2487
+ hexInput.style.outline = "none";
2488
+ });
2489
+ hexInput.addEventListener("mouseenter", () => {
2490
+ if (document.activeElement !== hexInput) {
2491
+ hexInput.style.borderColor = "var(--fb-border-hover-color)";
2492
+ }
2493
+ });
2494
+ hexInput.addEventListener("mouseleave", () => {
2495
+ if (document.activeElement !== hexInput) {
2496
+ hexInput.style.borderColor = "var(--fb-border-color)";
2497
+ }
2498
+ });
2499
+ pickerWrapper.appendChild(swatch);
2500
+ pickerWrapper.appendChild(hexInput);
2501
+ pickerWrapper.appendChild(colourInput);
2502
+ return pickerWrapper;
2312
2503
  }
2313
- function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2504
+ function renderColourElement(element, ctx, wrapper, pathKey) {
2314
2505
  const state = ctx.state;
2315
- const containerWrap = document.createElement("div");
2316
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
2317
- const header = document.createElement("div");
2318
- header.className = "flex justify-between items-center mb-4";
2319
- const left = document.createElement("div");
2320
- left.className = "flex-1";
2321
- const right = document.createElement("div");
2322
- right.className = "flex gap-2";
2323
- const itemsWrap = document.createElement("div");
2324
- itemsWrap.className = "space-y-4";
2325
- containerWrap.appendChild(header);
2326
- header.appendChild(left);
2327
- if (!state.config.readonly) {
2328
- header.appendChild(right);
2506
+ const initialValue = ctx.prefill[element.key] || element.default || "#000000";
2507
+ if (state.config.readonly) {
2508
+ const readonlyUI = createReadonlyColourUI(initialValue);
2509
+ wrapper.appendChild(readonlyUI);
2510
+ } else {
2511
+ const editUI = createEditColourUI(initialValue, pathKey, ctx);
2512
+ wrapper.appendChild(editUI);
2329
2513
  }
2330
- const min = element.minCount ?? 0;
2331
- const max = element.maxCount ?? Infinity;
2332
- const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
2333
- const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
2334
- const createAddButton = () => {
2335
- const add = document.createElement("button");
2336
- add.type = "button";
2337
- add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
2338
- add.textContent = t("addElement", state);
2339
- add.onclick = () => {
2340
- if (countItems() < max) {
2341
- const idx = countItems();
2342
- const subCtx = {
2343
- state: ctx.state,
2344
- path: pathJoin(ctx.path, `${element.key}[${idx}]`),
2345
- prefill: {},
2346
- formData: ctx.formData ?? ctx.prefill
2347
- // Complete root data for displayIf
2348
- };
2349
- const item = document.createElement("div");
2350
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2351
- item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2352
- element.elements.forEach((child) => {
2353
- if (!child.hidden) {
2354
- item.appendChild(renderElement(child, subCtx));
2355
- }
2356
- });
2357
- if (!state.config.readonly) {
2358
- const rem = document.createElement("button");
2359
- rem.type = "button";
2360
- 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";
2361
- rem.textContent = "\xD7";
2362
- rem.onclick = () => {
2363
- item.remove();
2364
- updateAddButton();
2365
- };
2366
- item.style.position = "relative";
2367
- item.appendChild(rem);
2368
- }
2369
- itemsWrap.appendChild(item);
2370
- updateAddButton();
2371
- }
2372
- };
2373
- return add;
2374
- };
2375
- const updateAddButton = () => {
2376
- const currentCount = countItems();
2377
- const addBtn = right.querySelector("button");
2378
- if (addBtn) {
2514
+ const colourHint = document.createElement("p");
2515
+ colourHint.className = "mt-1";
2516
+ colourHint.style.cssText = `
2517
+ font-size: var(--fb-font-size-small);
2518
+ color: var(--fb-text-secondary-color);
2519
+ `;
2520
+ colourHint.textContent = makeFieldHint(element);
2521
+ wrapper.appendChild(colourHint);
2522
+ }
2523
+ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2524
+ const state = ctx.state;
2525
+ const prefillValues = ctx.prefill[element.key] || [];
2526
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
2527
+ const minCount = element.minCount ?? 1;
2528
+ const maxCount = element.maxCount ?? Infinity;
2529
+ while (values.length < minCount) {
2530
+ values.push(element.default || "#000000");
2531
+ }
2532
+ const container = document.createElement("div");
2533
+ container.className = "space-y-2";
2534
+ wrapper.appendChild(container);
2535
+ function updateIndices() {
2536
+ const items = container.querySelectorAll(".multiple-colour-item");
2537
+ items.forEach((item, index) => {
2538
+ const input = item.querySelector("input");
2539
+ if (input) {
2540
+ input.name = `${pathKey}[${index}]`;
2541
+ }
2542
+ });
2543
+ }
2544
+ function addColourItem(value = "#000000", index = -1) {
2545
+ const itemWrapper = document.createElement("div");
2546
+ itemWrapper.className = "multiple-colour-item flex items-center gap-2";
2547
+ if (state.config.readonly) {
2548
+ const readonlyUI = createReadonlyColourUI(value);
2549
+ while (readonlyUI.firstChild) {
2550
+ itemWrapper.appendChild(readonlyUI.firstChild);
2551
+ }
2552
+ } else {
2553
+ const tempPathKey = `${pathKey}[${container.children.length}]`;
2554
+ const editUI = createEditColourUI(value, tempPathKey, ctx);
2555
+ editUI.style.flex = "1";
2556
+ itemWrapper.appendChild(editUI);
2557
+ }
2558
+ if (index === -1) {
2559
+ container.appendChild(itemWrapper);
2560
+ } else {
2561
+ container.insertBefore(itemWrapper, container.children[index]);
2562
+ }
2563
+ updateIndices();
2564
+ return itemWrapper;
2565
+ }
2566
+ function updateRemoveButtons() {
2567
+ if (state.config.readonly) return;
2568
+ const items = container.querySelectorAll(".multiple-colour-item");
2569
+ const currentCount = items.length;
2570
+ items.forEach((item) => {
2571
+ let removeBtn = item.querySelector(
2572
+ ".remove-item-btn"
2573
+ );
2574
+ if (!removeBtn) {
2575
+ removeBtn = document.createElement("button");
2576
+ removeBtn.type = "button";
2577
+ removeBtn.className = "remove-item-btn px-2 py-1 rounded";
2578
+ removeBtn.style.cssText = `
2579
+ color: var(--fb-error-color);
2580
+ background-color: transparent;
2581
+ transition: background-color var(--fb-transition-duration);
2582
+ `;
2583
+ removeBtn.innerHTML = "\u2715";
2584
+ removeBtn.addEventListener("mouseenter", () => {
2585
+ removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2586
+ });
2587
+ removeBtn.addEventListener("mouseleave", () => {
2588
+ removeBtn.style.backgroundColor = "transparent";
2589
+ });
2590
+ removeBtn.onclick = () => {
2591
+ const currentIndex = Array.from(container.children).indexOf(
2592
+ item
2593
+ );
2594
+ if (container.children.length > minCount) {
2595
+ values.splice(currentIndex, 1);
2596
+ item.remove();
2597
+ updateIndices();
2598
+ updateAddButton();
2599
+ updateRemoveButtons();
2600
+ }
2601
+ };
2602
+ item.appendChild(removeBtn);
2603
+ }
2604
+ const disabled = currentCount <= minCount;
2605
+ removeBtn.disabled = disabled;
2606
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2607
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2608
+ });
2609
+ }
2610
+ function updateAddButton() {
2611
+ const existingAddBtn = wrapper.querySelector(".add-colour-btn");
2612
+ if (existingAddBtn) existingAddBtn.remove();
2613
+ if (!state.config.readonly && values.length < maxCount) {
2614
+ const addBtn = document.createElement("button");
2615
+ addBtn.type = "button";
2616
+ addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
2617
+ addBtn.style.cssText = `
2618
+ color: var(--fb-primary-color);
2619
+ border: var(--fb-border-width) solid var(--fb-primary-color);
2620
+ background-color: transparent;
2621
+ font-size: var(--fb-font-size);
2622
+ transition: all var(--fb-transition-duration);
2623
+ `;
2624
+ addBtn.textContent = `+ Add ${element.label || "Colour"}`;
2625
+ addBtn.addEventListener("mouseenter", () => {
2626
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2627
+ });
2628
+ addBtn.addEventListener("mouseleave", () => {
2629
+ addBtn.style.backgroundColor = "transparent";
2630
+ });
2631
+ addBtn.onclick = () => {
2632
+ const defaultColour = element.default || "#000000";
2633
+ values.push(defaultColour);
2634
+ addColourItem(defaultColour);
2635
+ updateAddButton();
2636
+ updateRemoveButtons();
2637
+ };
2638
+ wrapper.appendChild(addBtn);
2639
+ }
2640
+ }
2641
+ values.forEach((value) => addColourItem(value));
2642
+ updateAddButton();
2643
+ updateRemoveButtons();
2644
+ const hint = document.createElement("p");
2645
+ hint.className = "mt-1";
2646
+ hint.style.cssText = `
2647
+ font-size: var(--fb-font-size-small);
2648
+ color: var(--fb-text-secondary-color);
2649
+ `;
2650
+ hint.textContent = makeFieldHint(element);
2651
+ wrapper.appendChild(hint);
2652
+ }
2653
+ function validateColourElement(element, key, context) {
2654
+ const errors = [];
2655
+ const { scopeRoot, skipValidation } = context;
2656
+ const markValidity = (input, errorMessage) => {
2657
+ if (!input) return;
2658
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
2659
+ let errorElement = document.getElementById(errorId);
2660
+ if (errorMessage) {
2661
+ input.classList.add("invalid");
2662
+ input.title = errorMessage;
2663
+ if (!errorElement) {
2664
+ errorElement = document.createElement("div");
2665
+ errorElement.id = errorId;
2666
+ errorElement.className = "error-message";
2667
+ errorElement.style.cssText = `
2668
+ color: var(--fb-error-color);
2669
+ font-size: var(--fb-font-size-small);
2670
+ margin-top: 0.25rem;
2671
+ `;
2672
+ if (input.nextSibling) {
2673
+ input.parentNode?.insertBefore(errorElement, input.nextSibling);
2674
+ } else {
2675
+ input.parentNode?.appendChild(errorElement);
2676
+ }
2677
+ }
2678
+ errorElement.textContent = errorMessage;
2679
+ errorElement.style.display = "block";
2680
+ } else {
2681
+ input.classList.remove("invalid");
2682
+ input.title = "";
2683
+ if (errorElement) {
2684
+ errorElement.remove();
2685
+ }
2686
+ }
2687
+ };
2688
+ const validateColourValue = (input, val, fieldKey) => {
2689
+ if (!val) {
2690
+ if (!skipValidation && element.required) {
2691
+ errors.push(`${fieldKey}: required`);
2692
+ markValidity(input, "required");
2693
+ return "";
2694
+ }
2695
+ markValidity(input, null);
2696
+ return "";
2697
+ }
2698
+ const normalized = normalizeColourValue(val);
2699
+ if (!skipValidation && !isValidHexColour(normalized)) {
2700
+ errors.push(`${fieldKey}: invalid hex colour format`);
2701
+ markValidity(input, "invalid hex colour format");
2702
+ return val;
2703
+ }
2704
+ markValidity(input, null);
2705
+ return normalized;
2706
+ };
2707
+ if (element.multiple) {
2708
+ const hexInputs = scopeRoot.querySelectorAll(
2709
+ `.colour-hex-input`
2710
+ );
2711
+ const values = [];
2712
+ hexInputs.forEach((input, index) => {
2713
+ const val = input?.value ?? "";
2714
+ const validated = validateColourValue(input, val, `${key}[${index}]`);
2715
+ values.push(validated);
2716
+ });
2717
+ if (!skipValidation) {
2718
+ const minCount = element.minCount ?? 1;
2719
+ const maxCount = element.maxCount ?? Infinity;
2720
+ const filteredValues = values.filter((v) => v !== "");
2721
+ if (element.required && filteredValues.length === 0) {
2722
+ errors.push(`${key}: required`);
2723
+ }
2724
+ if (filteredValues.length < minCount) {
2725
+ errors.push(`${key}: minimum ${minCount} items required`);
2726
+ }
2727
+ if (filteredValues.length > maxCount) {
2728
+ errors.push(`${key}: maximum ${maxCount} items allowed`);
2729
+ }
2730
+ }
2731
+ return { value: values, errors };
2732
+ } else {
2733
+ const hexInput = scopeRoot.querySelector(
2734
+ `[name="${key}"].colour-hex-input`
2735
+ );
2736
+ const val = hexInput?.value ?? "";
2737
+ if (!skipValidation && element.required && val === "") {
2738
+ errors.push(`${key}: required`);
2739
+ markValidity(hexInput, "required");
2740
+ return { value: "", errors };
2741
+ }
2742
+ const validated = validateColourValue(hexInput, val, key);
2743
+ return { value: validated, errors };
2744
+ }
2745
+ }
2746
+ function updateColourField(element, fieldPath, value, context) {
2747
+ const { scopeRoot } = context;
2748
+ if (element.multiple) {
2749
+ if (!Array.isArray(value)) {
2750
+ console.warn(
2751
+ `updateColourField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
2752
+ );
2753
+ return;
2754
+ }
2755
+ const hexInputs = scopeRoot.querySelectorAll(
2756
+ `.colour-hex-input`
2757
+ );
2758
+ hexInputs.forEach((hexInput, index) => {
2759
+ if (index < value.length) {
2760
+ const normalized = normalizeColourValue(value[index]);
2761
+ hexInput.value = normalized;
2762
+ hexInput.classList.remove("invalid");
2763
+ hexInput.title = "";
2764
+ const wrapper = hexInput.closest(".colour-picker-wrapper");
2765
+ if (wrapper) {
2766
+ const swatch = wrapper.querySelector(".colour-swatch");
2767
+ const colourInput = wrapper.querySelector(".colour-picker-hidden");
2768
+ if (swatch) {
2769
+ swatch.style.backgroundColor = normalized;
2770
+ }
2771
+ if (colourInput) {
2772
+ colourInput.value = normalized.toLowerCase();
2773
+ }
2774
+ }
2775
+ }
2776
+ });
2777
+ if (value.length !== hexInputs.length) {
2778
+ console.warn(
2779
+ `updateColourField: Multiple field "${fieldPath}" has ${hexInputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
2780
+ );
2781
+ }
2782
+ } else {
2783
+ const hexInput = scopeRoot.querySelector(
2784
+ `[name="${fieldPath}"].colour-hex-input`
2785
+ );
2786
+ if (hexInput) {
2787
+ const normalized = normalizeColourValue(value);
2788
+ hexInput.value = normalized;
2789
+ hexInput.classList.remove("invalid");
2790
+ hexInput.title = "";
2791
+ const wrapper = hexInput.closest(".colour-picker-wrapper");
2792
+ if (wrapper) {
2793
+ const swatch = wrapper.querySelector(".colour-swatch");
2794
+ const colourInput = wrapper.querySelector(".colour-picker-hidden");
2795
+ if (swatch) {
2796
+ swatch.style.backgroundColor = normalized;
2797
+ }
2798
+ if (colourInput) {
2799
+ colourInput.value = normalized.toLowerCase();
2800
+ }
2801
+ }
2802
+ }
2803
+ }
2804
+ }
2805
+
2806
+ // src/components/slider.ts
2807
+ function positionToExponential(position, min, max) {
2808
+ if (min <= 0) {
2809
+ throw new Error("Exponential scale requires min > 0");
2810
+ }
2811
+ const logMin = Math.log(min);
2812
+ const logMax = Math.log(max);
2813
+ return Math.exp(logMin + position * (logMax - logMin));
2814
+ }
2815
+ function exponentialToPosition(value, min, max) {
2816
+ if (min <= 0) {
2817
+ throw new Error("Exponential scale requires min > 0");
2818
+ }
2819
+ const logMin = Math.log(min);
2820
+ const logMax = Math.log(max);
2821
+ const logValue = Math.log(value);
2822
+ return (logValue - logMin) / (logMax - logMin);
2823
+ }
2824
+ function alignToStep(value, step) {
2825
+ return Math.round(value / step) * step;
2826
+ }
2827
+ function createSliderUI(value, pathKey, element, ctx, readonly) {
2828
+ const container = document.createElement("div");
2829
+ container.className = "slider-container space-y-2";
2830
+ const sliderRow = document.createElement("div");
2831
+ sliderRow.className = "flex items-center gap-3";
2832
+ const slider = document.createElement("input");
2833
+ slider.type = "range";
2834
+ slider.name = pathKey;
2835
+ slider.className = "slider-input flex-1";
2836
+ slider.disabled = readonly;
2837
+ const scale = element.scale || "linear";
2838
+ const min = element.min;
2839
+ const max = element.max;
2840
+ const step = element.step ?? 1;
2841
+ if (scale === "exponential") {
2842
+ if (min <= 0) {
2843
+ throw new Error(
2844
+ `Slider "${element.key}": exponential scale requires min > 0 (got ${min})`
2845
+ );
2846
+ }
2847
+ slider.min = "0";
2848
+ slider.max = "1000";
2849
+ slider.step = "1";
2850
+ const position = exponentialToPosition(value, min, max);
2851
+ slider.value = (position * 1e3).toString();
2852
+ } else {
2853
+ slider.min = min.toString();
2854
+ slider.max = max.toString();
2855
+ slider.step = step.toString();
2856
+ slider.value = value.toString();
2857
+ }
2858
+ slider.style.cssText = `
2859
+ height: 6px;
2860
+ border-radius: 3px;
2861
+ background: linear-gradient(
2862
+ to right,
2863
+ var(--fb-primary-color) 0%,
2864
+ var(--fb-primary-color) ${(value - min) / (max - min) * 100}%,
2865
+ var(--fb-border-color) ${(value - min) / (max - min) * 100}%,
2866
+ var(--fb-border-color) 100%
2867
+ );
2868
+ outline: none;
2869
+ transition: background 0.1s ease-in-out;
2870
+ cursor: ${readonly ? "not-allowed" : "pointer"};
2871
+ opacity: ${readonly ? "0.6" : "1"};
2872
+ `;
2873
+ const valueDisplay = document.createElement("span");
2874
+ valueDisplay.className = "slider-value";
2875
+ valueDisplay.style.cssText = `
2876
+ min-width: 60px;
2877
+ text-align: right;
2878
+ font-size: var(--fb-font-size);
2879
+ color: var(--fb-text-color);
2880
+ font-family: var(--fb-font-family-mono, monospace);
2881
+ font-weight: 500;
2882
+ `;
2883
+ valueDisplay.textContent = value.toFixed(step < 1 ? 2 : 0);
2884
+ sliderRow.appendChild(slider);
2885
+ sliderRow.appendChild(valueDisplay);
2886
+ container.appendChild(sliderRow);
2887
+ const labelsRow = document.createElement("div");
2888
+ labelsRow.className = "flex justify-between";
2889
+ labelsRow.style.cssText = `
2890
+ font-size: var(--fb-font-size-small);
2891
+ color: var(--fb-text-secondary-color);
2892
+ `;
2893
+ const minLabel = document.createElement("span");
2894
+ minLabel.textContent = min.toString();
2895
+ const maxLabel = document.createElement("span");
2896
+ maxLabel.textContent = max.toString();
2897
+ labelsRow.appendChild(minLabel);
2898
+ labelsRow.appendChild(maxLabel);
2899
+ container.appendChild(labelsRow);
2900
+ if (!readonly) {
2901
+ const updateValue = () => {
2902
+ let displayValue;
2903
+ if (scale === "exponential") {
2904
+ const position = parseFloat(slider.value) / 1e3;
2905
+ displayValue = positionToExponential(position, min, max);
2906
+ displayValue = alignToStep(displayValue, step);
2907
+ displayValue = Math.max(min, Math.min(max, displayValue));
2908
+ } else {
2909
+ displayValue = parseFloat(slider.value);
2910
+ displayValue = alignToStep(displayValue, step);
2911
+ }
2912
+ valueDisplay.textContent = displayValue.toFixed(step < 1 ? 2 : 0);
2913
+ const percentage = (displayValue - min) / (max - min) * 100;
2914
+ slider.style.background = `linear-gradient(
2915
+ to right,
2916
+ var(--fb-primary-color) 0%,
2917
+ var(--fb-primary-color) ${percentage}%,
2918
+ var(--fb-border-color) ${percentage}%,
2919
+ var(--fb-border-color) 100%
2920
+ )`;
2921
+ if (ctx.instance) {
2922
+ ctx.instance.triggerOnChange(pathKey, displayValue);
2923
+ }
2924
+ };
2925
+ slider.addEventListener("input", updateValue);
2926
+ slider.addEventListener("change", updateValue);
2927
+ }
2928
+ return container;
2929
+ }
2930
+ function renderSliderElement(element, ctx, wrapper, pathKey) {
2931
+ if (element.min === void 0 || element.min === null) {
2932
+ throw new Error(
2933
+ `Slider field "${element.key}" requires "min" property`
2934
+ );
2935
+ }
2936
+ if (element.max === void 0 || element.max === null) {
2937
+ throw new Error(
2938
+ `Slider field "${element.key}" requires "max" property`
2939
+ );
2940
+ }
2941
+ if (element.min >= element.max) {
2942
+ throw new Error(
2943
+ `Slider field "${element.key}": min (${element.min}) must be less than max (${element.max})`
2944
+ );
2945
+ }
2946
+ const state = ctx.state;
2947
+ const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
2948
+ const initialValue = ctx.prefill[element.key] ?? defaultValue;
2949
+ const sliderUI = createSliderUI(
2950
+ initialValue,
2951
+ pathKey,
2952
+ element,
2953
+ ctx,
2954
+ state.config.readonly
2955
+ );
2956
+ wrapper.appendChild(sliderUI);
2957
+ const hint = document.createElement("p");
2958
+ hint.className = "mt-1";
2959
+ hint.style.cssText = `
2960
+ font-size: var(--fb-font-size-small);
2961
+ color: var(--fb-text-secondary-color);
2962
+ `;
2963
+ hint.textContent = makeFieldHint(element);
2964
+ wrapper.appendChild(hint);
2965
+ }
2966
+ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
2967
+ if (element.min === void 0 || element.min === null) {
2968
+ throw new Error(
2969
+ `Slider field "${element.key}" requires "min" property`
2970
+ );
2971
+ }
2972
+ if (element.max === void 0 || element.max === null) {
2973
+ throw new Error(
2974
+ `Slider field "${element.key}" requires "max" property`
2975
+ );
2976
+ }
2977
+ if (element.min >= element.max) {
2978
+ throw new Error(
2979
+ `Slider field "${element.key}": min (${element.min}) must be less than max (${element.max})`
2980
+ );
2981
+ }
2982
+ const state = ctx.state;
2983
+ const prefillValues = ctx.prefill[element.key] || [];
2984
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
2985
+ const minCount = element.minCount ?? 1;
2986
+ const maxCount = element.maxCount ?? Infinity;
2987
+ const defaultValue = element.default !== void 0 ? element.default : (element.min + element.max) / 2;
2988
+ while (values.length < minCount) {
2989
+ values.push(defaultValue);
2990
+ }
2991
+ const container = document.createElement("div");
2992
+ container.className = "space-y-3";
2993
+ wrapper.appendChild(container);
2994
+ function updateIndices() {
2995
+ const items = container.querySelectorAll(".multiple-slider-item");
2996
+ items.forEach((item, index) => {
2997
+ const slider = item.querySelector("input[type=range]");
2998
+ if (slider) {
2999
+ slider.setAttribute("name", `${pathKey}[${index}]`);
3000
+ }
3001
+ });
3002
+ }
3003
+ function addSliderItem(value = defaultValue, index = -1) {
3004
+ const itemWrapper = document.createElement("div");
3005
+ itemWrapper.className = "multiple-slider-item flex items-start gap-2";
3006
+ const tempPathKey = `${pathKey}[${container.children.length}]`;
3007
+ const sliderUI = createSliderUI(
3008
+ value,
3009
+ tempPathKey,
3010
+ element,
3011
+ ctx,
3012
+ state.config.readonly
3013
+ );
3014
+ sliderUI.style.flex = "1";
3015
+ itemWrapper.appendChild(sliderUI);
3016
+ if (index === -1) {
3017
+ container.appendChild(itemWrapper);
3018
+ } else {
3019
+ container.insertBefore(itemWrapper, container.children[index]);
3020
+ }
3021
+ updateIndices();
3022
+ return itemWrapper;
3023
+ }
3024
+ function updateRemoveButtons() {
3025
+ if (state.config.readonly) return;
3026
+ const items = container.querySelectorAll(".multiple-slider-item");
3027
+ const currentCount = items.length;
3028
+ items.forEach((item) => {
3029
+ let removeBtn = item.querySelector(
3030
+ ".remove-item-btn"
3031
+ );
3032
+ if (!removeBtn) {
3033
+ removeBtn = document.createElement("button");
3034
+ removeBtn.type = "button";
3035
+ removeBtn.className = "remove-item-btn px-2 py-1 rounded";
3036
+ removeBtn.style.cssText = `
3037
+ color: var(--fb-error-color);
3038
+ background-color: transparent;
3039
+ transition: background-color var(--fb-transition-duration);
3040
+ margin-top: 8px;
3041
+ `;
3042
+ removeBtn.innerHTML = "\u2715";
3043
+ removeBtn.addEventListener("mouseenter", () => {
3044
+ removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3045
+ });
3046
+ removeBtn.addEventListener("mouseleave", () => {
3047
+ removeBtn.style.backgroundColor = "transparent";
3048
+ });
3049
+ removeBtn.onclick = () => {
3050
+ const currentIndex = Array.from(container.children).indexOf(
3051
+ item
3052
+ );
3053
+ if (container.children.length > minCount) {
3054
+ values.splice(currentIndex, 1);
3055
+ item.remove();
3056
+ updateIndices();
3057
+ updateAddButton();
3058
+ updateRemoveButtons();
3059
+ }
3060
+ };
3061
+ item.appendChild(removeBtn);
3062
+ }
3063
+ const disabled = currentCount <= minCount;
3064
+ removeBtn.disabled = disabled;
3065
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
3066
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
3067
+ });
3068
+ }
3069
+ function updateAddButton() {
3070
+ const existingAddBtn = wrapper.querySelector(".add-slider-btn");
3071
+ if (existingAddBtn) existingAddBtn.remove();
3072
+ if (!state.config.readonly && values.length < maxCount) {
3073
+ const addBtn = document.createElement("button");
3074
+ addBtn.type = "button";
3075
+ addBtn.className = "add-slider-btn mt-2 px-3 py-1 rounded";
3076
+ addBtn.style.cssText = `
3077
+ color: var(--fb-primary-color);
3078
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3079
+ background-color: transparent;
3080
+ font-size: var(--fb-font-size);
3081
+ transition: all var(--fb-transition-duration);
3082
+ `;
3083
+ addBtn.textContent = `+ Add ${element.label || "Slider"}`;
3084
+ addBtn.addEventListener("mouseenter", () => {
3085
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
3086
+ });
3087
+ addBtn.addEventListener("mouseleave", () => {
3088
+ addBtn.style.backgroundColor = "transparent";
3089
+ });
3090
+ addBtn.onclick = () => {
3091
+ values.push(defaultValue);
3092
+ addSliderItem(defaultValue);
3093
+ updateAddButton();
3094
+ updateRemoveButtons();
3095
+ };
3096
+ wrapper.appendChild(addBtn);
3097
+ }
3098
+ }
3099
+ values.forEach((value) => addSliderItem(value));
3100
+ updateAddButton();
3101
+ updateRemoveButtons();
3102
+ const hint = document.createElement("p");
3103
+ hint.className = "mt-1";
3104
+ hint.style.cssText = `
3105
+ font-size: var(--fb-font-size-small);
3106
+ color: var(--fb-text-secondary-color);
3107
+ `;
3108
+ hint.textContent = makeFieldHint(element);
3109
+ wrapper.appendChild(hint);
3110
+ }
3111
+ function validateSliderElement(element, key, context) {
3112
+ const errors = [];
3113
+ const { scopeRoot, skipValidation } = context;
3114
+ if (element.min === void 0 || element.min === null) {
3115
+ throw new Error(
3116
+ `Slider validation: field "${key}" requires "min" property`
3117
+ );
3118
+ }
3119
+ if (element.max === void 0 || element.max === null) {
3120
+ throw new Error(
3121
+ `Slider validation: field "${key}" requires "max" property`
3122
+ );
3123
+ }
3124
+ const min = element.min;
3125
+ const max = element.max;
3126
+ const step = element.step ?? 1;
3127
+ const scale = element.scale || "linear";
3128
+ const markValidity = (input, errorMessage) => {
3129
+ if (!input) return;
3130
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
3131
+ let errorElement = document.getElementById(errorId);
3132
+ if (errorMessage) {
3133
+ input.classList.add("invalid");
3134
+ input.title = errorMessage;
3135
+ if (!errorElement) {
3136
+ errorElement = document.createElement("div");
3137
+ errorElement.id = errorId;
3138
+ errorElement.className = "error-message";
3139
+ errorElement.style.cssText = `
3140
+ color: var(--fb-error-color);
3141
+ font-size: var(--fb-font-size-small);
3142
+ margin-top: 0.25rem;
3143
+ `;
3144
+ const sliderContainer = input.closest(".slider-container");
3145
+ if (sliderContainer && sliderContainer.nextSibling) {
3146
+ sliderContainer.parentNode?.insertBefore(errorElement, sliderContainer.nextSibling);
3147
+ } else if (sliderContainer) {
3148
+ sliderContainer.parentNode?.appendChild(errorElement);
3149
+ }
3150
+ }
3151
+ errorElement.textContent = errorMessage;
3152
+ errorElement.style.display = "block";
3153
+ } else {
3154
+ input.classList.remove("invalid");
3155
+ input.title = "";
3156
+ if (errorElement) {
3157
+ errorElement.remove();
3158
+ }
3159
+ }
3160
+ };
3161
+ const validateSliderValue = (slider, fieldKey) => {
3162
+ const rawValue = slider.value;
3163
+ if (!rawValue) {
3164
+ if (!skipValidation && element.required) {
3165
+ errors.push(`${fieldKey}: required`);
3166
+ markValidity(slider, "required");
3167
+ return null;
3168
+ }
3169
+ markValidity(slider, null);
3170
+ return null;
3171
+ }
3172
+ let value;
3173
+ if (scale === "exponential") {
3174
+ const position = parseFloat(rawValue) / 1e3;
3175
+ value = positionToExponential(position, min, max);
3176
+ value = alignToStep(value, step);
3177
+ } else {
3178
+ value = parseFloat(rawValue);
3179
+ value = alignToStep(value, step);
3180
+ }
3181
+ if (!skipValidation) {
3182
+ if (value < min) {
3183
+ errors.push(`${fieldKey}: value ${value} < min ${min}`);
3184
+ markValidity(slider, `value must be >= ${min}`);
3185
+ return value;
3186
+ }
3187
+ if (value > max) {
3188
+ errors.push(`${fieldKey}: value ${value} > max ${max}`);
3189
+ markValidity(slider, `value must be <= ${max}`);
3190
+ return value;
3191
+ }
3192
+ }
3193
+ markValidity(slider, null);
3194
+ return value;
3195
+ };
3196
+ if (element.multiple) {
3197
+ const sliders = scopeRoot.querySelectorAll(
3198
+ `input[type="range"][name^="${key}["]`
3199
+ );
3200
+ const values = [];
3201
+ sliders.forEach((slider, index) => {
3202
+ const value = validateSliderValue(slider, `${key}[${index}]`);
3203
+ values.push(value);
3204
+ });
3205
+ if (!skipValidation) {
3206
+ const minCount = element.minCount ?? 1;
3207
+ const maxCount = element.maxCount ?? Infinity;
3208
+ const filteredValues = values.filter((v) => v !== null);
3209
+ if (element.required && filteredValues.length === 0) {
3210
+ errors.push(`${key}: required`);
3211
+ }
3212
+ if (filteredValues.length < minCount) {
3213
+ errors.push(`${key}: minimum ${minCount} items required`);
3214
+ }
3215
+ if (filteredValues.length > maxCount) {
3216
+ errors.push(`${key}: maximum ${maxCount} items allowed`);
3217
+ }
3218
+ }
3219
+ return { value: values, errors };
3220
+ } else {
3221
+ const slider = scopeRoot.querySelector(
3222
+ `input[type="range"][name="${key}"]`
3223
+ );
3224
+ if (!slider) {
3225
+ if (!skipValidation && element.required) {
3226
+ errors.push(`${key}: required`);
3227
+ }
3228
+ return { value: null, errors };
3229
+ }
3230
+ const value = validateSliderValue(slider, key);
3231
+ return { value, errors };
3232
+ }
3233
+ }
3234
+ function updateSliderField(element, fieldPath, value, context) {
3235
+ const { scopeRoot } = context;
3236
+ const min = element.min;
3237
+ const max = element.max;
3238
+ const step = element.step ?? 1;
3239
+ const scale = element.scale || "linear";
3240
+ if (element.multiple) {
3241
+ if (!Array.isArray(value)) {
3242
+ console.warn(
3243
+ `updateSliderField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
3244
+ );
3245
+ return;
3246
+ }
3247
+ const sliders = scopeRoot.querySelectorAll(
3248
+ `input[type="range"][name^="${fieldPath}["]`
3249
+ );
3250
+ sliders.forEach((slider, index) => {
3251
+ if (index < value.length && value[index] !== null) {
3252
+ const numValue = Number(value[index]);
3253
+ if (scale === "exponential") {
3254
+ const position = exponentialToPosition(numValue, min, max);
3255
+ slider.value = (position * 1e3).toString();
3256
+ } else {
3257
+ slider.value = numValue.toString();
3258
+ }
3259
+ const sliderContainer = slider.closest(".slider-container");
3260
+ if (sliderContainer) {
3261
+ const valueDisplay = sliderContainer.querySelector(".slider-value");
3262
+ if (valueDisplay) {
3263
+ valueDisplay.textContent = numValue.toFixed(step < 1 ? 2 : 0);
3264
+ }
3265
+ const percentage = (numValue - min) / (max - min) * 100;
3266
+ slider.style.background = `linear-gradient(
3267
+ to right,
3268
+ var(--fb-primary-color) 0%,
3269
+ var(--fb-primary-color) ${percentage}%,
3270
+ var(--fb-border-color) ${percentage}%,
3271
+ var(--fb-border-color) 100%
3272
+ )`;
3273
+ }
3274
+ slider.classList.remove("invalid");
3275
+ slider.title = "";
3276
+ }
3277
+ });
3278
+ if (value.length !== sliders.length) {
3279
+ console.warn(
3280
+ `updateSliderField: Multiple field "${fieldPath}" has ${sliders.length} sliders but received ${value.length} values. Consider re-rendering for add/remove.`
3281
+ );
3282
+ }
3283
+ } else {
3284
+ const slider = scopeRoot.querySelector(
3285
+ `input[type="range"][name="${fieldPath}"]`
3286
+ );
3287
+ if (slider && value !== null && value !== void 0) {
3288
+ const numValue = Number(value);
3289
+ if (scale === "exponential") {
3290
+ const position = exponentialToPosition(numValue, min, max);
3291
+ slider.value = (position * 1e3).toString();
3292
+ } else {
3293
+ slider.value = numValue.toString();
3294
+ }
3295
+ const sliderContainer = slider.closest(".slider-container");
3296
+ if (sliderContainer) {
3297
+ const valueDisplay = sliderContainer.querySelector(".slider-value");
3298
+ if (valueDisplay) {
3299
+ valueDisplay.textContent = numValue.toFixed(step < 1 ? 2 : 0);
3300
+ }
3301
+ const percentage = (numValue - min) / (max - min) * 100;
3302
+ slider.style.background = `linear-gradient(
3303
+ to right,
3304
+ var(--fb-primary-color) 0%,
3305
+ var(--fb-primary-color) ${percentage}%,
3306
+ var(--fb-border-color) ${percentage}%,
3307
+ var(--fb-border-color) 100%
3308
+ )`;
3309
+ }
3310
+ slider.classList.remove("invalid");
3311
+ slider.title = "";
3312
+ }
3313
+ }
3314
+ }
3315
+
3316
+ // src/components/container.ts
3317
+ var renderElementFunc = null;
3318
+ function setRenderElement(fn) {
3319
+ renderElementFunc = fn;
3320
+ }
3321
+ function renderElement(element, ctx) {
3322
+ if (!renderElementFunc) {
3323
+ throw new Error(
3324
+ "renderElement not initialized. Import from components/index.ts"
3325
+ );
3326
+ }
3327
+ return renderElementFunc(element, ctx);
3328
+ }
3329
+ function createPrefillHints(element, pathKey) {
3330
+ if (!element.prefillHints || element.prefillHints.length === 0) {
3331
+ return null;
3332
+ }
3333
+ const hintsContainer = document.createElement("div");
3334
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
3335
+ element.prefillHints.forEach((hint, index) => {
3336
+ const hintButton = document.createElement("button");
3337
+ hintButton.type = "button";
3338
+ hintButton.className = "fb-prefill-hint";
3339
+ hintButton.textContent = hint.label;
3340
+ hintButton.setAttribute("data-hint-values", JSON.stringify(hint.values));
3341
+ hintButton.setAttribute("data-container-key", pathKey);
3342
+ hintButton.setAttribute("data-hint-index", String(index));
3343
+ hintsContainer.appendChild(hintButton);
3344
+ });
3345
+ return hintsContainer;
3346
+ }
3347
+ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
3348
+ const containerWrap = document.createElement("div");
3349
+ containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3350
+ containerWrap.setAttribute("data-container", pathKey);
3351
+ const header = document.createElement("div");
3352
+ header.className = "flex justify-between items-center mb-4";
3353
+ const left = document.createElement("div");
3354
+ left.className = "flex-1";
3355
+ const itemsWrap = document.createElement("div");
3356
+ const columns = element.columns || 1;
3357
+ if (columns === 1) {
3358
+ itemsWrap.className = "space-y-4";
3359
+ } else {
3360
+ itemsWrap.className = `grid grid-cols-${columns} gap-4`;
3361
+ }
3362
+ containerWrap.appendChild(header);
3363
+ header.appendChild(left);
3364
+ if (!ctx.state.config.readonly) {
3365
+ const hintsElement = createPrefillHints(element, pathKey);
3366
+ if (hintsElement) {
3367
+ containerWrap.appendChild(hintsElement);
3368
+ }
3369
+ }
3370
+ const subCtx = {
3371
+ path: pathJoin(ctx.path, element.key),
3372
+ prefill: ctx.prefill?.[element.key] || {},
3373
+ // Sliced data for value population
3374
+ formData: ctx.formData ?? ctx.prefill,
3375
+ // Complete root data for displayIf evaluation
3376
+ state: ctx.state
3377
+ };
3378
+ element.elements.forEach((child) => {
3379
+ if (!child.hidden) {
3380
+ itemsWrap.appendChild(renderElement(child, subCtx));
3381
+ }
3382
+ });
3383
+ containerWrap.appendChild(itemsWrap);
3384
+ left.innerHTML = `<span>${element.label || element.key}</span>`;
3385
+ wrapper.appendChild(containerWrap);
3386
+ }
3387
+ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3388
+ const state = ctx.state;
3389
+ const containerWrap = document.createElement("div");
3390
+ containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
3391
+ const header = document.createElement("div");
3392
+ header.className = "flex justify-between items-center mb-4";
3393
+ const left = document.createElement("div");
3394
+ left.className = "flex-1";
3395
+ const right = document.createElement("div");
3396
+ right.className = "flex gap-2";
3397
+ const itemsWrap = document.createElement("div");
3398
+ itemsWrap.className = "space-y-4";
3399
+ containerWrap.appendChild(header);
3400
+ header.appendChild(left);
3401
+ if (!state.config.readonly) {
3402
+ header.appendChild(right);
3403
+ }
3404
+ if (!ctx.state.config.readonly) {
3405
+ const hintsElement = createPrefillHints(element, element.key);
3406
+ if (hintsElement) {
3407
+ containerWrap.appendChild(hintsElement);
3408
+ }
3409
+ }
3410
+ const min = element.minCount ?? 0;
3411
+ const max = element.maxCount ?? Infinity;
3412
+ const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
3413
+ const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
3414
+ const createAddButton = () => {
3415
+ const add = document.createElement("button");
3416
+ add.type = "button";
3417
+ add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
3418
+ add.textContent = t("addElement", state);
3419
+ add.onclick = () => {
3420
+ if (countItems() < max) {
3421
+ const idx = countItems();
3422
+ const subCtx = {
3423
+ state: ctx.state,
3424
+ path: pathJoin(ctx.path, `${element.key}[${idx}]`),
3425
+ prefill: {},
3426
+ formData: ctx.formData ?? ctx.prefill
3427
+ // Complete root data for displayIf
3428
+ };
3429
+ const item = document.createElement("div");
3430
+ item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
3431
+ item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3432
+ const childWrapper = document.createElement("div");
3433
+ const columns = element.columns || 1;
3434
+ if (columns === 1) {
3435
+ childWrapper.className = "space-y-4";
3436
+ } else {
3437
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3438
+ }
3439
+ element.elements.forEach((child) => {
3440
+ if (!child.hidden) {
3441
+ childWrapper.appendChild(renderElement(child, subCtx));
3442
+ }
3443
+ });
3444
+ item.appendChild(childWrapper);
3445
+ if (!state.config.readonly) {
3446
+ const rem = document.createElement("button");
3447
+ rem.type = "button";
3448
+ 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";
3449
+ rem.textContent = "\xD7";
3450
+ rem.onclick = () => {
3451
+ item.remove();
3452
+ updateAddButton();
3453
+ };
3454
+ item.style.position = "relative";
3455
+ item.appendChild(rem);
3456
+ }
3457
+ itemsWrap.appendChild(item);
3458
+ updateAddButton();
3459
+ }
3460
+ };
3461
+ return add;
3462
+ };
3463
+ const updateAddButton = () => {
3464
+ const currentCount = countItems();
3465
+ const addBtn = right.querySelector("button");
3466
+ if (addBtn) {
2379
3467
  addBtn.disabled = currentCount >= max;
2380
3468
  addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
2381
3469
  }
@@ -2396,11 +3484,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2396
3484
  const item = document.createElement("div");
2397
3485
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2398
3486
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3487
+ const childWrapper = document.createElement("div");
3488
+ const columns = element.columns || 1;
3489
+ if (columns === 1) {
3490
+ childWrapper.className = "space-y-4";
3491
+ } else {
3492
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3493
+ }
2399
3494
  element.elements.forEach((child) => {
2400
3495
  if (!child.hidden) {
2401
- item.appendChild(renderElement(child, subCtx));
3496
+ childWrapper.appendChild(renderElement(child, subCtx));
2402
3497
  }
2403
3498
  });
3499
+ item.appendChild(childWrapper);
2404
3500
  if (!state.config.readonly) {
2405
3501
  const rem = document.createElement("button");
2406
3502
  rem.type = "button";
@@ -2429,11 +3525,19 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2429
3525
  const item = document.createElement("div");
2430
3526
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2431
3527
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3528
+ const childWrapper = document.createElement("div");
3529
+ const columns = element.columns || 1;
3530
+ if (columns === 1) {
3531
+ childWrapper.className = "space-y-4";
3532
+ } else {
3533
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3534
+ }
2432
3535
  element.elements.forEach((child) => {
2433
3536
  if (!child.hidden) {
2434
- item.appendChild(renderElement(child, subCtx));
3537
+ childWrapper.appendChild(renderElement(child, subCtx));
2435
3538
  }
2436
3539
  });
3540
+ item.appendChild(childWrapper);
2437
3541
  const rem = document.createElement("button");
2438
3542
  rem.type = "button";
2439
3543
  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";
@@ -2704,34 +3808,33 @@ if (typeof document !== "undefined") {
2704
3808
  }
2705
3809
  });
2706
3810
  }
2707
- function renderElement2(element, ctx) {
2708
- if (element.displayIf) {
2709
- try {
2710
- const dataForCondition = ctx.formData ?? ctx.prefill;
2711
- const shouldDisplay = evaluateDisplayCondition(
2712
- element.displayIf,
2713
- dataForCondition
2714
- );
2715
- if (!shouldDisplay) {
2716
- const hiddenWrapper = document.createElement("div");
2717
- hiddenWrapper.className = "fb-field-wrapper-hidden";
2718
- hiddenWrapper.style.display = "none";
2719
- hiddenWrapper.setAttribute("data-field-key", element.key);
2720
- hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
2721
- return hiddenWrapper;
2722
- }
2723
- } catch (error) {
2724
- console.error(
2725
- `Error evaluating displayIf for field "${element.key}":`,
2726
- error
2727
- );
3811
+ function checkDisplayCondition(element, ctx) {
3812
+ if (!element.displayIf) {
3813
+ return null;
3814
+ }
3815
+ try {
3816
+ const dataForCondition = ctx.formData ?? ctx.prefill;
3817
+ const shouldDisplay = evaluateDisplayCondition(
3818
+ element.displayIf,
3819
+ dataForCondition
3820
+ );
3821
+ if (!shouldDisplay) {
3822
+ const hiddenWrapper = document.createElement("div");
3823
+ hiddenWrapper.className = "fb-field-wrapper-hidden";
3824
+ hiddenWrapper.style.display = "none";
3825
+ hiddenWrapper.setAttribute("data-field-key", element.key);
3826
+ hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
3827
+ return hiddenWrapper;
2728
3828
  }
3829
+ } catch (error) {
3830
+ console.error(
3831
+ `Error evaluating displayIf for field "${element.key}":`,
3832
+ error
3833
+ );
2729
3834
  }
2730
- const wrapper = document.createElement("div");
2731
- wrapper.className = "mb-6 fb-field-wrapper";
2732
- wrapper.setAttribute("data-field-key", element.key);
2733
- const label = document.createElement("div");
2734
- label.className = "flex items-center mb-2";
3835
+ return null;
3836
+ }
3837
+ function createFieldLabel(element) {
2735
3838
  const title = document.createElement("label");
2736
3839
  title.className = "text-sm font-medium text-gray-900";
2737
3840
  title.textContent = element.label || element.key;
@@ -2741,59 +3844,71 @@ function renderElement2(element, ctx) {
2741
3844
  req.textContent = "*";
2742
3845
  title.appendChild(req);
2743
3846
  }
3847
+ return title;
3848
+ }
3849
+ function createInfoButton(element) {
3850
+ const infoBtn = document.createElement("button");
3851
+ infoBtn.type = "button";
3852
+ infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
3853
+ 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>';
3854
+ const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
3855
+ const tooltip = document.createElement("div");
3856
+ tooltip.id = tooltipId;
3857
+ 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";
3858
+ tooltip.style.position = "fixed";
3859
+ tooltip.textContent = element.description || element.hint || "Field information";
3860
+ document.body.appendChild(tooltip);
3861
+ infoBtn.onclick = (e) => {
3862
+ e.preventDefault();
3863
+ e.stopPropagation();
3864
+ showTooltip(tooltipId, infoBtn);
3865
+ };
3866
+ return infoBtn;
3867
+ }
3868
+ function createLabelContainer(element) {
3869
+ const label = document.createElement("div");
3870
+ label.className = "flex items-center mb-2";
3871
+ const title = createFieldLabel(element);
2744
3872
  label.appendChild(title);
2745
3873
  if (element.description || element.hint) {
2746
- const infoBtn = document.createElement("button");
2747
- infoBtn.type = "button";
2748
- infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
2749
- 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>';
2750
- const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
2751
- const tooltip = document.createElement("div");
2752
- tooltip.id = tooltipId;
2753
- 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";
2754
- tooltip.style.position = "fixed";
2755
- tooltip.textContent = element.description || element.hint || "Field information";
2756
- document.body.appendChild(tooltip);
2757
- infoBtn.onclick = (e) => {
2758
- e.preventDefault();
2759
- e.stopPropagation();
2760
- showTooltip(tooltipId, infoBtn);
2761
- };
3874
+ const infoBtn = createInfoButton(element);
2762
3875
  label.appendChild(infoBtn);
2763
3876
  }
2764
- wrapper.appendChild(label);
2765
- const pathKey = pathJoin(ctx.path, element.key);
3877
+ return label;
3878
+ }
3879
+ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
3880
+ const isMultiple = "multiple" in element && element.multiple;
2766
3881
  switch (element.type) {
2767
3882
  case "text":
2768
- if ("multiple" in element && element.multiple) {
3883
+ if (isMultiple) {
2769
3884
  renderMultipleTextElement(element, ctx, wrapper, pathKey);
2770
3885
  } else {
2771
3886
  renderTextElement(element, ctx, wrapper, pathKey);
2772
3887
  }
2773
3888
  break;
2774
3889
  case "textarea":
2775
- if ("multiple" in element && element.multiple) {
3890
+ if (isMultiple) {
2776
3891
  renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
2777
3892
  } else {
2778
3893
  renderTextareaElement(element, ctx, wrapper, pathKey);
2779
3894
  }
2780
3895
  break;
2781
3896
  case "number":
2782
- if ("multiple" in element && element.multiple) {
3897
+ if (isMultiple) {
2783
3898
  renderMultipleNumberElement(element, ctx, wrapper, pathKey);
2784
3899
  } else {
2785
3900
  renderNumberElement(element, ctx, wrapper, pathKey);
2786
3901
  }
2787
3902
  break;
2788
3903
  case "select":
2789
- if ("multiple" in element && element.multiple) {
3904
+ if (isMultiple) {
2790
3905
  renderMultipleSelectElement(element, ctx, wrapper, pathKey);
2791
3906
  } else {
2792
3907
  renderSelectElement(element, ctx, wrapper, pathKey);
2793
3908
  }
2794
3909
  break;
2795
3910
  case "file":
2796
- if ("multiple" in element && element.multiple) {
3911
+ if (isMultiple) {
2797
3912
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
2798
3913
  } else {
2799
3914
  renderFileElement(element, ctx, wrapper, pathKey);
@@ -2802,11 +3917,25 @@ function renderElement2(element, ctx) {
2802
3917
  case "files":
2803
3918
  renderFilesElement(element, ctx, wrapper, pathKey);
2804
3919
  break;
3920
+ case "colour":
3921
+ if (isMultiple) {
3922
+ renderMultipleColourElement(element, ctx, wrapper, pathKey);
3923
+ } else {
3924
+ renderColourElement(element, ctx, wrapper, pathKey);
3925
+ }
3926
+ break;
3927
+ case "slider":
3928
+ if (isMultiple) {
3929
+ renderMultipleSliderElement(element, ctx, wrapper, pathKey);
3930
+ } else {
3931
+ renderSliderElement(element, ctx, wrapper, pathKey);
3932
+ }
3933
+ break;
2805
3934
  case "group":
2806
3935
  renderGroupElement(element, ctx, wrapper, pathKey);
2807
3936
  break;
2808
3937
  case "container":
2809
- if ("multiple" in element && element.multiple) {
3938
+ if (isMultiple) {
2810
3939
  renderMultipleContainerElement(element, ctx, wrapper);
2811
3940
  } else {
2812
3941
  renderSingleContainerElement(element, ctx, wrapper, pathKey);
@@ -2819,6 +3948,19 @@ function renderElement2(element, ctx) {
2819
3948
  wrapper.appendChild(unsupported);
2820
3949
  }
2821
3950
  }
3951
+ }
3952
+ function renderElement2(element, ctx) {
3953
+ const hiddenElement = checkDisplayCondition(element, ctx);
3954
+ if (hiddenElement) {
3955
+ return hiddenElement;
3956
+ }
3957
+ const wrapper = document.createElement("div");
3958
+ wrapper.className = "mb-6 fb-field-wrapper";
3959
+ wrapper.setAttribute("data-field-key", element.key);
3960
+ const label = createLabelContainer(element);
3961
+ wrapper.appendChild(label);
3962
+ const pathKey = pathJoin(ctx.path, element.key);
3963
+ dispatchToRenderer(element, ctx, wrapper, pathKey);
2822
3964
  return wrapper;
2823
3965
  }
2824
3966
  setRenderElement(renderElement2);
@@ -3102,6 +4244,14 @@ var componentRegistry = {
3102
4244
  validate: validateFileElement,
3103
4245
  update: updateFileField
3104
4246
  },
4247
+ colour: {
4248
+ validate: validateColourElement,
4249
+ update: updateColourField
4250
+ },
4251
+ slider: {
4252
+ validate: validateSliderElement,
4253
+ update: updateSliderField
4254
+ },
3105
4255
  container: {
3106
4256
  validate: validateContainerElement,
3107
4257
  update: updateContainerField
@@ -3458,6 +4608,33 @@ var FormBuilderInstance = class {
3458
4608
  this.renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
3459
4609
  }
3460
4610
  }
4611
+ /**
4612
+ * Handle prefill hint click - updates container fields with hint values
4613
+ */
4614
+ handlePrefillHintClick(event) {
4615
+ const target = event.target;
4616
+ if (!target.classList.contains("fb-prefill-hint")) {
4617
+ return;
4618
+ }
4619
+ event.preventDefault();
4620
+ event.stopPropagation();
4621
+ const hintValuesJson = target.getAttribute("data-hint-values");
4622
+ const containerKey = target.getAttribute("data-container-key");
4623
+ if (!hintValuesJson || !containerKey) {
4624
+ console.warn("Prefill hint missing required data attributes");
4625
+ return;
4626
+ }
4627
+ try {
4628
+ const hintValues = JSON.parse(hintValuesJson);
4629
+ for (const fieldKey in hintValues) {
4630
+ const fullPath = `${containerKey}.${fieldKey}`;
4631
+ const value = hintValues[fieldKey];
4632
+ this.updateField(fullPath, value);
4633
+ }
4634
+ } catch (error) {
4635
+ console.error("Error parsing prefill hint values:", error);
4636
+ }
4637
+ }
3461
4638
  /**
3462
4639
  * Render form from schema
3463
4640
  */
@@ -3490,6 +4667,9 @@ var FormBuilderInstance = class {
3490
4667
  formEl.appendChild(block);
3491
4668
  });
3492
4669
  root.appendChild(formEl);
4670
+ if (!this.state.config.readonly) {
4671
+ root.addEventListener("click", this.handlePrefillHintClick.bind(this));
4672
+ }
3493
4673
  if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
3494
4674
  this.renderExternalActions();
3495
4675
  }