@dmitryvim/form-builder 0.2.6 → 0.2.7

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
@@ -1222,6 +1222,172 @@ function t(key, state) {
1222
1222
  }
1223
1223
 
1224
1224
  // src/components/file.ts
1225
+ function renderLocalImagePreview(container, file, fileName) {
1226
+ const img = document.createElement("img");
1227
+ img.className = "w-full h-full object-contain";
1228
+ img.alt = fileName || "Preview";
1229
+ const reader = new FileReader();
1230
+ reader.onload = (e) => {
1231
+ img.src = e.target?.result || "";
1232
+ };
1233
+ reader.readAsDataURL(file);
1234
+ container.appendChild(img);
1235
+ }
1236
+ function renderLocalVideoPreview(container, file, videoType, resourceId, state, deps) {
1237
+ const videoUrl = URL.createObjectURL(file);
1238
+ container.onclick = null;
1239
+ const newContainer = container.cloneNode(false);
1240
+ if (container.parentNode) {
1241
+ container.parentNode.replaceChild(newContainer, container);
1242
+ }
1243
+ newContainer.innerHTML = `
1244
+ <div class="relative group h-full">
1245
+ <video class="w-full h-full object-contain" controls preload="auto" muted>
1246
+ <source src="${videoUrl}" type="${videoType}">
1247
+ Your browser does not support the video tag.
1248
+ </video>
1249
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
1250
+ <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
1251
+ ${t("removeElement", state)}
1252
+ </button>
1253
+ <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
1254
+ Change
1255
+ </button>
1256
+ </div>
1257
+ </div>
1258
+ `;
1259
+ attachVideoButtonHandlers(newContainer, resourceId, state, deps);
1260
+ return newContainer;
1261
+ }
1262
+ function attachVideoButtonHandlers(container, resourceId, state, deps) {
1263
+ const changeBtn = container.querySelector(".change-file-btn");
1264
+ if (changeBtn) {
1265
+ changeBtn.onclick = (e) => {
1266
+ e.stopPropagation();
1267
+ if (deps?.picker) {
1268
+ deps.picker.click();
1269
+ }
1270
+ };
1271
+ }
1272
+ const deleteBtn = container.querySelector(".delete-file-btn");
1273
+ if (deleteBtn) {
1274
+ deleteBtn.onclick = (e) => {
1275
+ e.stopPropagation();
1276
+ handleVideoDelete(container, resourceId, state, deps);
1277
+ };
1278
+ }
1279
+ }
1280
+ function handleVideoDelete(container, resourceId, state, deps) {
1281
+ state.resourceIndex.delete(resourceId);
1282
+ const hiddenInput = container.parentElement?.querySelector(
1283
+ 'input[type="hidden"]'
1284
+ );
1285
+ if (hiddenInput) {
1286
+ hiddenInput.value = "";
1287
+ }
1288
+ if (deps?.fileUploadHandler) {
1289
+ container.onclick = deps.fileUploadHandler;
1290
+ }
1291
+ if (deps?.dragHandler) {
1292
+ setupDragAndDrop(container, deps.dragHandler);
1293
+ }
1294
+ container.innerHTML = `
1295
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1296
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1297
+ <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"/>
1298
+ </svg>
1299
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1300
+ </div>
1301
+ `;
1302
+ }
1303
+ function renderUploadedVideoPreview(container, thumbnailUrl, videoType) {
1304
+ const video = document.createElement("video");
1305
+ video.className = "w-full h-full object-contain";
1306
+ video.controls = true;
1307
+ video.preload = "metadata";
1308
+ video.muted = true;
1309
+ const source = document.createElement("source");
1310
+ source.src = thumbnailUrl;
1311
+ source.type = videoType;
1312
+ video.appendChild(source);
1313
+ video.appendChild(document.createTextNode("Your browser does not support the video tag."));
1314
+ container.appendChild(video);
1315
+ }
1316
+ function renderDeleteButton(container, resourceId, state) {
1317
+ addDeleteButton(container, state, () => {
1318
+ state.resourceIndex.delete(resourceId);
1319
+ const hiddenInput = container.parentElement?.querySelector(
1320
+ 'input[type="hidden"]'
1321
+ );
1322
+ if (hiddenInput) {
1323
+ hiddenInput.value = "";
1324
+ }
1325
+ container.innerHTML = `
1326
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1327
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1328
+ <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"/>
1329
+ </svg>
1330
+ <div class="text-sm text-center">${t("clickDragText", state)}</div>
1331
+ </div>
1332
+ `;
1333
+ });
1334
+ }
1335
+ async function renderLocalFilePreview(container, meta, fileName, resourceId, isReadonly, state, deps) {
1336
+ if (!meta.file || !(meta.file instanceof File)) {
1337
+ return;
1338
+ }
1339
+ if (meta.type && meta.type.startsWith("image/")) {
1340
+ renderLocalImagePreview(container, meta.file, fileName);
1341
+ } else if (meta.type && meta.type.startsWith("video/")) {
1342
+ const newContainer = renderLocalVideoPreview(
1343
+ container,
1344
+ meta.file,
1345
+ meta.type,
1346
+ resourceId,
1347
+ state,
1348
+ deps
1349
+ );
1350
+ container = newContainer;
1351
+ } else {
1352
+ 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>`;
1353
+ }
1354
+ if (!isReadonly && !(meta.type && meta.type.startsWith("video/"))) {
1355
+ renderDeleteButton(container, resourceId, state);
1356
+ }
1357
+ }
1358
+ async function renderUploadedFilePreview(container, resourceId, fileName, meta, state) {
1359
+ if (!state.config.getThumbnail) {
1360
+ setEmptyFileContainer(container, state);
1361
+ return;
1362
+ }
1363
+ try {
1364
+ const thumbnailUrl = await state.config.getThumbnail(resourceId);
1365
+ if (thumbnailUrl) {
1366
+ clear(container);
1367
+ if (meta && meta.type && meta.type.startsWith("video/")) {
1368
+ renderUploadedVideoPreview(container, thumbnailUrl, meta.type);
1369
+ } else {
1370
+ const img = document.createElement("img");
1371
+ img.className = "w-full h-full object-contain";
1372
+ img.alt = fileName || "Preview";
1373
+ img.src = thumbnailUrl;
1374
+ container.appendChild(img);
1375
+ }
1376
+ } else {
1377
+ setEmptyFileContainer(container, state);
1378
+ }
1379
+ } catch (error) {
1380
+ console.error("Failed to get thumbnail:", error);
1381
+ container.innerHTML = `
1382
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
1383
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
1384
+ <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"/>
1385
+ </svg>
1386
+ <div class="text-sm text-center">${fileName || "Preview unavailable"}</div>
1387
+ </div>
1388
+ `;
1389
+ }
1390
+ }
1225
1391
  async function renderFilePreview(container, resourceId, state, options = {}) {
1226
1392
  const { fileName = "", isReadonly = false, deps = null } = options;
1227
1393
  if (!isReadonly && deps && (!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)) {
@@ -1233,141 +1399,19 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
1233
1399
  if (isReadonly) {
1234
1400
  container.classList.add("cursor-pointer");
1235
1401
  }
1236
- const img = document.createElement("img");
1237
- img.className = "w-full h-full object-contain";
1238
- img.alt = fileName || "Preview";
1239
1402
  const meta = state.resourceIndex.get(resourceId);
1240
1403
  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
- }
1404
+ await renderLocalFilePreview(
1405
+ container,
1406
+ meta,
1407
+ fileName,
1408
+ resourceId,
1409
+ isReadonly,
1410
+ state,
1411
+ deps
1412
+ );
1369
1413
  } else {
1370
- setEmptyFileContainer(container, state);
1414
+ await renderUploadedFilePreview(container, resourceId, fileName, meta, state);
1371
1415
  }
1372
1416
  }
1373
1417
  async function renderFilePreviewReadonly(resourceId, state, fileName) {
@@ -2268,6 +2312,460 @@ function updateFileField(element, fieldPath, value, context) {
2268
2312
  }
2269
2313
  }
2270
2314
 
2315
+ // src/components/colour.ts
2316
+ function normalizeColourValue(value) {
2317
+ if (!value) return "#000000";
2318
+ return value.toUpperCase();
2319
+ }
2320
+ function isValidHexColour(value) {
2321
+ return /^#[0-9A-F]{6}$/i.test(value) || /^#[0-9A-F]{3}$/i.test(value);
2322
+ }
2323
+ function expandHexColour(value) {
2324
+ if (/^#[0-9A-F]{3}$/i.test(value)) {
2325
+ const r = value[1];
2326
+ const g = value[2];
2327
+ const b = value[3];
2328
+ return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
2329
+ }
2330
+ return value.toUpperCase();
2331
+ }
2332
+ function createReadonlyColourUI(value) {
2333
+ const container = document.createElement("div");
2334
+ container.className = "flex items-center gap-2";
2335
+ const normalizedValue = normalizeColourValue(value);
2336
+ const swatch = document.createElement("div");
2337
+ swatch.style.cssText = `
2338
+ width: 32px;
2339
+ height: 32px;
2340
+ border-radius: var(--fb-border-radius);
2341
+ border: var(--fb-border-width) solid var(--fb-border-color);
2342
+ background-color: ${normalizedValue};
2343
+ `;
2344
+ const hexText = document.createElement("span");
2345
+ hexText.style.cssText = `
2346
+ font-size: var(--fb-font-size);
2347
+ color: var(--fb-text-color);
2348
+ font-family: var(--fb-font-family-mono, monospace);
2349
+ `;
2350
+ hexText.textContent = normalizedValue;
2351
+ container.appendChild(swatch);
2352
+ container.appendChild(hexText);
2353
+ return container;
2354
+ }
2355
+ function createEditColourUI(value, pathKey, ctx) {
2356
+ const normalizedValue = normalizeColourValue(value);
2357
+ const pickerWrapper = document.createElement("div");
2358
+ pickerWrapper.className = "colour-picker-wrapper";
2359
+ pickerWrapper.style.cssText = `
2360
+ display: flex;
2361
+ align-items: center;
2362
+ gap: 8px;
2363
+ `;
2364
+ const swatch = document.createElement("div");
2365
+ swatch.className = "colour-swatch";
2366
+ swatch.style.cssText = `
2367
+ width: 40px;
2368
+ height: 40px;
2369
+ border-radius: var(--fb-border-radius);
2370
+ border: var(--fb-border-width) solid var(--fb-border-color);
2371
+ background-color: ${normalizedValue};
2372
+ cursor: pointer;
2373
+ transition: border-color var(--fb-transition-duration) ease-in-out;
2374
+ flex-shrink: 0;
2375
+ `;
2376
+ const hexInput = document.createElement("input");
2377
+ hexInput.type = "text";
2378
+ hexInput.className = "colour-hex-input";
2379
+ hexInput.name = pathKey;
2380
+ hexInput.value = normalizedValue;
2381
+ hexInput.placeholder = "#000000";
2382
+ hexInput.style.cssText = `
2383
+ width: 100px;
2384
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
2385
+ border: var(--fb-border-width) solid var(--fb-border-color);
2386
+ border-radius: var(--fb-border-radius);
2387
+ background-color: var(--fb-background-color);
2388
+ color: var(--fb-text-color);
2389
+ font-size: var(--fb-font-size);
2390
+ font-family: var(--fb-font-family-mono, monospace);
2391
+ transition: all var(--fb-transition-duration) ease-in-out;
2392
+ `;
2393
+ const colourInput = document.createElement("input");
2394
+ colourInput.type = "color";
2395
+ colourInput.className = "colour-picker-hidden";
2396
+ colourInput.value = normalizedValue.toLowerCase();
2397
+ colourInput.style.cssText = `
2398
+ position: absolute;
2399
+ opacity: 0;
2400
+ pointer-events: none;
2401
+ `;
2402
+ hexInput.addEventListener("input", () => {
2403
+ const inputValue = hexInput.value.trim();
2404
+ if (isValidHexColour(inputValue)) {
2405
+ const expanded = expandHexColour(inputValue);
2406
+ swatch.style.backgroundColor = expanded;
2407
+ colourInput.value = expanded.toLowerCase();
2408
+ hexInput.classList.remove("invalid");
2409
+ if (ctx.instance) {
2410
+ ctx.instance.triggerOnChange(pathKey, expanded);
2411
+ }
2412
+ } else {
2413
+ hexInput.classList.add("invalid");
2414
+ }
2415
+ });
2416
+ hexInput.addEventListener("blur", () => {
2417
+ const inputValue = hexInput.value.trim();
2418
+ if (isValidHexColour(inputValue)) {
2419
+ const expanded = expandHexColour(inputValue);
2420
+ hexInput.value = expanded;
2421
+ swatch.style.backgroundColor = expanded;
2422
+ colourInput.value = expanded.toLowerCase();
2423
+ hexInput.classList.remove("invalid");
2424
+ }
2425
+ });
2426
+ colourInput.addEventListener("change", () => {
2427
+ const normalized = normalizeColourValue(colourInput.value);
2428
+ hexInput.value = normalized;
2429
+ swatch.style.backgroundColor = normalized;
2430
+ if (ctx.instance) {
2431
+ ctx.instance.triggerOnChange(pathKey, normalized);
2432
+ }
2433
+ });
2434
+ swatch.addEventListener("click", () => {
2435
+ colourInput.click();
2436
+ });
2437
+ swatch.addEventListener("mouseenter", () => {
2438
+ swatch.style.borderColor = "var(--fb-border-hover-color)";
2439
+ });
2440
+ swatch.addEventListener("mouseleave", () => {
2441
+ swatch.style.borderColor = "var(--fb-border-color)";
2442
+ });
2443
+ hexInput.addEventListener("focus", () => {
2444
+ hexInput.style.borderColor = "var(--fb-border-focus-color)";
2445
+ hexInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
2446
+ hexInput.style.outlineOffset = "0";
2447
+ });
2448
+ hexInput.addEventListener("blur", () => {
2449
+ hexInput.style.borderColor = "var(--fb-border-color)";
2450
+ hexInput.style.outline = "none";
2451
+ });
2452
+ hexInput.addEventListener("mouseenter", () => {
2453
+ if (document.activeElement !== hexInput) {
2454
+ hexInput.style.borderColor = "var(--fb-border-hover-color)";
2455
+ }
2456
+ });
2457
+ hexInput.addEventListener("mouseleave", () => {
2458
+ if (document.activeElement !== hexInput) {
2459
+ hexInput.style.borderColor = "var(--fb-border-color)";
2460
+ }
2461
+ });
2462
+ pickerWrapper.appendChild(swatch);
2463
+ pickerWrapper.appendChild(hexInput);
2464
+ pickerWrapper.appendChild(colourInput);
2465
+ return pickerWrapper;
2466
+ }
2467
+ function renderColourElement(element, ctx, wrapper, pathKey) {
2468
+ const state = ctx.state;
2469
+ const initialValue = ctx.prefill[element.key] || element.default || "#000000";
2470
+ if (state.config.readonly) {
2471
+ const readonlyUI = createReadonlyColourUI(initialValue);
2472
+ wrapper.appendChild(readonlyUI);
2473
+ } else {
2474
+ const editUI = createEditColourUI(initialValue, pathKey, ctx);
2475
+ wrapper.appendChild(editUI);
2476
+ }
2477
+ const colourHint = document.createElement("p");
2478
+ colourHint.className = "mt-1";
2479
+ colourHint.style.cssText = `
2480
+ font-size: var(--fb-font-size-small);
2481
+ color: var(--fb-text-secondary-color);
2482
+ `;
2483
+ colourHint.textContent = makeFieldHint(element);
2484
+ wrapper.appendChild(colourHint);
2485
+ }
2486
+ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2487
+ const state = ctx.state;
2488
+ const prefillValues = ctx.prefill[element.key] || [];
2489
+ const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
2490
+ const minCount = element.minCount ?? 1;
2491
+ const maxCount = element.maxCount ?? Infinity;
2492
+ while (values.length < minCount) {
2493
+ values.push(element.default || "#000000");
2494
+ }
2495
+ const container = document.createElement("div");
2496
+ container.className = "space-y-2";
2497
+ wrapper.appendChild(container);
2498
+ function updateIndices() {
2499
+ const items = container.querySelectorAll(".multiple-colour-item");
2500
+ items.forEach((item, index) => {
2501
+ const input = item.querySelector("input");
2502
+ if (input) {
2503
+ input.name = `${pathKey}[${index}]`;
2504
+ }
2505
+ });
2506
+ }
2507
+ function addColourItem(value = "#000000", index = -1) {
2508
+ const itemWrapper = document.createElement("div");
2509
+ itemWrapper.className = "multiple-colour-item flex items-center gap-2";
2510
+ if (state.config.readonly) {
2511
+ const readonlyUI = createReadonlyColourUI(value);
2512
+ while (readonlyUI.firstChild) {
2513
+ itemWrapper.appendChild(readonlyUI.firstChild);
2514
+ }
2515
+ } else {
2516
+ const tempPathKey = `${pathKey}[${container.children.length}]`;
2517
+ const editUI = createEditColourUI(value, tempPathKey, ctx);
2518
+ editUI.style.flex = "1";
2519
+ itemWrapper.appendChild(editUI);
2520
+ }
2521
+ if (index === -1) {
2522
+ container.appendChild(itemWrapper);
2523
+ } else {
2524
+ container.insertBefore(itemWrapper, container.children[index]);
2525
+ }
2526
+ updateIndices();
2527
+ return itemWrapper;
2528
+ }
2529
+ function updateRemoveButtons() {
2530
+ if (state.config.readonly) return;
2531
+ const items = container.querySelectorAll(".multiple-colour-item");
2532
+ const currentCount = items.length;
2533
+ items.forEach((item) => {
2534
+ let removeBtn = item.querySelector(
2535
+ ".remove-item-btn"
2536
+ );
2537
+ if (!removeBtn) {
2538
+ removeBtn = document.createElement("button");
2539
+ removeBtn.type = "button";
2540
+ removeBtn.className = "remove-item-btn px-2 py-1 rounded";
2541
+ removeBtn.style.cssText = `
2542
+ color: var(--fb-error-color);
2543
+ background-color: transparent;
2544
+ transition: background-color var(--fb-transition-duration);
2545
+ `;
2546
+ removeBtn.innerHTML = "\u2715";
2547
+ removeBtn.addEventListener("mouseenter", () => {
2548
+ removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2549
+ });
2550
+ removeBtn.addEventListener("mouseleave", () => {
2551
+ removeBtn.style.backgroundColor = "transparent";
2552
+ });
2553
+ removeBtn.onclick = () => {
2554
+ const currentIndex = Array.from(container.children).indexOf(
2555
+ item
2556
+ );
2557
+ if (container.children.length > minCount) {
2558
+ values.splice(currentIndex, 1);
2559
+ item.remove();
2560
+ updateIndices();
2561
+ updateAddButton();
2562
+ updateRemoveButtons();
2563
+ }
2564
+ };
2565
+ item.appendChild(removeBtn);
2566
+ }
2567
+ const disabled = currentCount <= minCount;
2568
+ removeBtn.disabled = disabled;
2569
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2570
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2571
+ });
2572
+ }
2573
+ function updateAddButton() {
2574
+ const existingAddBtn = wrapper.querySelector(".add-colour-btn");
2575
+ if (existingAddBtn) existingAddBtn.remove();
2576
+ if (!state.config.readonly && values.length < maxCount) {
2577
+ const addBtn = document.createElement("button");
2578
+ addBtn.type = "button";
2579
+ addBtn.className = "add-colour-btn mt-2 px-3 py-1 rounded";
2580
+ addBtn.style.cssText = `
2581
+ color: var(--fb-primary-color);
2582
+ border: var(--fb-border-width) solid var(--fb-primary-color);
2583
+ background-color: transparent;
2584
+ font-size: var(--fb-font-size);
2585
+ transition: all var(--fb-transition-duration);
2586
+ `;
2587
+ addBtn.textContent = `+ Add ${element.label || "Colour"}`;
2588
+ addBtn.addEventListener("mouseenter", () => {
2589
+ addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2590
+ });
2591
+ addBtn.addEventListener("mouseleave", () => {
2592
+ addBtn.style.backgroundColor = "transparent";
2593
+ });
2594
+ addBtn.onclick = () => {
2595
+ const defaultColour = element.default || "#000000";
2596
+ values.push(defaultColour);
2597
+ addColourItem(defaultColour);
2598
+ updateAddButton();
2599
+ updateRemoveButtons();
2600
+ };
2601
+ wrapper.appendChild(addBtn);
2602
+ }
2603
+ }
2604
+ values.forEach((value) => addColourItem(value));
2605
+ updateAddButton();
2606
+ updateRemoveButtons();
2607
+ const hint = document.createElement("p");
2608
+ hint.className = "mt-1";
2609
+ hint.style.cssText = `
2610
+ font-size: var(--fb-font-size-small);
2611
+ color: var(--fb-text-secondary-color);
2612
+ `;
2613
+ hint.textContent = makeFieldHint(element);
2614
+ wrapper.appendChild(hint);
2615
+ }
2616
+ function validateColourElement(element, key, context) {
2617
+ const errors = [];
2618
+ const { scopeRoot, skipValidation } = context;
2619
+ const markValidity = (input, errorMessage) => {
2620
+ if (!input) return;
2621
+ const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
2622
+ let errorElement = document.getElementById(errorId);
2623
+ if (errorMessage) {
2624
+ input.classList.add("invalid");
2625
+ input.title = errorMessage;
2626
+ if (!errorElement) {
2627
+ errorElement = document.createElement("div");
2628
+ errorElement.id = errorId;
2629
+ errorElement.className = "error-message";
2630
+ errorElement.style.cssText = `
2631
+ color: var(--fb-error-color);
2632
+ font-size: var(--fb-font-size-small);
2633
+ margin-top: 0.25rem;
2634
+ `;
2635
+ if (input.nextSibling) {
2636
+ input.parentNode?.insertBefore(errorElement, input.nextSibling);
2637
+ } else {
2638
+ input.parentNode?.appendChild(errorElement);
2639
+ }
2640
+ }
2641
+ errorElement.textContent = errorMessage;
2642
+ errorElement.style.display = "block";
2643
+ } else {
2644
+ input.classList.remove("invalid");
2645
+ input.title = "";
2646
+ if (errorElement) {
2647
+ errorElement.remove();
2648
+ }
2649
+ }
2650
+ };
2651
+ const validateColourValue = (input, val, fieldKey) => {
2652
+ if (!val) {
2653
+ if (!skipValidation && element.required) {
2654
+ errors.push(`${fieldKey}: required`);
2655
+ markValidity(input, "required");
2656
+ return "";
2657
+ }
2658
+ markValidity(input, null);
2659
+ return "";
2660
+ }
2661
+ const normalized = normalizeColourValue(val);
2662
+ if (!skipValidation && !isValidHexColour(normalized)) {
2663
+ errors.push(`${fieldKey}: invalid hex colour format`);
2664
+ markValidity(input, "invalid hex colour format");
2665
+ return val;
2666
+ }
2667
+ markValidity(input, null);
2668
+ return normalized;
2669
+ };
2670
+ if (element.multiple) {
2671
+ const hexInputs = scopeRoot.querySelectorAll(
2672
+ `.colour-hex-input`
2673
+ );
2674
+ const values = [];
2675
+ hexInputs.forEach((input, index) => {
2676
+ const val = input?.value ?? "";
2677
+ const validated = validateColourValue(input, val, `${key}[${index}]`);
2678
+ values.push(validated);
2679
+ });
2680
+ if (!skipValidation) {
2681
+ const minCount = element.minCount ?? 1;
2682
+ const maxCount = element.maxCount ?? Infinity;
2683
+ const filteredValues = values.filter((v) => v !== "");
2684
+ if (element.required && filteredValues.length === 0) {
2685
+ errors.push(`${key}: required`);
2686
+ }
2687
+ if (filteredValues.length < minCount) {
2688
+ errors.push(`${key}: minimum ${minCount} items required`);
2689
+ }
2690
+ if (filteredValues.length > maxCount) {
2691
+ errors.push(`${key}: maximum ${maxCount} items allowed`);
2692
+ }
2693
+ }
2694
+ return { value: values, errors };
2695
+ } else {
2696
+ const hexInput = scopeRoot.querySelector(
2697
+ `[name="${key}"].colour-hex-input`
2698
+ );
2699
+ const val = hexInput?.value ?? "";
2700
+ if (!skipValidation && element.required && val === "") {
2701
+ errors.push(`${key}: required`);
2702
+ markValidity(hexInput, "required");
2703
+ return { value: "", errors };
2704
+ }
2705
+ const validated = validateColourValue(hexInput, val, key);
2706
+ return { value: validated, errors };
2707
+ }
2708
+ }
2709
+ function updateColourField(element, fieldPath, value, context) {
2710
+ const { scopeRoot } = context;
2711
+ if (element.multiple) {
2712
+ if (!Array.isArray(value)) {
2713
+ console.warn(
2714
+ `updateColourField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
2715
+ );
2716
+ return;
2717
+ }
2718
+ const hexInputs = scopeRoot.querySelectorAll(
2719
+ `.colour-hex-input`
2720
+ );
2721
+ hexInputs.forEach((hexInput, index) => {
2722
+ if (index < value.length) {
2723
+ const normalized = normalizeColourValue(value[index]);
2724
+ hexInput.value = normalized;
2725
+ hexInput.classList.remove("invalid");
2726
+ hexInput.title = "";
2727
+ const wrapper = hexInput.closest(".colour-picker-wrapper");
2728
+ if (wrapper) {
2729
+ const swatch = wrapper.querySelector(".colour-swatch");
2730
+ const colourInput = wrapper.querySelector(".colour-picker-hidden");
2731
+ if (swatch) {
2732
+ swatch.style.backgroundColor = normalized;
2733
+ }
2734
+ if (colourInput) {
2735
+ colourInput.value = normalized.toLowerCase();
2736
+ }
2737
+ }
2738
+ }
2739
+ });
2740
+ if (value.length !== hexInputs.length) {
2741
+ console.warn(
2742
+ `updateColourField: Multiple field "${fieldPath}" has ${hexInputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
2743
+ );
2744
+ }
2745
+ } else {
2746
+ const hexInput = scopeRoot.querySelector(
2747
+ `[name="${fieldPath}"].colour-hex-input`
2748
+ );
2749
+ if (hexInput) {
2750
+ const normalized = normalizeColourValue(value);
2751
+ hexInput.value = normalized;
2752
+ hexInput.classList.remove("invalid");
2753
+ hexInput.title = "";
2754
+ const wrapper = hexInput.closest(".colour-picker-wrapper");
2755
+ if (wrapper) {
2756
+ const swatch = wrapper.querySelector(".colour-swatch");
2757
+ const colourInput = wrapper.querySelector(".colour-picker-hidden");
2758
+ if (swatch) {
2759
+ swatch.style.backgroundColor = normalized;
2760
+ }
2761
+ if (colourInput) {
2762
+ colourInput.value = normalized.toLowerCase();
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ }
2768
+
2271
2769
  // src/components/container.ts
2272
2770
  var renderElementFunc = null;
2273
2771
  function setRenderElement(fn) {
@@ -2704,34 +3202,33 @@ if (typeof document !== "undefined") {
2704
3202
  }
2705
3203
  });
2706
3204
  }
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
- );
3205
+ function checkDisplayCondition(element, ctx) {
3206
+ if (!element.displayIf) {
3207
+ return null;
3208
+ }
3209
+ try {
3210
+ const dataForCondition = ctx.formData ?? ctx.prefill;
3211
+ const shouldDisplay = evaluateDisplayCondition(
3212
+ element.displayIf,
3213
+ dataForCondition
3214
+ );
3215
+ if (!shouldDisplay) {
3216
+ const hiddenWrapper = document.createElement("div");
3217
+ hiddenWrapper.className = "fb-field-wrapper-hidden";
3218
+ hiddenWrapper.style.display = "none";
3219
+ hiddenWrapper.setAttribute("data-field-key", element.key);
3220
+ hiddenWrapper.setAttribute("data-conditionally-hidden", "true");
3221
+ return hiddenWrapper;
2728
3222
  }
3223
+ } catch (error) {
3224
+ console.error(
3225
+ `Error evaluating displayIf for field "${element.key}":`,
3226
+ error
3227
+ );
2729
3228
  }
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";
3229
+ return null;
3230
+ }
3231
+ function createFieldLabel(element) {
2735
3232
  const title = document.createElement("label");
2736
3233
  title.className = "text-sm font-medium text-gray-900";
2737
3234
  title.textContent = element.label || element.key;
@@ -2741,59 +3238,71 @@ function renderElement2(element, ctx) {
2741
3238
  req.textContent = "*";
2742
3239
  title.appendChild(req);
2743
3240
  }
3241
+ return title;
3242
+ }
3243
+ function createInfoButton(element) {
3244
+ const infoBtn = document.createElement("button");
3245
+ infoBtn.type = "button";
3246
+ infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
3247
+ 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>';
3248
+ const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
3249
+ const tooltip = document.createElement("div");
3250
+ tooltip.id = tooltipId;
3251
+ 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";
3252
+ tooltip.style.position = "fixed";
3253
+ tooltip.textContent = element.description || element.hint || "Field information";
3254
+ document.body.appendChild(tooltip);
3255
+ infoBtn.onclick = (e) => {
3256
+ e.preventDefault();
3257
+ e.stopPropagation();
3258
+ showTooltip(tooltipId, infoBtn);
3259
+ };
3260
+ return infoBtn;
3261
+ }
3262
+ function createLabelContainer(element) {
3263
+ const label = document.createElement("div");
3264
+ label.className = "flex items-center mb-2";
3265
+ const title = createFieldLabel(element);
2744
3266
  label.appendChild(title);
2745
3267
  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
- };
3268
+ const infoBtn = createInfoButton(element);
2762
3269
  label.appendChild(infoBtn);
2763
3270
  }
2764
- wrapper.appendChild(label);
2765
- const pathKey = pathJoin(ctx.path, element.key);
3271
+ return label;
3272
+ }
3273
+ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
3274
+ const isMultiple = "multiple" in element && element.multiple;
2766
3275
  switch (element.type) {
2767
3276
  case "text":
2768
- if ("multiple" in element && element.multiple) {
3277
+ if (isMultiple) {
2769
3278
  renderMultipleTextElement(element, ctx, wrapper, pathKey);
2770
3279
  } else {
2771
3280
  renderTextElement(element, ctx, wrapper, pathKey);
2772
3281
  }
2773
3282
  break;
2774
3283
  case "textarea":
2775
- if ("multiple" in element && element.multiple) {
3284
+ if (isMultiple) {
2776
3285
  renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
2777
3286
  } else {
2778
3287
  renderTextareaElement(element, ctx, wrapper, pathKey);
2779
3288
  }
2780
3289
  break;
2781
3290
  case "number":
2782
- if ("multiple" in element && element.multiple) {
3291
+ if (isMultiple) {
2783
3292
  renderMultipleNumberElement(element, ctx, wrapper, pathKey);
2784
3293
  } else {
2785
3294
  renderNumberElement(element, ctx, wrapper, pathKey);
2786
3295
  }
2787
3296
  break;
2788
3297
  case "select":
2789
- if ("multiple" in element && element.multiple) {
3298
+ if (isMultiple) {
2790
3299
  renderMultipleSelectElement(element, ctx, wrapper, pathKey);
2791
3300
  } else {
2792
3301
  renderSelectElement(element, ctx, wrapper, pathKey);
2793
3302
  }
2794
3303
  break;
2795
3304
  case "file":
2796
- if ("multiple" in element && element.multiple) {
3305
+ if (isMultiple) {
2797
3306
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
2798
3307
  } else {
2799
3308
  renderFileElement(element, ctx, wrapper, pathKey);
@@ -2802,11 +3311,18 @@ function renderElement2(element, ctx) {
2802
3311
  case "files":
2803
3312
  renderFilesElement(element, ctx, wrapper, pathKey);
2804
3313
  break;
3314
+ case "colour":
3315
+ if (isMultiple) {
3316
+ renderMultipleColourElement(element, ctx, wrapper, pathKey);
3317
+ } else {
3318
+ renderColourElement(element, ctx, wrapper, pathKey);
3319
+ }
3320
+ break;
2805
3321
  case "group":
2806
3322
  renderGroupElement(element, ctx, wrapper, pathKey);
2807
3323
  break;
2808
3324
  case "container":
2809
- if ("multiple" in element && element.multiple) {
3325
+ if (isMultiple) {
2810
3326
  renderMultipleContainerElement(element, ctx, wrapper);
2811
3327
  } else {
2812
3328
  renderSingleContainerElement(element, ctx, wrapper, pathKey);
@@ -2819,6 +3335,19 @@ function renderElement2(element, ctx) {
2819
3335
  wrapper.appendChild(unsupported);
2820
3336
  }
2821
3337
  }
3338
+ }
3339
+ function renderElement2(element, ctx) {
3340
+ const hiddenElement = checkDisplayCondition(element, ctx);
3341
+ if (hiddenElement) {
3342
+ return hiddenElement;
3343
+ }
3344
+ const wrapper = document.createElement("div");
3345
+ wrapper.className = "mb-6 fb-field-wrapper";
3346
+ wrapper.setAttribute("data-field-key", element.key);
3347
+ const label = createLabelContainer(element);
3348
+ wrapper.appendChild(label);
3349
+ const pathKey = pathJoin(ctx.path, element.key);
3350
+ dispatchToRenderer(element, ctx, wrapper, pathKey);
2822
3351
  return wrapper;
2823
3352
  }
2824
3353
  setRenderElement(renderElement2);
@@ -3102,6 +3631,10 @@ var componentRegistry = {
3102
3631
  validate: validateFileElement,
3103
3632
  update: updateFileField
3104
3633
  },
3634
+ colour: {
3635
+ validate: validateColourElement,
3636
+ update: updateColourField
3637
+ },
3105
3638
  container: {
3106
3639
  validate: validateContainerElement,
3107
3640
  update: updateContainerField