@dmitryvim/form-builder 0.2.7 → 0.2.9

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) {
@@ -373,7 +410,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
373
410
  font-size: var(--fb-font-size);
374
411
  transition: all var(--fb-transition-duration);
375
412
  `;
376
- addBtn.textContent = `+ Add ${element.label || "Text"}`;
413
+ addBtn.textContent = "+";
377
414
  addBtn.addEventListener("mouseenter", () => {
378
415
  addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
379
416
  });
@@ -622,7 +659,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
622
659
  removeBtn = document.createElement("button");
623
660
  removeBtn.type = "button";
624
661
  removeBtn.className = "remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
625
- removeBtn.innerHTML = "\u2715 Remove";
662
+ removeBtn.innerHTML = "\u2715";
626
663
  removeBtn.onclick = () => {
627
664
  const currentIndex = Array.from(container.children).indexOf(
628
665
  item
@@ -650,7 +687,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
650
687
  const addBtn = document.createElement("button");
651
688
  addBtn.type = "button";
652
689
  addBtn.className = "add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
653
- addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
690
+ addBtn.textContent = "+";
654
691
  addBtn.onclick = () => {
655
692
  values.push(element.default || "");
656
693
  addTextareaItem(element.default || "");
@@ -792,7 +829,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
792
829
  const addBtn = document.createElement("button");
793
830
  addBtn.type = "button";
794
831
  addBtn.className = "add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
795
- addBtn.textContent = `+ Add ${element.label || "Number"}`;
832
+ addBtn.textContent = "+";
796
833
  addBtn.onclick = () => {
797
834
  values.push(element.default || "");
798
835
  addNumberItem(element.default || "");
@@ -1074,7 +1111,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1074
1111
  const addBtn = document.createElement("button");
1075
1112
  addBtn.type = "button";
1076
1113
  addBtn.className = "add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1077
- addBtn.textContent = `+ Add ${element.label || "Selection"}`;
1114
+ addBtn.textContent = "+";
1078
1115
  addBtn.onclick = () => {
1079
1116
  const defaultValue = element.default || element.options?.[0]?.value || "";
1080
1117
  values.push(defaultValue);
@@ -2584,7 +2621,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
2584
2621
  font-size: var(--fb-font-size);
2585
2622
  transition: all var(--fb-transition-duration);
2586
2623
  `;
2587
- addBtn.textContent = `+ Add ${element.label || "Colour"}`;
2624
+ addBtn.textContent = "+";
2588
2625
  addBtn.addEventListener("mouseenter", () => {
2589
2626
  addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
2590
2627
  });
@@ -2766,6 +2803,516 @@ function updateColourField(element, fieldPath, value, context) {
2766
2803
  }
2767
2804
  }
2768
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 = "+";
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
+
2769
3316
  // src/components/container.ts
2770
3317
  var renderElementFunc = null;
2771
3318
  function setRenderElement(fn) {
@@ -2779,6 +3326,24 @@ function renderElement(element, ctx) {
2779
3326
  }
2780
3327
  return renderElementFunc(element, ctx);
2781
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
+ }
2782
3347
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2783
3348
  const containerWrap = document.createElement("div");
2784
3349
  containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
@@ -2788,9 +3353,20 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2788
3353
  const left = document.createElement("div");
2789
3354
  left.className = "flex-1";
2790
3355
  const itemsWrap = document.createElement("div");
2791
- itemsWrap.className = "space-y-4";
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
+ }
2792
3362
  containerWrap.appendChild(header);
