From 0442e1186ae642c86947d03961fde7d461ba054d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Feb 2026 21:23:47 +0000
Subject: [PATCH] =minor Form.js fixes

---
 /dev/null                           | 1857 --------------------------------------------------------
 inc/meta/Form.php                   |   12 
 assets/js/min/form.min.js           |    2 
 inc/managers/SEO/_setup.php         |    2 
 assets/js/concise/FormController.js |   50 
 5 files changed, 42 insertions(+), 1,881 deletions(-)

diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 8996cbf..5f26f7e 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -863,7 +863,8 @@
 					let data = {};
 					data.repeater = repeater;
 					repeater.append(this.templates.create(repeater.dataset.repeaterId, data));
-					this.initializeFields(repeater, this.getField(repeater).config??{});
+					let form = this.getForm(repeater);
+					this.initializeFields(repeater, form);
 					this.a11y.announce('Row added');
 				}
 				removeRepeaterRow(row) {
@@ -919,11 +920,11 @@
 			}
 				addTagListListeners(el) {
 					el.addEventListener('click', this.tagListClick);
-					el.addEventListener('keypress', this.tagListInput, {passive: true})
+					el.addEventListener('keypress', this.tagListInput);
 				}
 				removeTagListListeners(el) {
 					el.removeEventListener('click', this.tagListClick);
-					el.removeEventListener('keypress', this.tagListInput)
+					el.removeEventListener('keypress', this.tagListInput);
 				}
 
 				handleTagListClick(e) {
@@ -1129,25 +1130,32 @@
 						}
 					});
 				}
-			checkForCharacterLimits(form) {
-				if (!form.querySelector(this.selectors.limits.hasLimit)) return;
-				this.countUpdaters = this.updateCount.bind(this);
+	checkForCharacterLimits(form) {
+		if (!form.querySelector(this.selectors.limits.hasLimit)) return;
+		this.countUpdaters = this.updateCount.bind(this);
 
-				form.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach(input => {
-					let id = window.generateID('limit');
-					input.dataset.charLimitId = id;
-					let config = {
-						element: input,
-						form: form.dataset.formId,
-						ui: window.uiFromSelectors(this.selectors.limits, input.closest('.field'))
-					};
-					config.ui.limit.textContent = input.dataset.limit;
-					this.charLimits.set(id, config);
+		form.querySelectorAll(this.selectors.limits.hasLimit).forEach(field => {
+			const input = field.querySelector('input, textarea, select');
+			if (!input) return;
 
-					this.addCharacterLimitListeners(input);
-				});
+			let id = window.generateID('limit');
+			input.dataset.charLimitId = id;
+			input.dataset.limit = field.dataset.limit;
 
+			let config = {
+				element: input,
+				form: form.dataset.formId,
+				ui: window.uiFromSelectors(this.selectors.limits, field)
+			};
+
+			if (config.ui.limit) {
+				config.ui.limit.textContent = field.dataset.limit;
 			}
+
+			this.charLimits.set(id, config);
+			this.addCharacterLimitListeners(input);
+		});
+	}
 				addCharacterLimitListeners(input) {
 					input.addEventListener('input', this.countUpdaters, {passive: true});
 				}
@@ -1965,10 +1973,10 @@
 			});
 			this.tagLists.clear();
 		}
