@dmitryvim/form-builder 0.3.0 → 0.3.2

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
@@ -435,6 +435,36 @@ function ensureThemingHooks(doc) {
435
435
  color: var(--fb-error-color);
436
436
  background-color: var(--fb-background-hover-color);
437
437
  }
438
+ /* Prefill-suggestion pills rendered by createPrefillHints. Outline pill at rest,
439
+ soft-fill on hover, solid-fill when selected. All colors flow from the active
440
+ theme \u2014 consumers don't need to ship their own CSS. */
441
+ .fb-prefill-hint {
442
+ padding: 0.25rem 0.625rem;
443
+ border: var(--fb-border-width) solid var(--fb-primary-color);
444
+ border-radius: 9999px;
445
+ background: var(--fb-background-color);
446
+ color: var(--fb-primary-color);
447
+ font-size: var(--fb-font-size-small);
448
+ font-weight: var(--fb-font-weight-medium);
449
+ font-family: var(--fb-font-family);
450
+ cursor: pointer;
451
+ transition: background-color var(--fb-transition-duration), border-color var(--fb-transition-duration), color var(--fb-transition-duration);
452
+ }
453
+ .fb-prefill-hint:hover {
454
+ background: var(--fb-primary-soft-color);
455
+ border-color: var(--fb-primary-hover-color);
456
+ color: var(--fb-primary-hover-color);
457
+ }
458
+ .fb-prefill-hint:focus-visible {
459
+ outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
460
+ outline-offset: 2px;
461
+ }
462
+ .fb-prefill-hint[aria-pressed="true"],
463
+ .fb-prefill-hint.active {
464
+ background: var(--fb-primary-color);
465
+ color: #ffffff;
466
+ border-color: var(--fb-primary-color);
467
+ }
438
468
  `;
439
469
  doc.head.appendChild(style);
440
470
  }
@@ -446,12 +476,28 @@ function applyAutoExpand(textarea) {
446
476
  const resize = () => {
447
477
  if (!textarea.isConnected) return;
448
478
  textarea.style.height = "0";
449
- textarea.style.height = `${textarea.scrollHeight}px`;
479
+ const cs = getComputedStyle(textarea);
480
+ const borderY = parseFloat(cs.borderTopWidth || "0") + parseFloat(cs.borderBottomWidth || "0");
481
+ textarea.style.height = `${textarea.scrollHeight + borderY}px`;
450
482
  };
451
483
  textarea.addEventListener("input", resize);
452
484
  setTimeout(() => {
453
485
  if (textarea.isConnected) resize();
454
486
  }, 0);
487
+ if (typeof ResizeObserver === "undefined") return;
488
+ let lastWidth = -1;
489
+ const ro = new ResizeObserver((entries) => {
490
+ if (!textarea.isConnected) {
491
+ ro.disconnect();
492
+ return;
493
+ }
494
+ const entry = entries[0];
495
+ const w = entry?.contentBoxSize?.[0]?.inlineSize ?? entry?.contentRect?.width ?? 0;
496
+ if (w === lastWidth) return;
497
+ lastWidth = w;
498
+ resize();
499
+ });
500
+ ro.observe(textarea);
455
501
  }
456
502
  function applySingleLineMode(textarea) {
457
503
  textarea.addEventListener("keydown", (e) => {
@@ -4215,15 +4261,25 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
4215
4261
  }
4216
4262
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
4217
4263
  }
4218
- async function uploadBatch(accepted, resourceIds, listEl, state) {
4219
- if (listEl) {
4264
+ async function uploadBatch(opts) {
4265
+ const {
4266
+ accepted,
4267
+ listEl,
4268
+ state,
4269
+ shouldHideAddTile,
4270
+ buildSuccessTile,
4271
+ prepareForUpload
4272
+ } = opts;
4273
+ if (listEl && shouldHideAddTile) {
4220
4274
  const tilesWrap = ensureTilesWrap(listEl);
4221
4275
  const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
4222
4276
  if (addTile) addTile.style.display = "none";
4223
4277
  }
4278
+ prepareForUpload?.();
4279
+ const orderedIds = new Array(accepted.length).fill(null);
4224
4280
  const failures = [];
4225
4281
  await Promise.allSettled(
4226
- accepted.map(async (file) => {
4282
+ accepted.map(async (file, index) => {
4227
4283
  const placeholder = createUploadingTile(file.name, state);
4228
4284
  if (listEl) {
4229
4285
  const tilesWrap = ensureTilesWrap(listEl);
@@ -4236,20 +4292,24 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
4236
4292
  type: file.type,
4237
4293
  size: file.size,
4238
4294
  uploadedAt: /* @__PURE__ */ new Date(),
4239
- file: void 0
4295
+ file
4240
4296
  });
4241
- resourceIds.push(rid);
4297
+ orderedIds[index] = rid;
4298
+ if (buildSuccessTile && placeholder.parentNode) {
4299
+ placeholder.replaceWith(buildSuccessTile(rid));
4300
+ } else {
4301
+ placeholder.remove();
4302
+ }
4242
4303
  } catch (err) {
4243
4304
  const wrapped = err instanceof Error ? err : new Error(String(err));
4244
4305
  const cause = wrapped.cause;
4245
4306
  const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
4246
4307
  failures.push({ file, error: root });
4247
- } finally {
4248
4308
  placeholder.remove();
4249
4309
  }
4250
4310
  })
4251
4311
  );
4252
- return { failures };
4312
+ return { failures, orderedIds };
4253
4313
  }
4254
4314
  function buildBatchErrorMessage(filterError, failures, state) {
4255
4315
  if (failures.length === 0) return filterError;
@@ -4261,68 +4321,70 @@ function buildBatchErrorMessage(filterError, failures, state) {
4261
4321
  ).join(" \u2022 ");
4262
4322
  return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
4263
4323
  }
4264
- function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
4324
+ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
4325
+ const {
4326
+ resourceIds,
4327
+ state,
4328
+ updateCallback,
4329
+ constraints,
4330
+ pathKey,
4331
+ instance,
4332
+ buildSuccessTile,
4333
+ prepareForUpload,
4334
+ coordinator
4335
+ } = opts;
4336
+ const { accepted, errorMessage } = filterAndSlice(
4337
+ files,
4338
+ coordinator.getOccupiedCount(),
4339
+ constraints,
4340
+ state
4341
+ );
4342
+ if (errorTarget) {
4343
+ if (errorMessage) showFileError(errorTarget, errorMessage);
4344
+ else clearFileError(errorTarget);
4345
+ }
4346
+ const handle = coordinator.beginBatch(accepted.length);
4347
+ const shouldHideAddTile = coordinator.getOccupiedCount() >= constraints.maxCount;
4348
+ const { failures, orderedIds } = await uploadBatch({
4349
+ accepted,
4350
+ listEl,
4351
+ state,
4352
+ shouldHideAddTile,
4353
+ buildSuccessTile,
4354
+ prepareForUpload
4355
+ });
4356
+ handle.setResults(orderedIds);
4357
+ if (instance && pathKey && !state.config.readonly) {
4358
+ instance.triggerOnChange(pathKey, resourceIds);
4359
+ }
4360
+ const { wasLast } = handle.end();
4361
+ if (wasLast) updateCallback();
4362
+ if (errorTarget) {
4363
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4364
+ if (combined) showFileError(errorTarget, combined);
4365
+ else clearFileError(errorTarget);
4366
+ }
4367
+ }
4368
+ function setupFilesDropHandler(opts) {
4369
+ const { filesContainer } = opts;
4265
4370
  setupDragAndDrop(filesContainer, async (files) => {
4266
- const { accepted, errorMessage } = filterAndSlice(
4267
- Array.from(files),
4268
- resourceIds.length,
4269
- constraints,
4270
- state
4271
- );
4272
- if (errorMessage) {
4273
- showFileError(filesContainer, errorMessage);
4274
- } else {
4275
- clearFileError(filesContainer);
4276
- }
4277
4371
  const list = filesContainer.querySelector(".files-list") ?? filesContainer;
4278
- const { failures } = await uploadBatch(accepted, resourceIds, list, state);
4279
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
4280
- if (combined) {
4281
- showFileError(filesContainer, combined);
4282
- } else {
4283
- clearFileError(filesContainer);
4284
- }
4285
- updateCallback();
4286
- if (instance && pathKey && !state.config.readonly) {
4287
- instance.triggerOnChange(pathKey, resourceIds);
4288
- }
4372
+ await runMultiFileBatch(opts, Array.from(files), list, filesContainer);
4289
4373
  });
4290
4374
  }
4291
- function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
4375
+ function setupFilesPickerHandler(opts) {
4376
+ const { filesPicker } = opts;
4292
4377
  filesPicker.onchange = async () => {
4293
4378
  if (!filesPicker.files) return;
4294
- const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
4295
- const { accepted, errorMessage } = filterAndSlice(
4379
+ const wrapperEl = filesPicker.closest("[data-files-wrapper]") ?? filesPicker.parentElement;
4380
+ const listEl = wrapperEl?.querySelector(".files-list") ?? null;
4381
+ await runMultiFileBatch(
4382
+ opts,
4296
4383
  Array.from(filesPicker.files),
4297
- resourceIds.length,
4298
- constraints,
4299
- state
4384
+ listEl,
4385
+ wrapperEl
4300
4386
  );
4301
- if (errorMessage && wrapperEl) {
4302
- showFileError(wrapperEl, errorMessage);
4303
- } else if (wrapperEl) {
4304
- clearFileError(wrapperEl);
4305
- }
4306
- const listEl = wrapperEl?.querySelector(".files-list");
4307
- const { failures } = await uploadBatch(
4308
- accepted,
4309
- resourceIds,
4310
- listEl ?? null,
4311
- state
4312
- );
4313
- if (wrapperEl) {
4314
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
4315
- if (combined) {
4316
- showFileError(wrapperEl, combined);
4317
- } else {
4318
- clearFileError(wrapperEl);
4319
- }
4320
- }
4321
- updateCallback();
4322
4387
  filesPicker.value = "";
4323
- if (instance && pathKey && !state.config.readonly) {
4324
- instance.triggerOnChange(pathKey, resourceIds);
4325
- }
4326
4388
  };
4327
4389
  }
4328
4390
 
@@ -4363,16 +4425,6 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
4363
4425
  }
4364
4426
  return null;
4365
4427
  }
4366
- function readCurrentResourceIds(wrapper) {
4367
- const raw = wrapper.dataset.resourceIds;
4368
- if (!raw) return [];
4369
- try {
4370
- const parsed = JSON.parse(raw);
4371
- return Array.isArray(parsed) ? parsed : [];
4372
- } catch {
4373
- return [];
4374
- }
4375
- }
4376
4428
  function registerPickedResource(resource, state) {
4377
4429
  const existing = state.resourceIndex.get(resource.resourceId);
4378
4430
  state.resourceIndex.set(resource.resourceId, {
@@ -4387,13 +4439,27 @@ function extractPickerError(error, state) {
4387
4439
  if (error instanceof Error && error.message) return error.message;
4388
4440
  return t("pickerError", state);
4389
4441
  }
4390
- async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resourceIds, maxCount, updateCallback, instance) {
4442
+ async function handleLibraryPickMulti(opts) {
4443
+ const {
4444
+ state,
4445
+ element,
4446
+ wrapper,
4447
+ fieldPath,
4448
+ resourceIds,
4449
+ maxCount,
4450
+ updateCallback,
4451
+ instance,
4452
+ coordinator,
4453
+ list,
4454
+ buildSuccessTile
4455
+ } = opts;
4391
4456
  if (!state.config.pickExistingFiles) return;
4392
4457
  const allowedExtensions = getAllowedExtensions(element.accept);
4393
4458
  const allowedMimes = getAllowedMimes(element.accept);
4394
4459
  const maxSizeMB = element.maxSize ?? Infinity;
4395
- const currentIds = readCurrentResourceIds(wrapper);
4396
- const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - currentIds.length);
4460
+ const knownRids = coordinator.getAllKnownRids();
4461
+ const existingSet = new Set(knownRids);
4462
+ const remaining = maxCount === Infinity ? Infinity : Math.max(0, maxCount - coordinator.getOccupiedCount());
4397
4463
  let picked;
4398
4464
  try {
4399
4465
  picked = await state.config.pickExistingFiles({
@@ -4402,14 +4468,15 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
4402
4468
  accept: buildAcceptContext(element),
4403
4469
  maxSizeMB: maxSizeMB === Infinity ? void 0 : maxSizeMB,
4404
4470
  remainingSlots: remaining === Infinity ? void 0 : remaining,
4405
- selectedResourceIds: [...currentIds]
4471
+ // Hand the host every rid that's already selected (committed or
4472
+ // staged) so it can grey them out or filter them.
4473
+ selectedResourceIds: knownRids
4406
4474
  });
4407
4475
  } catch (error) {
4408
4476
  showFileError(wrapper, extractPickerError(error, state));
4409
4477
  return;
4410
4478
  }
4411
4479
  if (picked.length === 0) return;
4412
- const existingSet = new Set(currentIds);
4413
4480
  const seen = /* @__PURE__ */ new Set();
4414
4481
  const deduped = picked.filter((r) => {
4415
4482
  if (existingSet.has(r.resourceId)) return false;
@@ -4427,10 +4494,18 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
4427
4494
  );
4428
4495
  return err === null;
4429
4496
  });
4430
- const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
4497
+ const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - coordinator.getOccupiedCount());
4431
4498
  const accepted = validItems.slice(0, freshRemaining);
4432
4499
  const skipped = validItems.length - accepted.length;
4433
- if (accepted.length === 0) return;
4500
+ if (accepted.length === 0) {
4501
+ if (skipped > 0) {
4502
+ showFileError(
4503
+ wrapper,
4504
+ t("filesLimitExceeded", state, { skipped, max: maxCount })
4505
+ );
4506
+ }
4507
+ return;
4508
+ }
4434
4509
  clearFileError(wrapper);
4435
4510
  if (skipped > 0) {
4436
4511
  showFileError(
@@ -4440,13 +4515,22 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
4440
4515
  }
4441
4516
  for (const resource of accepted) {
4442
4517
  registerPickedResource(resource, state);
4443
- resourceIds.push(resource.resourceId);
4444
4518
  }
4445
- wrapper.dataset.resourceIds = JSON.stringify(resourceIds);
4446
- updateCallback();
4519
+ const acceptedIds = accepted.map((r) => r.resourceId);
4520
+ const handle = coordinator.beginBatch(accepted.length);
4521
+ handle.setResults(acceptedIds);
4447
4522
  if (!state.config.readonly) {
4448
4523
  instance.triggerOnChange(fieldPath, resourceIds);
4449
4524
  }
4525
+ const { wasLast } = handle.end();
4526
+ if (wasLast) {
4527
+ updateCallback();
4528
+ } else {
4529
+ const tilesWrap = ensureTilesWrap(list);
4530
+ for (const rid of acceptedIds) {
4531
+ tilesWrap.appendChild(buildSuccessTile(rid));
4532
+ }
4533
+ }
4450
4534
  }
4451
4535
  async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
4452
4536
  if (!state.config.pickExistingFiles) return;
@@ -4767,6 +4851,14 @@ function buildMetaDot() {
4767
4851
  return dot;
4768
4852
  }
4769
4853
  var gridResizeObservers = /* @__PURE__ */ new WeakMap();
4854
+ function disposePlaceholdersForUpload(container) {
4855
+ const observer = gridResizeObservers.get(container);
4856
+ if (observer) {
4857
+ observer.disconnect();
4858
+ gridResizeObservers.delete(container);
4859
+ }
4860
+ container.querySelectorAll(".fb-multi-placeholder").forEach((p) => p.remove());
4861
+ }
4770
4862
  function renderResourcePills(opts) {
4771
4863
  const {
4772
4864
  container,
@@ -5114,16 +5206,19 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5114
5206
  filesPicker.click();
5115
5207
  };
5116
5208
  const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
5117
- handleLibraryPickMulti(
5209
+ handleLibraryPickMulti({
5118
5210
  state,
5119
5211
  element,
5120
- filesWrapper,
5121
- pathKey,
5122
- initialFiles,
5123
- maxFiles,
5124
- updateFilesDisplay,
5125
- ctx.instance
5126
- ).catch((err) => {
5212
+ wrapper: filesWrapper,
5213
+ fieldPath: pathKey,
5214
+ resourceIds: initialFiles,
5215
+ maxCount: maxFiles,
5216
+ updateCallback: updateFilesDisplay,
5217
+ instance: ctx.instance,
5218
+ coordinator,
5219
+ list,
5220
+ buildSuccessTile
5221
+ }).catch((err) => {
5127
5222
  console.error("Library pick failed:", err);
5128
5223
  });
5129
5224
  } : null;
@@ -5137,37 +5232,153 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5137
5232
  releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
5138
5233
  const index = initialFiles.indexOf(ridToRemove);
5139
5234
  if (index > -1) initialFiles.splice(index, 1);
5140
- updateFilesDisplay();
5235
+ if (coordinator.hasInFlightBatches()) {
5236
+ pendingRemovals.add(ridToRemove);
5237
+ list.querySelector(`[data-resource-id="${ridToRemove}"]`)?.remove();
5238
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5239
+ } else {
5240
+ updateFilesDisplay();
5241
+ }
5242
+ if (ctx.instance && pathKey && !state.config.readonly) {
5243
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5244
+ }
5141
5245
  },
5142
5246
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
5143
5247
  isReadonly: currentlyReadonly,
5144
5248
  onLibraryPick: currentlyReadonly ? null : onLibraryPick,
5145
5249
  element,
5146
5250
  onClearAll: currentlyReadonly ? void 0 : () => {
5251
+ for (const rid of initialFiles) {
5252
+ releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
5253
+ }
5147
5254
  initialFiles.splice(0);
5148
- updateFilesDisplay();
5255
+ if (coordinator.hasInFlightBatches()) {
5256
+ const visibleTiles = list.querySelectorAll("[data-resource-id]");
5257
+ for (const tile of visibleTiles) {
5258
+ const rid = tile.dataset.resourceId;
5259
+ if (rid) {
5260
+ releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
5261
+ pendingRemovals.add(rid);
5262
+ }
5263
+ tile.remove();
5264
+ }
5265
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5266
+ } else {
5267
+ updateFilesDisplay();
5268
+ }
5269
+ if (ctx.instance && pathKey && !state.config.readonly) {
5270
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5271
+ }
5149
5272
  },
5150
5273
  openPicker
5151
5274
  });
5152
5275
  }
5153
- setupFilesDropHandler(
5154
- filesContainer,
5155
- initialFiles,
5156
- state,
5157
- updateFilesDisplay,
5158
- constraints,
5159
- pathKey,
5160
- ctx.instance
5161
- );
5162
- setupFilesPickerHandler(
5163
- filesPicker,
5164
- initialFiles,
5276
+ let inFlightFiles = 0;
5277
+ let activeBatches = 0;
5278
+ let nextBatchOrdinal = 0;
5279
+ let nextCommitOrdinal = 0;
5280
+ const stagedResults = /* @__PURE__ */ new Map();
5281
+ const batchReservations = /* @__PURE__ */ new Map();
5282
+ const pendingRemovals = /* @__PURE__ */ new Set();
5283
+ const drainContiguousStagedResults = () => {
5284
+ while (stagedResults.has(nextCommitOrdinal)) {
5285
+ const ordinal = nextCommitOrdinal;
5286
+ const ids = stagedResults.get(ordinal);
5287
+ stagedResults.delete(ordinal);
5288
+ nextCommitOrdinal += 1;
5289
+ const reservation = batchReservations.get(ordinal) ?? 0;
5290
+ batchReservations.delete(ordinal);
5291
+ inFlightFiles -= reservation;
5292
+ for (const rid of ids) {
5293
+ if (rid === null) continue;
5294
+ if (pendingRemovals.has(rid)) {
5295
+ pendingRemovals.delete(rid);
5296
+ continue;
5297
+ }
5298
+ initialFiles.push(rid);
5299
+ }
5300
+ }
5301
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5302
+ };
5303
+ const coordinator = {
5304
+ getOccupiedCount: () => initialFiles.length + inFlightFiles,
5305
+ getAllKnownRids: () => {
5306
+ const out = [...initialFiles];
5307
+ for (const ids of stagedResults.values()) {
5308
+ for (const rid of ids) {
5309
+ if (rid !== null) out.push(rid);
5310
+ }
5311
+ }
5312
+ return out;
5313
+ },
5314
+ hasInFlightBatches: () => activeBatches > 0 || batchReservations.size > 0,
5315
+ wasRemovedDuringBatch: (rid) => pendingRemovals.has(rid),
5316
+ beginBatch: (count) => {
5317
+ inFlightFiles += count;
5318
+ activeBatches += 1;
5319
+ const ordinal = nextBatchOrdinal++;
5320
+ batchReservations.set(ordinal, count);
5321
+ return {
5322
+ setResults: (orderedIds) => {
5323
+ stagedResults.set(ordinal, orderedIds);
5324
+ drainContiguousStagedResults();
5325
+ },
5326
+ end: () => {
5327
+ activeBatches -= 1;
5328
+ const wasLast = activeBatches === 0 && batchReservations.size === 0;
5329
+ if (wasLast) {
5330
+ pendingRemovals.clear();
5331
+ }
5332
+ return { wasLast };
5333
+ }
5334
+ };
5335
+ }
5336
+ };
5337
+ const buildSuccessTile = (rid) => {
5338
+ const currentlyReadonly = isElementReadonly(element, state);
5339
+ return buildPreviewTile(
5340
+ rid,
5341
+ state,
5342
+ !currentlyReadonly,
5343
+ currentlyReadonly ? null : () => {
5344
+ releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
5345
+ const idx = initialFiles.indexOf(rid);
5346
+ if (idx > -1) {
5347
+ initialFiles.splice(idx, 1);
5348
+ if (coordinator.hasInFlightBatches()) {
5349
+ pendingRemovals.add(rid);
5350
+ list.querySelector(`[data-resource-id="${rid}"]`)?.remove();
5351
+ filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
5352
+ } else {
5353
+ updateFilesDisplay();
5354
+ }
5355
+ if (ctx.instance && pathKey && !state.config.readonly) {
5356
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5357
+ }
5358
+ return;
5359
+ }
5360
+ pendingRemovals.add(rid);
5361
+ list.querySelector(`[data-resource-id="${rid}"]`)?.remove();
5362
+ if (ctx.instance && pathKey && !state.config.readonly) {
5363
+ ctx.instance.triggerOnChange(pathKey, initialFiles);
5364
+ }
5365
+ }
5366
+ );
5367
+ };
5368
+ const prepareForUpload = () => disposePlaceholdersForUpload(list);
5369
+ const sharedHandlerOpts = {
5370
+ resourceIds: initialFiles,
5165
5371
  state,
5166
- updateFilesDisplay,
5372
+ updateCallback: updateFilesDisplay,
5167
5373
  constraints,
5168
5374
  pathKey,
5169
- ctx.instance
5170
- );
5375
+ instance: ctx.instance,
5376
+ buildSuccessTile,
5377
+ prepareForUpload,
5378
+ coordinator
5379
+ };
5380
+ setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
5381
+ setupFilesPickerHandler({ ...sharedHandlerOpts, filesPicker });
5171
5382
  updateFilesDisplay();
5172
5383
  wrapper.appendChild(filesWrapper);
5173
5384
  }
@@ -6514,6 +6725,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6514
6725
  formData: ctx.formData ?? ctx.prefill,
6515
6726
  // Complete root data for enableIf evaluation
6516
6727
  state: ctx.state,
6728
+ instance: ctx.instance,
6517
6729
  inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
6518
6730
  };
6519
6731
  element.elements.forEach((child) => {
@@ -6600,6 +6812,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6600
6812
  const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
6601
6813
  const subCtx = {
6602
6814
  state: ctx.state,
6815
+ instance: ctx.instance,
6603
6816
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6604
6817
  prefill: childDefaults,
6605
6818
  // Defaults for enableIf evaluation
@@ -6665,6 +6878,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6665
6878
  const mergedPrefill = mergeWithDefaults(prefillObj || {}, childDefaults);
6666
6879
  const subCtx = {
6667
6880
  state: ctx.state,
6881
+ instance: ctx.instance,
6668
6882
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6669
6883
  prefill: mergedPrefill,
6670
6884
  // Merged prefill with defaults for enableIf
@@ -6711,6 +6925,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
6711
6925
  const idx = countItems();
6712
6926
  const subCtx = {
6713
6927
  state: ctx.state,
6928
+ instance: ctx.instance,
6714
6929
  path: pathJoin(ctx.path, `${element.key}[${idx}]`),
6715
6930
  prefill: childDefaults,
6716
6931
  // Defaults for enableIf evaluation
@@ -8353,6 +8568,20 @@ function applyAutoExpand2(textarea, backdrop) {
8353
8568
  setTimeout(() => {
8354
8569
  if (textarea.isConnected) resize();
8355
8570
  }, 0);
8571
+ if (typeof ResizeObserver === "undefined") return;
8572
+ let lastWidth = -1;
8573
+ const ro = new ResizeObserver((entries) => {
8574
+ if (!textarea.isConnected) {
8575
+ ro.disconnect();
8576
+ return;
8577
+ }
8578
+ const entry = entries[0];
8579
+ const w = entry?.contentBoxSize?.[0]?.inlineSize ?? entry?.contentRect?.width ?? 0;
8580
+ if (w === lastWidth) return;
8581
+ lastWidth = w;
8582
+ resize();
8583
+ });
8584
+ ro.observe(textarea);
8356
8585
  }
8357
8586
  function buildFileLabels(files, state) {
8358
8587
  const labels = /* @__PURE__ */ new Map();
@@ -10118,6 +10347,33 @@ function extractDOMValue(fieldPath, formRoot) {
10118
10347
  }
10119
10348
  return void 0;
10120
10349
  }
10350
+ function buildScopedDataAtPath(path, value) {
10351
+ const segments = path.match(/[^.[\]]+|\[\d+\]/g);
10352
+ if (!segments || segments.length === 0) {
10353
+ return {};
10354
+ }
10355
+ const root = {};
10356
+ let current = root;
10357
+ for (let i = 0; i < segments.length - 1; i++) {
10358
+ const seg = segments[i];
10359
+ const next = segments[i + 1];
10360
+ const placeholder = next.startsWith("[") && next.endsWith("]") ? [] : {};
10361
+ if (seg.startsWith("[") && seg.endsWith("]")) {
10362
+ const idx = parseInt(seg.slice(1, -1), 10);
10363
+ current[idx] = placeholder;
10364
+ } else {
10365
+ current[seg] = placeholder;
10366
+ }
10367
+ current = placeholder;
10368
+ }
10369
+ const last = segments[segments.length - 1];
10370
+ if (last.startsWith("[") && last.endsWith("]")) {
10371
+ current[parseInt(last.slice(1, -1), 10)] = value;
10372
+ } else {
10373
+ current[last] = value;
10374
+ }
10375
+ return root;
10376
+ }
10121
10377
  function reevaluateEnableIf(wrapper, element, ctx) {
10122
10378
  if (!element.enableIf) {
10123
10379
  return;
@@ -10129,54 +10385,13 @@ function reevaluateEnableIf(wrapper, element, ctx) {
10129
10385
  }
10130
10386
  const condition = element.enableIf;
10131
10387
  const scope = condition.scope ?? "relative";
10132
- let rootFormData = {};
10133
- const containerData = {};
10134
10388
  const effectiveScope = !ctx.path || ctx.path === "" ? "absolute" : scope;
10135
- if (effectiveScope === "relative" && ctx.path) {
10136
- const containerMatch = ctx.path.match(/^(.+)\[(\d+)\]$/);
10137
- if (containerMatch) {
10138
- const containerKey = containerMatch[1];
10139
- const containerIndex = parseInt(containerMatch[2], 10);
10140
- const containerItemElement = formRoot.querySelector(
10141
- `[data-container-item="${containerKey}[${containerIndex}]"]`
10142
- );
10143
- if (containerItemElement) {
10144
- const inputs = containerItemElement.querySelectorAll("input, select, textarea");
10145
- inputs.forEach((input) => {
10146
- const fieldName = input.getAttribute("name");
10147
- if (fieldName) {
10148
- const fieldKeyMatch = fieldName.match(/\.([^.[\]]+)$/);
10149
- if (fieldKeyMatch) {
10150
- const fieldKey = fieldKeyMatch[1];
10151
- if (input instanceof HTMLSelectElement) {
10152
- containerData[fieldKey] = input.value;
10153
- } else if (input instanceof HTMLInputElement) {
10154
- if (input.type === "checkbox") {
10155
- containerData[fieldKey] = input.checked;
10156
- } else if (input.type === "radio") {
10157
- if (input.checked) {
10158
- containerData[fieldKey] = input.value;
10159
- }
10160
- } else {
10161
- containerData[fieldKey] = input.value;
10162
- }
10163
- } else if (input instanceof HTMLTextAreaElement) {
10164
- containerData[fieldKey] = input.value;
10165
- }
10166
- }
10167
- }
10168
- });
10169
- }
10170
- }
10171
- } else {
10172
- const dependencyKey = condition.key;
10173
- const dependencyValue = extractDOMValue(dependencyKey, formRoot);
10174
- if (dependencyValue !== void 0) {
10175
- rootFormData[dependencyKey] = dependencyValue;
10176
- } else {
10177
- rootFormData = ctx.formData ?? ctx.prefill;
10178
- }
10179
- }
10389
+ const dependencyKey = condition.key;
10390
+ const dependencyFieldPath = effectiveScope === "relative" && ctx.path ? `${ctx.path}.${dependencyKey}` : dependencyKey;
10391
+ const dependencyValue = extractDOMValue(dependencyFieldPath, formRoot);
10392
+ const scopedData = buildScopedDataAtPath(dependencyKey, dependencyValue);
10393
+ const rootFormData = effectiveScope === "relative" ? {} : scopedData;
10394
+ const containerData = effectiveScope === "relative" ? scopedData : void 0;
10180
10395
  try {
10181
10396
  const shouldEnable = evaluateEnableCondition(
10182
10397
  condition,
@@ -10850,6 +11065,7 @@ var exampleThemes = {
10850
11065
  },
10851
11066
  // Picaz wizard design tokens — derived from the Picaz Wizard mockups.
10852
11067
  // Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
11068
+ // Assumes Inter is loaded by the host (e.g. via Google Fonts in index.html).
10853
11069
  picaz: {
10854
11070
  ...defaultTheme,
10855
11071
  primaryColor: "#2f5bea",
@@ -10881,6 +11097,9 @@ var exampleThemes = {
10881
11097
  fileUploadBgColor: "#fafcff",
10882
11098
  fileUploadBorderColor: "#cdd6e3",
10883
11099
  fileUploadHoverBorderColor: "#2f5bea",
11100
+ // Picaz uses roomier inputs (11/14px) than the defaultTheme compact density.
11101
+ inputPaddingX: "14px",
11102
+ inputPaddingY: "11px",
10884
11103
  borderRadius: "12px",
10885
11104
  borderRadiusSmall: "8px",
10886
11105
  borderRadiusLarge: "16px",
@@ -10888,7 +11107,17 @@ var exampleThemes = {
10888
11107
  fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
10889
11108
  shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
10890
11109
  shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
10891
- focusRingColor: "#2f5bea"
11110
+ focusRingColor: "#2f5bea",
11111
+ // Slide cards: subtle gradient lift, no rest-state shadow (mockup adds it
11112
+ // only on hover, which form-builder doesn't yet differentiate).
11113
+ slideCardBg: "linear-gradient(180deg, #f7f9fc 0%, #dde3ee 100%)",
11114
+ slideCardShadow: "none",
11115
+ slideCardRadius: "16px",
11116
+ // Tiny uppercase captions above grouped lists ("ПРЕИМУЩЕСТВА" in the mockup).
11117
+ labelSectionFontSize: "0.625rem",
11118
+ // 10px
11119
+ labelSectionLetterSpacing: "0.07em",
11120
+ labelSectionTextTransform: "uppercase"
10892
11121
  }
10893
11122
  };
10894
11123
 
@@ -11254,6 +11483,13 @@ var FormBuilderInstance = class {
11254
11483
  const value = hintValues[fieldKey];
11255
11484
  this.updateField(fullPath, value);
11256
11485
  }
11486
+ const group = target.closest(".fb-prefill-hints");
11487
+ if (group) {
11488
+ group.querySelectorAll(
11489
+ '.fb-prefill-hint[aria-pressed="true"]'
11490
+ ).forEach((el) => el.removeAttribute("aria-pressed"));
11491
+ }
11492
+ target.setAttribute("aria-pressed", "true");
11257
11493
  } catch (error) {
11258
11494
  console.error("Error parsing prefill hint values:", error);
11259
11495
  }