2793
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
+ }
2794
3370
  const subCtx = {
2795
3371
  path: pathJoin(ctx.path, element.key),
2796
3372
  prefill: ctx.prefill?.[element.key] || {},
@@ -2816,14 +3392,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2816
3392
  header.className = "flex justify-between items-center mb-4";
2817
3393
  const left = document.createElement("div");
2818
3394
  left.className = "flex-1";
2819
- const right = document.createElement("div");
2820
- right.className = "flex gap-2";
2821
3395
  const itemsWrap = document.createElement("div");
2822
3396
  itemsWrap.className = "space-y-4";
2823
3397
  containerWrap.appendChild(header);
2824
3398
  header.appendChild(left);
2825
- if (!state.config.readonly) {
2826
- header.appendChild(right);
3399
+ if (!ctx.state.config.readonly) {
3400
+ const hintsElement = createPrefillHints(element, element.key);
3401
+ if (hintsElement) {
3402
+ containerWrap.appendChild(hintsElement);
3403
+ }
2827
3404
  }
2828
3405
  const min = element.minCount ?? 0;
2829
3406
  const max = element.maxCount ?? Infinity;
@@ -2832,8 +3409,21 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2832
3409
  const createAddButton = () => {
2833
3410
  const add = document.createElement("button");
2834
3411
  add.type = "button";
2835
- add.className = "px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
2836
- add.textContent = t("addElement", state);
3412
+ add.className = "add-container-btn mt-2 px-3 py-1 rounded";
3413
+ add.style.cssText = `
3414
+ color: var(--fb-primary-color);
3415
+ border: var(--fb-border-width) solid var(--fb-primary-color);
3416
+ background-color: transparent;
3417
+ font-size: var(--fb-font-size);
3418
+ transition: all var(--fb-transition-duration);
3419
+ `;
3420
+ add.textContent = "+";
3421
+ add.addEventListener("mouseenter", () => {
3422
+ add.style.backgroundColor = "var(--fb-background-hover-color)";
3423
+ });
3424
+ add.addEventListener("mouseleave", () => {
3425
+ add.style.backgroundColor = "transparent";
3426
+ });
2837
3427
  add.onclick = () => {
2838
3428
  if (countItems() < max) {
2839
3429
  const idx = countItems();
@@ -2847,16 +3437,35 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2847
3437
  const item = document.createElement("div");
2848
3438
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2849
3439
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3440
+ const childWrapper = document.createElement("div");
3441
+ const columns = element.columns || 1;
3442
+ if (columns === 1) {
3443
+ childWrapper.className = "space-y-4";
3444
+ } else {
3445
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3446
+ }
2850
3447
  element.elements.forEach((child) => {
2851
3448
  if (!child.hidden) {
2852
- item.appendChild(renderElement(child, subCtx));
3449
+ childWrapper.appendChild(renderElement(child, subCtx));
2853
3450
  }
2854
3451
  });
3452
+ item.appendChild(childWrapper);
2855
3453
  if (!state.config.readonly) {
2856
3454
  const rem = document.createElement("button");
2857
3455
  rem.type = "button";
2858
- 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";
2859
- rem.textContent = "\xD7";
3456
+ rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
3457
+ rem.style.cssText = `
3458
+ color: var(--fb-error-color);
3459
+ background-color: transparent;
3460
+ transition: background-color var(--fb-transition-duration);
3461
+ `;
3462
+ rem.textContent = "\u2715";
3463
+ rem.addEventListener("mouseenter", () => {
3464
+ rem.style.backgroundColor = "var(--fb-background-hover-color)";
3465
+ });
3466
+ rem.addEventListener("mouseleave", () => {
3467
+ rem.style.backgroundColor = "transparent";
3468
+ });
2860
3469
  rem.onclick = () => {
2861
3470
  item.remove();
2862
3471
  updateAddButton();
@@ -2872,16 +3481,14 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2872
3481
  };
2873
3482
  const updateAddButton = () => {
2874
3483
  const currentCount = countItems();
2875
- const addBtn = right.querySelector("button");
2876
- if (addBtn) {
2877
- addBtn.disabled = currentCount >= max;
2878
- addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3484
+ const existingAddBtn = containerWrap.querySelector(".add-container-btn");
3485
+ if (existingAddBtn) {
3486
+ existingAddBtn.disabled = currentCount >= max;
3487
+ existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3488
+ existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
2879
3489
  }
2880
3490
  left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "\u221E" : max})</span>`;
2881
3491
  };
2882
- if (!state.config.readonly) {
2883
- right.appendChild(createAddButton());
2884
- }
2885
3492
  if (pre && Array.isArray(pre)) {
2886
3493
  pre.forEach((prefillObj, idx) => {
2887
3494
  const subCtx = {
@@ -2894,16 +3501,35 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2894
3501
  const item = document.createElement("div");
2895
3502
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2896
3503
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3504
+ const childWrapper = document.createElement("div");
3505
+ const columns = element.columns || 1;
3506
+ if (columns === 1) {
3507
+ childWrapper.className = "space-y-4";
3508
+ } else {
3509
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3510
+ }
2897
3511
  element.elements.forEach((child) => {
2898
3512
  if (!child.hidden) {
2899
- item.appendChild(renderElement(child, subCtx));
3513
+ childWrapper.appendChild(renderElement(child, subCtx));
2900
3514
  }
2901
3515
  });
3516
+ item.appendChild(childWrapper);
2902
3517
  if (!state.config.readonly) {
2903
3518
  const rem = document.createElement("button");
2904
3519
  rem.type = "button";
2905
- 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";
2906
- rem.textContent = "\xD7";
3520
+ rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
3521
+ rem.style.cssText = `
3522
+ color: var(--fb-error-color);
3523
+ background-color: transparent;
3524
+ transition: background-color var(--fb-transition-duration);
3525
+ `;
3526
+ rem.textContent = "\u2715";
3527
+ rem.addEventListener("mouseenter", () => {
3528
+ rem.style.backgroundColor = "var(--fb-background-hover-color)";
3529
+ });
3530
+ rem.addEventListener("mouseleave", () => {
3531
+ rem.style.backgroundColor = "transparent";
3532
+ });
2907
3533
  rem.onclick = () => {
2908
3534
  item.remove();
2909
3535
  updateAddButton();
@@ -2927,15 +3553,34 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2927
3553
  const item = document.createElement("div");
2928
3554
  item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2929
3555
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
3556
+ const childWrapper = document.createElement("div");
3557
+ const columns = element.columns || 1;
3558
+ if (columns === 1) {
3559
+ childWrapper.className = "space-y-4";
3560
+ } else {
3561
+ childWrapper.className = `grid grid-cols-${columns} gap-4`;
3562
+ }
2930
3563
  element.elements.forEach((child) => {
2931
3564
  if (!child.hidden) {
2932
- item.appendChild(renderElement(child, subCtx));
3565
+ childWrapper.appendChild(renderElement(child, subCtx));
2933
3566
  }
2934
3567
  });
3568
+ item.appendChild(childWrapper);
2935
3569
  const rem = document.createElement("button");
2936
3570
  rem.type = "button";
2937
- 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";
2938
- rem.textContent = "\xD7";
3571
+ rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
3572
+ rem.style.cssText = `
3573
+ color: var(--fb-error-color);
3574
+ background-color: transparent;
3575
+ transition: background-color var(--fb-transition-duration);
3576
+ `;
3577
+ rem.textContent = "\u2715";
3578
+ rem.addEventListener("mouseenter", () => {
3579
+ rem.style.backgroundColor = "var(--fb-background-hover-color)";
3580
+ });
3581
+ rem.addEventListener("mouseleave", () => {
3582
+ rem.style.backgroundColor = "transparent";
3583
+ });
2939
3584
  rem.onclick = () => {
2940
3585
  if (countItems() > min) {
2941
3586
  item.remove();
@@ -2948,6 +3593,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2948
3593
  }
2949
3594
  }
2950
3595
  containerWrap.appendChild(itemsWrap);
3596
+ if (!state.config.readonly) {
3597
+ containerWrap.appendChild(createAddButton());
3598
+ }
2951
3599
  updateAddButton();
2952
3600
  wrapper.appendChild(containerWrap);
2953
3601
  }
@@ -3318,6 +3966,13 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
3318
3966
  renderColourElement(element, ctx, wrapper, pathKey);
3319
3967
  }
3320
3968
  break;
3969
+ case "slider":
3970
+ if (isMultiple) {
3971
+ renderMultipleSliderElement(element, ctx, wrapper, pathKey);
3972
+ } else {
3973
+ renderSliderElement(element, ctx, wrapper, pathKey);
3974
+ }
3975
+ break;
3321
3976
  case "group":
3322
3977
  renderGroupElement(element, ctx, wrapper, pathKey);
3323
3978
  break;
@@ -3635,6 +4290,10 @@ var componentRegistry = {
3635
4290
  validate: validateColourElement,
3636
4291
  update: updateColourField
3637
4292
  },
4293
+ slider: {
4294
+ validate: validateSliderElement,
4295
+ update: updateSliderField
4296
+ },
3638
4297
  container: {
3639
4298
  validate: validateContainerElement,
3640
4299
  update: updateContainerField
@@ -3991,6 +4650,33 @@ var FormBuilderInstance = class {
3991
4650
  this.renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
3992
4651
  }
3993
4652
  }
4653
+ /**
4654
+ * Handle prefill hint click - updates container fields with hint values
4655
+ */
4656
+ handlePrefillHintClick(event) {
4657
+ const target = event.target;
4658
+ if (!target.classList.contains("fb-prefill-hint")) {
4659
+ return;
4660
+ }
4661
+ event.preventDefault();
4662
+ event.stopPropagation();
4663
+ const hintValuesJson = target.getAttribute("data-hint-values");
4664
+ const containerKey = target.getAttribute("data-container-key");
4665
+ if (!hintValuesJson || !containerKey) {
4666
+ console.warn("Prefill hint missing required data attributes");
4667
+ return;
4668
+ }
4669
+ try {
4670
+ const hintValues = JSON.parse(hintValuesJson);
4671
+ for (const fieldKey in hintValues) {
4672
+ const fullPath = `${containerKey}.${fieldKey}`;
4673
+ const value = hintValues[fieldKey];
4674
+ this.updateField(fullPath, value);
4675
+ }
4676
+ } catch (error) {
4677
+ console.error("Error parsing prefill hint values:", error);
4678
+ }
4679
+ }
3994
4680
  /**
3995
4681
  * Render form from schema
3996
4682
  */
@@ -4023,6 +4709,9 @@ var FormBuilderInstance = class {
4023
4709
  formEl.appendChild(block);
4024
4710
  });
4025
4711
  root.appendChild(formEl);
4712
+ if (!this.state.config.readonly) {
4713
+ root.addEventListener("click", this.handlePrefillHintClick.bind(this));
4714
+ }
4026
4715
  if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
4027
4716
  this.renderExternalActions();
4028
4717
  }