-		if(this.charLimits.size > 0) {
+		if (this.charLimits.size > 0) {
 			Array.from(this.charLimits.values()).forEach(charLimit => {
-				charLimit.removeEventListener('input', this.countUpdaters);
-			})
+				charLimit.element.removeEventListener('input', this.countUpdaters);
+			});
 		}
 		this.inputs.clear();
 		this.forms.clear();
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index e36ea87..a23602d 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore" type="button" data-form-id="${e}">Restore</button>\n        <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if("repeater"===t.dataset.fieldType||"tag-list"===t.dataset.fieldType)return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t)),this.initializeFields(e,this.getField(e).config??{}),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput,{passive:!0})}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find((e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e)));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""})),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach((t=>{let s=window.generateID("limit");t.dataset.charLimitId=s;let i={element:t,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t.closest(".field"))};i.ui.limit.textContent=t.dataset.limit,this.charLimits.set(s,i),this.addCharacterLimitListeners(t)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore" type="button" data-form-id="${e}">Restore</button>\n        <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if("repeater"===t.dataset.fieldType||"tag-list"===t.dataset.fieldType)return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t));let s=this.getForm(e);this.initializeFields(e,s),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput)}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find((e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e)));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""})),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(this.selectors.limits.hasLimit).forEach((t=>{const s=t.querySelector("input, textarea, select");if(!s)return;let i=window.generateID("limit");s.dataset.charLimitId=i,s.dataset.limit=t.dataset.limit;let a={element:s,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t)};a.ui.limit&&(a.ui.limit.textContent=t.dataset.limit),this.charLimits.set(i,a),this.addCharacterLimitListeners(s)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.element.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
diff --git a/inc/managers/SEO/SchemaRegistry.php b/inc/managers/SEO/SchemaRegistry.php
deleted file mode 100644
index 175209f..0000000
--- a/inc/managers/SEO/SchemaRegistry.php
+++ /dev/null
@@ -1,1857 +0,0 @@
-<?php
-namespace JVBase\managers\SEO;
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-/**
- * Schema.org Registry - Centralized field and type definitions
- *
- * Field definitions use Meta.php field types and include transformer hints.
- * Types reference field names and support inheritance via 'extends'.
- */
-class SchemaRegistry
-{
-	private static ?self $instance = null;
-	private array $fieldDefinitions = [];
-	private array $typeDefinitions = [];
-	private array $typeGroups = [];
-
-	private array $metaFields = ['metaTitle', 'metaDescription','socialPreviewImage', 'twitterImage'];
-
-	private array $defaultMetaValues = [
-		'title' => '{{post_title}} | {{site_name}}',
-		'description' => '{{post_excerpt}}',
-		'image' => '{{featured_image}}',
-		'twitter_image' => ''
-	];
-
-	public static function getInstance(): self
-	{
-		if (self::$instance === null) {
-			self::$instance = new self();
-		}
-		return self::$instance;
-	}
-
-	public array $schemaTypes = [
-		'WebSite'			=> 'Web Site',
-		'Organization' 		=> 'Organization',
-		'LocalBusiness' 	=> '  - Local Business',
-		'TattooParlor' 		=> '    - - Tattoo Shop',
-		'HealthBusiness' 	=> '    - - Health Business',
-		'FoodEstablishment' => '    - - Restaurant',
-		'WebPage' 			=> 'Web Page',
-		'CollectionPage'	=> '  - Collection Page',
-		'FAQPage'			=> '  - FAQ Page',
-		'Person'			=> 'Person',
-		'CreativeWork'		=> 'Creative Work',
-		'DefinedTerm'		=> '  - Defined Term',
-		'VisualArtwork'		=> '  - Visual Artwork',
-		'Tattoo'			=> '    - - Tattoo',
-		'BeforeAfter'		=> '  - Before & After',
-		'Product'			=> 'Product',
-		'Event'				=> 'Event',
-	];
-
-	private function __construct()
-	{
-		$this->registerFieldDefinitions();
-		$this->registerTypeDefinitions();
-		$this->registerTypeGroups();
-
-		do_action(BASE . 'schema_registry_loaded', $this);
-	}
-
-	/**
-	 * Get field definition for a specific field
-	 */
-	public function getFieldDefinition(string $fieldName): ?array
-	{
-		$definitions = $this->getFieldDefinitions();
-		return $definitions[$fieldName] ?? null;
-	}
-
-	/**
-	 * Get all field definitions
-	 */
-	public function getFieldDefinitions(): array
-	{
-		return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
-	}
-
-	public function getMetaFields(): array
-	{
-		return $this->metaFields;
-	}
-
-	public function getDefaultMetaValues(): array
-	{
-		return $this->defaultMetaValues;
-	}
-
-	/**
-	 * Get type definition
-	 */
-	public function getTypeDefinition(string $type): ?array
-	{
-		$definitions = $this->getTypeDefinitions();
-		return $definitions[$type] ?? null;
-	}
-
-	/**
-	 * Get all type definitions
-	 */
-	public function getTypeDefinitions(): array
-	{
-		return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions);
-	}
-
-	/**
-	 * Get all fields for a specific type (with inheritance)
-	 */
-	public function getFieldsForType(string $type): array
-	{
-		$fields = [];
-
-		$typeDefinition = $this->getTypeDefinition($type);
-		if (!$typeDefinition) {
-			return $fields;
-		}
-
-		$fields = array_merge($fields, $typeDefinition['fields'] ?? []);
-
-		// Handle inheritance
-		if (!empty($typeDefinition['extends'])) {
-			$parentFields = $this->getFieldsForType($typeDefinition['extends']);
-			$fields = array_unique(array_merge($parentFields, $fields));
-		}
-
-		return $fields;
-	}
-
-	/**
-	 * Get Meta configuration for a schema type
-	 * This creates the form fields for the selected @type
-	 */
-	public function getMetaConfigForType(string $type): array
-	{
-		$fields = $this->getFieldsForType($type);
-		$config = [];
-
-		foreach ($fields as $fieldName) {
-			$fieldDef = $this->getFieldDefinition($fieldName);
-			if ($fieldDef) {
-				// Use the field name as the key (this IS the schema property)
-				$config[$fieldName] = $fieldDef;
-			}
-		}
-
-		return $config;
-	}
-
-	/**
-	 * Get types organized by group for UI display
-	 */
-	public function getTypesByGroup(): array
-	{
-		$types = $this->getTypeDefinitions();
-		$grouped = [];
-
-		foreach ($types as $typeName => $config) {
-			$group = $config['group'] ?? 'general';
-
-			if (!isset($grouped[$group])) {
-				$grouped[$group] = [
-					'label' => $this->typeGroups[$group] ?? ucfirst($group),
-					'types' => []
-				];
-			}
-
-			$grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName;
-		}
-
-		return $grouped;
-	}
-
-	/**
-	 * Register all field definitions
-	 * Array key = schema property name
-	 */
-	private function registerFieldDefinitions(): void
-	{
-		$this->fieldDefinitions = [
-			'type'	=> [
-				'type' => 'select',
-				'label' => 'Type',
-				'options' => array_merge(['' => '-- Content Type'], $this->schemaTypes)
-			],
-			/**************************************************************
-			META FIELDS
-			 **************************************************************/
-			'metaTitle' => [
-				'type' => 'text',
-				'label' => 'Meta Title',
-				'hint' => 'Used in search results and when shared on social media. Leave blank to use default.',
-				'default' => '{{post_title}} | {{site_name}}'
-			],
-			'metaDescription' => [
-				'type' => 'textarea',
-				'label' => 'Meta Description',
-				'hint' => 'Brief description shown in search results and social previews.',
-				'default' => '{{post_excerpt}}',
-				'rows' => 3
-			],
-			'socialPreviewImage' => [
-				'type' => 'upload',
-				'label' => 'Social Preview Image',
-				'hint' => 'Image shown when shared on social media. Recommended: 1200x630px.',
-				'transformer' => 'image_url'
-			],
-			'twitterImage' => [
-				'type' => 'upload',
-				'label' => 'Twitter Card Image (Optional)',
-				'hint' => 'Separate image for Twitter. Falls back to main image if empty.',
-				'transformer' => 'image_url'
-			],
-			/**************************************************************
-			 CORE IDENTITY FIELDS
-			**************************************************************/
-			'name' => [
-				'type' => 'text',
-				'label' => 'Name',
-				'description' => 'The name of the item',
-				'transformer' => 'text',
-			],
-
-			'alternateName' => [
-				'type' => 'repeater',
-				'label' => 'Alternate Name(s)',
-				'description' => 'Alternative names or nicknames',
-				'transformer' => 'text_array',
-				'fields' => [
-					'name' => [
-						'type' => 'text',
-						'label' => 'Name',
-					]
-				]
-			],
-
-			'legalName' => [
-				'type' => 'text',
-				'label' => 'Legal Name',
-				'description' => 'The official legal name',
-				'transformer' => 'text',
-			],
-
-			'description' => [
-				'type' => 'textarea',
-				'label' => 'Description',
-				'description' => 'A description of the item',
-				'transformer' => 'text',
-			],
-
-			'disambiguatingDescription' => [
-				'type' => 'textarea',
-				'label' => 'Disambiguating Description',
-				'description' => 'Brief clarification to distinguish from similar items',
-				'transformer' => 'text',
-			],
-
-			'url' => [
-				'type' => 'url',
-				'label' => 'URL',
-				'description' => 'Website URL',
-				'transformer' => 'url',
-			],
-
-			'slogan' => [
-				'type' => 'text',
-				'label' => 'Slogan',
-				'description' => 'A slogan or tagline',
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			Before/After
-			 **************************************************************/
-			'about' => [
-				'type' => 'reference',
-				'label' => 'About (Service/Topic)',
-				'transformer' => 'reference',
-			],
-
-			'temporalCoverage' => [
-				'type' => 'text',
-				'label' => 'Time Period',
-				'description' => 'ISO 8601 format: 2024-01-10/2024-09-01',
-				'transformer' => 'text',
-			],
-
-			'associatedMedia' => [
-				'type' => 'repeater',
-				'label' => 'Associated Media',
-				'transformer' => 'image_object_array',
-				'fields' => [
-					'image' => ['type' => 'image', 'label' => 'Image'],
-					'caption' => ['type' => 'text', 'label' => 'Caption'],
-					'position' => ['type' => 'number', 'label' => 'Position'],
-				]
-			],
-
-			'additionalProperty' => [
-				'type' => 'repeater',
-				'label' => 'Additional Properties',
-				'transformer' => 'property_value_array',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Property Name'],
-					'value' => ['type' => 'text', 'label' => 'Value'],
-				]
-			],
-			/**************************************************************
-			 IMAGE FIELDS
-			**************************************************************/
-			'image' => [
-				'type' => 'image',
-				'label' => 'Image',
-				'description' => 'Primary image',
-				'transformer' => 'image_object',
-			],
-
-			'logo' => [
-				'type' => 'upload',
-				'label' => 'Logo',
-				'transformer' => 'image_object',
-			],
-
-			'photo' => [
-				'type' => 'upload',
-				'label' => 'Photo of Location',
-				'transformer' => 'image_object',
-			],
-
-			/**************************************************************
-			 LOCATION & CONTACT FIELDS
-			**************************************************************/
-			'location' => [
-				'type' => 'location',
-				'label' => 'Location',
-				'description' => 'Physical location with address and coordinates',
-				'transformer' => 'location_complex', // Returns array with 'address' and 'geo'
-			],
-
-			'address' => [
-				'type' => 'location',
-				'label' => 'Address',
-				'description' => 'Postal address',
-				'transformer' => 'postal_address',
-			],
-
-			'geo' => [
-				'type' => 'group',
-				'label' => 'Geographic Coordinates',
-				'description' => 'Latitude and longitude',
-				'transformer' => 'geo_coordinates',
-				'fields' => [
-					'latitude' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Latitude',
-					],
-					'longitude' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Longitude',
-					]
-				]
-			],
-
-			'telephone' => [
-				'type' => 'text',
-				'subtype'=> 'tel',
-				'label' => 'Telephone',
-				'description' => 'Phone number',
-				'transformer' => 'text',
-			],
-
-			'faxNumber' => [
-				'type' => 'text',
-				'subtype'=> 'tel',
-				'label' => 'Fax Number',
-				'transformer' => 'text',
-			],
-
-			'email' => [
-				'type' => 'email',
-				'label' => 'Email',
-				'description' => 'Email address',
-				'transformer' => 'email',
-			],
-
-			'contactPoint' => [
-				'type' => 'repeater',
-				'label' => 'Contact Points',
-				'description' => 'Additional contact methods',
-				'transformer' => 'contact_point_array',
-				'fields' => [
-					'contactType' => [
-						'type' => 'text',
-						'label' => 'Contact Type',
-						'description' => 'e.g., customer service, sales',
-					],
-					'telephone' => [
-						'type' => 'text',
-						'label' => 'Phone',
-					],
-					'email' => [
-						'type' => 'email',
-						'label' => 'Email',
-					]
-				]
-			],
-
-			'potentialAction' => [
-				'type'	=> 'repeater',
-				'label'	=> 'Potential Actions',
-				'fields'	=> [
-					'action' => [
-						'type'	=> 'radio',
-						'label'	=> 'Action',
-						'options'	=> [
-							'searchAction'	=> 'Search Action',
-							'communicateAction' => 'Contact Action',
-							'scheduleAction'	=> 'Reserve Action',
-							'applyAction'		=> 'Estimate Action'
-						]
-					],
-					'name'	=> [
-						'type'	=> 'text',
-						'label' => 'Name',
-					],
-					'target'	=> [
-						'type'	=> 'url',
-						'label'	=> 'Action URL',
-					],
-					'description'	=> [
-						'type'	=> 'textarea',
-						'label'	=> 'Description'
-					]
-				],
-				'default'	=> [
-					[
-						'action'	=> 'searchAction',
-						'target'	=> get_home_url(null,'/search/?s={query}')
-					]
-				],
-				'transformer' 	=> 'potential_action_array'
-			],
-
-			/**************************************************************
-			 HOURS & OPERATIONAL FIELDS
-			**************************************************************/
-			'openingHours' => [
-				'type' => 'group',
-				'label' => 'Opening Hours',
-				'description' => 'Business hours specification',
-				'transformer' => 'opening_hours_specification',
-				'fields' => [
-					'monday' => [
-						'type' => 'group',
-						'label' => 'Monday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'tuesday' =>  [
-						'type' => 'group',
-						'label' => 'Tuesday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'wednesday' =>  [
-						'type' => 'group',
-						'label' => 'Wednesday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'thursday' =>  [
-						'type' => 'group',
-						'label' => 'Thursday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'friday' =>  [
-						'type' => 'group',
-						'label' => 'Friday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'saturday' =>  [
-						'type' => 'group',
-						'label' => 'Saturday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-					'sunday' =>  [
-						'type' => 'group',
-						'label' => 'Sunday',
-						'fields'	=> [
-							'opens' => [
-								'type'	=> 'time',
-								'label'	=> 'Opens'
-							],
-							'closes' => [
-								'type'	=> 'time',
-								'label'	=> 'Closes'
-							]
-						]
-					],
-				]
-			],
-			'hasPart' => [
-				'type' => 'repeater',
-				'label' => 'Site Navigation',
-				'description' => 'Main navigation menu items',
-				'transformer' => 'navigation_array',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Link Text'],
-					'url' => ['type' => 'url', 'label' => 'URL'],
-					'description' => ['type' => 'textarea', 'label' => 'Description (optional)'],
-				]
-			],
-
-			'priceRange' => [
-				'type' => 'text',
-				'label' => 'Price Range',
-				'description' => 'e.g., $$, $100-$500',
-				'transformer' => 'text',
-			],
-
-			'currenciesAccepted' => [
-				'type' => 'checkbox',
-				'label' => 'Currencies Accepted',
-				'options'	=> [
-					'CAD'	=> 'CAD',
-					'USD'	=> 'USD',
-				],
-				'transformer' => 'text',
-			],
-
-			'paymentAccepted' => [
-				'type' => 'checkbox',
-				'label' => 'Payment Methods',
-				'options' => [
-					'Cash'        => 'Cash',
-					'Credit Card' => 'Credit Card',
-					'Debit'       => 'Debit',
-					'Google Pay'  => 'Google Pay',
-					'Apple Pay'   => 'Apple Pay',
-					'PayPal'      => 'PayPal',
-					'Interac'     => 'Interac',
-					'AMEX'        => 'AMEX',
-				],
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 ORGANIZATION & BUSINESS FIELDS
-			**************************************************************/
-			'foundingDate' => [
-				'type' => 'date',
-				'label' => 'Founding Date',
-				'description' => 'Date the organization was founded',
-				'transformer' => 'date',
-			],
-
-			'dissolutionDate' => [
-				'type' => 'date',
-				'label' => 'Dissolution Date',
-				'description' => 'Date the organization closed',
-				'transformer' => 'date',
-			],
-
-			'founders' => [
-				'type' => 'repeater',
-				'label' => 'Founders',
-				'description' => 'Name of founder(s)',
-				'fields'	=> [
-					'name'	=> [
-						'type'	=> 'text',
-						'label'	=> 'Name',
-					],
-					'url'	=> [
-						'type'	=> 'url',
-						'label'	=> 'URL',
-					]
-				],
-				'transformer' => 'founders',
-			],
-
-			'numberOfEmployees' => [
-				'type' => 'text',
-				'subtype' => 'number',
-				'label' => 'Number of Employees',
-				'transformer' => 'number',
-			],
-
-			'taxID' => [
-				'type' => 'text',
-				'label' => 'Tax ID',
-				'description' => 'Tax identification number',
-				'transformer' => 'text',
-			],
-
-			'vatID' => [
-				'type' => 'text',
-				'label' => 'VAT ID',
-				'description' => 'VAT registration number',
-				'transformer' => 'text',
-			],
-
-			'duns' => [
-				'type' => 'text',
-				'label' => 'D-U-N-S Number',
-				'description' => 'Dun & Bradstreet number',
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 SOCIAL & LINKS
-			**************************************************************/
-			'sameAs' => [
-				'type' => 'repeater',
-				'label' => 'Social Media & Links',
-				'description' => 'URLs to social profiles and related pages',
-				'transformer' => 'url_array',
-				'fields' => [
-					'url' => [
-						'type' => 'url',
-						'label' => 'URL',
-					]
-				]
-			],
-
-			/**************************************************************
-			 AREA & GEOGRAPHY
-			**************************************************************/
-			'areaServed' => [
-				'type' => 'repeater',
-				'label' => 'Area Served',
-				'description' => 'Geographic areas served',
-				'transformer' => 'text_array',
-				'fields' => [
-					'name' => [
-						'type' => 'text',
-						'label' => 'Location Name',
-					],
-					'url'	=> [
-						'type'	=> 'url',
-						'label'	=> 'Wikipedia Page',
-					]
-				]
-			],
-
-			'hasMap' => [
-				'type' => 'url',
-				'label' => 'Map URL',
-				'description' => 'Link to a map (e.g., Google Maps)',
-				'transformer' => 'url',
-			],
-
-			/**************************************************************
-			 AMENITIES & FEATURES
-			**************************************************************/
-			'amenityFeature' => [
-				'type' => 'checkbox',
-				'label' => 'Amenity Features',
-				'description' => 'Available facilities and features',
-				'transformer' => 'text',
-				'options' => [
-					'Wheelchair Accessible'        => 'Wheelchair Accessible',
-					'Free Parking'                 => 'Free Parking',
-					'Private Rooms'                => 'Private Rooms',
-					'Air Conditioning'             => 'Air Conditioning',
-					'WiFi'                         => 'WiFi',
-					'Gender Neutral Restroom'      => 'Gender Neutral Restroom',
-					'LGBTQ+ Friendly'              => 'LGBTQ+ Friendly',
-					'Sterilization Room'           => 'Sterilization Room',
-					'Refreshments Available'       => 'Refreshments Available',
-					'Street Level Access'          => 'Street Level Access',
-					'Single Use Needles'           => 'Single Use Needles',
-					'Consultation Room'            => 'Consultation Room',
-					'Aftercare Products Available' => 'Aftercare Products Available',
-					'Walk-Ins Welcome'             => 'Walk-Ins Welcome',
-					'By Appointment'				=> 'By Appointment Only',
-				],
-			],
-
-			/**************************************************************
-			 LANGUAGES
-			**************************************************************/
-			'availableLanguage' => [
-				'type' => 'repeater',
-				'label' => 'Languages Available',
-				'description' => 'Languages spoken or supported',
-				'transformer' => 'language_array',
-				'fields' => [
-					'language' => [
-						'type' => 'text',
-						'label' => 'Language',
-					]
-				]
-			],
-
-			'knowsLanguage' => [
-				'type' => 'repeater',
-				'label' => 'Languages Known',
-				'description' => 'Languages the person knows',
-				'transformer' => 'language_array',
-				'fields' => [
-					'language' => [
-						'type' => 'text',
-						'label' => 'Language',
-					]
-				]
-			],
-
-			'inLanguage' => [
-				'type' => 'radio',
-				'label' => 'In Language',
-				'options'	=> [
-					'en-CA'	=> 'English, Canadian',
-					'en-US' => 'English, American',
-					'fr-CA'	=> 'French, Canadian'
-				],
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 RATINGS & REVIEWS
-			**************************************************************/
-			'aggregateRating' => [
-				'type' => 'group',
-				'label' => 'Aggregate Rating',
-				'description' => 'Overall rating and review count',
-				'transformer' => 'aggregate_rating',
-				'fields' => [
-					'ratingValue' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Rating Value',
-						'description' => 'Average rating (e.g., 4.5)',
-					],
-					'bestRating' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Best Rating',
-						'default'	=> 5,
-						'description' => 'Highest possible rating (e.g., 5)',
-					],
-					'worstRating' => [
-						'default'	=> 1,
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Worst Rating',
-						'description' => 'Lowest possible rating (e.g., 1)',
-					],
-					'ratingCount' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Rating Count',
-						'description' => 'Total number of ratings',
-					],
-					'reviewCount' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Review Count',
-						'description' => 'Total number of reviews',
-					]
-				]
-			],
-
-			/**************************************************************
-			 KEYWORDS & CATEGORIZATION
-			**************************************************************/
-			'keywords' => [
-				'type' => 'repeater',
-				'label' => 'Keywords',
-				'description' => 'Keywords or tags',
-				'transformer' => 'text_array',
-				'fields' => [
-					'keyword' => [
-						'type' => 'text',
-						'label' => 'Keyword',
-					]
-				]
-			],
-
-			/**************************************************************
-			 PERSON FIELDS
-			**************************************************************/
-			'givenName' => [
-				'type' => 'text',
-				'label' => 'First Name',
-				'transformer' => 'text',
-			],
-
-			'familyName' => [
-				'type' => 'text',
-				'label' => 'Last Name',
-				'transformer' => 'text',
-			],
-
-			'honorificPrefix' => [
-				'type' => 'text',
-				'label' => 'Honorific Prefix',
-				'description' => 'e.g., Dr., Mr., Ms.',
-				'transformer' => 'text',
-			],
-
-			'honorificSuffix' => [
-				'type' => 'text',
-				'label' => 'Honorific Suffix',
-				'description' => 'e.g., PhD, MD',
-				'transformer' => 'text',
-			],
-
-			'jobTitle' => [
-				'type' => 'text',
-				'label' => 'Job Title',
-				'transformer' => 'text',
-			],
-
-			'birthDate' => [
-				'type' => 'date',
-				'label' => 'Birth Date',
-				'description' => 'For public figures',
-				'transformer' => 'date',
-			],
-
-			'gender' => [
-				'type' => 'text',
-				'label' => 'Gender',
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 CREATIVE WORK FIELDS
-			**************************************************************/
-			'author' => [
-				'type' => 'text',
-				'label' => 'Author',
-				'description' => 'Author name or reference',
-				'transformer' => 'text',
-			],
-
-			'creator' => [
-				'type' => 'text',
-				'label' => 'Creator',
-				'description' => 'Creator name or reference',
-				'transformer' => 'text',
-			],
-
-			'dateCreated' => [
-				'type' => 'date',
-				'label' => 'Date Created',
-				'transformer' => 'date',
-			],
-
-			'datePublished' => [
-				'type' => 'date',
-				'label' => 'Date Published',
-				'transformer' => 'date',
-			],
-
-			'dateModified' => [
-				'type' => 'date',
-				'label' => 'Date Modified',
-				'transformer' => 'date',
-			],
-
-			/**************************************************************
-			 VISUAL ARTWORK FIELDS
-			**************************************************************/
-			'artform' => [
-				'type' => 'text',
-				'label' => 'Art Form',
-				'description' => 'e.g., Painting, Sculpture, Tattoo',
-				'transformer' => 'text',
-			],
-
-			'artMedium' => [
-				'type' => 'text',
-				'label' => 'Art Medium',
-				'description' => 'e.g., Oil, Watercolor, Ink',
-				'transformer' => 'text',
-			],
-
-			'artworkSurface' => [
-				'type' => 'text',
-				'label' => 'Artwork Surface',
-				'description' => 'e.g., Canvas, Paper, Skin',
-				'transformer' => 'text',
-			],
-
-			'width' => [
-				'type' => 'text',
-				'label' => 'Width',
-				'description' => 'Width with unit (e.g., 10cm, 5in)',
-				'transformer' => 'dimension',
-			],
-
-			'height' => [
-				'type' => 'text',
-				'label' => 'Height',
-				'description' => 'Height with unit (e.g., 15cm, 8in)',
-				'transformer' => 'dimension',
-			],
-
-			/**************************************************************
-			 EVENT FIELDS
-			**************************************************************/
-			'startDate' => [
-				'type' => 'datetime',
-				'label' => 'Start Date/Time',
-				'transformer' => 'datetime',
-			],
-
-			'endDate' => [
-				'type' => 'datetime',
-				'label' => 'End Date/Time',
-				'transformer' => 'datetime',
-			],
-
-			'eventStatus' => [
-				'type' => 'select',
-				'label' => 'Event Status',
-				'options' => [
-					'https://schema.org/EventScheduled' => 'Scheduled',
-					'https://schema.org/EventCancelled' => 'Cancelled',
-					'https://schema.org/EventPostponed' => 'Postponed',
-					'https://schema.org/EventRescheduled' => 'Rescheduled',
-				],
-				'transformer' => 'text',
-			],
-
-			'eventAttendanceMode' => [
-				'type' => 'select',
-				'label' => 'Attendance Mode',
-				'options' => [
-					'https://schema.org/OfflineEventAttendanceMode' => 'In-Person',
-					'https://schema.org/OnlineEventAttendanceMode' => 'Online',
-					'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid',
-				],
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 PRODUCT FIELDS
-			**************************************************************/
-			'brand' => [
-				'type' => 'group',
-				'label' => 'Brand',
-				'transformer' => 'brand_object',
-				'fields' => [
-					'type' => [
-						'type' => 'select',
-						'label' => 'Brand Type',
-						'options' => [
-							'text' => 'Text Only',
-							'organization' => 'Organization/Brand',
-						]
-					],
-					'name' => [
-						'type' => 'text',
-						'label' => 'Brand Name',
-					],
-					'url' => [
-						'type' => 'url',
-						'label' => 'Brand Website',
-						'condition' => [
-							'field' => 'type',
-							'value' => 'organization'
-						]
-					],
-					'logo' => [
-						'type' => 'upload',
-						'label' => 'Brand Logo',
-						'condition' => [
-							'field' => 'type',
-							'value' => 'organization'
-						]
-					],
-				]
-			],
-
-			'sku' => [
-				'type' => 'text',
-				'label' => 'SKU',
-				'description' => 'Stock Keeping Unit',
-				'transformer' => 'text',
-			],
-
-			'gtin' => [
-				'type' => 'text',
-				'label' => 'GTIN',
-				'description' => 'Global Trade Item Number',
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 SERVICES & OFFERS
-			**************************************************************/
-			'hasOfferCatalog' => [
-				'type' => 'group',
-				'label' => 'Offer Catalog',
-				'transformer' => 'offer_catalog_from_posts',
-				'fields' => [
-					'source' => [
-						'type' => 'select',
-						'label' => 'Source',
-						'options' => [
-							'auto' => 'Auto from post type',
-							'manual' => 'Manual entry',
-						]
-					],
-					'post_type' => [
-						'type' => 'select',
-						'label' => 'Post Type',
-						'options' => $this->getContentPostTypes(),
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'group_by_taxonomy' => [
-						'type' => 'true_false',
-						'label' => 'Group by category/taxonomy',
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'taxonomy' => [
-						'type' => 'select',
-						'label' => 'Taxonomy',
-						'options' => $this->getContentTaxonomies(),
-						'condition' => [
-							'field' => 'group_by_taxonomy',
-							'value' => '1' // or '1' depending on how checkbox stores
-						]
-					]
-				]
-			],
-
-			'knowsAbout' => [
-				'type' => 'repeater',
-				'label' => 'Areas of Expertise',
-				'description' => 'Skills and specialties',
-				'transformer' => 'text_array',
-				'fields' => [
-					'topic' => [
-						'type' => 'text',
-						'label' => 'Topic',
-					]
-				]
-			],
-
-			/**************************************************************
-			 CREDENTIALS & CERTIFICATIONS
-			**************************************************************/
-			'hasCredential' => [
-				'type' => 'repeater',
-				'label' => 'Credentials / Certifications',
-				'description' => 'Professional certifications',
-				'transformer' => 'credential_array',
-				'fields' => [
-					'credentialCategory' => [
-						'type' => 'text',
-						'label' => 'Category',
-					],
-					'name' => [
-						'type' => 'text',
-						'label' => 'Name',
-					],
-					'issuedBy' => [
-						'type' => 'text',
-						'label' => 'Issued By',
-					]
-				]
-			],
-
-			'award' => [
-				'type' => 'repeater',
-				'label' => 'Awards & Recognition',
-				'transformer' => 'text_array',
-				'fields' => [
-					'award' => ['type' => 'text', 'label' => 'Award'],
-				]
-			],
-			'serviceArea' => [
-				'type' => 'repeater',
-				'label' => 'Service Areas',
-				'description' => 'Geographic areas served (cities, neighborhoods, or radius)',
-				'transformer' => 'service_area_array',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Area Name'],
-					'type' => [
-						'type' => 'select',
-						'label' => 'Type',
-						'options' => [
-							'City' => 'City',
-							'AdministrativeArea' => 'Region/Province',
-							'GeoCircle' => 'Radius',
-						]
-					],
-					'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'],
-				]
-			],
-
-			// Specialties
-			'makesOffer' => [
-				'type' => 'group',
-				'label' => 'Featured Offerings',
-				'transformer' => 'offers_from_posts',
-				'fields' => [
-					'source' => [
-						'type' => 'select',
-						'label' => 'Source',
-						'options' => [
-							'auto' => 'Auto from post type',
-							'manual' => 'Manual entry',
-						]
-					],
-					'post_type' => [
-						'type' => 'select',
-						'label' => 'Post Type',
-						'options' => $this->getContentPostTypes(),
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'limit' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Featured Count',
-						'default' => 5,
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'manual_items' => [
-						'type' => 'repeater',
-						'label' => 'Manual Offers',
-						'condition' => [
-							'field' => 'source',
-							'value' => 'manual'
-						],
-						'fields' => [
-							'name' => ['type' => 'text', 'label' => 'Offer Name'],
-							'description' => ['type' => 'textarea', 'label' => 'Description'],
-							'price' => ['type' => 'text', 'label' => 'Price/Range'],
-						]
-					]
-				]
-			],
-
-			'hasMenu' => [
-				'type' => 'group',
-				'label' => 'Menu Items',
-				'description' => 'Auto-populate from post type or enter manually',
-				'transformer' => 'menu_from_posts',
-				'fields' => [
-					'source' => [
-						'type' => 'select',
-						'label' => 'Source',
-						'options' => [
-							'auto' => 'Auto from post type',
-							'manual' => 'Manual entry',
-						],
-					],
-					'post_type' => [
-						'type' => 'select',
-						'label' => 'Post Type',
-						'options' => $this->getContentPostTypes(), // Dynamic callback
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto',
-							'operator' => '=='
-						]
-					],
-					'limit' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Number of items',
-						'default' => 10,
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'orderby' => [
-						'type' => 'select',
-						'label' => 'Order By',
-						'options' => [
-							'menu_order' => 'Menu Order',
-							'title' => 'Title',
-							'date' => 'Date',
-						],
-						'condition' => [
-							'field' => 'source',
-							'value' => 'auto'
-						]
-					],
-					'manual_items' => [
-						'type' => 'repeater',
-						'label' => 'Manual Items',
-						'condition' => [
-							'field' => 'source',
-							'value' => 'manual'
-						],
-						'fields' => [
-							'name' => ['type' => 'text', 'label' => 'Item Name'],
-							'description' => ['type' => 'textarea', 'label' => 'Description'],
-							'price' => ['type' => 'text', 'label' => 'Price'],
-						]
-					]
-				]
-			],
-
-			/**************************************************************
-			 FAQ FIELDS
-			**************************************************************/
-			'mainEntity' => [
-				'type' => 'repeater',
-				'label' => 'FAQ Items',
-				'description' => 'Question and Answer pairs',
-				'transformer' => 'faq_array',
-				'fields' => [
-					'question' => [
-						'type' => 'text',
-						'label' => 'Question',
-					],
-					'answer' => [
-						'type' => 'text',
-						'label' => 'Answer',
-					]
-				]
-			],
-			/**************************************************************
-			 FOOD & CUISINE
-			**************************************************************/
-			'servesCuisine' => [
-				'type' => 'repeater',
-				'label' => 'Cuisine Types',
-				'description' => 'Types of cuisine served',
-				'transformer' => 'text_array',
-				'fields' => [
-					'cuisine' => [
-						'type' => 'text',
-						'label' => 'Cuisine Type',
-						'description' => 'e.g., Italian, Mexican, Vegan'
-					]
-				]
-			],
-
-			'menu' => [
-				'type' => 'url',
-				'label' => 'Menu URL',
-				'description' => 'Link to online menu',
-				'transformer' => 'url',
-			],
-
-			/**************************************************************
-			 PRODUCT/OFFER FIELDS
-			**************************************************************/
-			'offers' => [
-				'type' => 'group',
-				'label' => 'Offer Details',
-				'description' => 'Price and availability information',
-				'transformer' => 'offer_object',
-				'fields' => [
-					'price' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Price',
-					],
-					'priceCurrency' => [
-						'type' => 'text',
-						'label' => 'Currency',
-						'default' => 'USD',
-					],
-					'availability' => [
-						'type' => 'select',
-						'label' => 'Availability',
-						'options' => [
-							'InStock' => 'In Stock',
-							'PreOrder' => 'Pre-Order',
-							'SoldOut' => 'Sold Out',
-							'OutOfStock' => 'Out of Stock',
-							'Discontinued' => 'Discontinued',
-						]
-					],
-					'validFrom' => [
-						'type' => 'date',
-						'label' => 'Valid From',
-					],
-					'validThrough' => [
-						'type' => 'date',
-						'label' => 'Valid Through',
-					],
-				]
-			],
-
-			'mpn' => [
-				'type' => 'text',
-				'label' => 'Manufacturer Part Number',
-				'transformer' => 'text',
-			],
-
-			/**************************************************************
-			 BUSINESS POLICIES & FEATURES
-			**************************************************************/
-			'isAccessibleForFree' => [
-				'type' => 'true_false',
-				'label' => 'Accessible For Free',
-				'description' => 'Is this service/location accessible without payment?',
-				'transformer' => 'boolean',
-			],
-
-			'smokingAllowed' => [
-				'type' => 'true_false',
-				'label' => 'Smoking Allowed',
-				'transformer' => 'boolean',
-			],
-
-			'petsAllowed' => [
-				'type' => 'select',
-				'label' => 'Pets Allowed',
-				'options' => [
-					'' => 'Not specified',
-					'yes' => 'Yes',
-					'no' => 'No',
-				],
-				'transformer' => 'boolean',
-			],
-
-			/**************************************************************
-			 ORGANIZATION RELATIONSHIPS
-			**************************************************************/
-			'parentOrganization' => [
-				'type' => 'group',
-				'label' => 'Parent Organization',
-				'description' => 'Organization this is a part of',
-				'transformer' => 'organization_reference',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Organization Name'],
-					'url' => ['type' => 'url', 'label' => 'Website'],
-				]
-			],
-
-			'subOrganization' => [
-				'type' => 'repeater',
-				'label' => 'Sub-Organizations',
-				'description' => 'Child organizations or departments',
-				'transformer' => 'organization_reference_array',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Organization Name'],
-					'url' => ['type' => 'url', 'label' => 'Website'],
-				]
-			],
-
-			'employee' => [
-				'type' => 'repeater',
-				'label' => 'Employees',
-				'transformer' => 'person_reference_array',
-				'fields' => [
-					'name' => ['type' => 'text', 'label' => 'Name'],
-					'jobTitle' => ['type' => 'text', 'label' => 'Job Title'],
-				]
-			],
-
-			/**************************************************************
-			 HOSPITALITY (for hotels, etc.)
-			**************************************************************/
-			'checkinTime' => [
-				'type' => 'time',
-				'label' => 'Check-in Time',
-				'transformer' => 'time',
-			],
-
-			'checkoutTime' => [
-				'type' => 'time',
-				'label' => 'Check-out Time',
-				'transformer' => 'time',
-			],
-
-			'starRating' => [
-				'type' => 'group',
-				'label' => 'Star Rating',
-				'transformer' => 'rating_object',
-				'fields' => [
-					'ratingValue' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Rating',
-						'min' => 1,
-						'max' => 5,
-					],
-				]
-			],
-
-			/**************************************************************
-			 REVIEW & RATING
-			**************************************************************/
-			'review' => [
-				'type' => 'repeater',
-				'label' => 'Reviews',
-				'transformer' => 'review_array',
-				'fields' => [
-					'author' => ['type' => 'text', 'label' => 'Reviewer Name'],
-					'reviewRating' => [
-						'type' => 'text',
-						'subtype' => 'number',
-						'label' => 'Rating',
-						'min' => 1,
-						'max' => 5,
-					],
-					'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'],
-					'datePublished' => ['type' => 'date', 'label' => 'Date'],
-				]
-			],
-
-			/**************************************************************
-			 HEALTH & MEDICAL
-			**************************************************************/
-			'medicalSpecialty' => [
-				'type' => 'repeater',
-				'label' => 'Medical Specialties',
-				'transformer' => 'text_array',
-				'fields' => [
-					'specialty' => ['type' => 'text', 'label' => 'Specialty']
-				]
-			],
-
-			'healthcareService' => [
-				'type' => 'repeater',
-				'label' => 'Healthcare Services',
-				'transformer' => 'text_array',
-				'fields' => [
-					'service' => ['type' => 'text', 'label' => 'Service']
-				]
-			],
-		];
-	}
-
-	/**
-	 * Register all type definitions
-	 * Each type lists the fields it uses
-	 */
-	private function registerTypeDefinitions(): void
-	{
-		$this->typeDefinitions = [
-			/**************************************************************
-			 GENERAL / SITE-WIDE
-			**************************************************************/
-			'WebSite' => [
-				'label' => 'Website',
-				'group' => 'general',
-				'fields' => [
-					'name',
-					'description',
-					'url',
-					'inLanguage',
-					'potentialAction',
-					'hasPart',
-					'creator',
-				],
-			],
-
-			/**************************************************************
-			 PAGE TYPES
-			**************************************************************/
-			'WebPage' => [
-				'label' => 'Web Page',
-				'group' => 'page',
-				'fields' => [
-					'name',
-					'description',
-					'url',
-					'image',
-					'datePublished',
-					'dateModified',
-					'author',
-				],
-			],
-
-			'CollectionPage' => [
-				'label' => 'Collection Page',
-				'group' => 'page',
-				'extends' => 'WebPage',
-			],
-
-			'FAQPage' => [
-				'label' => 'FAQ Page',
-				'group' => 'page',
-				'extends' => 'WebPage',
-				'fields' => [
-					'mainEntity', // FAQ items
-				],
-			],
-
-			/**************************************************************
-			 ORGANIZATION & BUSINESS
-			**************************************************************/
-			'Organization' => [
-				'label' => 'Organization',
-				'group' => 'business',
-				'fields' => [
-					'name',
-					'legalName',
-					'alternateName',
-					'description',
-					'url',
-					'logo',
-					'image',
-					'email',
-					'telephone',
-					'sameAs',
-					'founders',
-					'foundingDate',
-					'numberOfEmployees',
-					'taxID',
-					'vatID',
-					'duns',
-					'slogan',
-					'disambiguatingDescription',
-				],
-			],
-
-
-
-			'LocalBusiness' => [
-				'label' => 'Local Business',
-				'group' => 'business',
-				'extends' => 'Organization',
-				'fields' => [
-					'location',
-					'openingHours',
-					'priceRange',
-					'currenciesAccepted',
-					'paymentAccepted',
-					'serviceArea',
-					'areaServed',
-					'hasMap',
-					'amenityFeature',
-					'availableLanguage',
-					'hasOfferCatalog',
-					'makesOffer',
-					'hasMenu',
-					'knowsAbout',
-					'hasCredential',
-					'aggregateRating',
-					'award',
-				],
-			],
-
-			'TattooParlor' => [
-				'label' => 'Tattoo Parlor',
-				'group' => 'business',
-				'extends' => 'LocalBusiness',
-				'fields' => [
-					'makesOffer',           // Tattoo styles/services
-					'hasOfferCatalog',      // Portfolio as catalog
-					'award',
-				],
-			],
-
-			'HealthBusiness' => [
-				'label' => 'Health Business',
-				'group' => 'business',
-				'extends' => 'LocalBusiness',
-				'description' => 'Healthcare providers',
-			],
-
-			'FoodEstablishment' => [
-				'label' => 'Food Establishment',
-				'group' => 'business',
-				'extends' => 'LocalBusiness',
-				'fields' => [
-					'hasMenu',
-					'servesCuisine',
-				],
-			],
-			'FoodTruck' => [
-				'label' => 'Food Truck',
-				'group' => 'business',
-				'extends' => 'FoodEstablishment',
-				'fields' => [
-					'serviceArea',
-				],
-			],
-
-			'Store' => [
-				'label' => 'Store / Shop',
-				'group' => 'business',
-				'extends' => 'LocalBusiness',
-				'fields' => [
-					'hasOfferCatalog',
-					'makesOffer',
-				],
-			],
-
-			'ProfessionalService' => [
-				'label' => 'Professional Service',
-				'group' => 'business',
-				'extends' => 'LocalBusiness',
-				'fields' => [
-					'serviceArea',          // Where they operate
-					'makesOffer',           // Services offered
-					'award',                // Professional recognition
-				],
-			],
-
-			/**************************************************************
-			 PERSON
-			**************************************************************/
-			'Person' => [
-				'label' => 'Person',
-				'group' => 'person',
-				'fields' => [
-					'name',
-					'givenName',
-					'familyName',
-					'honorificPrefix',
-					'honorificSuffix',
-					'alternateName',
-					'description',
-					'image',
-					'url',
-					'email',
-					'telephone',
-					'sameAs',
-					'jobTitle',
-					'knowsLanguage',
-					'birthDate',
-					'gender',
-				],
-			],
-
-			/**************************************************************
-			 CREATIVE WORKS
-			**************************************************************/
-			'CreativeWork' => [
-				'label' => 'Creative Work',
-				'group' => 'creative',
-				'fields' => [
-					'name',
-					'description',
-					'image',
-					'author',
-					'creator',
-					'dateCreated',
-					'datePublished',
-					'dateModified',
-					'keywords',
-				],
-			],
-
-			'DefinedTermSet'	=> [
-				'label'	=> 'Defined Term',
-				'group'	=> 'creative',
-				'extends'	=> 'CreativeWork',
-				'fields'	=> [
-					'DefinedTerm',
-				]
-			],
-
-			'BeforeAfter' => [
-				'label' => 'Before & After Case',
-				'group' => 'creative',
-				'extends' => 'CreativeWork',
-				'fields' => [
-					'about',              // Service (Laser Tattoo Removal)
-					'temporalCoverage',   // Treatment period
-					'hasPart',            // Individual images (as references)
-					'associatedMedia',    // Alternative to hasPart
-					'additionalProperty', // Sessions, treatment area
-				],
-			],
-
-			'VisualArtwork' => [
-				'label' => 'Visual Artwork',
-				'group' => 'creative',
-				'extends' => 'CreativeWork',
-				'fields' => [
-					'artform',
-					'artMedium',
-					'artworkSurface',
-					'width',
-					'height',
-				],
-			],
-
-			'Tattoo' => [
-				'label' => 'Tattoo',
-				'group' => 'creative',
-				'extends' => 'VisualArtwork',
-				'description' => 'A tattoo artwork (custom extension)',
-			],
-
-			'Product' => [
-				'label' => 'Product',
-				'group' => 'creative',
-				'fields' => [
-					'name',
-					'description',
-					'image',
-					'brand',
-					'sku',
-					'gtin',
-					'offers',               // Price, availability
-					'aggregateRating',      // Reviews
-					'award',                // Product awards
-				],
-			],
-
-			/**************************************************************
-			 EVENTS
-			**************************************************************/
-			'Event' => [
-				'label' => 'Event',
-				'group' => 'event',
-				'fields' => [
-					'name',
-					'description',
-					'image',
-					'startDate',
-					'endDate',
-					'location',
-					'eventStatus',
-					'eventAttendanceMode',
-				],
-			],
-		];
-	}
-
-	/**
-	 * Register type groups for UI organization
-	 */
-	private function registerTypeGroups(): void
-	{
-		$this->typeGroups = [
-			'general' => 'General',
-			'page' => 'Page Types',
-			'business' => 'Business & Organization',
-			'person' => 'People',
-			'creative' => 'Creative Works',
-			'event' => 'Events',
-		];
-	}
-
-	/**
-	 * Register a custom field definition
-	 */
-	public function registerField(string $fieldName, array $config): void
-	{
-		$this->fieldDefinitions[$fieldName] = $config;
-	}
-
-	/**
-	 * Register a custom type definition
-	 */
-	public function registerType(string $typeName, array $config): void
-	{
-		$this->typeDefinitions[$typeName] = $config;
-	}
-
-	/**
-	 * Register a type group
-	 */
-	public function registerGroup(string $key, string $label): void
-	{
-		$this->typeGroups[$key] = $label;
-	}
-
-	/**
-	 * Get post types for select options
-	 */
-	public static function getContentPostTypes(): array
-	{
-		$options = ['' => '-- Select Post Type --'];
-
-		if (defined('JVB_CONTENT')) {
-			foreach (JVB_CONTENT as $key => $config) {
-				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
-			}
-		}
-
-		return $options;
-	}
-
-	/**
-	 * Get taxonomies for select options
-	 */
-	public static function getContentTaxonomies(): array
-	{
-		$options = ['' => '-- Select Taxonomy --'];
-
-		if (defined('JVB_TAXONOMY')) {
-			foreach (JVB_TAXONOMY as $key => $config) {
-				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
-			}
-		}
-
-		return $options;
-	}
-}
diff --git a/inc/managers/SEO/_setup.php b/inc/managers/SEO/_setup.php
index 7cb84e1..517bd54 100644
--- a/inc/managers/SEO/_setup.php
+++ b/inc/managers/SEO/_setup.php
@@ -1,6 +1,4 @@
 <?php
-
-//require(JVB_DIR . '/inc/managers/SEO/SchemaRegistry.php');
 require(JVB_DIR . '/inc/managers/SEO/FieldBuilder.php');
 require(JVB_DIR . '/inc/managers/SEO/FieldOverrideBuilder.php');
 require(JVB_DIR . '/inc/managers/SEO/TypeBuilder.php');
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index f6de609..e2b4a45 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -109,6 +109,7 @@
 		}
 
 		$output .= static::buildHint($config);
+		$output .= static::buildCharacterLimit($config);
 		$output .= static::buildDescription($name, $config);
 
 		$output .= '</div>';
@@ -179,6 +180,17 @@
 				);
 			}
 
+			protected static function buildCharacterLimit(array $config): string
+			{
+				if (empty($config['data']['limit'])) {
+					return '';
+				}
+				return sprintf(
+					'<span class="char-limit"><span class="current">0</span> / <span class="limit">%s</span></span>',
+					esc_html($config['data']['limit'])
+				);
+			}
+
 		protected static function buildLabel(string $name, array $config):string
 		{
 			if (!empty($config['label'])) {

--
Gitblit v1.10.0