From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind

---
 assets/js/min/crud.min.js              |    2 
 assets/js/concise/PopulateForm.js      |  864 +--
 assets/css/dash.min.css                |    2 
 assets/js/min/quill.min.js             |    2 
 assets/js/concise/PopulateFormOld.js   |  683 +++
 inc/managers/OperationQueue.php        |  142 
 assets/js/min/form.min.js              |    2 
 build/forms/view.asset.php             |    2 
 inc/rest/routes/ContentRoutes.php      | 1058 ++--
 assets/js/concise/SchemaManager.js     |   54 
 assets/js/concise/UtilityFunctions.js  |  160 
 inc/meta/MetaForm.php                  |   25 
 assets/js/min/populate.min.js          |    2 
 assets/js/min/queue.min.js             |    2 
 assets/js/min/tabs.min.js              |    2 
 assets/js/min/utility.min.js           |    2 
 assets/js/concise/CRUD.js              | 1095 ++--
 assets/js/min/schema.min.js            |    2 
 inc/blocks/TimelineBlock.php           |    3 
 inc/meta/MetaManager.php               |  254 
 assets/js/concise/TaxonomySelector.js  |  309 
 assets/js/min/uploader.min.js          |    2 
 assets/js/concise/Queue.js             |   41 
 inc/ui/CRUDSkeleton.php                |   20 
 assets/js/concise/FormControllerOLD.js | 2420 +++++++++++
 build/feed/view.asset.php              |    2 
 assets/js/concise/DataStore.js         |  150 
 assets/js/concise/quill.js             |    4 
 assets/js/concise/Tabs.js              |  480 +-
 assets/js/concise/TabsOLD.js           |  233 +
 src/forms/view.js                      |   27 
 assets/js/min/dataStore.min.js         |    2 
 build/feed/view.js                     |    2 
 assets/js/min/selector.min.js          |    2 
 assets/js/concise/HandleSelection.js   |    4 
 assets/js/concise/FormController.js    | 3503 ++++++-----------
 build/forms/view.js                    |    2 
 assets/js/min/handleSelection.min.js   |    2 
 inc/registry/CheckCustomTables.php     |   47 
 assets/js/concise/UploadManager.js     |  490 +-
 src/feed/view.js                       |  135 
 41 files changed, 7,333 insertions(+), 4,902 deletions(-)

diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 7bd590d..89f6350 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 5815706..e3adbea 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -10,15 +10,16 @@
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
+		this.populate = window.jvbPopulate;
 		this.cache = new window.jvbCache(this.content);
 
 		this.activeItem = null;
 		this.isTimeline = false;
-		this.items = {
-			list: new Map(),
-			grid: new Map(),
-			table: new Map()
-		}; //DOM references
+		this.isPopulating = false;
+
+		//Track Changes
+		this.changes = new Map();
+		this.items = new Map();
 
 		this.init();
 	}
@@ -26,6 +27,7 @@
 	init() {
 		this.initElements();
 		this.initListeners();
+		this.defineTemplates();
 		let cached = this.initSettings();
 		this.initStore(cached);
 		this.checkHideFilters();
@@ -34,6 +36,216 @@
 		this.initModals();
 	}
 
+	defineTemplates() {
+		const T = window.jvbTemplates;
+		const crud = this;
+
+		const baseSetup = (el, refs, data) => {
+			el.dataset.itemId = data.id;
+
+			window.prefixInput(refs.checkbox, `select-${data.id}`, true);
+			refs.checkbox.value = data.id;
+			refs.checkbox.checked = crud.selected.has(parseInt(data.id));
+			if (refs.selectLabel) refs.selectLabel.htmlFor = `select-${data.id}`;
+
+			if (refs.edit) refs.edit.dataset.id = data.id;
+			if (refs.trash) refs.trash.dataset.id = data.id;
+		};
+		const imageSetup = function(el, refs, data) {
+			if (data?.fields?.post_thumbnail) {
+				const thumbnail = data.images[data.fields.post_thumbnail] ?? {};
+				refs.img.src = thumbnail.medium??'';
+				refs.img.alt = thumbnail.alt??data.fields.post_title??'';
+			}
+
+		}
+
+		T.define('gridView', {
+			refs: {
+				img: 'img',
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				edit: '[data-action="edit"]',
+				trash: '[data-action="trash"]'
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+				imageSetup(el, refs, data);
+			}
+		});
+
+		T.define('listView', {
+			refs: {
+				img: 'img',
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				edit: '[data-action="edit"]',
+				trash: '[data-action="trash"]'
+			},
+			manyRefs: {
+				attrs: '[data-attr]',
+				fields: '[data-field]'
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+				imageSetup(el, refs, data);
+				manyRefs?.attrs?.forEach(el => {
+					const value = data[el.dataset.attr];
+					if (value && value !=='') {
+						el.textContent = value;
+					} else {
+						el.remove();
+					}
+				});
+				manyRefs?.fields?.forEach(el => {
+					const value = data.fields?.[el.dataset.field];
+					if (value && value !== '') {
+						el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
+					} else {
+						el.remove();
+					}
+				});
+			}
+		});
+
+		let tableRefs = {};
+		let tableMany = {};
+		if (this.isTimeline) {
+			tableRefs.sharedRow = 'tr.shared';
+			tableRefs.point = 'tr.timeline-point';
+		}
+		T.define('tableView', {
+			refs: {
+				checkbox: '.select-item',
+				selectLabel: 'label.select-item-label',
+				... tableRefs,
+			},
+			manyRefs: {
+				inputs: 'input,select,textarea',
+				status: 'input[name="post_status"]',
+				selectors: '[data-type="selector"]',
+				fields: '[data-field]',
+				... tableMany,
+			},
+			setup({ el, refs, manyRefs, data }) {
+				baseSetup(el, refs, data);
+
+				manyRefs?.inputs?.forEach(el => {
+					window.prefixInput(el, `${data.id}-`);
+				});
+
+				manyRefs?.status?.forEach(el => {
+					if (el.value === data.status) {
+						el.checked = true;
+					}
+				});
+
+				if (crud.isTimeline) {
+					if (refs.sharedRow) {
+						refs.sharedRow.querySelectorAll('input,select,textarea').forEach(input => {
+							window.prefixInput(input, `${data.id}-`);
+						});
+
+						crud.populate.populate(refs.sharedRow, data);
+
+						// Handle status radios in shared row
+						refs.sharedRow.querySelectorAll('input[name="post_status"]').forEach(el => {
+							if (el.value === data.status) {
+								el.checked = true;
+							}
+						});
+					}
+
+					if (refs.point && data.fields?.timeline) {
+
+						Object.entries(data.fields.timeline).forEach(([nuthing, timeline], index) => {
+							const point = refs.point.cloneNode(true);
+							point.dataset.index = `${index}`;
+							point.dataset.itemId = timeline.id;
+
+							point.querySelectorAll('input,select,textarea').forEach(input => {
+								window.prefixInput(input, `${timeline.id}-`);
+							});
+
+							crud.populate.populate(point, {
+								fields: timeline,
+								images: data.images,
+								taxonomies: data.taxonomies
+							});
+
+							const imgData = data.images?.[timeline.post_thumbnail];
+							if (imgData) {
+								point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title']??'');
+							}
+							el.insertBefore(point, refs.point);
+						});
+						refs.point.remove();
+					}
+				} else {
+					if (crud.ui.table.form?.dataset.edit !== undefined) {
+						// Non-timeline: prefix all inputs normally
+						manyRefs?.inputs?.forEach(input => {
+							window.prefixInput(input, `${data.id}-`);
+						});
+
+						manyRefs?.status?.forEach(el => {
+							if (el.value === data.status) {
+								el.checked = true;
+							}
+						});
+
+						crud.populate.populate(el, data);
+					} else {
+						const fields = (Object.hasOwn(data, 'fields')) ? data.fields : data;
+						manyRefs?.fields?.forEach(field => {
+							if (Object.hasOwn(fields, field.dataset.field) && fields[field.dataset.field] !== '') {
+								let value = fields[field.dataset.field];
+								let p = fields.children[0];
+								if (p) {
+									p.textContent = field.dataset.field === 'date'
+										? window.formatTimeAgo(value)
+										: value;
+								}
+							}
+						});
+					}
+				}
+
+				manyRefs?.selectors?.forEach(selector => selector.setAttribute('data-lazy', ''));
+			}
+		});
+
+		T.define('emptyState');
+
+		T.define('bulkItem', {
+			refs: {
+				checkbox: 'input',
+				img: 'img',
+				label: 'label'
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (refs.checkbox) {
+					refs.checkbox.id = `bulk_${data.id}`;
+					refs.checkbox.value = data.id;
+					refs.checkbox.checked = true;
+					refs.checkbox.name ='selected[]';
+				}
+				let thumbnail = data?.images[data?.fields?.post_thumnbail]??{};
+				if (refs.img && Object.keys(thumbnail).length >0) {
+					refs.img.src = thumbnail.medium??'';
+					refs.img.alt = thumbnail.alt??'';
+				}
+
+				if (refs.label) {
+					refs.label.title = item.fields.post_title;
+				}
+			}
+		});
+		T.define('trashOptions');
+		T.define('notTrashOptions');
+		T.define('contentTable');
+	}
+
 	initElements() {
 		this.allowedFilters = ['status', 'orderby', 'order', 'search', 'date-filter', 'dateFrom', 'dateTo'];
 		this.selectors = {
@@ -147,7 +359,7 @@
 						case 'modal-close':
 							const formId = this.ui.modals[name].form.dataset.formId;
 							if (formId) {
-								this.formController.cleanupForm(formId);
+								this.forms.clearForm(formId);
 							}
 
 							this.ui.modals[name].form.reset();
@@ -170,29 +382,39 @@
 			... this.defaults,
 			...cached
 		};
-		const store = window.jvbStore.register(
+
+
+		const stores = window.jvbStore.register(
 			this.content,
-			{
-				storeName: this.content,
-				keyPath: 'id',
-				endpoint: this.endpoint??'content',	//for taxonomy stores
-				headers: {
-					'action_nonce': window.auth.getNonce('dash'),
+		[
+				{
+					storeName: this.content,
+					keyPath: 'id',
+					endpoint: this.endpoint??'content',	//for taxonomy stores
+					headers: {
+						'action_nonce': window.auth.getNonce('dash'),
+					},
+					indexes: [
+						{name: 'id', keyPath: 'id'},
+						{ name: 'status', keyPath: 'status'},
+						{ name: 'date', keyPath: 'date'},
+						{ name: 'modified', keyPath: 'modified'},
+						{ name: 'title', keyPath: 'title'},
+					],
+					filters: filters,
+					ignore: ['content', 'user'],
+					TTL: 60 * 60 * 1000, 		//1 hour cache
+					showLoading: true,
 				},
-				indexes: [
-					{name: 'id', keyPath: 'id'},
-					{ name: 'status', keyPath: 'status'},
-					{ name: 'date', keyPath: 'date'},
-					{ name: 'modified', keyPath: 'modified'},
-					{ name: 'title', keyPath: 'title'}
-				],
-				filters: filters,
-				ignore: ['content', 'user'],
-				TTL: 60 * 60 * 1000, 		//1 hour cache
-				showLoading: true,
-			}
+				{
+					storeName: 'changes',
+					keyPath: 'id'
+				}
+			]
 		);
-		this.store = store[this.content];
+
+		this.changesStore = stores['changes'];
+		this.store = stores[this.content];
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
@@ -201,18 +423,35 @@
 					this.selectionHandler.collectItems();
 					break;
 			}
-		})
+		});
+
+		this.changesStore.subscribe((event, data) => {
+			switch (event) {
+				case 'data-ready':
+					let changes = this.changesStore.getAll();
+					if (changes.length > 0) {
+						changes.forEach(change => {
+							this.changes.set(change.id, change);
+						});
+						this.savePosts('', false).then(()=>{});
+					}
+					break;
+			}
+		});
 	}
 	initIntegrations() {
 		this.selected = new Set();
 		this.selectionHandler = new window.jvbHandleSelection(this.container, {
 			selectAll: {
-				checkbox: '.crud #select-all',
+				checkbox: '#select-all',
 				label: '.bulk-select label',
 				span: '.bulk-select label span'
 			},
 			wrapper: {
 				wrapper: '.wrap'
+			},
+			item: {
+				idAttribute: 'itemId'
 			}
 		});
 		this.selectionHandler.subscribe((event, data) => {
@@ -222,16 +461,16 @@
 			this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
 		});
 
-		this.formController = new window.jvbForm();
+		this.forms = window.jvbForm;
 
-		this.formController.subscribe((event, data) => {
-			switch(event) {
-				case 'form-submit':
-				case 'form-autosave':
-					this.handleFormChange(event,data);
-					break;
-			}
-		});
+		// this.forms.subscribe((event, data) => {
+		// 	switch(event) {
+		// 		case 'form-submit':
+		// 		case 'form-autosave':
+		// 			// this.handleFormChange(event,data);
+		// 			break;
+		// 	}
+		// });
 
 		this.queue.subscribe((event, data) => {
 			if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
@@ -239,6 +478,41 @@
 				&& data.status === 'completed') {
 				this.store.clearCache();
 			}
+			if (event === 'operation-status'
+				&& data.status === 'completed'
+				&& data.endpoint === 'content'
+				&& Object.keys(data.data?.posts??{}).length > 0) {
+
+				this.store.clearCache();
+				let ids = Object.keys(data.data.posts);
+				let storedChanges = this.changesStore.getMany(ids);
+
+				this.changesStore.deleteMany(ids);
+
+				for (let id of ids) {
+					let stored = storedChanges.filter(change => change.id === id)[0]??false;
+
+					let sentChanges = data.data.posts[id];
+					let remainingChanges = {};
+
+					for (let [key, value] of Object.entries(sentChanges)) {
+						if (stored && !Object.hasOwn(stored, key)) continue;
+						if (stored[key] === value) {
+							delete stored[key];
+						}
+						remainingChanges[key] = value;
+					}
+					if (Object.keys(remainingChanges).length > 0) {
+						remainingChanges['id'] = id;
+						remainingChanges['content'] = this.content;
+						this.changes.set(id, remainingChanges);
+					}
+				}
+				if (Object.values(this.changes).length > 0) {
+					this.scheduleBackup();
+				}
+			}
+
 		});
 	}
 
@@ -326,53 +600,74 @@
 		this.changeHandler = this.handleChange.bind(this);
 		this.clickHandler = this.handleClick.bind(this);
 		this.inputHandler = this.handleInput.bind(this);
+		this.submitHandler = this.handleModalSubmit.bind(this);
 
 		document.addEventListener('change', this.changeHandler);
 		document.addEventListener('click', this.clickHandler);
 		if (this.ui.filters.search) {
 			this.ui.filters.search.addEventListener('input', this.inputHandler);
 		}
+
+		for (let [name, modal] of Object.entries(this.ui.modals)) {
+			if (modal.form) {
+				modal.form.addEventListener('submit', this.submitHandler);
+			}
+		}
+	}
+
+	handleModalSubmit(e) {
+		e.preventDefault();
+		const form = e.target;
+		const modal = form.closest('dialog');
+		if (!modal) return;
+		let title = `Saving changes for multiple ${this.plural}`;
+		if (modal.classList.contains('edit')) {
+			title = 'Saving your edits...';
+		} else if (modal.classList.contains('create')) {
+			title = `Creating your new ${this.singular}`;
+		}
+		this.savePosts(title,false);
 	}
 	handleChange(e) {
-		const isSearch = window.targetCheck(e, this.selectors.filters.search);
-		if (isSearch) {
-			return;
-		}
-		const bulkAction = window.targetCheck(e, this.selectors.bulk.action);
-		if (bulkAction) {
-			this.handleBulkAction(bulkAction);
+		// Early bailout - target must be in an item or be a filter
+		const inItem = e.target.closest('[data-item-id]');
+		const isFilter = e.target.matches('[data-filter]');
+		const isBulkAction = e.target.matches('.bulk-action-select');
+		const isView = e.target.matches('[data-view]');
+
+		if (!inItem && !isFilter && !isBulkAction && !isView) return;
+
+		if (!this.isPopulating && inItem && !e.target.closest('[data-ignore], .select-item')) {
+			this.handleItemUpdate(e);
 			return;
 		}
 
-		let filter = window.targetCheck(e, '[data-filter]');
-		if (filter) {
-			this.handleFilterChange(filter);
+		if (isView) {
+			this.items.clear();
+			this.handleViewChange(e.target);
 			return;
 		}
 
-		let view = window.targetCheck(e, 'input[data-view]');
-		if (view) {
-			this.handleViewChange(view);
+		if (isBulkAction) {
+			this.handleBulkAction(e.target);
 			return;
 		}
 
+		if (isFilter) {
+			this.handleFilterChange(e.target);
+			return;
+		}
+
+		// Table-specific handlers
 		if (this.view === 'table') {
-			let target = window.targetCheck(e, '[data-id]');
-			if (target) {
-				this.handleTableChange(e);
-				return;
-			}
-
-			let multiSelect = window.targetCheck(e, 'details.multi-select');
-			if (multiSelect) {
+			if (e.target.matches('details.multi-select')) {
 				this.toggleColumn(e.target.id, e.target.checked);
 				return;
 			}
 
-			let tabNav = window.targetCheck(e, this.selectors.table.nav);
-			if (tabNav) {
-				this.tabNav = tabNav.checked;
-				this.cache.set('tabNav', tabNav.checked ? 'vertical' : 'horizontal');
+			if (e.target.matches(this.selectors.table.nav)) {
+				this.tabNav = e.target.checked;
+				this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
 			}
 		}
 	}
@@ -431,13 +726,46 @@
 
 		if (Object.keys(changes).length > 0) {
 			this.savePosts(
-				changes,
-				`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
-			);
+				`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`,
+				false
+			).then(()=>{});
 		}
 
 		this.selectionHandler.clearSelection();
 	}
+
+	handleItemUpdate(e) {
+		let item = window.targetCheck(e, '[data-item-id]');
+
+		if (!item) return;
+		item.dataset.itemId.split(',').forEach(itemId => {
+			let field = this.forms.getField(e.target);
+			let name = field.dataset.field;
+			let value = this.forms.getFieldValue(e.target);
+			this.updateItem(itemId, name, value);
+		});
+		this.savePosts('', true).then(()=>{});
+	}
+	updateItem(itemId, name, value) {
+		if (!this.changes.has(itemId)) {
+			this.changes.set(itemId, { id: itemId, content: this.content });
+		}
+		this.changes.get(itemId)[name] = value;
+
+		this.scheduleBackup();
+	}
+	scheduleBackup() {
+		window.debouncer.schedule(
+			`changes-${this.content}`,
+			async () => {
+				if (this.changes.size > 0) {
+					await this.changesStore.saveMany(this.changes);
+					this.changes.clear();
+				}
+			},
+			2000
+		);
+	}
 	handleFilterChange(target) {
 		let filter = target.dataset.filter;
 
@@ -521,40 +849,59 @@
 			this.cache.set('view', this.view);
 			this.render();
 		}
+
 	handleClick(e) {
-		let clearSearch = window.targetCheck(e, '.clear-search');
-		if (clearSearch) {
+		// Use matches() instead of closest() where possible (faster)
+		if (e.target.matches('.clear-search')) {
 			this.deleteFilter('search', '');
+			return;
 		}
-		let actionButton = window.targetCheck(e, '[data-action]');
+
+		const actionButton = e.target.closest('[data-action]');
 		if (actionButton) {
 			e.preventDefault();
-			let itemID = actionButton.dataset.id;
+			this.handleActionButton(actionButton);
+			return;
+		}
 
-			switch (actionButton.dataset.action) {
+		if (e.target.matches('.apply-date-filter')) {
+			this.handleCustomDateSelection();
+			this.modals.date.handleClose();
+			return;
+		}
+
+		if (e.target.matches(this.selectors.buttons.create)) {
+			this.openCreateModal();
+		}
+	}
+		openCreateModal(){
+			this.ui.modals.create.form.reset();
+			this.forms.registerForm(this.ui.modals.create.form,{
+				cache: false,
+			});
+
+			this.ui.modals.create.modal.dataset.itemId = window.generateID('new');
+			this.modals.create.handleOpen();
+		}
+		handleActionButton(button) {
+			const itemID = button.dataset.id;
+
+			switch (button.dataset.action) {
 				case 'edit':
 					this.openEditModal(itemID);
 					break;
 				case 'delete':
 					if (confirm('Delete this item? This cannot be undone')) {
-						let changes = {};
-						changes[itemID] = {
-							'post_status': 'delete',
-							'content': this.content
-						};
-						window.fade(actionButton.closest('.item'), false);
-						this.savePosts(changes, `Sending ${this.singular} to trash...`);
+						this.updateItem(itemID, 'post_status', 'delete');
+						window.fade(button.closest('.item'), false);
+						this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{});
 						this.store.delete(itemID);
 					}
 					break;
 				case 'trash':
-					let changes = {};
-					changes[itemID] = {
-						'post_status': 'trash',
-						'content': this.content
-					};
-					window.fade(actionButton.closest('.item'), false);
-					this.savePosts(changes, `Sending ${this.singular} to trash...`);
+					this.updateItem(itemID, 'post_status', 'trash');
+					window.fade(button.closest('.item'), false);
+					this.savePosts(`Sending ${this.singular} to trash...`).then(()=>{});
 					break;
 				case 'bulk-edit':
 					if (this.selected.size > 0) {
@@ -562,10 +909,7 @@
 					}
 					break;
 				case 'bulk-delete':
-					if (this.selected.size > 0 && confirm(`Delete  ${this.selected.size} items?`)) {
-						this.selected.forEach(id => this.store.delete(id));
-						this.selectionHandler.clearSelection();
-					}
+					this.handleBulkDelete();
 					break;
 				case 'refresh':
 					this.store.clearCache();
@@ -576,22 +920,21 @@
 					break;
 			}
 		}
-
-		const applyDate = window.targetCheck(e, '.apply-date-filter');
-		if (applyDate) {
-			this.handleCustomDateSelection();
-			this.modals.date.handleClose();
-			return;
+		handleBulkDelete() {
+			let isTrash = this.store.filters.status === 'trash';
+			if (this.selected.size > 0 && confirm(`${isTrash ? 'Permanently delete' : 'Send'} ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}${isTrash ? '' : 'to trash'}?`)) {
+				this.selected.forEach(id => {
+					this.store.delete(id);
+					this.updateItem(id, 'post_status', isTrash ? 'delete' : 'trash');
+				});
+				let title = isTrash
+					? `Permanently deleting ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}`
+					: `Sending ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural} to trash`;
+				this.savePosts(title).then(()=>{});
+				this.selectionHandler.clearSelection();
+			}
 		}
 
-		const createButton = window.targetCheck(e, this.selectors.buttons.create);
-		if (createButton) {
-			this.ui.modals.create.form.reset();
-			this.formController.registerForm(this.ui.modals.create.form);
-			this.modals.create.handleOpen();
-		}
-	}
-
 	handleInput(e) {
 		e.preventDefault();
 		e.stopPropagation();
@@ -688,7 +1031,7 @@
 		}
 
 		// For regular tables, check for data-id
-		return !!row.dataset.id;
+		return !!row.dataset.itemId;
 	}
 
 	focusFieldInRow(row, fieldName, fromAbove = false) {
@@ -726,82 +1069,6 @@
 		return null;
 	}
 
-	toggleTimelineListeners(on = true) {
-		if (!this.isTimeline || this.view !== 'table') return;
-
-		// Cleanup existing instances
-		if (this.timelineSortables) {
-			this.timelineSortables.forEach(sortable => sortable.destroy());
-			this.timelineSortables = [];
-		}
-
-		if (!on) return;
-
-		// Initialize sortable for each tbody
-		this.timelineSortables = [];
-		const tbodies = this.ui.table.form.querySelectorAll('tbody.item');
-
-		tbodies.forEach(tbody => {
-			const sortable = new Sortable(tbody, {
-				animation: 150,
-				handle: '.drag-handle',
-				draggable: 'tr.timeline-point',
-				ghostClass: 'sortable-ghost',
-				chosenClass: 'sortable-chosen',
-				dragClass: 'sortable-drag',
-
-				// Prevent dragging between different tbodies
-				group: {
-					name: `timeline-${tbody.dataset.id}`,
-					pull: false,
-					put: false
-				},
-
-				onEnd: (evt) => this.handleTimelineReorder(evt)
-			});
-
-			this.timelineSortables.push(sortable);
-		});
-	}
-
-	handleTimelineReorder(evt) {
-		const { item: draggedRow, oldIndex, newIndex, from: tbody } = evt;
-
-		// No change
-		if (oldIndex === newIndex) return;
-
-		const itemID = parseInt(tbody.dataset.id);
-		const item = this.store.get(itemID);
-		if (!item?.fields?.timeline) return;
-
-		// Get current order of image IDs from DOM
-		const newOrder = Array.from(tbody.querySelectorAll('tr.timeline-point'))
-			.map(row => row.dataset.imageId)
-			.filter(Boolean);
-
-		// Rebuild timeline object in new order
-		const reorderedTimeline = {};
-		newOrder.forEach((imgId, index) => {
-			if (item.fields.timeline[imgId]) {
-				reorderedTimeline[imgId] = {
-					...item.fields.timeline[imgId],
-					order: index
-				};
-			}
-		});
-
-		item.fields.timeline = reorderedTimeline;
-
-		// Save to store and server
-		this.store.save(item);
-		this.savePosts(
-			{ [itemID]: { timeline: reorderedTimeline, content: this.content } },
-			'Reordering timeline...',
-			true
-		);
-
-		this.a11y?.announce(`Moved to position ${newIndex + 1}`);
-	}
 
 	/*******************************************************************
 	 MODALS
@@ -816,107 +1083,78 @@
 		this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
 
 		this.ui.modals.edit.form.reset();
-		new window.jvbPopulate(this.ui.modals.edit.form, item);
-		this.formController.registerForm(this.ui.modals.edit.form);
+
+		this.forms.registerForm(this.ui.modals.edit.form, {cache: false});
+
+		this.isPopulating = true;
+		this.populate.populate(this.ui.modals.edit.form, item);
+		this.isPopulating = false;
 
 		this.modals.edit.handleOpen();
 	}
 	openBulkEditModal() {
 		window.removeChildren(this.ui.modals.bulkEdit.selected);
 		this.ui.modals.edit.form.reset();
-		this.modals.bulkEdit.handleOpen();
-		this.selected.forEach(itemId => {
-			let template = window.getTemplate('bulkItem');
-			if (!template) return;
-			let item = this.store.get(parseInt(itemId));
-			if (!item) return;
-			let [checkbox, img, label] = [template.querySelector('input'), template.querySelector('img'), template.querySelector('label')];
-			if (checkbox) {
-				checkbox.id = `bulk_${item.id}`;
-				checkbox.value = item.id;
-				checkbox.checked = true;
-				checkbox.name = 'selected[]';
-			}
 
-			let thumbnail = item.images[item.fields.post_thumbnail]??{};
-			if (img) {
-				img.src = thumbnail.medium??'';
-				img.alt = thumbnail.alt??''
-			}
+		window.chunkIt(
+			this.selected,
+			(itemId) => {
+				let item = this.store.get(parseInt(itemId));
+				if (!item) return;
+				itemIds.push(item.id);
 
-			label.title = item.fields['post_title']??'';
-			this.ui.modals.bulkEdit.selected.append(template);
-		});
+				return window.jvbTemplates.create('bulkItem', item);
+			},
+			(fragment) => this.ui.modals.bulkEdit.selected.append(fragment)
+		).then(()=>{});
+		let itemIds = Array.from(this.selected).map(id => this.store.get(parseInt(id))).filter(Boolean);
+
+		this.ui.modals.bulkEdit.modal.dataset.itemId = itemIds.join(',');
+
 		if (this.ui.modals.bulkEdit.h2) {
 			this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
 		}
-		this.formController.registerForm(this.ui.modals.bulkEdit.form);
+		this.modals.bulkEdit.handleOpen();
+
+		this.forms.registerForm(this.ui.modals.bulkEdit.form, {cache:false});
+
+		// this.isPopulating = true;
+		// this.populate.populate(this.ui.modals.edit.form, item);
+		// this.isPopulating = false;
 	}
 
 	/*****************************************************************
 	 FIELD HANDLING
 	*****************************************************************/
-	handleFormChange(event, data) {
-		let title = data.fullData.post_title;
-		let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
 
-		let theChanges = {};
-		if (this.isTimeline) {
-			theChanges[this.activeItem] = changes;
-			if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-				this.removeItems([this.activeItem]);
-			}
-			this.savePosts(theChanges, title, event === 'form-submit');
-			return;
+	async savePosts(title = '', delay = false) {
+		const memoryChanges = Array.from(this.changes.values());
+		const storedChanges = await this.changesStore.getAll();
+		const changes = window.deepMerge(storedChanges, memoryChanges);
+		if (changes.length === 0) return;
+
+		if (title === '') {
+			title = `Saving ${changes.length} ${changes.length === 1 ? this.singular : this.plural}`;
 		}
 
+		let allChanges = {};
 		let remove = [];
-		let el = data.config.element;
-		if (el === this.ui.modals.edit.form) {
-			theChanges[this.activeItem] = changes;
-			title = `Saving ${title} Changes`;
-			if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-				remove.push(this.activeItem);
+
+		changes.forEach(change => {
+			let itemId = change.id;
+
+			// Create a new object without the id field (don't mutate original!)
+			const { id, ...changeWithoutId } = change;
+			allChanges[itemId] = changeWithoutId;
+
+			if (change.post_status && this.shouldRemoveItemUI(change.post_status)) {
+				remove.push(itemId);
 			}
-		} else if (el === this.ui.modals.bulkEdit.form) {
-			let num = 0;
-			el.querySelectorAll('.selected input:checked').forEach(selected => {
-				theChanges[selected.value] = changes;
-				if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
-					remove.push(selected.value);
-				}
-				num++;
-			});
-			title = `Updating ${num} ${this.plural} Changes`;
-		} else if (el === this.ui.modals.create.form) {
-			if (event === 'form-submit') {
-				theChanges[el.dataset.formId] = changes;
-				title = `Saving ${title} Changes`;
-			}
-		}
+		});
+
 		if (remove.length > 0) {
 			this.removeItems(remove);
 		}
-		this.selectionHandler.clearSelection();
-		if (Object.keys(theChanges).length === 0) {
-			return;
-		}
-		this.savePosts(theChanges, title, event === 'form-submit');
-	}
-	savePosts(changes, title, delay = false) {
-		if (Object.keys(changes).length === 0) return;
-
-		let validChanges = {};
-		for (let itemId in changes) {
-			// Validate ID exists and is not null/undefined
-			if (!itemId || itemId === 'null' || itemId === 'undefined') {
-				console.warn('Skipping save for invalid ID:', itemId);
-				continue;
-			}
-			if (!changes[itemId]['content']) changes[itemId]['content'] = this.content;
-
-			validChanges[itemId] = changes[itemId];
-		}
 
 		let operation = {
 			endpoint: this.endpoint,
@@ -924,7 +1162,7 @@
 				'action_nonce': window.auth.getNonce('dash'),
 			},
 			data: {
-				posts: validChanges,
+				posts: allChanges,
 			},
 			delay: delay,
 			popup: `Saving changes`,
@@ -933,59 +1171,13 @@
 		this.queue.addToQueue(operation);
 	}
 
-	handleTableChange(e) {
-		const container = this.isTimeline
-			? e.target.closest('tbody[data-id]')
-			: e.target.closest('tr[data-id]');
-		if (!container) return;
 
-		const input = e.target;
-		const field = input.closest('[data-field]')?.dataset.field;
-		if (!field) return;
-
-		const itemID = parseInt(container.dataset.id);
-		const item = this.store.get(itemID);
-		if (!item) return;
-
-		const value = this.getInputValue(input);
-
-		// Timeline-specific: check if it's a point or shared value
-		if (this.isTimeline) {
-			const timelinePoint = input.closest('tr.timeline-point');
-			if (timelinePoint) {
-				const imgID = timelinePoint.dataset.imageId;
-				item.fields.timeline ??= {};
-				item.fields.timeline[imgID] ??= {};
-				item.fields.timeline[imgID][field] = value;
-			} else {
-				item.fields[field] = value;
-			}
-		} else {
-			item.fields[field] = value;
-		}
-
-		this.store.save(item);
-		this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
-	}
-
-	getInputValue(input) {
-		if (input.type === 'checkbox') {
-			return input.checked ? (input.value || '1') : '';
-		}
-		if (input.type === 'radio') {
-			return input.checked ? input.value : null;
-		}
-		return input.value;
-	}
 	setBulkStatus(status) {
 		if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
-
-		let changes = {};
+		let ids = [];
 		this.selected.forEach(itemID => {
-			changes[itemID] = {
-				post_status: status,
-				content: this.content
-			};
+			ids.push(itemID);
+			this.updateItem(itemID, 'post_status', status);
 		});
 		let title;
 		switch (status) {
@@ -996,12 +1188,12 @@
 				title = window.uppercaseFirst(status)+'ing';
 		}
 		if (this.shouldRemoveItemUI(status)) {
-			this.removeItems(Object.keys(changes));
+			this.removeItems(ids);
 		}
 		this.selectionHandler.clearSelection();
-		if (Object.keys(changes).length !== 0) {
-			this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
-		}
+
+		this.savePosts(`${title} ${ids.length} ${ids.length === 1 ? this.singular : this.plural}...`).then(()=>{});
+
 	}
 	/***************************************************************
 	 VIEW
@@ -1018,7 +1210,7 @@
 				this.renderGrid(items);
 				break;
 			case 'table':
-				this.renderTable(items);
+				this.renderTable(items).then(()=>{});
 				break;
 			case 'list':
 				this.renderList(items);
@@ -1032,10 +1224,10 @@
 			let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
 			if (this.status === 'trash' && hasEdit) {
 				window.removeChildren(this.ui.bulk.action);
-				options = window.getTemplate('trashOptions');
+				options = window.jvbTemplates.create('trashOptions');
 			} else if (this.status !== 'trash' && !hasEdit) {
 				window.removeChildren(this.ui.bulk.action);
-				options = window.getTemplate('notTrashOptions');
+				options = window.jvbTemplates.create('notTrashOptions');
 			}
 			if (options) {
 				options.querySelectorAll('option').forEach((option, index)=> {
@@ -1053,7 +1245,7 @@
 	renderEmpty() {
 		this.toggleTable(false);
 		window.removeChildren(this.ui.grid);
-		const empty = window.getTemplate('emptyState');
+		const empty = window.jvbTemplates.create('emptyState');
 		if (empty) {
 			this.ui.grid.append(empty);
 			this.a11y.announceItems(0,false,false);
@@ -1064,7 +1256,7 @@
 		if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
 
 		if (on && !this.ui.table.form) {
-			let table = window.getTemplate('contentTable');
+			let table = window.jvbTemplates.create('contentTable');
 			this.container.append(table);
 			this.ui.table = window.uiFromSelectors(this.selectors.table);
 			this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
@@ -1072,14 +1264,8 @@
 
 		if (this.ui.table.form) {
 			this.ui.table.form.hidden = !on;
-			if (on) {
-				this.formController.registerForm(this.ui.table.form, {
-					autosave: false,
-					formStatus: false,
-					isTable: true
-				});
-			} else {
-				this.formController.cleanupForm(this.ui.table.form.dataset.formId)
+			if (!on){
+				this.forms.clearForm(this.ui.table.form.dataset.formId)
 			}
 			if (this.ui.table.body) {
 				window.removeChildren(this.ui.table.body);
@@ -1091,7 +1277,7 @@
 		} else {
 			document.removeEventListener('keydown', this.keyHandler);
 		}
-		if (this.isTimeline && !on) this.toggleTimelineListeners(on);
+
 	}
 
 	renderGrid(items) {
@@ -1101,12 +1287,11 @@
 		this.ui.grid.classList.remove('list-view');
 		this.ui.grid.classList.add('grid-view');
 
-		const fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let card = this.renderGridItem(item);
-			fragment.appendChild(card);
-		});
-		this.ui.grid.appendChild(fragment);
+		window.chunkIt(
+			items,
+			(item) => this.renderGridItem(item),
+			(fragment) => this.ui.grid.append(fragment)
+		).then(()=>{});
 	}
 
 	renderList(items) {
@@ -1115,221 +1300,56 @@
 
 		this.ui.grid.classList.remove('grid-view');
 		this.ui.grid.classList.add('list-view');
-		const fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let row = this.renderListItem(item);
-			fragment.append(row);
-		});
-		this.ui.grid.append(fragment);
+		window.chunkIt(
+			items,
+			(item) => this.renderListItem(item),
+			(fragment) => this.ui.grid.append(fragment)
+		).then(()=>{});
 	}
 
-	renderTable(items) {
+	async renderTable(items) {
 		this.toggleTable();
 		window.removeChildren(this.ui.grid);
 
-		let fragment = document.createDocumentFragment();
-		items.forEach(item => {
-			let row = this.renderTableItem(item);
-			if (row) fragment.append(row);
+		await window.chunkIt(
+			items,
+			(item) => this.renderTableItem(item),
+			(fragment) => {
+				if (this.ui.table.body) {
+					this.ui.table.body.append(fragment);
+				} else {
+					this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
+				}
+			},
+			5
+		);
+
+		requestAnimationFrame(() => {
+			window.jvbSelector?.scanExistingFields(this.ui.table.table);
 		});
-
-		if (this.ui.table.body) {
-			this.ui.table.body.append(fragment);
-		} else {
-			this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
-		}
-
-		if (this.isTimeline) {
-			this.toggleTimelineListeners(true);
-		}
 	}
 
 	/***************************************************************
  	RENDER HELPERS
 	 ***************************************************************/
-	setupItemElement(element, item, templateName) {
-		if (this.items[this.view].has(item.id)) {
-			return this.items[this.view].get(item.id);
-		}
-
-		element.dataset.id = item.id;
-		if (item._pending) element.classList.add('pending');
-
-		// Setup selection checkbox/label
-		this.setupItemSelection(element, item);
-
-		// Setup thumbnail
-		this.setupItemThumbnail(element, item);
-
-		// Setup action buttons
-		this.setupItemActions(element, item);
-
-		this.items[this.view].set(item.id, element);
-		return element;
-	}
-
-	setupItemSelection(element, item) {
-		const checkbox = element.querySelector('.select-item');
-		const label = element.querySelector('.select-item + label, .select-item-label');
-		if (!checkbox) return;
-
-		checkbox.id = `select-${item.id}`;
-		checkbox.value = item.id;
-		checkbox.checked = this.selected.has(parseInt(item.id));
-
-		if (label) {
-			label.htmlFor = `select-${item.id}`;
-		}
-	}
-
-	setupItemThumbnail(element, item) {
-		const img = element.querySelector('img');
-		if (!img) return;
-
-		const thumbnail = item.images?.[item.fields?.post_thumbnail] ?? {};
-		img.src = thumbnail.medium ?? '';
-		img.alt = thumbnail.alt ?? '';
-	}
-
-	setupItemActions(element, item) {
-		element.querySelectorAll('[data-action]').forEach(btn => {
-			btn.dataset.id = item.id;
-		});
-	}
-
 	renderGridItem(item) {
-		if (this.items.grid.has(item.id)) {
-			return this.items.grid.get(item.id);
-		}
-		const card = window.getTemplate('gridView');
-		if (!card) return null;
-		return this.setupItemElement(card, item, 'grid');
+		let gridItem = window.jvbTemplates.create('gridView', item);
+		this.items.set(item.id, gridItem);
+		return gridItem;
 	}
 
 	renderListItem(item) {
-		if (this.items.list.has(item.id)) {
-			return this.items.list.get(item.id);
-		}
-		let row = window.getTemplate('listView');
-		if (!row) return null;
-
-		row = this.setupItemElement(row, item, 'list');
-
-		// List-specific: populate data attributes
-		row.querySelectorAll('[data-attr]').forEach(el => {
-			const value = item[el.dataset.attr];
-			if (value && value !== '') {
-				el.textContent = value;
-			} else {
-				el.remove();
-			}
-		});
-
-		row.querySelectorAll('[data-field]').forEach(el => {
-			const value = item.fields?.[el.dataset.field];
-			if (value && value !== '') {
-				el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
-			} else {
-				el.remove();
-			}
-		});
-		this.items.list.set(item.id, row);
-
-		return row;
+		let listItem = window.jvbTemplates.create('listView', item);
+		this.items.set(item.id, listItem);
+		return listItem;
 	}
 
 	renderTableItem(item) {
-		if (this.items.table.has(item.id)) {
-			return this.items.table.get(item.id);
-		}
-		let row = window.getTemplate('tableView');
-		if (!row) return null;
-
-		row = this.setupItemElement(row, item, 'table');
-
-		const status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
-		if (status) status.checked = true;
-
-		if (this.isTimeline) {
-			// Timeline: populate shared row, clone points
-			const sharedRow = row.querySelector('tr.shared');
-			if (sharedRow) {
-				new window.jvbPopulate(sharedRow, item);
-				this.cleanupTableRow(sharedRow);
-			}
-
-			const pointTemplate = row.querySelector('tr.timeline-point');
-			if (pointTemplate && item.fields?.timeline) {
-				Object.entries(item.fields.timeline).forEach(([imgId, timeline], index) => {
-					const point = pointTemplate.cloneNode(true);
-					point.dataset.index = index;
-					point.dataset.imageId = imgId;
-
-					new window.jvbPopulate(point, {
-						fields: timeline,
-						images: item.images,
-						taxonomies: item.taxonomies
-					});
-					this.cleanupTableRow(point);
-
-					const imgData = item.images?.[timeline.post_thumbnail];
-					if (imgData) {
-						point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title'] || '');
-					}
-
-					row.insertBefore(point, pointTemplate);
-				});
-				pointTemplate.remove();
-			}
-		} else {
-			// Standard table row
-			if (this.ui.table.form?.dataset.edit !== undefined) {
-				new window.jvbPopulate(row, item);
-			} else {
-				this.populateTableReadOnly(row, item);
-			}
-			this.cleanupTableRow(row);
-		}
-
-		this.items.table.set(item.id, row);
-
-		return row;
+		let tableItem = window.jvbTemplates.create('tableView', item);
+		this.items.set(item.id, tableItem);
+		return tableItem;
 	}
 
-	populateTableReadOnly(row, item) {
-		for (const [key, value] of Object.entries(item)) {
-			const col = row.querySelector(`[data-field="${key}"]`);
-			if (!col) continue;
-
-			const p = col.querySelector('p');
-			if (p) {
-				p.textContent = col.dataset.fieldType === 'date'
-					? window.formatTimeAgo(value)
-					: value;
-			}
-		}
-	}
-
-	cleanupTableRow(row) {
-		row.querySelectorAll('td[data-field]').forEach(field => {
-			field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
-				if (!label.closest('.radio-options')) {
-					label.remove();
-				}
-			});
-
-			//Remove toggle labels for true_false fields
-			if (field.dataset.fieldType === 'true_false') {
-				field.querySelector('.toggle-label')?.remove();
-			}
-
-			//Remove field label for checkbox/radio groups
-			if (['checkbox','radio','select'].includes(field.dataset.fieldType)) {
-				field.querySelector('.label')?.remove();
-			}
-
-		});
-	}
 	toggleColumn(column, show) {
 		this.ui.table.table.querySelectorAll(`.${column}`).forEach(el =>{
 			el.hidden = !show;
@@ -1343,15 +1363,11 @@
 			|| newStatus !== this.store.filters.status;
 	}
 	removeItems(items) {
-		let delay = 0;
 		items.forEach(itemId => {
-			setTimeout(() => {
-				let item = this.items[this.view].get(itemId);
-				if (item) {
-					window.fade(item, false);
-				}
-			}, delay);
-			delay += 50;
+			if (this.items.has(itemId)) {
+				let item = this.items.get(itemId);
+				if (item) window.fade(item, false);
+			}
 		});
 	}
 
@@ -1446,18 +1462,25 @@
 	 CLEANUP
 	***************************************************************/
 	destroy() {
+		window.debouncer.cancel(`changes-${this.content}`);
+		if (this.changes.size > 0) {
+			this.changesStore.saveMany(this.changes).then(()=>{});
+			this.changes.clear();
+		}
 		if (this.timelineSortables) {
 			this.timelineSortables.forEach(sortable => sortable.destroy());
 			this.timelineSortables = [];
 		}
+		for (let [name, modal] of Object.entries(this.ui.modals)) {
+			if (modal.form) {
+				modal.form.removeEventListener('submit', this.submitHandler);
+			}
+		}
 		document.removeEventListener('click', this.clickHandler);
 		document.removeEventListener('change', this.changeHandler);
 		if (this.ui.filters.search) {
 			this.ui.filters.search.removeEventListener('input', this.handleInput);
 		}
-		for (let map of Object.values(this.items)) {
-			map.clear();
-		}
 	}
 }
 
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 16c17ad..3610c26 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -144,8 +144,11 @@
 			// Data methods
 			fetch: () => this.fetch(name),
 			save: (item) => this.save(name, item),
+			saveMany: (items) => this.saveMany(name, items),
 			delete: (id) => this.delete(name, id),
+			deleteMany: (items) => this.deleteMany(name, items),
 			get: (id) => this.get(name, id),
+			getMany: (ids) => this.getMany(name, ids),
 			getAll: () => this.getAll(name),
 			getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
 			filterByIndex: (criteria) => this.filterByIndex(name, criteria),
@@ -784,6 +787,47 @@
 		return changeInfo.key;
 	}
 
+	/**
+	 * Save multiple items in a single transaction (batch write)
+	 * @param {string} name - Store name
+	 * @param {Array|Map} items - Array of items or Map of items to save
+	 * @returns {Promise<Array>} - Array of saved keys
+	 */
+	async saveMany(name, items) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Map to array if needed
+		const itemArray = items instanceof Map
+			? Array.from(items.values())
+			: Array.isArray(items) ? items : Object.values(items);
+
+		if (itemArray.length === 0) return [];
+
+		const changes = [];
+
+		// Process all items and update in-memory store
+		itemArray.forEach(item => {
+			const changeInfo = this._saveItem(name, item);
+			changes.push(changeInfo);
+		});
+
+		// Single transaction for all writes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			changes.forEach(changeInfo => {
+				objectStore.put(changeInfo.processed);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-saved', {
+			count: changes.length,
+			keys: changes.map(c => c.key)
+		});
+
+		return changes.map(c => c.key);
+	}
+
 	processForStorage(obj, validate = true, path = 'root') {
 		if (obj === null || obj === undefined) return { valid: true, data: obj };
 
@@ -871,11 +915,78 @@
 		this.notify(name, 'item-deleted', { id });
 	}
 
+	/**
+	 * Delete multiple items in a single transaction (batch delete)
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to delete
+	 * @returns {Promise<Array>} - Array of deleted IDs
+	 */
+	async deleteMany(name, ids) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Set to array if needed
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		// Update in-memory store
+		idArray.forEach(id => {
+			store.data.delete(id);
+		});
+
+		// Single transaction for all deletes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			idArray.forEach(id => {
+				objectStore.delete(id);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-deleted', {
+			count: idArray.length,
+			ids: idArray
+		});
+
+		return idArray;
+	}
+
 	get(name, id) {
 		const store = this.stores.get(name);
 		return store.data.get(id);
 	}
 
+	/**
+	 * Get multiple items by IDs in a single call
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to retrieve
+	 * @param {boolean} skipMissing - If true, omit missing items; if false, include null for missing
+	 * @returns {Array} - Array of items (in same order as IDs)
+	 */
+	getMany(name, ids, skipMissing = true) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		if (skipMissing) {
+			return idArray.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		// Preserve order, include null for missing
+		return idArray.map(id => store.data.get(id) ?? null);
+	}
+
 	getAll(name) {
 		const store = this.stores.get(name);
 		return Array.from(store.data.values());
@@ -959,27 +1070,30 @@
 		const allItems = Array.from(store.data.values());
 		const searchQuery = store.filters.search?.toLowerCase().trim() || '';
 
+		const filterPredicates = [];
+		for (const [key, value] of Object.entries(store.filters)) {
+			if (store.ignoreFilters.has(key)) continue;
+			if (value === null || value === undefined || value === '') continue;
+			if (value === 'all') continue;
+
+			// Comma-separated values
+			if (typeof value === 'string' && value.includes(',')) {
+				const accepted = value.split(',').map(v => v.trim());
+				filterPredicates.push(item => accepted.includes(String(item[key])));
+				continue;
+			}
+
+			filterPredicates.push(item => String(item[key]) === String(value));
+		}
+
 		const filtered = allItems.filter(item => {
-			// Apply all non-ignored filters
-			for (const [key, value] of Object.entries(store.filters)) {
-				if (store.ignoreFilters.has(key)) continue;
-				if (value === null || value === undefined || value === '') continue;
-				if (value === 'all') continue;
-
-				// Comma-separated values
-				if (typeof value === 'string' && value.includes(',')) {
-					const accepted = value.split(',').map(v => v.trim());
-					if (!accepted.includes(String(item[key]))) return false;
-					continue;
-				}
-
-				if (String(item[key]) !== String(value)) return false;
+			// Apply all non-search filters
+			for (const predicate of filterPredicates) {
+				if (!predicate(item)) return false;
 			}
 
 			// Apply search if present
 			return !(searchQuery && !this.searchObject(item, searchQuery));
-
-
 		});
 
 		return this.applyOrdering(filtered, store);
@@ -1021,7 +1135,9 @@
 	}
 
 	searchObject(obj, search) {
-		if (!obj || typeof obj !== 'object') return false;
+		if (!obj || typeof obj !== 'object') {
+			return typeof obj === 'string' && obj.toLowerCase().includes(search);
+		}
 
 		for (const value of Object.values(obj)) {
 			if (value === null || value === undefined) continue;
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index a0388e2..76ab6c3 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,1306 +1,193 @@
 class FormController {
-	constructor(config = {}) {
-		this.config = {
-			collectFormData: false,
-			... config
-		}
+	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(); // formId -> Set of quill instances
+		this.dependencies = new Map();
+
+		this.subscribers = new Set();
+
 		this.isRestoring = false;
+		this.hasListeners = false;
+		this.summaryTemplate = false;
+
+		this.init();
+	}
+	init() {
+		this.templates = window.jvbTemplates;
+		this.initElements();
+		this.initListeners();
+		this.initStore();
+		this.initValidators();
+	}
+	initElements() {
+		this.inputSelectors = 'input, textarea, select';
+		this.selectors = {
+			tabs: {
+				nav: 'nav.tabs',
+				sections: '.tab.content',			//querySelectorAll
+				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,					//querySelectorAll
+			fields: {
+				field: '.field',					//querySelectorAll
+				label: 'label',
+				success: '.success',
+				error: '.success',
+				message: '.validation-message',
+			},
+			repeater: {
+				repeater: '.repeater',				//querySelectorAll
+				header: '.repeater-row-header',
+				remove: '.remove-row',
+				add: '.add-repeater-row',
+				template: 'template',
+				items: '.repeater-items',
+				inputs: this.inputSelectors					//querySelectorAll
+			},
+			tagList: {
+				tagList: '.field.tag-list',			//querySelectorAll
+				input: '.tag-input-row',
+				add: '.add-tag',
+				remove: '.remove-tag',
+				label: '.tag-label',
+				items: '.tag-items',
+				inputs: this.inputSelectors,				//querySelectorAll
+				value: 'input[type="hidden"]'		//querySelectorAll
+			},
+			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(form) {
+			form.addEventListener('click', this.clickHandler);
+			form.addEventListener('change', this.changeHandler);
+			form.addEventListener('input', this.inputHandler);
+			form.addEventListener('blur', this.blurHandler);
+			form.addEventListener('submit', this.submitHandler);
+		}
+		removeFormListeners(form) {
+			form.removeEventListener('click', this.clickHandler);
+			form.removeEventListener('change', this.changeHandler);
+			form.removeEventListener('input', this.inputHandler);
+			form.removeEventListener('blur', this.blurHandler);
+			form.removeEventListener('submit', this.submitHandler);
+		}
+	initStore() {
 		const store = window.jvbStore.register(
 			'forms',
 			{
 				storeName: 'forms',
-				keyPath: 'formId',
+				keyPath: 'id',
 				indexes: [
-					{ name: 'status', keyPath: 'status' },
-					{ name: 'operationId', keyPath: 'operationId' },
+					{ name: 'src', keyPath: 'src'},
 					{ name: 'timestamp', keyPath: 'timestamp' },
 					{ name: 'formType', keyPath: 'type' }
 				],
 				TTL: 7 * 24 * 60 * 1000, //7 days
-				validateData: true,
-				delayFetch: true
 			});
 		this.store = store.forms;
 
-		this.debouncer = window.debouncer;
+		this.store.subscribe((event, data)=> {
+			if (event === 'data-loaded') {
+				let stored = this.store.getFiltered();
 
-		this.ignore = [];
-
-		this.populateForm = window.jvbPopulate;
-
-		this.subscribers = new Set();
-		this.forms = new Map();
-		this.specialFields = new Map();
-		this.dependencies = new Map();
-
-		// Validation (YOU ARE GREAT!)
-		this.validators = this.initValidators();
-		this.touchedFields = new Set();
-
-		// Auto-save configuration
-		this.autoSaveDefaults = {
-			delay: 3000, // 3 seconds
-			typingDelay: 1500, // 1.5 seconds for text fields
-			enabled: true
-		};
-
-		// Repeater field management
-		this.activeRepeaters = new Map();
-		this.repeaterDelays = {
-			change: 6000,
-			typing: 3000,
-			blur: 1500,
-			add: 500,
-			remove: 800,
-			reorder: 1000
-		};
-
-		this.isTimeline = window.crudManager && window.crudManager.isTimeline;
-
-		// Bind handlers
-		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.submitHandler = this.handleSubmit.bind(this);
-		this.inputHandler = this.handleInput.bind(this);
-		this.blurHandler = this.handleBlur.bind(this);
-		//Processors
-		this.processRepeaterField = this.processRepeaterField.bind(this);
-		this.processGroupField = this.processGroupField.bind(this);
-		this.processLocationField = this.processLocationField.bind(this);
-		this.processRegularField = this.processRegularField.bind(this);
-
-		this.init();
-	}
-
-	async init() {
-		this.store.subscribe(this.handleStoreEvent.bind(this));
-
-		// Set up global form handlers for standalone forms
-		this.initListeners();
-		if (window.jvbQueue) {
-			window.jvbQueue.subscribe((event, data) => {
-				if (event === 'operation-completed' && data.type === 'form') {
-					this.handleOperationComplete(data);
+				let pending = stored.filter(form=> form.src ===  window.location.pathname);
+				for (let form of pending) {
+					this.showPendingNotification(form.id, form.changes);
 				}
-			});
-		}
-	}
-
-	/**
-	 * Handle operation completion - clear related form cache
-	 */
-	async handleOperationComplete(operation) {
-		// Clear the form data from store
-		if (operation.formId) {
-			try {
-				await this.store.delete(operation.formId);
-			} catch (error) {
-				console.warn('Failed to clear form cache:', error);
+			} else if (event === 'operation-status' && data.status === 'completed') {
+				if (data.config) {
+					this.store.remove(data.config.id);
+				}
 			}
-		}
-
-		// Clear any related form state
-		const form = this.forms.get(operation.formId);
-		if (form) {
-			form.isDirty = false;
-			form.lastSaved = Date.now();
-			form.data = {};
-		}
-	}
-
-	handleStoreEvent(event, data) {
-		switch(event) {
-			case 'item-saved':
-				if (data.item.status === 'autosave') {
-					// this.showFormStatus(data.item.formId, 'autosave');
-				}
-				break;
-			case 'data-loaded':
-				this.checkPendingForms();
-				break;
-		}
-	}
-
-	/**
-	 * Check for pending forms from current page
-	 */
-	async checkPendingForms() {
-		const allForms = await this.store.getAll();
-		const currentPath = window.location.pathname;
-
-		const pendingForms = allForms.filter(form => {
-			if (form.status !== 'draft') return false;
-
-			// Check if form is from current page
-			const formPath = form.data?._wp_http_referer;
-			return formPath === currentPath;
 		});
-
-		pendingForms.forEach(item => {
-			const formElement = this.findFormElement(item);
-			if (!formElement) return;
-
-			// Register form if not already registered
-			let formConfig = this.forms.get(item.formId);
-			if (!formElement.dataset.formId) {
-				formConfig = this.registerForm(formElement);
+	}
+		showPendingNotification(formId, changes) {
+			let form = this.forms.get(formId);
+			if (!form) return;
+			let element = form.element;
+			if (!element) {
+				console.warn(`Form element not found for: ${formId}`);
+				return;
 			}
 
-			// Set flag to prevent event handlers from firing
-			this.isRestoring = true;
-			// Auto-populate the form
-			new this.populateForm(formElement, item.data);
+			const notification = document.createElement('div');
+			notification.className = 'pendingChanges';
+			notification.innerHTML = `
+			<p>We noticed unsaved changes from last time. Would you like to restore them?</p>
+        <button class="restore" data-form-id="${formId}">Restore</button>
+        <button class="discard" data-form-id="${formId}">Discard</button>`;
 
-			// Reset flag after a tick (gives DOM time to settle)
-			setTimeout(() => {
+			element.insertBefore(notification, form.ui.status.status);
+
+			notification.querySelector('.restore').addEventListener('click', async () => {
+				this.isRestoring = true;
+
+				new this.populate(element, changes);
+				this.a11y.announce('Previous changes restored');
+
 				this.isRestoring = false;
-			}, 0);
-
-			// Show restore status
-			this.showFormStatus(item.formId, 'restored');
-
-			if (window.jvbA11y) {
-				window.jvbA11y.announce('Your previous entry has been restored');
-			}
-		});
-	}
-
-	/**
-	 * Find form element that matches the cached data
-	 */
-	findFormElement(formData) {
-		// Try by form_id first (hidden field)
-		if (formData.data?.form_id) {
-			const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
-			if (form) return form;
-		}
-
-		// Try by form_type
-		if (formData.data?.form_type) {
-			const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
-			if (form) return form;
-		}
-
-		// Fallback: try by formId (if it was already registered)
-		return document.querySelector(`[data-form-id="${formData.formId}"]`);
-	}
-
-	/**
-	 * Show notification for pending changes
-	 */
-	/**
-	 * Show notification for pending changes
-	 */
-	showPendingNotification(formId, formData) {
-		const formElement = document.querySelector(`[data-form-id="${formId}"]`);
-		if (!formElement) return;
-
-		const notification = document.createElement('div');
-		notification.className = 'pending-changes-notification';
-		notification.innerHTML = `
-        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-        <button class="restore-changes" data-form-id="${formId}">Restore</button>
-        <button class="discard-changes" data-form-id="${formId}">Discard</button>
-    `;
-
-		formElement.insertBefore(notification, formElement.firstChild);
-
-		// Add handlers
-		notification.querySelector('.restore-changes').addEventListener('click', async () => {
-			await this.restorePendingForm(formId, formData);
-			notification.remove();
-		});
-
-		notification.querySelector('.discard-changes').addEventListener('click', async () => {
-			await this.discardPendingForm(formId);
-			notification.remove();
-		});
-	}
-
-	/**
-	 * Restore pending form data
-	 */
-	async restorePendingForm(formId, formData) {
-		const form = document.querySelector(`[data-form-id="${formId}"]`);
-		if (!form) return;
-
-		// Populate form with cached data
-		new this.populateForm(form, formData);
-
-		// Update status in store (mark as restored, not draft)
-		await this.store.save({
-			formId: formId,
-			data: formData,
-			status: 'restored',
-			timestamp: Date.now()
-		});
-
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Previous changes restored');
-		}
-	}
-
-	/**
-	 * Discard pending form data
-	 */
-	async discardPendingForm(formId) {
-		try {
-			await this.store.delete(formId);
-
-			if (window.jvbA11y) {
-				window.jvbA11y.announce('Previous changes discarded');
-			}
-		} catch (error) {
-			console.error('Failed to discard pending form:', error);
-		}
-	}
-
-	/**
-	 * Setup global handlers for standalone forms
-	 */
-	initListeners() {
-		// Only add if not already added
-		if (!this.globalHandlersAdded) {
-			document.addEventListener('click', this.clickHandler);
-			document.addEventListener('change', this.changeHandler);
-			document.addEventListener('blur', this.blurHandler, true);
-			document.addEventListener('input', this.inputHandler);
-			this.globalHandlersAdded = true;
-		}
-	}
-
-	/**
-	 * Register a standalone form (for front-end forms)
-	 */
-	registerForm(formElement, options = {}) {
-		if (!formElement) return;
-		const formId = formElement.dataset.formId || `form_${Date.now()}`;
-		formElement.dataset.formId = formId;
-
-		formElement.addEventListener('submit', this.submitHandler);
-
-		const formConfig = {
-			element: formElement,
-			id: formId,
-			status: '',
-			options: {
-				autosave: 'autosave' in formElement.dataset,
-				autoUpload: true,
-				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save ?? '',
-				formStatus: true,
-				cache: true,
-				...options
-			},
-			dependencies: new Map(),
-			data: this.collectFormData(formElement, true),
-		};
-
-		this.initializeFormFields(formElement, formConfig);
-		this.forms.set(formId, formConfig);
-
-		// Check for pending data - FIXED
-		if (this.store && formConfig.options.cache) {
-			const cached = this.store.get(formId);
-			if (cached && cached.data) {
-				this.showPendingNotification(formId, cached.data);
-			}
-		}
-
-		return formConfig;
-	}
-
-	/**
-	 * Initialize all special fields in a form
-	 */
-	initializeFormFields(form, formConfig = null) {
-		// Initialize Quill editors
-		this.initQuillEditors(form);
-
-		// Initialize repeater fields
-		this.initRepeaterFields(form, formConfig);
-
-		this.initTagListFields(form, formConfig);
-
-		// Initialize conditional fields
-		if (formConfig) {
-			this.initConditionalFields(form, formConfig);
-		}
-
-		// Initialize character limits
-		this.initCharacterLimits(form);
-
-		// Initialize image upload fields
-		this.initImageUploadFields(form, formConfig);
-
-		// Initialize tabs if present
-		if (window.jvbTabs && form.querySelector('nav.tabs')) {
-			formConfig.tabs = new window.jvbTabs(form);
-			this.forms.set(formConfig.formId, formConfig);
-			this.initSteppedForm(formConfig.formId);
-		}
-
-		// Scan for existing selector fields
-		if (window.jvbSelector) {
-			window.jvbSelector.scanExistingFields(form);
-		}
-	}
-
-	/**
-	 * Initialize stepped form functionality
-	 */
-	initSteppedForm(formId) {
-		const formConfig = this.forms.get(formId);
-		const form = formConfig.element;
-		const tabsInstance = formConfig.tabs;
-
-		const sections = form.querySelectorAll('.tab-content');
-		const totalSteps = sections.length;
-		const progressBar = form.querySelector('.form-progress .fill');
-		const stepText = form.querySelector('.step-text .current');
-		const tabButtons = form.querySelectorAll('nav.tabs button');
-
-		// Update progress display
-		const updateProgress = (currentStep) => {
-			const progress = (currentStep / totalSteps) * 100;
-			if (progressBar) {
-				progressBar.style.width = progress + '%';
-			}
-			if (stepText) {
-				stepText.textContent = currentStep;
-			}
-
-			// Update tab states
-			tabButtons.forEach((btn, idx) => {
-				const stepNum = idx + 1;
-				btn.classList.remove('current', 'completed', 'pending');
-
-				if (stepNum < currentStep) {
-					btn.classList.add('completed');
-				} else if (stepNum === currentStep) {
-					btn.classList.add('current');
-				} else {
-					btn.classList.add('pending');
-				}
-			});
-		};
-
-		// Next/Previous button handling
-		form.addEventListener('click', (e) => {
-			const nextBtn = e.target.closest('[data-action="next-step"]');
-			const prevBtn = e.target.closest('[data-action="prev-step"]');
-
-			if (nextBtn) {
-				e.preventDefault();
-				const currentSection = nextBtn.closest('.tab-content');
-				const currentStep = parseInt(currentSection.dataset.step);
-				const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
-
-				if (nextSection && this.validateStep(currentSection)) {
-					const nextTab = nextSection.dataset.tab;
-					tabsInstance.switchTab(nextTab, true);
-					updateProgress(currentStep + 1);
-
-					// Scroll to top of form
-					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
-				}
-			}
-
-			if (prevBtn) {
-				e.preventDefault();
-				const currentSection = prevBtn.closest('.tab-content');
-				const currentStep = parseInt(currentSection.dataset.step);
-				const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
-
-				if (prevSection) {
-					const prevTab = prevSection.dataset.tab;
-					tabsInstance.switchTab(prevTab, true);
-					updateProgress(currentStep - 1);
-
-					// Scroll to top of form
-					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
-				}
-			}
-		});
-
-		// Update progress when tabs are clicked directly
-		const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
-		tabsInstance.switchTab = (tab, updateHistory) => {
-			originalSwitchTab(tab, updateHistory);
-			const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
-			if (activeSection) {
-				const step = parseInt(activeSection.dataset.step);
-				updateProgress(step);
-			}
-		};
-
-		// Initialize progress
-		updateProgress(1);
-	}
-
-	/**
-	 * Validate current step before allowing progression
-	 * Can be enhanced with custom validation rules
-	 */
-	validateStep(section) {
-		const fields = section.querySelectorAll('.field');
-		let allValid = true;
-
-		fields.forEach(fieldWrapper => {
-			const input = fieldWrapper.querySelector('input, textarea, select');
-			if (input && !input.closest('[hidden]')) {
-				const isValid = this.validateField(input, fieldWrapper);
-				if (!isValid) {
-					allValid = false;
-				}
-			}
-		});
-
-		return allValid;
-	}
-
-
-	/**
-	 * Initialize Quill editors
-	 */
-	initQuillEditors(form) {
-		window.jvbQuill(form);
-	}
-
-	/**
-	 * Initialize repeater fields
-	 */
-	initRepeaterFields(form, formConfig) {
-		form.querySelectorAll('.repeater').forEach(repeater => {
-			const addButton = repeater.querySelector('.add-repeater-row');
-			const container = repeater.querySelector('.repeater-items');
-			const template = repeater.querySelector('template');
-
-			if (!addButton || !template || !container) return;
-
-			// Initialize Sortable for drag-and-drop
-			if (window.Sortable) {
-				new Sortable(container, {
-					handle: '.repeater-row-header',
-					animation: 150,
-					onEnd: () => {
-						this.updateRepeaterOrder(repeater, formConfig);
-					}
-				});
-			}
-
-			// Add row handler
-			addButton.addEventListener('click', () => {
-				this.addRepeaterRow(repeater, formConfig);
+				notification.remove();
 			});
 
-			// Remove row handlers
-			container.addEventListener('click', (e) => {
-				if (e.target.closest('.remove-row')) {
-					this.removeRepeaterRow(e.target.closest('.repeater-row'), formConfig);
-				}
-			});
-		});
-	}
-
-	/**
-	 * Add repeater row
-	 */
-	addRepeaterRow(repeater, formConfig) {
-		const container = repeater.querySelector('.repeater-items');
-		const template = repeater.querySelector('template');
-		const index = container.children.length;
-		const fieldName = repeater.dataset.field;
-
-		// Clone template
-		const row = template.content.cloneNode(true).firstElementChild;
-		row.dataset.index = index;
-
-		// Update field names
-		row.querySelectorAll('input, select, textarea').forEach(field => {
-			const originalName = field.name;
-			field.name = `${fieldName}:${index}:${originalName}`;
-			field.id = `${fieldName}-${index}-${originalName}`;
-
-			// Update label if exists
-			const label = field.nextElementSibling;
-			if (label && label.tagName === 'LABEL') {
-				label.htmlFor = field.id;
-			}
-		});
-
-		container.appendChild(row);
-
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'add',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.add
-			});
-		}
-
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Row added');
-		}
-	}
-
-	/**
-	 * Remove repeater row
-	 */
-	removeRepeaterRow(row, formConfig) {
-		const repeater = row.closest('.repeater');
-		const fieldName = repeater.dataset.field;
-
-		row.remove();
-
-		// Reindex remaining rows
-		this.updateRepeaterOrder(repeater, formConfig);
-
-		// Schedule save
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'remove',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.remove
-			});
-		}
-
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Row removed');
-		}
-	}
-
-	/**
-	 * Update repeater order after sorting
-	 */
-	updateRepeaterOrder(repeater, formConfig) {
-		const container = repeater.querySelector('.repeater-items');
-		const fieldName = repeater.dataset.field;
-
-		// Reindex all rows
-		Array.from(container.children).forEach((row, index) => {
-			row.dataset.index = index;
-
-			// Update field names
-			row.querySelectorAll('input, select, textarea').forEach(field => {
-				const parts = field.name.split(':');
-				if (parts.length === 3) {
-					const originalName = parts[2];
-					field.name = `${fieldName}:${index}:${originalName}`;
-					field.id = `${fieldName}-${index}-${originalName}`;
-
-					// Update label
-					const label = field.nextElementSibling;
-					if (label && label.tagName === 'LABEL') {
-						label.htmlFor = field.id;
-					}
-				}
-			});
-		});
-
-		// Schedule save
-		if (formConfig) {
-			this.scheduleSave(formConfig, {
-				type: 'repeater',
-				action: 'reorder',
-				fieldName: fieldName,
-				delay: this.repeaterDelays.reorder
-			});
-		}
-	}
-
-	/**
-	 * Initialize tag list fields
-	 */
-	initTagListFields(form, formConfig) {
-		form.querySelectorAll('.field.tag-list').forEach(field => {
-			const inputRow = field.querySelector('.tag-input-row');
-			const addButton = field.querySelector('.add-tag-item');
-			const tagsContainer = field.querySelector('.tag-items');
-			const template = field.querySelector('.tag-template');
-			const fieldName = field.dataset.field;
-			const tagFormat = field.dataset.tagFormat || 'first_field';
-
-			if (!inputRow || !addButton || !tagsContainer || !template) return;
-
-			// Get all input fields in the input row (excluding the button)
-			const getInputFields = () => {
-				return Array.from(inputRow.querySelectorAll('input, select, textarea'))
-					.filter(input => !input.closest('button'));
-			};
-
-			// Add tag handler
-			const addTag = () => {
-				const inputs = getInputFields();
-				const data = {};
-				let hasValue = false;
-
-				// Collect values from inputs
-				inputs.forEach(input => {
-					const fieldName = input.name.replace('new_', '');
-					const value = this.getFieldValue(input);
-
-					if (value) hasValue = true;
-					data[fieldName] = value;
-				});
-
-				if (!hasValue) {
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Please fill in at least one field', 'error');
-					}
-					inputs[0].focus();
-					return;
-				}
-
-				// Validate required fields using data-required attribute
-				const invalidField = inputs.find(input => {
-					const isRequired = ('required' in input.dataset && input.dataset.required === '1');
-					const value = this.getFieldValue(input);
-					return isRequired && !value;
-				});
-
-				if (invalidField) {
-					const fieldWrapper = invalidField.closest('.field');
-					const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
-					this.showError(fieldWrapper, `${fieldLabel} is required.`);
-
-					invalidField.focus();
-					return;
-				}
-
-				for (let input of inputs) {
-					let wrapper = field.closest('.field');
-					if (!this.validateField(input, wrapper)){
-						input.focus();
-						return;
-					}
-				}
-
-				// Clone template and populate
-				const index = tagsContainer.children.length;
-				const newTag = template.content.cloneNode(true).firstElementChild;
-				newTag.dataset.index = index;
-
-				// Update tag label
-				const tagLabel = newTag.querySelector('.tag-label');
-				if (tagLabel) {
-					tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
-				}
-
-				// Update hidden inputs
-				newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
-					const fieldKey = input.dataset.field;
-					input.name = `${fieldName}:${index}:${fieldKey}`;
-					input.value = data[fieldKey] || '';
-				});
-
-				tagsContainer.appendChild(newTag);
-
-				// Clear inputs
-				inputs.forEach(input => {
-					if (input.type === 'checkbox' || input.type === 'radio') {
-						input.checked = false;
-					} else {
-						input.value = '';
-					}
-					let field = input.closest('.field');
-					this.clearValidation(field);
-				});
-
-				// Focus first input
-				if (inputs.length > 0) {
-					inputs[0].focus();
-				}
-
-
-				// Schedule save
-				if (formConfig) {
-					this.scheduleSave(formConfig, {
-						type: 'tag_list',
-						action: 'add',
-						fieldName: fieldName,
-						delay: this.autoSaveDefaults.delay
-					});
-				}
-
-				if (window.jvbA11y) {
-					window.jvbA11y.announce('Item added');
-				}
-			};
-
-			// Add button click
-			addButton.addEventListener('click', addTag);
-
-			// Enter key support on last input
-			const inputs = getInputFields();
-			if (inputs.length > 0) {
-				// Tab through inputs, Enter on last one adds the tag
-				inputs[inputs.length - 1].addEventListener('keypress', (e) => {
-					if (e.key === 'Enter') {
-						e.preventDefault();
-						addTag();
-					}
-				});
-
-				// Enter on other inputs moves to next field
-				inputs.slice(0, -1).forEach((input, i) => {
-					input.addEventListener('keypress', (e) => {
-						if (e.key === 'Enter') {
-							e.preventDefault();
-							inputs[i + 1].focus();
-						}
-					});
-				});
-			}
-
-			// Remove tag handler
-			tagsContainer.addEventListener('click', (e) => {
-				if (e.target.closest('.remove-tag')) {
-					const tag = e.target.closest('.tag-item');
-					const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
-
-					tag.remove();
-
-					// Reindex remaining tags
-					this.reindexTagList(tagsContainer, fieldName);
-
-					// Schedule save
-					if (formConfig) {
-						this.scheduleSave(formConfig, {
-							type: 'tag_list',
-							action: 'remove',
-							fieldName: fieldName,
-							delay: this.autoSaveDefaults.delay
-						});
-					}
-
-					if (window.jvbA11y) {
-						window.jvbA11y.announce(`${tagText} removed`);
-					}
-				}
-			});
-		});
-	}
-
-	/**
-	 * Reindex tag list items
-	 */
-	reindexTagList(container, baseFieldName) {
-		Array.from(container.children).forEach((tag, index) => {
-			tag.dataset.index = index;
-
-			tag.querySelectorAll('input[type="hidden"]').forEach(input => {
-				const fieldKey = input.dataset.field;
-				input.name = `${baseFieldName}:${index}:${fieldKey}`;
-			});
-		});
-	}
-
-	/**
-	 * Get display text for tag based on format
-	 */
-	getTagDisplayText(data, format) {
-		const values = Object.values(data).filter(v => v);
-
-		if (values.length === 0) return 'New Item';
-
-		switch (format) {
-			case 'first_field':
-				return values[0];
-
-			case 'all_fields':
-				return values.join(', ');
-
-			default:
-				// Template format like "{name} ({email})"
-				if (format.includes('{')) {
-					let text = format;
-					for (const [key, value] of Object.entries(data)) {
-						text = text.replace(`{${key}}`, value);
-					}
-					return text;
-				}
-				// Use specific field
-				return data[format] || values[0];
-		}
-	}
-
-	/**
-	 * HTML escape helper
-	 */
-	escapeHtml(text) {
-		const div = document.createElement('div');
-		div.textContent = text;
-		return div.innerHTML;
-	}
-
-	/**
-	 * Initialize conditional fields
-	 */
-	initConditionalFields(form, formConfig) {
-		form.querySelectorAll('[data-depends-on]').forEach(field => {
-			const dependsOn = field.dataset.dependsOn;
-			const requiredValue = field.dataset.dependsValue;
-			const operator = field.dataset.dependsOperator || '==';
-
-			// Store dependency
-			if (!formConfig.dependencies.has(dependsOn)) {
-				formConfig.dependencies.set(dependsOn, []);
-			}
-			formConfig.dependencies.get(dependsOn).push({
-				field: field,
-				requiredValue: requiredValue,
-				operator: operator
+			notification.querySelector('.discard').addEventListener('click', async () => {
+				await this.store.remove(formId);
+				this.a11y.announce('Previous changes discared');
+				notification.remove();
 			});
 
-			// Check initial state
-			this.checkFieldDependency(form, field, dependsOn, requiredValue, operator);
-		});
-	}
-
-	/**
-	 * Check field dependency
-	 */
-	checkFieldDependency(form, field, dependsOn, requiredValue, operator) {
-		const triggerField = form.querySelector(`[name="${dependsOn}"]`);
-		if (!triggerField) return;
-
-		const value = this.getFieldValue(triggerField);
-		const shouldShow = this.evaluateCondition(value, requiredValue, operator);
-
-		this.toggleFieldVisibility(field, shouldShow);
-	}
-
-	/**
-	 * Evaluate conditional operator
-	 */
-	evaluateCondition(value, requiredValue, operator) {
-		const fieldStr = String(value || '');
-		const requiredStr = String(requiredValue || '');
-
-		switch (operator) {
-			case '==': return fieldStr === requiredStr;
-			case '!=': return fieldStr !== requiredStr;
-			case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
-			case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
-			case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
-			case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
-			case 'contains': return fieldStr.includes(requiredStr);
-			case 'empty': return fieldStr === '';
-			case 'not_empty': return fieldStr !== '';
-			default: return fieldStr === requiredStr;
 		}
-	}
-
-	/**
-	 * Toggle field visibility
-	 */
-	toggleFieldVisibility(field, show) {
-		const wrapper = field.closest('.field, fieldset');
-		if (!wrapper) return;
-
-		wrapper.hidden = !show;
-		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
-			control.disabled = !show;
-			if (!show && control.hasAttribute('required')) {
-				control.dataset.wasRequired = 'true';
-				control.removeAttribute('required');
-			} else if (show && control.dataset.wasRequired === 'true') {
-				control.setAttribute('required', '');
-				delete control.dataset.wasRequired;
-			}
-		});
-	}
-
-	/**
-	 * Initialize character limits
-	 */
-	initCharacterLimits(form) {
-		form.querySelectorAll('[data-limit]').forEach(input => {
-			const limit = parseInt(input.dataset.limit, 10);
-			const field = input.closest('.field');
-
-			// Create counter if it doesn't exist
-			let counter = field?.querySelector('.char-count');
-			if (!counter && field) {
-				counter = document.createElement('div');
-				counter.className = 'char-count';
-				counter.innerHTML = `<span class="current">0</span> / <span class="limit">${limit}</span>`;
-				field.appendChild(counter);
-			}
-
-			const updateCount = () => {
-				const length = input.value.length;
-				if (counter) {
-					counter.querySelector('.current').textContent = length;
-					counter.classList.toggle('exceeded', length > limit);
-				}
-
-				// Truncate if exceeds limit
-				if (length > limit) {
-					input.value = input.value.substring(0, limit);
-					if (counter) {
-						counter.querySelector('.current').textContent = limit;
-					}
-				}
-			};
-
-			input.addEventListener('input', updateCount);
-			updateCount(); // Initial count
-		});
-	}
-
-	/**
-	 * Initialize image upload fields
-	 */
-	initImageUploadFields(form, config) {
-		window.jvbUploads.scanFields(form, config.options.autoUpload);
-	}
-
-	/* ========== Event Handlers ========== */
-
-	async handleSubmit(event) {
-		const form = event.target;
-
-		if (!form.dataset.formId) return;
-		const formConfig = this.forms.get(form.dataset.formId);
-
-		// Handle subscriber-based forms
-		if (this.subscribers.size > 0) {
-			event.preventDefault();
-			const formData = this.collectFormData(form);
-
-			// Notify subscribers (they'll handle actual submission)
-			this.notify('form-submit', {
-				formId: form.dataset.formId,
-				fullData: formData,
-				config: formConfig
-			});
-		}
-	}
-
-	handleFormSuccess(form, data) {
-		// Clear previous errors
-		form.querySelectorAll('.error-message').forEach(el => el.remove());
-		form.querySelectorAll('.field-error').forEach(el =>
-			el.classList.remove('field-error')
-		);
-
-		// Add success class to form
-		form.classList.add('form-success');
-
-		// Show success message if provided
-		if (data.message) {
-			const success = document.createElement('div');
-			success.className = 'form-success-message success-message';
-			success.textContent = data.message;
-			form.insertBefore(success, form.firstChild);
-
-			const icon = window.getIcon?.('check-circle');
-			if (icon) {
-				icon.classList.add('success-icon');
-				success.prepend(icon);
-			}
-		}
-
-		// If there's a title/description (for registration success)
-		if (data.title || data.description) {
-			const successBox = document.createElement('div');
-			successBox.className = 'success-box';
-
-			if (data.title) {
-				const title = document.createElement('h3');
-				title.textContent = data.title;
-				successBox.appendChild(title);
-			}
-
-			if (data.description) {
-				const descriptions = Array.isArray(data.description)
-					? data.description
-					: [data.description];
-
-				descriptions.forEach(desc => {
-					const p = document.createElement('p');
-					p.textContent = desc;
-					successBox.appendChild(p);
-				});
-			}
-
-			form.insertBefore(successBox, form.firstChild);
-		}
-
-		//  DELETE CACHED FORM DATA ON SUCCESS
-		if (form.dataset.formId) {
-			this.store.delete(form.dataset.formId).catch(err => {
-				console.warn('Failed to clear form cache:', err);
-			});
-
-			// Clear form config dirty state
-			const formConfig = this.forms.get(form.dataset.formId);
-			if (formConfig) {
-				formConfig.isDirty = false;
-				formConfig.lastSaved = Date.now();
-				formConfig.data = {}; // Clear cached data
-			}
-		}
-
-		// Announce success for accessibility
-		if (window.jvbA11y) {
-			window.jvbA11y.announce(data.message || 'Form submitted successfully');
-		}
-
-		// Trigger custom event
-		form.dispatchEvent(new CustomEvent('jvb-form-success', {
-			detail: data
-		}));
-	}
-
-	handleFormError(form, data) {
-		// Clear all previous errors
-		form.querySelectorAll('.error-message').forEach(el => el.remove());
-		form.querySelectorAll('.field-error, .has-error').forEach(el => {
-			el.classList.remove('field-error', 'has-error');
-		});
-
-		// Clear validation states using existing method
-		form.querySelectorAll('.field').forEach(fieldWrapper => {
-			this.clearValidation(fieldWrapper);
-		});
-
-		// Handle field-specific errors
-		if (data.field) {
-			const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
-			if (fieldWrapper) {
-				// Use existing showError method for consistency
-				this.showError(fieldWrapper, data.message);
-
-				// Mark as touched so validation persists
-				this.touchedFields.add(data.field);
-
-				// Scroll to error
-				fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
-
-				// Focus the input for better UX
-				const input = fieldWrapper.querySelector('input, textarea, select');
-				if (input) {
-					input.focus();
-				}
-			}
-		} else {
-			// General form error (not field-specific)
-			const error = document.createElement('div');
-			error.className = 'form-error error-message';
-			error.textContent = data.message;
-
-			// Add icon for consistency
-			const icon = window.getIcon?.('close-circle');
-			if (icon) {
-				icon.classList.add('error-icon');
-				error.prepend(icon);
-			}
-
-			form.insertBefore(error, form.firstChild);
-
-			// Scroll to top to show the error
-			form.scrollIntoView({ behavior: 'smooth', block: 'start' });
-		}
-
-		// Announce error for accessibility
-		if (window.jvbA11y) {
-			const announcement = data.field
-				? `Error in ${data.field}: ${data.message}`
-				: `Form error: ${data.message}`;
-			window.jvbA11y.announce(announcement);
-		}
-
-		// Trigger custom event
-		form.dispatchEvent(new CustomEvent('jvb-form-error', {
-			detail: data
-		}));
-	}
-
-	handleClick(e) {
-		if (window.targetCheck(e, 'div.quantity')) {
-			let container = window.targetCheck(e, 'div.quantity');
-			this.handleNumberClick(e, container.querySelector('input'));
-		} else if (window.targetCheck(e, '[data-action]')) {
-			let actionEl = window.targetCheck(e, '[data-action]');
-			let action = actionEl.dataset.action;
-			let form = actionEl.closest('form');
-
-			switch (action) {
-				case 'clear-form':
-					if (form?.dataset.formId) {
-						this.store.delete(form.dataset.formId);
-						form.reset();
-						// Hide the status message
-						form.querySelector('.fstatus').hidden = true;
-					}
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Form cleared, starting fresh');
-					}
-					break;
-
-				case 'dismiss-restore':
-					form.querySelector('.fstatus').hidden = true;
-					break;
-			}
-		}
-	}
-
-
-	handleNumberClick(e, input) {
-		let change = 0;
-
-		if (e.target.closest('.increase')) {
-			change += 1;
-		} else if (e.target.closest('.decrease')) {
-			change -=1;
-		}
-		if (change !== 0) {
-			let step = parseFloat(input.step);
-			//Allow for cents, but default to increasing by 1
-			step = Math.max(step, 1);
-
-			if(e.ctrlKey && e.shiftKey) {
-				step = step * 50;
-			} else if (e.ctrlKey) {
-				step = step * 5;
-			} else if (e.shiftKey) {
-				step = step * 10;
-			}
-
-			let value = (input.value === '') ? 0 : parseFloat(input.value);
-
-			input.value = (value + (step * change));
-			this.handleNumberLimits(input);
-		}
-	}
-
-	handleNumberLimits(input) {
-		let [
-			min,
-			max,
-			increase,
-			decrease
-		] = [
-			input.min,
-			input.max,
-			input.closest('.quantity')?.querySelector('.increase'),
-			input.closest('.quantity')?.querySelector('.decrease')
-		];
-		let value = parseFloat(input.value);
-		if (value < min) {
-			input.value = min;
-			decrease.disabled = true;
-		} else if (value > max) {
-			input.value = max;
-			increase.disabled = false;
-		} else if (increase.disabled) {
-			increase.disabled = false;
-		} else if (decrease.disabled) {
-			decrease.disabled = false;
-		}
-	}
-
-	handleChange(event) {
-		if (event.target.closest('[data-ignore]') || this.isRestoring) {
-			return;
-		}
-		const target = event.target;
-		const form = target.form || target.closest('form');if (!form) return;
-
-		const formConfig = this.forms?.get(form.dataset.formId);
-		if (!formConfig) return;
-
-		if (formConfig.options.autosave || this.subscribers.size > 0) {
-			// Check conditional fields
-			const dependencies = formConfig.dependencies.get(target.name);
-			if (dependencies) {
-				dependencies.forEach(dep => {
-					this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator);
-				});
-			}
-
-			// Schedule auto-save if enabled
-			const delay = this.getDelayForField(target);
-			this.scheduleSave(formConfig, delay);
-		}
-	}
-
-	handleBlur(e) {
-		if (e.target.closest('[data-ignore]') || this.isRestoring) {
-			return;
-		}
-		const target = e.target;
-		const form = target.form || target.closest('form');
-
-		if (!form) return;
-
-
-		const input = e.target.closest('input, textarea, select');
-		if (input) {
-			const fieldWrapper = this.findFieldWrapper(input);
-			if (fieldWrapper) {
-				// Mark as touched and validate
-				const fieldName = fieldWrapper.dataset.field;
-				if (fieldName) {
-					if (this.shouldDebounce(input)) {
-						window.debouncer.cancel(`validate_${fieldName}`);
-					}
-					this.touchedFields.add(fieldName);
-				}
-				this.validateField(input, fieldWrapper);
-			}
-			const formConfig = this.forms?.get(form.dataset.formId);
-			if (formConfig) {
-				// Shorter delay on blur
-				this.scheduleSave(formConfig, {
-					type: 'blur',
-					fieldName: target.name,
-					delay: 1500
-				});
-			}
-		}
-	}
-
-	handleInput(e) {
-		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
-			return;
-		}
-		const input = e.target.closest('input, textarea, select');
-		if (!input) return;
-
-		let form = input.closest('form');
-		this.showFormStatus(form.dataset.formId, 'pending');
-
-		const fieldWrapper = this.findFieldWrapper(input);
-		if (!fieldWrapper) return;
-
-		const fieldName = fieldWrapper.dataset.field;
-		if (fieldName) {
-			this.touchedFields.add(fieldName);
-		}
-
-		if (this.shouldDebounce(input)){
-			window.debouncer.schedule(
-				`validate_${fieldName}`,
-				() => this.validateField.bind(this),
-				500
-			)
-		}
-	}
-
-	/***************************************************************
-	 FORM VALIDATION
-	***************************************************************/
-	/**
-	 * Initialize validation rules
-	 */
 	initValidators() {
-		return {
+		this.validators = {
 			email: {
 				pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
 				message: 'Please enter a valid email address'
@@ -1346,1121 +233,1191 @@
 			}
 		};
 	}
-	/**
-	 * Find the field wrapper (handles both simple and complex fields)
-	 */
-	findFieldWrapper(input) {
-		// Try to find the closest .field wrapper
-		let wrapper = input.closest('.field');
-
-		// If we're in a repeater row, make sure we get the right field wrapper
-		if (!wrapper) {
-			wrapper = input.closest('[data-field]');
-		}
-
-		return wrapper;
+	validateField(input) {
+		const result = this.performValidation(input);
+		this.updateValidationUI(input, result);
+		return result.isValid;
 	}
-
-	/**
-	 * Check if input should be debounced
-	 */
-	shouldDebounce(input) {
-		const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
-		return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
-	}
-
-	/**
-	 * Validate a single field
-	 */
-	validateField(input, fieldWrapper) {
+	performValidation(input) {
+		const field = input.closest('.field');
 		const value = this.getFieldValue(input);
-		const fieldName = fieldWrapper.dataset.field;
 
-		// Skip validation if field hasn't been touched yet (unless it's required)
-		if (!this.touchedFields.has(fieldName) && !input.required) {
-			return true;
-		}
-
-		// Skip validation if field is empty and not required
 		if (!value && !input.required) {
-			this.clearValidation(fieldWrapper);
-			return true;
+			return { isValid: true, message: '' };
 		}
 
-		// Check required
 		if (input.required && !value) {
-			this.showError(fieldWrapper, 'This field is required');
-			return false;
+			return { isValid: false, message: 'This field is required' };
 		}
 
-		// Check HTML5 validity first
-		if (input.checkValidity && !input.checkValidity()) {
-			this.showError(fieldWrapper, input.validationMessage);
-			return false;
+		if(input.checkValidity && !input.checkValidity()){
+			return {isValid: false, message: input.validationMessage};
 		}
 
-		// Custom pattern validation from data attribute
-		const pattern = fieldWrapper.dataset.pattern;
-		if (pattern && value) {
-			const regex = new RegExp(pattern);
+		if (value && Object.hasOwn(field.dataset, 'pattern')) {
+			const regex = new RegExp(field.dataset.pattern);
 			if (!regex.test(value)) {
-				const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
-				this.showError(fieldWrapper, message);
-				return false;
+				return {isValid: false, message: field.dataset.validationMessage || 'Invalid format'};
 			}
 		}
 
-		// Type-specific validation
-		const validateType = fieldWrapper.dataset.validate || input.type;
-		if (validateType && this.validators[validateType]) {
-			const validator = this.validators[validateType];
+		if (Object.hasOwn(field.dataset, 'validate') || input.type) {
+			const validator = this.validators[field.dataset.validate||input.type];
 
 			if (validator.pattern && !validator.pattern.test(value)) {
-				this.showError(fieldWrapper, validator.message);
-				return false;
+				return {isValid: false, message: validator.message};
 			}
 
 			if (validator.test) {
-				const result = validator.test(value, fieldWrapper);
+				const result = validator.test(value, field);
 				if (result !== true) {
-					this.showError(fieldWrapper, result);
-					return false;
+					return {isValid: false, message: result};
 				}
 			}
 		}
 
-		// All validations passed
-		this.showSuccess(fieldWrapper);
-		this.notify('field-validated', input);
-		return true;
+		return {isValid: true, message: ''};
+	}
+	updateValidationUI(input, result) {
+		if (result.isValid) {
+			this.showSuccess(input, result.message);
+		} else {
+			this.showError(input, result.message);
+		}
 	}
 
+	handleClick(e) {
+		let form = this.getForm(e.target);
+		if (!form) return;
 
-
-	/**
-	 * Show success state (green checkmark)
-	 */
-	showSuccess(fieldWrapper, textMessage = '') {
-		if (!fieldWrapper) return;
-
-		// Find validation elements (they might be in field-input-wrapper or field-content)
-		const success = fieldWrapper.querySelector('.validation-icon.success');
-		const error = fieldWrapper.querySelector('.validation-icon.error');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
-
-		// Remove error state
-		fieldWrapper.classList.remove('has-error');
-		input?.classList.remove('error');
-
-		// Add success state
-		fieldWrapper.classList.add('has-success');
-
-		// Show checkmark (if element exists)
-		if (success) {
-			success.hidden = false;
-		}
-		if (error) {
-			error.hidden = true;
-		}
-
-		// Hide error message
-		if (message) {
-			if (textMessage === '') {
-				message.hidden = true;
-				message.textContent = '';
-			} else {
-				message.hidden = false;
-				message.textContent = textMessage;
+		const itemAction = window.targetCheck(e, '[data-action]');
+		if (itemAction) {
+			let action = itemAction.dataset.action;
+			switch (action) {
+				case 'clear-form':
+					this.store.delete(form.id);
+					form.element.reset();
+					form.ui.status.status.hidden = true;
+					this.a11y.announce('Form cleared, starting fresh');
+					break;
+				case 'dismiss-restore':
+					form.ui.status.status.hidden = true;
+					break;
 			}
 		}
 	}
 
-	/**
-	 * Show error state (red message below field)
-	 */
-	showError(fieldWrapper, errorMessage) {
-		if (!fieldWrapper) return;
+	handleChange(e) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) return;
 
-		const success = fieldWrapper.querySelector('.validation-icon.success');
-		const error = fieldWrapper.querySelector('.validation-icon.error');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
-
-		// Remove success state
-		fieldWrapper.classList.remove('has-success');
-
-		// Add error state
-		fieldWrapper.classList.add('has-error');
-		input?.classList.add('error');
-
-		// Hide checkmark (if element exists)
-		if (success) {
-			success.hidden = true;
-		}
-		//show x
-		if (error) {
-			error.hidden = false;
+		let field = this.getField(e.target);
+		//Dependencies
+		if (this.dependencies.has(field.dataset.field)) {
+			let dependency = this.dependencies.get(field.dataset.field);
+			dependency.items.forEach(item => {
+				this.checkFieldDependency(item, field.dataset.field);
+			});
 		}
 
-		// Show error message
-		if (message) {
-			message.hidden = false;
-			message.textContent = errorMessage;
+		let form = this.getForm(e.target);
+		//Autosave
+		if (!form || !form.options.cache) return;
+		this.updateItem(field.dataset.field, this.getFieldValue(e.target), form);
+	}
+
+	handleBlur(e) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) return;
+		let form = this.getForm(e.target);
+		if (!form) return;
+
+		let field = this.getField(e.target);
+		let fieldName = field.dataset.field;
+		window.debouncer.cancel(`form:${form.id}:validate:${fieldName}`);
+		this.validateField(e.target);
+
+		if (form.options.cache) {
+			this.updateItem(fieldName, this.getFieldValue(e.target), form);
 		}
 	}
 
-	/**
-	 * Clear validation state
-	 */
-	clearValidation(fieldWrapper) {
-		if (!fieldWrapper) return;
+	handleInput(e){
+		let form = this.getForm(e.target);
+		if (!form || !form.options.cache) return;
 
-		const icon = fieldWrapper.querySelector('.validation-icon');
-		const message = fieldWrapper.querySelector('.validation-message');
-		const input = fieldWrapper.querySelector('input, textarea, select');
+		let field = this.getField(e.target);
+		if (!field) return;
 
-		fieldWrapper.classList.remove('has-error', 'has-success');
-		input?.classList.remove('error');
-
-		if (icon) {
-			icon.hidden = true;
-		}
-
-		if (message) {
-			message.hidden = true;
-			message.textContent = '';
-		}
-	}
-
-	/* ========== Auto-save functionality ========== */
-	/**
-	 * Get appropriate delay based on field type and context
-	 */
-	getDelayForField(field) {
-		// Text fields get longer delay for typing
-		if (field.type === 'text' || field.type === 'textarea') {
-			return this.autoSaveDefaults.typingDelay;
-		}
-
-		// Checkboxes, radios, selects get shorter delay
-		if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) {
-			return 1000;
-		}
-
-		// Default delay
-		return this.autoSaveDefaults.delay;
-	}
-	scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
-		if (!formConfig.options.autosave) {
-			return;
-		}
-		document.addEventListener('input', this.saveCheck, {passive: true});
-		const saveKey = `autosave_${formConfig.id}`;
-
-		this.debouncer.schedule(
-			saveKey,
-			() => this.autosave(formConfig),
-			delay
+		this.showFormStatus(form, 'pending');
+		window.debouncer.schedule(
+			`form:${form.id}:validate:${field.dataset.field}`,
+			() => this.validateField.bind(this),
+			500
 		);
 	}
 
-	//Extend delay if user is currently typing
-	saveCheck(e) {
-		let form = e.target.closest('form[data-id]');
-		if (!form) {
-			return;
+	async handleSubmit(e) {
+		let form = this.getForm(e.target);
+		if (!form) return;
+
+		if (this.subscribers.size > 0) {
+			e.preventDefault();
+			const storedData = await this.store.get(form.id);
+
+			this.notify('form-submit', {
+				config: form,
+				data: storedData?.changes || {}
+			});
 		}
 
-		this.scheduleSave(this.forms.get(form.dataset.id));
-	}
-
-	async autosave(formConfig) {
-		const formData = this.collectFormData(formConfig.element);
-
-		this.showFormStatus(formConfig.id, 'saving');
-
-		// DataStore will now automatically:
-		// - Convert Sets/Maps to Arrays/Objects
-		// - Strip DOM references
-		// - Validate serializability
-		await this.store.save({
-			formId: formConfig.id,
-			data: formData,
-			status: 'draft',
-			timestamp: Date.now()
-		}).then(() => {
-			this.showFormStatus(formConfig.id, 'autosaved');
-		}).catch(error => {
-			console.error('Autosave failed:', error);
-			this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
-		});
-
-		// Get only changed fields
-		const changes = this.getChangedFields(formConfig.data, formData);
-		if (Object.keys(changes).length === 0) return;
-
-		// Update stored data
-		formConfig.data = formData;
-		this.forms.set(formConfig.id, formConfig);
-		document.removeEventListener('input', this.handleInput);
-
-		for (let [key, value] of Object.entries(formData)) {
-			// Complex fields need full data
-			if (typeof value === 'object') {
-				changes[key] = value;
-			}
+		if (form.options.showSummary) {
+			const storedData = await this.store.get(form.id);
+			this.showSummary(form.id, {
+				config: form,
+				data: storedData?.changes || {}
+			});
 		}
-
-		// Notify
-		this.notify('form-autosave', {
-			formId: formConfig.id,
-			changes: changes,
-			fullData: formData,
-			config: formConfig
-		});
 	}
 
 	/**
-	 * Check if form has unsaved changes
+	 * Updates the item, schedules caching if
+	 * @param name
+	 * @param value
+	 * @param form
 	 */
-	hasUnsavedChanges(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return false;
-
-		// Check if there are pending operations
-		if (formConfig.operations?.size > 0) return true;
-
-		// Check if current data differs from snapshot
-		const currentData = this.collectFormData(formConfig.element);
-		const changes = this.getChangedFields(formConfig.data, currentData);
-
-		return Object.keys(changes).length > 0;
+	updateItem(name, value, form) {
+		if (!this.changes.has(form.id)) {
+			this.changes.set(form.id, {
+				id: form.id,
+				timestamp: Date.now(),
+				src: window.location.pathname,
+				changes: {},
+			});
+		}
+		let changes = this.changes.get(form.id);
+		changes.changes[name] = value;
+		this.changes.set(form.id, changes);
+		this.scheduleBackup();
 	}
 
-	showFormStatus(formID, status, message='') {
-		let form = this.forms.get(formID);
-		if (!form?.options.formStatus) {
-			return;
+	scheduleBackup()  {
+		window.debouncer.schedule(
+			`form_changes`,
+			async () => {
+				if (this.changes.size > 0) {
+					await this.store.saveMany(this.changes);
+					for(let formId of this.changes.keys()) {
+						this.showFormStatus(formId, 'autosaved');
+					}
+					this.changes.clear();
+				}
+			},
+			2000
+		);
+	}
+
+	saveCache(formId) {
+		if (!this.changes.has(formId)) return;
+		let changes = this.changes.get(formId);
+		if (changes.size === 0) return;
+		this.store.save(changes).then(()=>{});
+		this.changes.delete(formId);
+	}
+
+
+	/**
+	 * Register a form for handling
+	 * @param {HTMLElement} form
+	 * @param {object} options
+	 */
+	registerForm(form, options) {
+		//Bail if form already registered
+		if (Object.hasOwn(form.dataset, 'formId') && this.forms.has(form.dataset.formId)) return;
+
+		if (!Object.hasOwn(form.dataset, 'formId')) {
+			form.dataset.formId = window.generateID('form_');
 		}
+		const formId = form.dataset.formId;
 
-		if (form.status === status){
-			return;
-		}
+		this.addFormListeners(form);
 
-		form.status = status;
-
-		const statusWrap = form.element.querySelector('.fstatus');
-		statusWrap.hidden = false;
-		const statusElement = statusWrap.querySelector('.message');
-		statusElement.textContent = '';
-		statusWrap.querySelector('.icon')?.remove();
-		statusWrap.querySelector('.actions')?.remove(); // Clear old actions
-
-		const messages = {
-			'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'
+		const config = {
+			element: form,
+			id: formId,
+			status: '',
+			options: {
+				autoUpload: false,
+				delay: options.delay??1500,
+				endpoint: options.save??form.dataset.save??'',
+				formStatus: options.showStatus??true,
+				showSummary: false,
+				cache: options.cache??true,
+			},
+			ui: window.uiFromSelectors(this.selectors.forms, form)
 		};
 
-		const icons = {
-			'autosaved': 'check-circle',
-			'submitted': 'check-circle',
-			'restored': 'history',
-			'error': 'close-circle',
-			'offline': 'cloud-slash',
-			'pending': 'exclamation-mark'
+		if (config.showSummary && !this.summaryTemplate) {
+			this.defineSummaryTemplate();
 		}
 
-		let icon = window.getIcon(icons[status]);
-		if (icon) {
-			statusWrap.prepend(icon);
-		}
+		this.initializeFields(form, config);
+		this.forms.set(formId, config);
 
-		if (message === '') {
-			message = messages[status] || status;
-		}
-		statusElement.textContent = message;
-		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
-
-		// Add action buttons for certain statuses
-		if (status === 'restored') {
-			const actions = document.createElement('div');
-			actions.className = 'actions';
-			actions.innerHTML = `
-            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
-            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
-        `;
-			statusWrap.appendChild(actions);
-
-			// Auto-dismiss after 10 seconds
-			setTimeout(() => statusWrap.hidden = true, 10000);
-		}
-
-		// Auto-hide success messages
-		if (status === 'submitted') {
-			setTimeout(() => statusWrap.hidden = true, 3000);
-		}
+		return config;
 	}
+		clearForm(formId) {
+			const config = this.forms.get(formId);
+			if (!config) return;
 
-	cleanupSpecialFields() {
-		this.specialFields.forEach(field => {
-			if (field.type === 'quill' && field.instance) {
-				// Remove Quill toolbar
-				const toolbar = field.instance.container.previousSibling;
-				if (toolbar?.classList.contains('ql-toolbar')) {
-					toolbar.remove();
+			if (config.unsubscribeTabs) {
+				config.unsubscribeTabs();
+			}
+			if(config.tabs) {
+				window.jvbTabs.removeTab(config.element);
+			}
+
+			if (config.cache && this.changes.has(formId)) this.saveCache(formId);
+
+			// Cleanup items
+			for (let [id, input] of this.inputs.entries()) {
+				if (input.form === formId) {
+					this.inputs.delete(id);
 				}
 			}
-		});
+			// Clean up dependencies for this form
+			this.dependencies.forEach((dependency, fieldName) => {
+				dependency.items = dependency.items.filter(item => item.form !== formId);
 
-		this.uploader?.destroy();
-
-		this.specialFields.clear();
-	}
-
-	/* ========== Form Data Methods ========== */
-
-	collectFormData(form) {
-		if (Object.hasOwn(form.dataset, 'timeline')) {
-			return this.collectTimeline(form);
-		}
-		//Table forms are handled separately
-		if (form.classList.contains('table') && form.tagName === 'FORM') {
-			return {};
-		}
-
-		const formData = new FormData(form);
-		let data = {};
-		const repeaterData = {};
-		const postData = {};
-
-		for (let [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) continue;
-
-			const processor = this.getFieldProcessor(key);
-			processor(key, value, data, repeaterData, postData, form);
-		}
-		if (Object.keys(postData).length !== 0) {
-			data = this.mergeRepeaterData(data, repeaterData);
-			return this.mergePostData(data, postData);
-		}
-		return this.mergeRepeaterData(data, repeaterData);
-	}
-
-	collectTimeline(form) {
-		let data = {};
-		let posts = {}; // Temporary object keyed by post ID
-		let postOrder = []; // Track order as encountered (preserves DOM/drag order)
-		let formData = new FormData(form);
-
-		for (const [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) {
-				continue;
-			}
-			const match = key.match(/^\[(\d+)](.+)$/);
-			if (match) {
-				// Timeline-specific field: [postId]fieldName
-				const [, postId, fieldName] = match;
-				if (!posts[postId]) {
-					posts[postId] = {
-						id: parseInt(postId),
-					};
-					postOrder.push(postId); // Track first occurrence
-				}
-				if (fieldName === 'post_thumbnail') {
-					posts[postId]['post_thumbnail'] = parseInt(form.querySelector(`[name="${key}"]`).closest('.item')?.dataset.id);
-				} else {
-					const processor = this.getFieldProcessor(fieldName);
-					processor(fieldName, value, posts[postId], {}, {}, form);
-				}
-
-			} else {
-				// Shared field (post_title, taxonomies, etc.)
-				const processor = this.getFieldProcessor(key);
-				processor(key, value, data, {}, {}, form);
-			}
-		}
-
-		// Convert to array in DOM order (matches menu_order)
-		data.timeline = postOrder.map(id => posts[id]);
-
-		delete data['form-id'];
-		delete data['sendAll'];
-		delete data['timeline_temp'];
-		delete data['']; // Empty key
-
-		return data;
-	}
-
-	getFieldProcessor(key) {
-		if (key.includes('::')) return this.processGroupField;
-		if (key.includes(':')) return this.processRepeaterField;
-		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
-		return this.processRegularField;
-	}
-
-	mergeRepeaterData(data, repeaterData) {
-		Object.keys(repeaterData).forEach(fieldName => {
-			// Clean up empty rows and convert to array format
-			const cleanedRows = {};
-			Object.keys(repeaterData[fieldName]).forEach(index => {
-				const rowData = repeaterData[fieldName][index];
-				if (Object.keys(rowData).length > 0) {
-					cleanedRows[index] = rowData;
+				// Remove the dependency entry entirely if no items left
+				if (dependency.items.length === 0) {
+					this.dependencies.delete(fieldName);
 				}
 			});
 
-			// Convert to sequential array
-			data[fieldName] = Object.values(cleanedRows);
-		});
-		return data;
-	}
+			if (Object.hasOwn(config, 'hasQuill') && this.quillInstances.has(formId)) {
+				const instances = this.quillInstances.get(formId);
+				instances.forEach(quillInstance => {
+					// Disable the editor
+					quillInstance.disable();
 
-	mergePostData(data, postData) {
-		for (let [postId, fields] of Object.entries(postData)) {
-			data[postId] = fields;
-		}
-		return data;
-	}
+					// Remove all event listeners
+					quillInstance.off('text-change');
+					quillInstance.off('selection-change');
 
-	processRepeaterField(key, value, data, repeaterData, postData, form) {
-		let [fieldName, index, subField] = key.split(':');
+					// Get the container elements
+					const container = quillInstance.container.parentElement;
+					const toolbar = container?.querySelector('.ql-toolbar');
 
-		const isArray = subField.endsWith('[]');
-		subField = subField.replace('[]', '');
+					// Remove toolbar
+					if (toolbar) {
+						toolbar.remove();
+					}
 
-		//Ensure this repeater and row is in repeaterData
-		if (!repeaterData[fieldName]) {
-			repeaterData[fieldName] = {};
-		}
-		if (!repeaterData[fieldName][index]) {
-			repeaterData[fieldName][index] = {};
-		}
+					// Clear the editor content
+					quillInstance.setText('');
 
-		if (isArray || repeaterData[fieldName][index][subField]) {
-			// Initialize as array if not already
-			if (!repeaterData[fieldName][index][subField]) {
-				repeaterData[fieldName][index][subField] = [];
-			} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
-				repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
+					// Remove container
+					if (container && container.classList.contains('editor-container')) {
+						const textarea = container.nextElementSibling;
+						if (textarea?.tagName === 'TEXTAREA') {
+							textarea.style.display = '';
+						}
+						container.remove();
+					}
+				});
+
+				this.quillInstances.delete(formId);
 			}
-			repeaterData[fieldName][index][subField].push(value);
-		} else {
-			// Single value field
-			repeaterData[fieldName][index][subField] = value;
-		}
-	}
-	processGroupField(key, value, data, repeaterData, postData, form) {
-		const keys = key.split('::');
-		const rootGroup = keys[0];
-
-		// Initialize root group if it doesn't exist
-		if (!data[rootGroup]) {
-			data[rootGroup] = {};
-		}
-
-		// Build nested structure step by step
-		let current = data[rootGroup];
-		for (let i = 1; i < keys.length - 1; i++) {
-			const groupKey = keys[i];
-			if (!current[groupKey]) {
-				current[groupKey] = {};
-			}
-			current = current[groupKey];
-		}
-
-		// Set the final field value
-		const fieldKey = keys[keys.length - 1];
-
-		// Handle array values (checkboxes, multi-selects)
-		if (current[fieldKey] !== undefined) {
-			if (!Array.isArray(current[fieldKey])) {
-				current[fieldKey] = [current[fieldKey]];
-			}
-			current[fieldKey].push(value);
-		} else {
-			current[fieldKey] = value;
-		}
-	}
-	processLocationField(key, value, data, repeaterData, postData, form) {
-		let [fieldKey, v ] = key.split('[');
-		v = v.replace(']','');
-		if (!Object.hasOwn(data, fieldKey)) {
-			data[fieldKey] = {};
-
-			if (!Object.hasOwn(data, 'sendAll')) {
-				data['sendAll'] = [fieldKey];
-			} else if (!data['sendAll'].includes(fieldKey)) {
-				data['sendAll'].push(fieldKey);
-			}
-		}
-		data[fieldKey][v] = value;
-	}
-
-	processRegularField(key, value, data, repeaterData, postData, form) {
-		//handle array values (like checkboxes/selects)
-		key = key.replace('[]','');
-		if (data[key]) {
-			if (!Array.isArray(data[key])) {
-				data[key] = [data[key]];
-			}
-			data[key].push(value);
-		} else {
-			data[key] = value;
-		}
-	}
-
-	/**
-	 * Get field value (handles different input types)
-	 */
-	getFieldValue(input) {
-		if (!input) return '';
-
-		if (input.type === 'checkbox') {
-			return input.checked ? input.value || '1' : '';
-		} else if (input.type === 'radio') {
-			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
-			return checked ? checked.value : '';
-		} else if (input.type === 'select-multiple') {
-			return Array.from(input.selectedOptions).map(o => o.value);
-		}
-
-		return input.value?.trim() || '';
-	}
-
-	getChangedFields(original, current) {
-		return window.getDifferences?.map(original, current) || {};
-	}
-
-	/*******************************************************
-	 Field Summary
-	*******************************************************/
-	/**
-	 * Show a comprehensive summary of form submission
-	 */
-	showSummary(formId, clear = 'form') {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
-		const summary = window.getTemplate('formSummary');
-		if (!summary) return;
-		const wrapper = summary.querySelector('.result');
-
-		// Fields to skip in summary
-		const skipFields = ['sendAll', ...this.ignore];
-
-		// Process each field in the form data
-		for (const [key, value] of Object.entries(formConfig.data)) {
-			// Skip ignored fields and empty values
-			if (skipFields.includes(key) || this.isEmptyValue(value)) {
-				continue;
-			}
-
-			// Get field info from form
-			const fieldInfo = this.getFieldInfo(form, key);
-
-			if (!fieldInfo.label) continue; // Skip if no label found
-
-			let field = wrapper.cloneNode(true);
-			let title = field.querySelector('h3');
-			let p = field.querySelector('p');
-
-			title.textContent = fieldInfo.label;
-
-
-			let formatted = this.formatFieldValue(value, fieldInfo.type, form);
-			if (this.isHtmlContent(formatted)) {
-				p.innerHTML = formatted;
-			} else {
-				p.textContent = formatted;
-			}
-
-			summary.append(field);
-		}
-		let uploads = form.querySelectorAll('[data-upload-field]');
-		if (uploads) {
-			uploads.forEach(upload => {
-				let label = upload.querySelector('h2').textContent;
-
-				let imgs = upload.querySelectorAll('.item-grid.preview img');
-				if (imgs) {
-					let field = wrapper.cloneNode(true);
-					let title = field.querySelector('h3');
-					let p = field.querySelector('p');
-					p.remove();
-
-					title.textContent = label;
-					imgs.forEach(img => {
-						img = img.cloneNode(true);
-						field.append(img);
+			let checks = {
+				repeater: this.repeaters,
+				tagList: this.tagLists,
+				charLimit: this.charLimits,
+				quantity: this.quantityFields
+			};
+			for (let [type, check] of Object.entries(checks)) {
+				if (check.size === 0) continue;
+				let hasAny = Array.from(check.values()).filter(item => item.form === formId);
+				if (hasAny.length > 0) {
+					hasAny.forEach(item => {
+						switch (type) {
+							case 'repeater':
+								this.removeRepeaterListeners(item.element);
+								break;
+							case 'tagList':
+								this.removeTagListListeners(item.element);
+								break;
+							case 'charLimit':
+								this.removeCharacterLimitListeners(item.element);
+								break;
+							case 'quantity':
+								this.removeQuantityListeners(item.element);
+								break;
+						}
 					});
-					summary.append(field);
+					check.delete(item.id);
 				}
+			}
+
+
+			this.removeFormListeners(config.element);
+			this.forms.delete(formId);
+
+			window.debouncer.cancel(`form_changes`);
+		}
+		defineSummaryTemplate() {
+			this.summaryTemplate = true;
+			let form = this;
+			this.templates.define(
+				'formSummary',
+				{
+					refs: {
+						result: '.result',
+						h3: 'h3',
+						p: 'p',
+					},
+					setup({ el, refs, manyRefs, data }) {
+						const skipFields = ['sendAll', ...form.ignore];
+
+						for (let [key, value] of Object.entries(data.changes)) {
+							if (skipFields.includes(key) || this.isEmptyValue(value)) continue;
+
+							let input = Array.from(this.inputs.values())
+								.find(temp => temp.field?.dataset.field === key);
+							if (!input) continue;
+
+							let entry = refs.result.cloneNode(true);
+							let title = entry.querySelector('h3');
+							let p = entry.querySelector('p');
+
+							title.textContent = input.label.textContent;
+
+							if (typeof value === 'string') {
+								p.textContent = value;
+							} else if (Array.isArray(value)) {
+								//Repeater or Tag Item
+							} else if (typeof value === 'object') {
+								//Location item
+								p.textContent = `${value.address}`;
+							}
+
+							el.append(entry);
+						}
+						let uploads = data.config?.element?.querySelectorAll('[data-upload-field]');
+						if (uploads) {
+							uploads.forEach(upload => {
+								let label = upload.querySelector('h2')?.textContent??'Upload:';
+								let imgs = upload.querySelectorAll('.item-grid.preview img');
+								if (imgs) {
+									let entry = refs.result.cloneNode(true);
+									let title = field.querySelector('h3');
+									let p = field.querySelector('p');
+									p?.remove();
+									if (title) title.textContent = label;
+									imgs.forEach(img => {
+										img = img.cloneNode(true);
+										entry.append(img);
+									});
+									el.append(entry);
+								}
+							});
+						}
+
+						refs.result?.remove();
+						data.config.element.after(el);
+						window.fade(data.config.element, false);
+					}
+				}
+			);
+		}
+		initializeFields(container, config = null) {
+			const fieldHandlers = {
+				'[data-editor]': () => this.checkForQuill(container,config),
+				'div.quantity': () => this.checkForQuantity(container),
+				'.repeater': () => this.checkForRepeaters(container, config),
+				'.field.tag-list': () => this.checkForTagLists(container),
+				'[data-depends-on]': () => this.checkForConditionalFields(container),
+				'[data-limit]': () => this.checkForCharacterLimits(container),
+				'[data-uploader]': () => this.checkForImageUploads(container, config),
+				'nav.tabs': () => this.checkForTabs(container, config),
+				'[data-type="selector"]': () => this.checkForSelectors(container)
+			};
+
+			for (const [selector, handler] of Object.entries(fieldHandlers)) {
+				if (container.querySelector(selector)) {
+					handler();
+				}
+			}
+
+			let inputs = Array.from(container.querySelectorAll(this.inputSelectors));
+			inputs.map(input => {
+				this.getItem(input, config?.id);
 			});
 		}
+			checkForQuill(form, config) {
+				if (!form.querySelector('[data-editor]')) return;
+				if (config && !Object.hasOwn(config, 'hasQuill')){
+					config.hasQuill = true;
+					this.forms.set(config.id, config);
+				}
 
-		// Remove template
-		wrapper.remove();
+				if (!this.quillInstances.has(config.id)) {
+					this.quillInstances.set(config.id, new Set());
+				}
 
-		// Insert summary and hide form
-		clear = (clear !== 'form') ? form.closest(clear)??form : form;
-
-		clear.after(summary);
-		window.fade(clear, false);
-	}
-
-	/**
-	 * Check if a value is empty (null, undefined, empty string, empty array, empty object)
-	 */
-	isEmptyValue(value) {
-		if (value === null || value === undefined || value === '') {
-			return true;
-		}
-		if (Array.isArray(value) && value.length === 0) {
-			return true;
-		}
-		return typeof value === 'object' && Object.keys(value).length === 0;
-
-	}
-
-	/**
-	 * Get field information (label, type, etc.) from the form
-	 * Handles special field name patterns ([], ::, :, etc.)
-	 */
-	getFieldInfo(form, fieldName) {
-		// Try to find label by 'for' attribute (exact match)
-		let label = form.querySelector(`label[for="${fieldName}"]`);
-		let input = form.querySelector(`[name=${fieldName}]`);
-		// Try to find the input field - check multiple patterns
-		if (!input) {
-			// Try exact match first
-			input = form.querySelector(`[name="${fieldName}"]`);
-		}
-
-		if (!input) {
-			// Try with [] suffix (for checkboxes, multi-selects)
-			input = form.querySelector(`[name="${fieldName}[]"]`);
-		}
-
-		if (!input) {
-			// Try as fieldset legend (for checkbox/radio groups)
-			const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
-			if (fieldset) {
-				label = fieldset.querySelector('legend');
-				input = fieldset.querySelector('input, select, textarea');
+				const instances = window.jvbQuill(form);
+				instances.forEach(instance => {
+					this.quillInstances.get(config.id).add(instance);
+				});
 			}
-		}
-
-		// Get label from input if not found yet
-		if (!label && input) {
-			// Try closest field wrapper first
-			const field = input.closest('.field, fieldset');
-			if (field) {
-				label = field.querySelector('label, legend, h2');
+			checkForQuantity(form) {
+				if (!form.querySelector(this.selectors.number.number)) return;
+				form.querySelectorAll(this.selectors.number.number).forEach(num => {
+					let config = {
+						id: window.generateID('quant'),
+						form: form.dataset.formId,
+						ui: window.uiFromSelectors(this.selectors.number, num),
+						element: num
+					};
+					num.dataset.numId = config.id;
+					this.quantityFields.set(config.id, config);
+					this.addQuantityListeners(num);
+				});
 			}
-		}
+				addQuantityListeners(el) {
+					el.addEventListener('click', this.quantityClick);
+				}
+				removeQuantityListeners(el) {
+					el.removeEventListener('click', this.quantityClick);
+				}
+				handleQuantityClick(e) {
+					let conf = this.quantityFields.get(e.target.closest('[data-num-id]')?.dataset.numId);
+					if(!conf) return;
+					let change = 0;
+					if (conf.increase.contains(e.target)) {
+						change++;
+					} else if (conf.decrease.contains(e.target)) {
+						change--;
+					}
+					if (change === 0) return;
+					let field = this.getField(e.target);
+					let step = conf.input.step;
+					step = Math.max(step, 1);
+					if (e.ctrlKey && e.shiftKey) {
+						step = step * 50;
+					} else if (e.ctrlKey) {
+						step = step *5;
+					} else if (e.shiftKey) {
+						step = step * 10;
+					}
+					let value = (conf.input.value === '') ? 0 : parseFloat(conf.input.value);
+					conf.input.value = (value + (step * change));
 
-		// Get field wrapper - always use base name (no special characters)
-		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+					value = parseFloat(conf.input.value);
 
-		// Determine field type
-		let fieldType = 'text';
-		if (fieldWrapper?.dataset.type) {
-			fieldType = fieldWrapper.dataset.type;
-		} else if (input) {
-			// Infer from input type
-			if (input.type === 'checkbox' && input.name.endsWith('[]')) {
-				fieldType = 'checkbox'; // checkbox group
-			} else if (input.type === 'checkbox') {
-				fieldType = 'true_false'; // single checkbox
-			} else if (input.tagName === 'SELECT' && input.multiple) {
-				fieldType = 'select'; // multi-select
-			} else {
-				fieldType = input.type || 'text';
+					if (conf.input.min && value < conf.input.min) {
+						conf.input.value = conf.input.min;
+						conf.decrease.disabled = true;
+					} else if (conf.input.max && value > conf.input.max) {
+						conf.input.value = conf.input.max;
+						conf.increase.disabled = true;
+					} else {
+						if (conf.decrease.disabled) conf.decrease.disabled = false;
+						if (conf.increase.disabled) conf.increase.disabled = false;
+					}
+				}
+			checkForRepeaters(form) {
+				if (!form.querySelector(this.selectors.repeater.repeater)) return;
+
+				form.querySelectorAll(this.selectors.repeater.repeater).forEach(repeater => {
+					let config = {
+						id: repeater.querySelector('template').className??window.generateID('repeater'),
+						ui: window.uiFromSelectors(this.selectors.repeater, repeater),
+						form: form.dataset.formId,
+						element: repeater,
+						field: this.getField(repeater),
+						sortable: false,
+					};
+
+					if (!config.ui.addButton) return;
+
+					let template = repeater.querySelector('template');
+					this.templates.define(
+						template.className,
+						{
+							manyRefs: {
+								inputs: this.inputSelectors,
+							},
+							setup({el, refs, manyRefs, data}) {
+								let index = config.ui.items?.children?.length??0;
+								el.dataset.index = index;
+								manyRefs.inputs?.forEach(input => {
+									window.prefixInput(input, `${el.dataset.fieldName}:${index}:`)
+								});
+							}
+						},
+					);
+
+					if (window.Sortable) {
+						config.sortable = new Sortable(repeater, {
+							handle: this.selectors.repeater.header,
+							animation: 150,
+							onEnd: () => {
+								this.reindexList(repeater);
+							}
+						});
+					}
+					repeater.dataset.repeaterId = config.id;
+					this.addRepeaterListeners(repeater);
+					this.repeaters.set(config.id, config);
+				});
+
 			}
-		}
+				addRepeaterListeners(el) {
+					el.addEventListener('click', this.repeaterClick);
+				}
+				removeRepeaterListeners(el) {
+					el.removeEventListener('click', this.repeaterClick);
+				}
+				handleRepeaterClick(e) {
+					if (e.target.matches(this.selectors.repeater.add)) {
+						this.addRepeaterRow(e.target.closest('[data-repeater-id]'));
+					} else if (e.target.matches(this.selectors.repeater.remove)) {
+						this.removeRepeaterRow(e.target);
+					}
+				}
+				addRepeaterRow(repeater) {
+					repeater.append(this.templates.create(repeater.dataset.repeaterId));
+					this.a11y.announce('Row added');
+				}
+				removeRepeaterRow(row) {
+					let repeater = row.closest('[data-repeater-id]');
+					row.remove();
+					this.reindexList(repeater);
+					this.a11y.announce('Row removed');
+				}
+			checkForTagLists(form) {
+				form.querySelectorAll(this.selectors.tagList.tagList)?.forEach(field=> {
+					let config = {
+						id: field.querySelector('template').className??window.generateID('tagList'),
+						ui: window.uiFromSelectors(this.selectors.tagList, field),
+						element: field,
+						form: form.dataset.formId,
+						format: field.dataset.tagFormat??'first_field'
+					};
+					if (!config.ui.input || !config.ui.add || !config.ui.items) return;
 
-		return {
-			label: label?.textContent.replace('*', '').trim() || null,
-			type: fieldType,
-			wrapper: fieldWrapper,
-			input: input
-		};
+					field.dataset.tagListId = config.id;
+
+					let template = field.querySelector('template');
+					this.templates.define(
+						template.className,
+						{
+							refs: {
+								label: this.selectors.tagList.label,
+							},
+							manyRefs: {
+								inputs: this.inputSelectors,
+							},
+							setup({el, refs, manyRefs, data}) {
+								let index = config.ui.items?.children?.length??0;
+								el.dataset.index = index;
+								manyRefs.inputs?.forEach(input => {
+									window.prefixInput(input, `${el.dataset.fieldName}:${index}:`)
+								});
+
+								if (refs.label) {
+									refs.label.textContent = data.label;
+								}
+							}
+						},
+					);
+
+					this.tagLists.set(config.id, config);
+					this.addTagListListeners(field);
+				});
+
+			}
+				addTagListListeners(el) {
+					el.addEventListener('click', this.tagListClick);
+					el.addEventListener('keypress', this.tagListInput, {passive: true})
+				}
+				removeTagListListeners(el) {
+					el.removeEventListener('click', this.tagListClick);
+					el.removeEventListener('keypress', this.tagListInput)
+				}
+
+				handleTagListClick(e) {
+					if (e.target.matches(this.selectors.tagList.add)) {
+						this.addTagListItem(e.target.closest('[data-tag-list-id]'));
+					} else if (e.target.matches(this.selectors.tagList.remove)) {
+						this.removeTagListItem(e.target.closest(this.selectors.tagList.remove));
+					}
+				}
+					addTagListItem(tagList) {
+						let config = this.tagLists.get(tagList.dataset.tagListId);
+						if (!config) return;
+
+						let data = {};
+						let hasValue = false;
+
+						for (let input of config.ui.inputs) {
+							this.validateField(input);
+							const fieldName = input.name.replace('new_','');
+							const value = this.getFieldValue(input);
+							if (value) hasValue = true;
+							data[fieldName] = value;
+
+							//clear values and validation
+							if (['checkbox', 'radio'].includes(input.type)) {
+								input.checked = false;
+							} else {
+								input.value = '';
+							}
+							this.clearValidation(input);
+						}
+
+						if (!hasValue) {
+							this.a11y.announce('Please fill in at least one field');
+							config.ui.inputs[0].focus();
+							return;
+						}
+
+						let label;
+						switch (config.format) {
+							case 'first_field':
+								label = Object.values(data)[0];
+								break;
+							case 'all_fields':
+								label = Object.values(data).join(', ');
+								break;
+							default:
+								if (format.includes('{')) {
+									let label = config.format;
+									for (const [key, value] of Object.entries(data)) {
+										label = label.replace(`{${key}}`, value);
+									}
+								} else {
+									label = data[config.format]??Object.values(data)[0];
+								}
+								break;
+						}
+
+						let newItem = this.templates.create(tagList.dataset.tagListId, {
+							label: label
+						});
+
+						const index = config.ui.items?.children?.length ?? 0;
+						newItem?.querySelectorAll('input[type=hidden]')?.forEach(input => {
+							const fieldKey = input.dataset.field;
+							input.name = `${config.element.field}:${index}:${fieldKey}`;
+							input.value = data[fieldKey] || '';
+						});
+
+						config.ui.items.append(newItem);
+						config.ui.inputs[0]?.focus();
+
+						this.a11y.announce('Item added');
+					}
+					removeTagListItem(tag) {
+						let tagList = tag.closest('[data-tag-list-id]');
+						tag.remove();
+						this.reindexList(tagList);
+						this.a11y.announce('Item removed');
+					}
+				handleTagListInput(e) {
+					let target = e.target;
+					let field = target.closest('[data-tag-list-id]');
+					if (!field) return;
+					let config = this.tagLists.get(field.dataset.tagListId);
+					if (!config) return;
+
+					if (e.key === 'Enter') {
+						if (target === config.ui.inputs[config.ui.inputs.length - 1]) {
+							e.preventDefault();
+							this.addTagListItem(target.closest('[data-tag-list-id]'));
+						} else {
+							e.preventDefault();
+							let index = config.ui.inputs.indexOf(target);
+							config.ui.inputs[index+1].focus();
+						}
+					}
+
+				}
+
+			checkForConditionalFields(form) {
+				form.querySelectorAll(this.selectors.dependsOn).forEach( field => {
+					const dependsOn = field.dataset.dependsOn;
+					const requiredValue = field.dataset.dependsValue;
+					const operator = field.dataset.dependsOperatior??'==';
+
+					if (!this.dependencies.has(dependsOn)) {
+						let element = document.querySelector(`[field="${dependsOn}"]`);
+						if (element) {
+							this.dependencies.set(dependsOn, {
+								element: element,
+								items: []
+							});
+						}
+					}
+					let dependency = this.dependencies.get(dependsOn);
+					dependency.items.push({
+						field: field,
+						form: form.dataset.formId,
+						requiredValue: requiredValue,
+						operator: operator
+					});
+					this.dependencies.set(dependsOn, dependency);
+					this.checkFieldDependency(dependency, dependsOn);
+				});
+			}
+				checkFieldDependency(dependentField, controlFieldName) {
+					const controlField = this.dependencies.get(controlFieldName);
+					if (!controlField) return;
+
+					const controlValue = this.getFieldValue(controlField.element);
+					const shouldShow = this.evaluateCondition(
+						controlValue,
+						dependentField.requiredValue,
+						dependentField.operator
+					);
+
+					this.toggleFieldVisibility(dependentField.field, shouldShow);
+				}
+				evaluateCondition(value, requiredValue, operator) {
+					const fieldStr = String(value || '');
+					const requiredStr = String(requiredValue || '');
+
+					switch (operator) {
+						case '==': return fieldStr === requiredStr;
+						case '!=': return fieldStr !== requiredStr;
+						case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
+						case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
+						case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
+						case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
+						case 'contains': return fieldStr.includes(requiredStr);
+						case 'empty': return fieldStr === '';
+						case 'not_empty': return fieldStr !== '';
+						default: return fieldStr === requiredStr;
+					}
+				}
+				toggleFieldVisibility(field, show) {
+					const wrapper = field.closest('.field, fieldset');
+					if (!wrapper) return;
+
+					wrapper.hidden = !show;
+					wrapper.querySelectorAll('input, select, textarea').forEach(control => {
+						control.disabled = !show;
+						if (!show && control.hasAttribute('required')) {
+							control.dataset.wasRequired = 'true';
+							control.removeAttribute('required');
+						} else if (show && control.dataset.wasRequired === 'true') {
+							control.setAttribute('required', '');
+							delete control.dataset.wasRequired;
+						}
+					});
+				}
+			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);
+
+					this.addCharacterLimitListeners(input);
+				});
+
+			}
+				addCharacterLimitListeners(input) {
+					input.addEventListener('input', this.countUpdaters, {passive: true});
+				}
+				removeCharacterLimitListeners(input) {
+					input.removeEventListener('input', this.countUpdaters, {passive: true});
+				}
+				updateCount(e) {
+					let target = e.target;
+					let config = this.charLimits.get(target.dataset.charLimitId);
+					if (!config) return;
+					let length = target.value.length;
+					let limit = target.dataset.limit;
+					if (config.ui.current) {
+						config.ui.current.textContent = length;
+						config.ui.current.classList.toggle('exceeded', length >= limit);
+					}
+					if (length > limit) {
+						target.value = target.value.slice(0, limit);
+					}
+				}
+			checkForImageUploads(form, config) {
+				window.jvbUploads.scanFields(form, config.autoUpload);
+			}
+
+			checkForTabs(form, config) {
+				if (window.jvbTabs && form.querySelector('nav.tabs')) {
+					config.tabs = window.jvbTabs.registerTab(form, {
+						preCheck: (section, tabConfig) => {
+							return this.validateStep(section, config);
+						}
+					});
+					config.ui.tabs = window.uiFromSelectors(this.selectors.tabs, form);
+					config.ui.tabs.sections = Array.from(form.querySelectorAll(this.selectors.tabs.sections));
+					config.ui.tabs.inputs = {};
+					config.ui.tabs.sections.forEach(section => {
+						config.ui.tabs.inputs[section.dataset.tab] = Array.from(section.querySelectorAll(this.inputs));
+					});
+					config.ui.tabs.buttons = Array.from(form.querySelectorAll(this.selectors.tabs.buttons));
+
+					config.unsubscribeTabs = window.jvbTabs.subscribe((event, data) => {
+						if (event === 'tab-switched') {
+							if (config.ui.tabs.progress) {
+								const section = config.ui.tabs.sections.filter(section => section.dataset.tab === data.current)[0]??false;
+								if (!section) return;
+								const step = section.dataset.step;
+								const total = config.ui.sections.length;
+
+								window.showProgress(
+									config.ui.tabs.progress,
+									step,
+									total
+								);
+							}
+						}
+					});
+					this.forms.set(config.id, config);
+				}
+			}
+			validateStep(section, config) {
+				const formId = section.closest('[data-form-id]')?.dataset.formId;
+				if (!formId) return true;
+
+				const form = this.forms.get(formId);
+				if (!form) return true;
+
+				const inputs = Array.from(this.inputs.values())
+					.filter(item =>
+						item &&
+						item.form === formId &&
+						item.section === section.dataset.tab &&
+						!item.element.closest('[hidden]')
+					);
+
+				return inputs.every(item => this.validateField(item.element) === true);
+			}
+			checkForSelectors(form) {
+				if (window.jvbSelector) window.jvbSelector.scanExistingFields(form);
+			}
+	/**
+	 * Mainly for repeaters or taglist
+	 * @param {HTMLElement} container
+	 */
+	reindexList(container) {
+		Array.from(container.children).forEach((item, index) => {
+			item.dataset.index = `${index}`;
+			Array.from(item.children).forEach(child => {
+				if (child.type === 'hidden') {
+					window.prefixInput(
+						child,
+						`${container.dataset.field}:${index}:${child.dataset.field}`
+					);
+				}
+			});
+		});
+
+		//schedule save
+	}
+	/**********************************************************************
+	 VALIDATION
+	**********************************************************************/
+	//text, email, url, tel, date, time, datetime, number
+	//select, checkbox, radio, true_false
+	//textarea
+	//repeater: subfields validation; no submission until all required are entered
+	//tag fields: similar to repeater; each separate field is its own hidden field
+	//upload: comma separated ints
+	//selector: comma separated ints
+	//location: hidden inputs for address, lat, lng, street, city, province, postal_code, country
+
+	clearValidation(input) {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
+
+		field.classList.remove('has-error', 'has-success');
+
+		if (item.ui.success) item.ui.success.hidden = true;
+		if (item.ui.error) item.ui.error.hidden = true;
+		if (item.ui.message) {
+			item.ui.message.hidden = true;
+			item.ui.message.textContent = '';
+		}
 	}
 
-	/**
-	 * Check if content should be treated as HTML
-	 */
-	isHtmlContent(content) {
-		return typeof content === 'string' && (
-			content.includes('<br>') ||
-			content.includes('<p>') ||
-			content.includes('<ul>') ||
-			content.includes('<ol>') ||
-			content.includes('<a ') ||
-			content.includes('<strong>') ||
-			content.includes('<em>') ||
-			content.includes('<div')
-		);
+	showError(input, message = 'Invalid field') {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
+
+		field.classList.remove('has-success');
+		field.classList.add('has-error');
+
+		if (item.ui.success) item.ui.success.hidden = true;
+		if (item.ui.error) item.ui.error.hidden = true;
+		if (item.ui.message) {
+			item.ui.message.hidden = false;
+			item.ui.message.textContent = message;
+		}
 	}
 
-	/**
-	 * Format field value based on type
-	 */
-	formatFieldValue(value, type, form) {
+	showSuccess(input, message = '') {
+		let field = this.getField(input);
+		if (!field) return;
+		let item = this.getItem(input);
+		if (!item) return;
+
+		field.classList.remove('has-error');
+		field.classList.add('has-success');
+
+		if (item.ui.success) item.ui.success.hidden = false;
+		if (item.ui.error) item.ui.error.hidden = true;
+
+		if (item.ui.message) {
+			item.ui.message.hidden = message=== '';
+			item.ui.message.textContent = message;
+		}
+	}
+
+	/**********************************************************************
+	STATUS
+	 **********************************************************************/
+	showFormStatus(formId, status, message ='') {
+		let form = this.forms.get(formId);
+		if (!form || !form.options.showStatus || !form.ui?.status?.status) return;
+		if (form.status === status) return;
+
+		form.ui.status.status.hidden = false;
+		form.ui.status.status.classList.toggle('loading', ['uploading', 'saving'].includes(status));
+
+		form.ui.status.message.textContent = message === '' ? this.getDefaultMessage(status) : message;
+
+		form.ui.status.icon.className = 'icon icon-'+this.getDefaultIcon(status);
+		setTimeout(()=> form.ui.status.status.hidden = true, (status === 'submitted') ? 3000 : 10000);
+	}
+		getDefaultMessage(status) {
+			const messages = {
+				'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'
+			};
+			return messages[status]??status;
+		}
+		getDefaultIcon(status) {
+			const icons = {
+				'autosaved': 'check-circle',
+				'submitted': 'check-circle',
+				'restored': 'history',
+				'error': 'close-circle',
+				'offline': 'cloud-slash',
+				'pending': 'exclamation-mark'
+			}
+			return icons[status]??'';
+		}
+
+
+	/**********************************************************************
+	 SUMMARY
+	**********************************************************************/
+	showSummary(data) {
+		this.templates.create('formSummary', data);
+	}
+	/**********************************************************************
+	 UTILITY
+	**********************************************************************/
+	getForm(element) {
+		let form = element.closest('[data-form-id]');
+		let id = form.dataset.formId;
+		if (!id) return false;
+		let config = this.forms.get(id);
+		if (!config) return false;
+		return config;
+	}
+	getField(element) {
+		return element.closest('[data-field]');
+	}
+	getFieldType(element) {
+		let field = this.getField(element);
+		if (!field) return;
+		return field.dataset.fieldType;
+	}
+	getFieldValue(element) {
+		let type = this.getFieldType(element);
+		let conf = this.getItem(element);
+		let fieldName = conf.field?.dataset.field??false;
+		if (!fieldName) return false;
+
 		switch (type) {
-			case 'textarea':
-			case 'wysiwyg':
-				// Handle rich text - check if it's actual HTML content from Quill
-				return this.formatTextareaValue(value, type);
-
-			case 'true_false':
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
-			case 'checkbox':
-				// Handle both single checkbox and checkbox groups
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
-				}
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
-
-			case 'select':
-				// Handle both single and multi-select
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
-				}
-				// Get label from select option
-				return this.getSelectLabel(value, form, type);
-			case 'date':
-			case 'datetime':
-			case 'time':
-				return window.formatDate ? window.formatDate(value) : value;
-
-			case 'radio':
-				// Get label from select option or radio label
-				return this.getSelectLabel(value, form, type);
-
 			case 'repeater':
-				return this.formatRepeaterValue(value);
+				return this.getRepeaterValue(element, conf);
+
+			case 'tag-list':
+				return this.getTagListValue(element, conf);
 
 			case 'group':
-				return this.formatGroupValue(value);
+				//Do we actually need anything here? I think each subfield just
+				break;
 
 			case 'location':
-				return this.formatLocationValue(value);
+				return this.getLocationValue(element, conf);
 
+			case 'selector':
 			case 'upload':
-				return this.formatFileValue(value);
+				return this.getHiddenInputValue(element, conf, fieldName);
 
-			case 'number':
-				return this.formatNumber(value);
-
-			case 'email':
-				return `<a href="mailto:${value}">${value}</a>`;
-
-			case 'url':
-				return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
-
-			case 'phone':
-				return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
+			case 'true-false':
+				return element.value === '1'||element.value === 'on'||element.value ==='true';
 
 			default:
-				// Handle arrays (multi-select, checkbox group)
-				if (Array.isArray(value)) {
-					return this.formatArrayValue(value);
-				}
-				return value;
+				return element.value;
 		}
 	}
-
-	/**
-	 * Format repeater field value
-	 */
-	formatRepeaterValue(rows) {
-		if (!Array.isArray(rows) || rows.length === 0) {
-			return '<em>No entries</em>';
-		}
-
-		let html = '<div class="repeater-summary">';
-		rows.forEach((row, index) => {
-			html += `<div class="repeater-row">`;
-			html += `<strong>Entry ${index + 1}:</strong><ul>`;
-			for (const [key, value] of Object.entries(row)) {
-				if (!this.isEmptyValue(value)) {
-					const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
-					html += `<li><strong>${label}:</strong> ${value}</li>`;
-				}
+	isEmptyValue(value) {
+		if (value === null || value === undefined || value === '') return true;
+		if (Array.isArray(value) && value.length === 0) return true;
+		return typeof value === 'object' && Object.keys(value).length === 0;
+	}
+		getRepeaterValue(element, conf) {
+			if (!conf.container) {
+				conf.container = conf.field?.querySelector('.repeater-items');
+				this.saveItem(conf);
 			}
-			html += `</ul></div>`;
-		});
-		html += '</div>';
-		return html;
-	}
-
-	/**
-	 * Format group field value
-	 */
-	formatGroupValue(groupData) {
-		if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
-			return '<em>No data</em>';
-		}
-
-		let html = '<div class="group-summary"><ul>';
-		for (const [key, value] of Object.entries(groupData)) {
-			if (!this.isEmptyValue(value)) {
-				const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
-				// Handle nested groups
-				if (typeof value === 'object' && !Array.isArray(value)) {
-					html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
-				} else {
-					html += `<li><strong>${label}:</strong> ${value}</li>`;
-				}
-			}
-		}
-		html += '</ul></div>';
-		return html;
-	}
-
-	/**
-	 * Format location field value
-	 */
-	formatLocationValue(location) {
-		if (typeof location !== 'object') return location;
-
-		const parts = [];
-		const fields = ['address', 'city', 'state', 'zip', 'country'];
-
-		fields.forEach(field => {
-			if (location[field]) {
-				parts.push(location[field]);
-			}
-		});
-
-		return parts.join(', ');
-	}
-
-	/**
-	 * Format file/image value
-	 */
-	formatFileValue(value) {
-		if (typeof value === 'string') {
-			// Single file - could be URL or filename
-			if (value.startsWith('http')) {
-				return `<a href="${value}" target="_blank">View file</a>`;
-			}
-			return value;
-		}
-
-		if (Array.isArray(value)) {
-			return value.map(file => {
-				if (typeof file === 'string') {
-					return `<a href="${file}" target="_blank">View file</a>`;
-				}
-				return file.name || 'File';
-			}).join(', ');
-		}
-
-		return 'File uploaded';
-	}
-
-	/**
-	 * Format number with proper locale formatting
-	 */
-	formatNumber(value) {
-		const num = parseFloat(value);
-		if (isNaN(num)) return value;
-
-		// Check if it's likely currency (has 2 decimal places)
-		if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
-			return new Intl.NumberFormat('en-CA', {
-				style: 'currency',
-				currency: 'USD'
-			}).format(num);
-		}
-
-		return new Intl.NumberFormat('en-CA').format(num);
-	}
-
-	/**
-	 * Format array values (checkboxes, multi-select)
-	 */
-	/**
-	 * Format array values (checkboxes, multi-select)
-	 */
-	formatArrayValue(arr, form = null, fieldInfo = null) {
-		if (arr.length === 0) return '<em>None selected</em>';
-
-		// If we have field info, try to get proper labels
-		if (form && fieldInfo && fieldInfo.input) {
-			const labeled = arr.map(val => {
-				return this.getSelectLabel(val, form, fieldInfo.type);
+			let value = [];
+			Array.from(conf.container.children).forEach(row => {
+				let rowData = {};
+				row.querySelectorAll('[data-field]').forEach(field => {
+					rowData[field.dataset.field] = this.getFieldValue(field);
+				});
+				value.push(rowData);
 			});
-			return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
-		}
-
-		// Fallback to raw values
-		return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
-	}
-
-	/**
-	 * Get label for select/radio option
-	 */
-	/**
-	 * Get label for select/radio/checkbox option
-	 */
-	getSelectLabel(value, form, type) {
-		if (type === 'select') {
-			const option = form.querySelector(`option[value="${value}"]`);
-			return option?.textContent || value;
-		}
-
-		if (type === 'radio') {
-			const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
-			const label = radio?.nextElementSibling;
-			return label?.textContent || value;
-		}
-
-		if (type === 'checkbox') {
-			// Try to find the checkbox with this value
-			const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
-			if (checkbox) {
-				// Look for associated label
-				const label = form.querySelector(`label[for="${checkbox.id}"]`);
-				if (label) {
-					return label.textContent.trim();
-				}
-				// Try next sibling
-				const nextLabel = checkbox.nextElementSibling;
-				if (nextLabel?.tagName === 'LABEL') {
-					return nextLabel.textContent.trim();
-				}
-			}
-		}
-
-		return value;
-	}
-
-	/**
-	 * Format textarea value - handles both rich text and plain text
-	 */
-	formatTextareaValue(value, type) {
-		if (!value) return '<em>Empty</em>';
-
-		// If it's explicitly a wysiwyg type or contains HTML tags, use as-is
-		if (type === 'wysiwyg' || this.containsHtml(value)) {
-			// Quill content already has proper HTML structure
 			return value;
 		}
-
-		// Plain textarea - preserve formatting
-		return this.formatPlainText(value);
-	}
-
-	/**
-	 * Check if string contains HTML content (more reliable than just checking for '<')
-	 */
-	containsHtml(str) {
-		// Check for common HTML tags that Quill uses
-		const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
-		return htmlPattern.test(str);
-	}
-
-	/**
-	 * Format plain text content - preserves whitespace and converts newlines
-	 */
-	formatPlainText(text) {
-		if (!text) return '';
-
-		// First, escape any HTML entities that might be in the text
-		text = text
-			.replace(/&/g, '&amp;')
-			.replace(/</g, '&lt;')
-			.replace(/>/g, '&gt;');
-
-		// Convert double newlines to paragraphs for better readability
-		const paragraphs = text.split(/\n\n+/);
-
-		if (paragraphs.length > 1) {
-			// Multiple paragraphs
-			return paragraphs
-				.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
-				.join('');
-		}
-
-		// Single paragraph - just convert newlines to breaks
-		return text.replace(/\n/g, '<br>');
-	}
-
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	/**
-	 * Cleanup when form is closed/destroyed
-	 */
-	cleanupForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Check for unsaved changes
-		if (this.hasUnsavedChanges(formId)) {
-			this.autosave(formConfig);
-		}
-
-		// Clean up special fields
-		this.cleanupSpecialFields();
-
-		// Remove form config
-		this.forms.delete(formId);
-	}
-
-	/**
-	 * Cleanup
-	 */
-	destroy() {
-		// Remove global handlers
-		if (this.globalHandlersAdded) {
-			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('blur', this.blurHandler, true);
-			document.removeEventListener('input', this.inputHandler, true);
-		}
-		this.forms.forEach((formConfig) => {
-			let element = formConfig.element;
-			if (element) {
-				element.removeEventListener('submit', this.submitHandler);
+		getTagListValue(element, conf) {
+			if (!conf.container) {
+				conf.container = conf.field?.querySelector('.tag-items');
+				this.saveItem(conf);
 			}
-		});
+			let value = [];
+			Array.from(conf.container.children).forEach(item => {
+				let inputs = item.querySelectorAll('input[type="hidden"]');
+				let fieldData = {};
+				inputs.forEach(input => {
+					fieldData[input.dataset.field] = input.value;
+				});
+				value.push(fieldData);
+			});
+			return value;
+		}
+		getLocationValue(element, conf) {
+			if(!conf.values){
+				conf.values = Array.from(conf.field?.querySelectorAll('[data-location-field]'));
+				this.saveItem(conf);
+			}
+			let value = {};
+			conf.values.forEach(input => {
+				value[input.dataset.locationField] = input.value;
+			});
+			return value;
+		}
+		getHiddenInputValue(element, conf, fieldName) {
+			if (!conf.value) {
+				conf.value = conf.field?.querySelector(`input[type=hidden][name="${fieldName}"]`);
+				this.saveItem(conf);
+			}
+			return conf.value.value;
+		}
 
-		// Clear maps
-		this.specialFields.clear();
-		this.forms.clear();
-		this.activeRepeaters.clear();
+	getItem(element, formId = null) {
+		const hasID = Object.hasOwn(element.dataset, 'ref');
+		let id = (hasID) ? element.dataset.ref : window.generateID('input');
+		if (!hasID) element.dataset.ref = id;
 
-		if (this.forms) {
+		//check if we have it already
+		if (!this.inputs.has(id)) {
+			if (!formId) {
+				formId = element.closest('[data-form-id]')?.dataset.formId??false;
+			}
+			let field = this.getField(element);
+
+			this.inputs.set(id, {
+				id: id,
+				element: element,
+				form: formId,
+				field: field,
+				section: element.closest('[data-tab]')?.dataset.tab ?? false,
+				ui: window.uiFromSelectors(this.selectors.fields, field)
+			});
+		}
+
+		return this.inputs.get(id);
+	}
+	saveItem(config) {
+		this.inputs.set(config.id, config);
+	}
+	/**********************************************************************
+	 Cleanup
+	**********************************************************************/
+	destroy() {
+		if (this.forms.size > 0) {
+			Array.from(this.forms.values()).forEach(form => {
+				this.removeFormListeners(form);
+			});
 			this.forms.clear();
 		}
+		if (this.repeaters.size > 0) {
+			Array.from(this.repeaters.values()).forEach(repeater => {
+				this.removeRepeaterListeners(repeater.element);
+				repeater.sortable?.destroy();
+			});
+			this.repeaters.clear();
+		}
+		if (this.quantityFields.size > 0) {
+			Array.from(this.quantityFields.values()).forEach(num => {
+				this.removeQuantityListeners(num.element);
+			});
+			this.quantityFields.clear();
+		}
+		if (this.tagLists.size > 0) {
+			Array.from(this.tagLists.values()).forEach(tagList => {
+				this.removeTagListListeners(tagList.element);
+			});
+			this.tagLists.clear();
+		}
+		if(this.charLimits.size > 0) {
+			Array.from(this.charLimits.values()).forEach(charLimit => {
+				charLimit.removeEventListener('input', this.countUpdaters);
+			})
+		}
+		this.inputs.clear();
+		this.forms.clear();
+		this.charLimits.clear();
+
 	}
 }
-
 document.addEventListener('DOMContentLoaded', async function () {
 	window.auth.subscribe(event => {
 		if (event === 'auth-loaded') {
-			window.jvbForm = FormController;
+			window.jvbForm = new FormController();
 		}
 	});
-
 });
diff --git a/assets/js/concise/FormControllerOLD.js b/assets/js/concise/FormControllerOLD.js
new file mode 100644
index 0000000..e77726b
--- /dev/null
+++ b/assets/js/concise/FormControllerOLD.js
@@ -0,0 +1,2420 @@
+class FormController {
+	constructor(config = {}) {
+		this.config = {
+			collectFormData: false,
+			... config
+		}
+		this.isRestoring = false;
+		const store = window.jvbStore.register(
+			'forms',
+			{
+				storeName: 'forms',
+				keyPath: 'formId',
+				indexes: [
+					{ name: 'status', keyPath: 'status' },
+					{ name: 'operationId', keyPath: 'operationId' },
+					{ name: 'timestamp', keyPath: 'timestamp' },
+					{ name: 'formType', keyPath: 'type' }
+				],
+				TTL: 7 * 24 * 60 * 1000, //7 days
+				validateData: true,
+				delayFetch: true
+			});
+		this.store = store.forms;
+
+		this.debouncer = window.debouncer;
+
+		this.ignore = [];
+
+		this.populateForm = window.jvbPopulate;
+
+		this.subscribers = new Set();
+		this.forms = new Map();
+		this.specialFields = new Map();
+		this.dependencies = new Map();
+
+		// Validation (YOU ARE GREAT!)
+		this.validators = this.initValidators();
+		this.touchedFields = new Set();
+
+		// Auto-save configuration
+		this.autoSaveDefaults = {
+			delay: 3000, // 3 seconds
+			typingDelay: 1500, // 1.5 seconds for text fields
+			enabled: true
+		};
+
+		// Repeater field management
+		this.activeRepeaters = new Map();
+		this.repeaterDelays = {
+			change: 6000,
+			typing: 3000,
+			blur: 1500,
+			add: 500,
+			remove: 800,
+			reorder: 1000
+		};
+
+		// Bind handlers
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.submitHandler = this.handleSubmit.bind(this);
+		this.inputHandler = this.handleInput.bind(this);
+		this.blurHandler = this.handleBlur.bind(this);
+		//Processors
+		this.processRepeaterField = this.processRepeaterField.bind(this);
+		this.processGroupField = this.processGroupField.bind(this);
+		this.processLocationField = this.processLocationField.bind(this);
+		this.processRegularField = this.processRegularField.bind(this);
+
+		this.init();
+	}
+
+	async init() {
+		this.store.subscribe(this.handleStoreEvent.bind(this));
+
+		// Set up global form handlers for standalone forms
+		this.initListeners();
+		if (window.jvbQueue) {
+			window.jvbQueue.subscribe((event, data) => {
+				if (event === 'operation-completed' && data.type === 'form') {
+					this.handleOperationComplete(data);
+				}
+			});
+		}
+	}
+
+	/**
+	 * Handle operation completion - clear related form cache
+	 */
+	async handleOperationComplete(operation) {
+		// Clear the form data from store
+		if (operation.formId) {
+			try {
+				await this.store.delete(operation.formId);
+			} catch (error) {
+				console.warn('Failed to clear form cache:', error);
+			}
+		}
+
+		// Clear any related form state
+		const form = this.forms.get(operation.formId);
+		if (form) {
+			form.isDirty = false;
+			form.lastSaved = Date.now();
+			form.data = {};
+		}
+	}
+
+	handleStoreEvent(event, data) {
+		switch(event) {
+			case 'item-saved':
+				if (data.item.status === 'autosave') {
+					// this.showFormStatus(data.item.formId, 'autosave');
+				}
+				break;
+			case 'data-loaded':
+				this.checkPendingForms();
+				break;
+		}
+	}
+
+	/**
+	 * Check for pending forms from current page
+	 */
+	async checkPendingForms() {
+		const allForms = await this.store.getAll();
+		const currentPath = window.location.pathname;
+
+		const pendingForms = allForms.filter(form => {
+			if (form.status !== 'draft') return false;
+
+			// Check if form is from current page
+			const formPath = form.data?._wp_http_referer;
+			return formPath === currentPath;
+		});
+
+		pendingForms.forEach(item => {
+			const formElement = this.findFormElement(item);
+			if (!formElement) return;
+
+			// Register form if not already registered
+			let formConfig = this.forms.get(item.formId);
+			if (!formElement.dataset.formId) {
+				formConfig = this.registerForm(formElement);
+			}
+
+			// Set flag to prevent event handlers from firing
+			this.isRestoring = true;
+			// Auto-populate the form
+			new this.populateForm(formElement, item.data);
+
+			// Reset flag after a tick (gives DOM time to settle)
+			setTimeout(() => {
+				this.isRestoring = false;
+			}, 0);
+
+			// Show restore status
+			this.showFormStatus(item.formId, 'restored');
+
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Your previous entry has been restored');
+			}
+		});
+	}
+
+	/**
+	 * Find form element that matches the cached data
+	 */
+	findFormElement(formData) {
+		// Try by form_id first (hidden field)
+		if (formData.data?.form_id) {
+			const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
+			if (form) return form;
+		}
+
+		// Try by form_type
+		if (formData.data?.form_type) {
+			const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
+			if (form) return form;
+		}
+
+		// Fallback: try by formId (if it was already registered)
+		return document.querySelector(`[data-form-id="${formData.formId}"]`);
+	}
+
+	/**
+	 * Show notification for pending changes
+	 */
+	/**
+	 * Show notification for pending changes
+	 */
+	showPendingNotification(formId, formData) {
+		const formElement = document.querySelector(`[data-form-id="${formId}"]`);
+		if (!formElement) return;
+
+		const notification = document.createElement('div');
+		notification.className = 'pending-changes-notification';
+		notification.innerHTML = `
+        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
+        <button class="restore-changes" data-form-id="${formId}">Restore</button>
+        <button class="discard-changes" data-form-id="${formId}">Discard</button>
+    `;
+
+		formElement.insertBefore(notification, formElement.firstChild);
+
+		// Add handlers
+		notification.querySelector('.restore-changes').addEventListener('click', async () => {
+			await this.restorePendingForm(formId, formData);
+			notification.remove();
+		});
+
+		notification.querySelector('.discard-changes').addEventListener('click', async () => {
+			await this.discardPendingForm(formId);
+			notification.remove();
+		});
+	}
+
+	/**
+	 * Restore pending form data
+	 */
+	async restorePendingForm(formId, formData) {
+		const form = document.querySelector(`[data-form-id="${formId}"]`);
+		if (!form) return;
+
+		// Populate form with cached data
+		new this.populateForm(form, formData);
+
+		// Update status in store (mark as restored, not draft)
+		await this.store.save({
+			formId: formId,
+			data: formData,
+			status: 'restored',
+			timestamp: Date.now()
+		});
+
+		if (window.jvbA11y) {
+			window.jvbA11y.announce('Previous changes restored');
+		}
+	}
+
+	/**
+	 * Discard pending form data
+	 */
+	async discardPendingForm(formId) {
+		try {
+			await this.store.delete(formId);
+
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Previous changes discarded');
+			}
+		} catch (error) {
+			console.error('Failed to discard pending form:', error);
+		}
+	}
+
+	/**
+	 * Setup global handlers for standalone forms
+	 */
+	initListeners() {
+		// Only add if not already added
+		if (!this.globalHandlersAdded) {
+			document.addEventListener('click', this.clickHandler);
+			document.addEventListener('change', this.changeHandler);
+			document.addEventListener('blur', this.blurHandler, true);
+			document.addEventListener('input', this.inputHandler);
+			this.globalHandlersAdded = true;
+		}
+	}
+
+	/**
+	 * Register a standalone form (for front-end forms)
+	 */
+	registerForm(formElement, options = {}) {
+		if (!formElement) return;
+		const formId = formElement.dataset.formId || `form_${Date.now()}`;
+		formElement.dataset.formId = formId;
+
+		formElement.addEventListener('submit', this.submitHandler);
+
+		const formConfig = {
+			element: formElement,
+			id: formId,
+			status: '',
+			options: {
+				autosave: 'autosave' in formElement.dataset,
+				autoUpload: true,
+				saveDelay: this.autoSaveDefaults.delay,
+				endpoint: formElement.dataset.save ?? '',
+				formStatus: true,
+				cache: true,
+				...options
+			},
+			dependencies: new Map(),
+			data: this.collectFormData(formElement, true),
+		};
+
+		this.initializeFormFields(formElement, formConfig);
+		this.forms.set(formId, formConfig);
+
+		// Check for pending data
+		if (this.store && formConfig.options.cache) {
+			const cached = this.store.get(formId);
+			if (cached && cached.data) {
+				this.showPendingNotification(formId, cached.data);
+			}
+		}
+
+		return formConfig;
+	}
+
+	/**
+	 * Initialize all special fields in a form
+	 */
+	initializeFormFields(form, formConfig = null) {
+		if (formConfig?.options?.isTable) {
+			if (window.jvbSelector) {
+				window.jvbSelector.scanExistingFields(form);
+			}
+
+			// Initialize character limits
+			this.initCharacterLimits(form);
+
+			// Initialize image upload fields
+			this.initImageUploadFields(form, formConfig);
+			return;
+		}
+		// Initialize Quill editors
+		this.initQuillEditors(form);
+
+		// Initialize repeater fields
+		this.initRepeaterFields(form, formConfig);
+
+		this.initTagListFields(form, formConfig);
+
+		// Initialize conditional fields
+		if (formConfig) {
+			this.initConditionalFields(form, formConfig);
+		}
+
+		// Initialize character limits
+		this.initCharacterLimits(form);
+
+		// Initialize image upload fields
+		this.initImageUploadFields(form, formConfig);
+
+		// Initialize tabs if present
+		if (window.jvbTabs && form.querySelector('nav.tabs')) {
+			formConfig.tabs = new window.jvbTabs(form);
+			this.forms.set(formConfig.formId, formConfig);
+			this.initSteppedForm(formConfig.formId);
+		}
+
+		// Scan for existing selector fields
+		if (window.jvbSelector) {
+			window.jvbSelector.scanExistingFields(form);
+		}
+	}
+
+	/**
+	 * Initialize stepped form functionality
+	 */
+	initSteppedForm(formId) {
+		const formConfig = this.forms.get(formId);
+		const form = formConfig.element;
+		const tabsInstance = formConfig.tabs;
+
+		const sections = form.querySelectorAll('.tab-content');
+		const totalSteps = sections.length;
+		const progressBar = form.querySelector('.form-progress .fill');
+		const stepText = form.querySelector('.step-text .current');
+		const tabButtons = form.querySelectorAll('nav.tabs button');
+
+		// Update progress display
+		const updateProgress = (currentStep) => {
+			const progress = (currentStep / totalSteps) * 100;
+			if (progressBar) {
+				progressBar.style.width = progress + '%';
+			}
+			if (stepText) {
+				stepText.textContent = currentStep;
+			}
+
+			// Update tab states
+			tabButtons.forEach((btn, idx) => {
+				const stepNum = idx + 1;
+				btn.classList.remove('current', 'completed', 'pending');
+
+				if (stepNum < currentStep) {
+					btn.classList.add('completed');
+				} else if (stepNum === currentStep) {
+					btn.classList.add('current');
+				} else {
+					btn.classList.add('pending');
+				}
+			});
+		};
+
+		// Next/Previous button handling
+		form.addEventListener('click', (e) => {
+			const nextBtn = e.target.closest('[data-action="next-step"]');
+			const prevBtn = e.target.closest('[data-action="prev-step"]');
+
+			if (nextBtn) {
+				e.preventDefault();
+				const currentSection = nextBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
+
+				if (nextSection && this.validateStep(currentSection)) {
+					const nextTab = nextSection.dataset.tab;
+					tabsInstance.switchTab(nextTab, true);
+					updateProgress(currentStep + 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+
+			if (prevBtn) {
+				e.preventDefault();
+				const currentSection = prevBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
+
+				if (prevSection) {
+					const prevTab = prevSection.dataset.tab;
+					tabsInstance.switchTab(prevTab, true);
+					updateProgress(currentStep - 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+		});
+
+		// Update progress when tabs are clicked directly
+		const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
+		tabsInstance.switchTab = (tab, updateHistory) => {
+			originalSwitchTab(tab, updateHistory);
+			const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
+			if (activeSection) {
+				const step = parseInt(activeSection.dataset.step);
+				updateProgress(step);
+			}
+		};
+
+		// Initialize progress
+		updateProgress(1);
+	}
+
+	/**
+	 * Validate current step before allowing progression
+	 * Can be enhanced with custom validation rules
+	 */
+	validateStep(section) {
+		const fields = section.querySelectorAll('.field');
+		let allValid = true;
+
+		fields.forEach(fieldWrapper => {
+			const input = fieldWrapper.querySelector('input, textarea, select');
+			if (input && !input.closest('[hidden]')) {
+				const isValid = this.validateField(input, fieldWrapper);
+				if (!isValid) {
+					allValid = false;
+				}
+			}
+		});
+
+		return allValid;
+	}
+
+
+	/**
+	 * Initialize Quill editors
+	 */
+	initQuillEditors(form) {
+		window.jvbQuill(form);
+	}
+
+	/**
+	 * Initialize repeater fields
+	 */
+	initRepeaterFields(form, formConfig) {
+		form.querySelectorAll('.repeater').forEach(repeater => {
+			const addButton = repeater.querySelector('.add-repeater-row');
+			const container = repeater.querySelector('.repeater-items');
+			const template = repeater.querySelector('template');
+
+			if (!addButton || !template || !container) return;
+
+			// Initialize Sortable for drag-and-drop
+			if (window.Sortable) {
+				new Sortable(container, {
+					handle: '.repeater-row-header',
+					animation: 150,
+					onEnd: () => {
+						this.updateRepeaterOrder(repeater, formConfig);
+					}
+				});
+			}
+
+			// Add row handler
+			addButton.addEventListener('click', () => {
+				this.addRepeaterRow(repeater, formConfig);
+			});
+
+			// Remove row handlers
+			container.addEventListener('click', (e) => {
+				if (e.target.closest('.remove-row')) {
+					this.removeRepeaterRow(e.target.closest('.repeater-row'), formConfig);
+				}
+			});
+		});
+	}
+
+	/**
+	 * Add repeater row
+	 */
+	addRepeaterRow(repeater, formConfig) {
+		const container = repeater.querySelector('.repeater-items');
+		const template = repeater.querySelector('template');
+		const index = container.children.length;
+		const fieldName = repeater.dataset.field;
+
+		// Clone template
+		const row = template.content.cloneNode(true).firstElementChild;
+		row.dataset.index = index;
+
+		// Update field names
+		row.querySelectorAll('input, select, textarea').forEach(field => {
+			const originalName = field.name;
+			field.name = `${fieldName}:${index}:${originalName}`;
+			window.prefixInput(field, `${fieldName}-${index}-`);
+		});
+
+		container.appendChild(row);
+
+		if (formConfig) {
+			this.scheduleSave(formConfig, {
+				type: 'repeater',
+				action: 'add',
+				fieldName: fieldName,
+				delay: this.repeaterDelays.add
+			});
+		}
+
+		if (window.jvbA11y) {
+			window.jvbA11y.announce('Row added');
+		}
+	}
+
+	/**
+	 * Remove repeater row
+	 */
+	removeRepeaterRow(row, formConfig) {
+		const repeater = row.closest('.repeater');
+		const fieldName = repeater.dataset.field;
+
+		row.remove();
+
+		// Reindex remaining rows
+		this.updateRepeaterOrder(repeater, formConfig);
+
+		// Schedule save
+		if (formConfig) {
+			this.scheduleSave(formConfig, {
+				type: 'repeater',
+				action: 'remove',
+				fieldName: fieldName,
+				delay: this.repeaterDelays.remove
+			});
+		}
+
+		if (window.jvbA11y) {
+			window.jvbA11y.announce('Row removed');
+		}
+	}
+
+	/**
+	 * Update repeater order after sorting
+	 */
+	updateRepeaterOrder(repeater, formConfig) {
+		const container = repeater.querySelector('.repeater-items');
+		const fieldName = repeater.dataset.field;
+
+		// Reindex all rows
+		Array.from(container.children).forEach((row, index) => {
+			row.dataset.index = index;
+
+			// Update field names
+			row.querySelectorAll('input, select, textarea').forEach(field => {
+				const parts = field.name.split(':');
+				if (parts.length === 3) {
+					const originalName = parts[2];
+					field.name = `${fieldName}:${index}:${originalName}`;
+					window.prefixInput(field,`${fieldName}-${index}-`);
+				}
+			});
+		});
+
+		// Schedule save
+		if (formConfig) {
+			this.scheduleSave(formConfig, {
+				type: 'repeater',
+				action: 'reorder',
+				fieldName: fieldName,
+				delay: this.repeaterDelays.reorder
+			});
+		}
+	}
+
+	/**
+	 * Initialize tag list fields
+	 */
+	initTagListFields(form, formConfig) {
+		form.querySelectorAll('.field.tag-list').forEach(field => {
+			const inputRow = field.querySelector('.tag-input-row');
+			const addButton = field.querySelector('.add-tag-item');
+			const tagsContainer = field.querySelector('.tag-items');
+			const template = field.querySelector('.tag-template');
+			const fieldName = field.dataset.field;
+			const tagFormat = field.dataset.tagFormat || 'first_field';
+
+			if (!inputRow || !addButton || !tagsContainer || !template) return;
+
+			// Get all input fields in the input row (excluding the button)
+			const getInputFields = () => {
+				return Array.from(inputRow.querySelectorAll('input, select, textarea'))
+					.filter(input => !input.closest('button'));
+			};
+
+			// Add tag handler
+			const addTag = () => {
+				const inputs = getInputFields();
+				const data = {};
+				let hasValue = false;
+
+				// Collect values from inputs
+				inputs.forEach(input => {
+					const fieldName = input.name.replace('new_', '');
+					const value = this.getFieldValue(input);
+
+					if (value) hasValue = true;
+					data[fieldName] = value;
+				});
+
+				if (!hasValue) {
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Please fill in at least one field', 'error');
+					}
+					inputs[0].focus();
+					return;
+				}
+
+				// Validate required fields using data-required attribute
+				const invalidField = inputs.find(input => {
+					const isRequired = ('required' in input.dataset && input.dataset.required === '1');
+					const value = this.getFieldValue(input);
+					return isRequired && !value;
+				});
+
+				if (invalidField) {
+					const fieldWrapper = invalidField.closest('.field');
+					const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
+					this.showError(fieldWrapper, `${fieldLabel} is required.`);
+
+					invalidField.focus();
+					return;
+				}
+
+				for (let input of inputs) {
+					let wrapper = field.closest('.field');
+					if (!this.validateField(input, wrapper)){
+						input.focus();
+						return;
+					}
+				}
+
+				// Clone template and populate
+				const index = tagsContainer.children.length;
+				const newTag = template.content.cloneNode(true).firstElementChild;
+				newTag.dataset.index = index;
+
+				// Update tag label
+				const tagLabel = newTag.querySelector('.tag-label');
+				if (tagLabel) {
+					tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
+				}
+
+				// Update hidden inputs
+				newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
+					const fieldKey = input.dataset.field;
+					input.name = `${fieldName}:${index}:${fieldKey}`;
+					input.value = data[fieldKey] || '';
+				});
+
+				tagsContainer.appendChild(newTag);
+
+				// Clear inputs
+				inputs.forEach(input => {
+					if (input.type === 'checkbox' || input.type === 'radio') {
+						input.checked = false;
+					} else {
+						input.value = '';
+					}
+					let field = input.closest('.field');
+					this.clearValidation(field);
+				});
+
+				// Focus first input
+				if (inputs.length > 0) {
+					inputs[0].focus();
+				}
+
+
+				// Schedule save
+				if (formConfig) {
+					this.scheduleSave(formConfig, {
+						type: 'tag_list',
+						action: 'add',
+						fieldName: fieldName,
+						delay: this.autoSaveDefaults.delay
+					});
+				}
+
+				if (window.jvbA11y) {
+					window.jvbA11y.announce('Item added');
+				}
+			};
+
+			// Add button click
+			addButton.addEventListener('click', addTag);
+
+			// Enter key support on last input
+			const inputs = getInputFields();
+			if (inputs.length > 0) {
+				// Tab through inputs, Enter on last one adds the tag
+				inputs[inputs.length - 1].addEventListener('keypress', (e) => {
+					if (e.key === 'Enter') {
+						e.preventDefault();
+						addTag();
+					}
+				});
+
+				// Enter on other inputs moves to next field
+				inputs.slice(0, -1).forEach((input, i) => {
+					input.addEventListener('keypress', (e) => {
+						if (e.key === 'Enter') {
+							e.preventDefault();
+							inputs[i + 1].focus();
+						}
+					});
+				});
+			}
+
+			// Remove tag handler
+			tagsContainer.addEventListener('click', (e) => {
+				if (e.target.closest('.remove-tag')) {
+					const tag = e.target.closest('.tag-item');
+					const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
+
+					tag.remove();
+
+					// Reindex remaining tags
+					this.reindexTagList(tagsContainer, fieldName);
+
+					// Schedule save
+					if (formConfig) {
+						this.scheduleSave(formConfig, {
+							type: 'tag_list',
+							action: 'remove',
+							fieldName: fieldName,
+							delay: this.autoSaveDefaults.delay
+						});
+					}
+
+					if (window.jvbA11y) {
+						window.jvbA11y.announce(`${tagText} removed`);
+					}
+				}
+			});
+		});
+	}
+
+	/**
+	 * Reindex tag list items
+	 */
+	reindexTagList(container, baseFieldName) {
+		Array.from(container.children).forEach((tag, index) => {
+			tag.dataset.index = index;
+
+			tag.querySelectorAll('input[type="hidden"]').forEach(input => {
+				const fieldKey = input.dataset.field;
+				input.name = `${baseFieldName}:${index}:${fieldKey}`;
+			});
+		});
+	}
+
+	/**
+	 * Get display text for tag based on format
+	 */
+	getTagDisplayText(data, format) {
+		const values = Object.values(data).filter(v => v);
+
+		if (values.length === 0) return 'New Item';
+
+		switch (format) {
+			case 'first_field':
+				return values[0];
+
+			case 'all_fields':
+				return values.join(', ');
+
+			default:
+				// Template format like "{name} ({email})"
+				if (format.includes('{')) {
+					let text = format;
+					for (const [key, value] of Object.entries(data)) {
+						text = text.replace(`{${key}}`, value);
+					}
+					return text;
+				}
+				// Use specific field
+				return data[format] || values[0];
+		}
+	}
+
+	/**
+	 * HTML escape helper
+	 */
+	escapeHtml(text) {
+		const div = document.createElement('div');
+		div.textContent = text;
+		return div.innerHTML;
+	}
+
+	/**
+	 * Initialize conditional fields
+	 */
+	initConditionalFields(form, formConfig) {
+		form.querySelectorAll('[data-depends-on]').forEach(field => {
+			const dependsOn = field.dataset.dependsOn;
+			const requiredValue = field.dataset.dependsValue;
+			const operator = field.dataset.dependsOperator || '==';
+
+			// Store dependency
+			if (!formConfig.dependencies.has(dependsOn)) {
+				formConfig.dependencies.set(dependsOn, []);
+			}
+			formConfig.dependencies.get(dependsOn).push({
+				field: field,
+				requiredValue: requiredValue,
+				operator: operator
+			});
+
+			// Check initial state
+			this.checkFieldDependency(form, field, dependsOn, requiredValue, operator);
+		});
+	}
+
+	/**
+	 * Check field dependency
+	 */
+	checkFieldDependency(form, field, dependsOn, requiredValue, operator) {
+		const triggerField = form.querySelector(`[name="${dependsOn}"]`);
+		if (!triggerField) return;
+
+		const value = this.getFieldValue(triggerField);
+		const shouldShow = this.evaluateCondition(value, requiredValue, operator);
+
+		this.toggleFieldVisibility(field, shouldShow);
+	}
+
+	/**
+	 * Evaluate conditional operator
+	 */
+	evaluateCondition(value, requiredValue, operator) {
+		const fieldStr = String(value || '');
+		const requiredStr = String(requiredValue || '');
+
+		switch (operator) {
+			case '==': return fieldStr === requiredStr;
+			case '!=': return fieldStr !== requiredStr;
+			case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
+			case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
+			case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
+			case '<=': return parseFloat(fieldStr) <= parseFloat(requiredStr);
+			case 'contains': return fieldStr.includes(requiredStr);
+			case 'empty': return fieldStr === '';
+			case 'not_empty': return fieldStr !== '';
+			default: return fieldStr === requiredStr;
+		}
+	}
+
+	/**
+	 * Toggle field visibility
+	 */
+	toggleFieldVisibility(field, show) {
+		const wrapper = field.closest('.field, fieldset');
+		if (!wrapper) return;
+
+		wrapper.hidden = !show;
+		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
+			control.disabled = !show;
+			if (!show && control.hasAttribute('required')) {
+				control.dataset.wasRequired = 'true';
+				control.removeAttribute('required');
+			} else if (show && control.dataset.wasRequired === 'true') {
+				control.setAttribute('required', '');
+				delete control.dataset.wasRequired;
+			}
+		});
+	}
+
+	/**
+	 * Initialize character limits
+	 */
+	initCharacterLimits(form) {
+		form.querySelectorAll('[data-limit]').forEach(input => {
+			const limit = parseInt(input.dataset.limit, 10);
+			const field = input.closest('.field');
+
+			// Create counter if it doesn't exist
+			let counter = field?.querySelector('.char-count');
+			if (!counter && field) {
+				counter = document.createElement('div');
+				counter.className = 'char-count';
+				counter.innerHTML = `<span class="current">0</span> / <span class="limit">${limit}</span>`;
+				field.appendChild(counter);
+			}
+
+			const updateCount = () => {
+				const length = input.value.length;
+				if (counter) {
+					counter.querySelector('.current').textContent = length;
+					counter.classList.toggle('exceeded', length > limit);
+				}
+
+				// Truncate if exceeds limit
+				if (length > limit) {
+					input.value = input.value.substring(0, limit);
+					if (counter) {
+						counter.querySelector('.current').textContent = limit;
+					}
+				}
+			};
+
+			input.addEventListener('input', updateCount);
+			updateCount(); // Initial count
+		});
+	}
+
+	/**
+	 * Initialize image upload fields
+	 */
+	initImageUploadFields(form, config) {
+		window.jvbUploads.scanFields(form, config.options.autoUpload);
+	}
+
+	/* ========== Event Handlers ========== */
+
+	async handleSubmit(event) {
+		const form = event.target;
+
+		if (!form.dataset.formId) return;
+		const formConfig = this.forms.get(form.dataset.formId);
+
+		// Handle subscriber-based forms
+		if (this.subscribers.size > 0) {
+			event.preventDefault();
+			const formData = this.collectFormData(form);
+
+			// Notify subscribers (they'll handle actual submission)
+			this.notify('form-submit', {
+				formId: form.dataset.formId,
+				fullData: formData,
+				config: formConfig
+			});
+		}
+	}
+
+	handleFormSuccess(form, data) {
+		// Clear previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error').forEach(el =>
+			el.classList.remove('field-error')
+		);
+
+		// Add success class to form
+		form.classList.add('form-success');
+
+		// Show success message if provided
+		if (data.message) {
+			const success = document.createElement('div');
+			success.className = 'form-success-message success-message';
+			success.textContent = data.message;
+			form.insertBefore(success, form.firstChild);
+
+			const icon = window.getIcon?.('check-circle');
+			if (icon) {
+				icon.classList.add('success-icon');
+				success.prepend(icon);
+			}
+		}
+
+		// If there's a title/description (for registration success)
+		if (data.title || data.description) {
+			const successBox = document.createElement('div');
+			successBox.className = 'success-box';
+
+			if (data.title) {
+				const title = document.createElement('h3');
+				title.textContent = data.title;
+				successBox.appendChild(title);
+			}
+
+			if (data.description) {
+				const descriptions = Array.isArray(data.description)
+					? data.description
+					: [data.description];
+
+				descriptions.forEach(desc => {
+					const p = document.createElement('p');
+					p.textContent = desc;
+					successBox.appendChild(p);
+				});
+			}
+
+			form.insertBefore(successBox, form.firstChild);
+		}
+
+		//  DELETE CACHED FORM DATA ON SUCCESS
+		if (form.dataset.formId) {
+			this.store.delete(form.dataset.formId).catch(err => {
+				console.warn('Failed to clear form cache:', err);
+			});
+
+			// Clear form config dirty state
+			const formConfig = this.forms.get(form.dataset.formId);
+			if (formConfig) {
+				formConfig.isDirty = false;
+				formConfig.lastSaved = Date.now();
+				formConfig.data = {}; // Clear cached data
+			}
+		}
+
+		// Announce success for accessibility
+		if (window.jvbA11y) {
+			window.jvbA11y.announce(data.message || 'Form submitted successfully');
+		}
+
+		// Trigger custom event
+		form.dispatchEvent(new CustomEvent('jvb-form-success', {
+			detail: data
+		}));
+	}
+
+	handleFormError(form, data) {
+		// Clear all previous errors
+		form.querySelectorAll('.error-message').forEach(el => el.remove());
+		form.querySelectorAll('.field-error, .has-error').forEach(el => {
+			el.classList.remove('field-error', 'has-error');
+		});
+
+		// Clear validation states using existing method
+		form.querySelectorAll('.field').forEach(fieldWrapper => {
+			this.clearValidation(fieldWrapper);
+		});
+
+		// Handle field-specific errors
+		if (data.field) {
+			const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
+			if (fieldWrapper) {
+				// Use existing showError method for consistency
+				this.showError(fieldWrapper, data.message);
+
+				// Mark as touched so validation persists
+				this.touchedFields.add(data.field);
+
+				// Scroll to error
+				fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+				// Focus the input for better UX
+				const input = fieldWrapper.querySelector('input, textarea, select');
+				if (input) {
+					input.focus();
+				}
+			}
+		} else {
+			// General form error (not field-specific)
+			const error = document.createElement('div');
+			error.className = 'form-error error-message';
+			error.textContent = data.message;
+
+			// Add icon for consistency
+			const icon = window.getIcon?.('close-circle');
+			if (icon) {
+				icon.classList.add('error-icon');
+				error.prepend(icon);
+			}
+
+			form.insertBefore(error, form.firstChild);
+
+			// Scroll to top to show the error
+			form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+		}
+
+		// Announce error for accessibility
+		if (window.jvbA11y) {
+			const announcement = data.field
+				? `Error in ${data.field}: ${data.message}`
+				: `Form error: ${data.message}`;
+			window.jvbA11y.announce(announcement);
+		}
+
+		// Trigger custom event
+		form.dispatchEvent(new CustomEvent('jvb-form-error', {
+			detail: data
+		}));
+	}
+
+	handleClick(e) {
+		if (window.targetCheck(e, 'div.quantity')) {
+			let container = window.targetCheck(e, 'div.quantity');
+			this.handleNumberClick(e, container.querySelector('input'));
+		} else if (window.targetCheck(e, '[data-action]')) {
+			let actionEl = window.targetCheck(e, '[data-action]');
+			let action = actionEl.dataset.action;
+			let form = actionEl.closest('form');
+
+			switch (action) {
+				case 'clear-form':
+					if (form?.dataset.formId) {
+						this.store.delete(form.dataset.formId);
+						form.reset();
+						// Hide the status message
+						form.querySelector('.fstatus').hidden = true;
+					}
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Form cleared, starting fresh');
+					}
+					break;
+
+				case 'dismiss-restore':
+					form.querySelector('.fstatus').hidden = true;
+					break;
+			}
+		}
+	}
+
+
+	handleNumberClick(e, input) {
+		let change = 0;
+
+		if (e.target.closest('.increase')) {
+			change += 1;
+		} else if (e.target.closest('.decrease')) {
+			change -=1;
+		}
+		if (change !== 0) {
+			let step = parseFloat(input.step);
+			//Allow for cents, but default to increasing by 1
+			step = Math.max(step, 1);
+
+			if(e.ctrlKey && e.shiftKey) {
+				step = step * 50;
+			} else if (e.ctrlKey) {
+				step = step * 5;
+			} else if (e.shiftKey) {
+				step = step * 10;
+			}
+
+			let value = (input.value === '') ? 0 : parseFloat(input.value);
+
+			input.value = (value + (step * change));
+			this.handleNumberLimits(input);
+		}
+	}
+
+	handleNumberLimits(input) {
+		let [
+			min,
+			max,
+			increase,
+			decrease
+		] = [
+			input.min,
+			input.max,
+			input.closest('.quantity')?.querySelector('.increase'),
+			input.closest('.quantity')?.querySelector('.decrease')
+		];
+		let value = parseFloat(input.value);
+		if (value < min) {
+			input.value = min;
+			decrease.disabled = true;
+		} else if (value > max) {
+			input.value = max;
+			increase.disabled = false;
+		} else if (increase.disabled) {
+			increase.disabled = false;
+		} else if (decrease.disabled) {
+			decrease.disabled = false;
+		}
+	}
+
+	handleChange(event) {
+		const form = event.target.form || event.target.closest('form');if (!form) return;
+		if (!form?.dataset.formId) return;  // Not our form
+		if (!this.forms.has(form.dataset.formId)) return;
+
+		if (event.target.closest('[data-ignore]') || this.isRestoring) {
+			return;
+		}
+		const target = event.target;
+
+		const formConfig = this.forms?.get(form.dataset.formId);
+		if (!formConfig) return;
+
+		if (formConfig.options.autosave && this.subscribers.size > 0) {
+			// Check conditional fields
+			const dependencies = formConfig.dependencies.get(target.name);
+			if (dependencies) {
+				dependencies.forEach(dep => {
+					this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator);
+				});
+			}
+
+			// Schedule auto-save if enabled
+			const delay = this.getDelayForField(target);
+			this.scheduleSave(formConfig, delay);
+		}
+	}
+
+	handleBlur(e) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) {
+			return;
+		}
+		const target = e.target;
+		const form = target.form || target.closest('form');
+
+		if (!form) return;
+
+
+		const input = e.target.closest('input, textarea, select');
+		if (input) {
+			const fieldWrapper = this.findFieldWrapper(input);
+			if (fieldWrapper) {
+				// Mark as touched and validate
+				const fieldName = fieldWrapper.dataset.field;
+				if (fieldName) {
+					if (this.shouldDebounce(input)) {
+						window.debouncer.cancel(`validate_${fieldName}`);
+					}
+					this.touchedFields.add(fieldName);
+				}
+				this.validateField(input, fieldWrapper);
+			}
+			const formConfig = this.forms?.get(form.dataset.formId);
+			if (formConfig && formConfig.autosave) {
+				// Shorter delay on blur
+				this.scheduleSave(formConfig, {
+					type: 'blur',
+					fieldName: target.name,
+					delay: 1500
+				});
+			}
+		}
+	}
+
+	handleInput(e) {
+		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
+			return;
+		}
+		const input = e.target.closest('input, textarea, select');
+		if (!input) return;
+
+		let form = input.closest('form');
+		this.showFormStatus(form.dataset.formId, 'pending');
+
+		const fieldWrapper = this.findFieldWrapper(input);
+		if (!fieldWrapper) return;
+
+		const fieldName = fieldWrapper.dataset.field;
+		if (fieldName) {
+			this.touchedFields.add(fieldName);
+		}
+
+		if (this.shouldDebounce(input)){
+			window.debouncer.schedule(
+				`validate_${fieldName}`,
+				() => this.validateField.bind(this),
+				500
+			)
+		}
+	}
+
+	/***************************************************************
+	 FORM VALIDATION
+	***************************************************************/
+	/**
+	 * Initialize validation rules
+	 */
+	initValidators() {
+		return {
+			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: (value, fieldWrapper) => {
+					const num = parseFloat(value);
+					if (isNaN(num)) return 'Please enter a valid number';
+
+					const min = fieldWrapper.dataset.min;
+					const max = fieldWrapper.dataset.max;
+
+					if (min !== undefined && num < parseFloat(min)) {
+						return `Value must be at least ${min}`;
+					}
+					if (max !== undefined && num > parseFloat(max)) {
+						return `Value must be at most ${max}`;
+					}
+					return true;
+				}
+			},
+			text: {
+				test: (value, fieldWrapper) => {
+					const minLength = fieldWrapper.dataset.minlength;
+					const maxLength = fieldWrapper.dataset.maxlength;
+
+					if (minLength && value.length < parseInt(minLength)) {
+						return `Must be at least ${minLength} characters`;
+					}
+					if (maxLength && value.length > parseInt(maxLength)) {
+						return `Must be no more than ${maxLength} characters`;
+					}
+					return true;
+				}
+			}
+		};
+	}
+	/**
+	 * Find the field wrapper (handles both simple and complex fields)
+	 */
+	findFieldWrapper(input) {
+		// Try to find the closest .field wrapper
+		let wrapper = input.closest('.field');
+
+		// If we're in a repeater row, make sure we get the right field wrapper
+		if (!wrapper) {
+			wrapper = input.closest('[data-field]');
+		}
+
+		return wrapper;
+	}
+
+	/**
+	 * Check if input should be debounced
+	 */
+	shouldDebounce(input) {
+		const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
+		return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
+	}
+
+	/**
+	 * Validate a single field
+	 */
+	validateField(input, fieldWrapper) {
+		const value = this.getFieldValue(input);
+		const fieldName = fieldWrapper.dataset.field;
+
+		// Skip validation if field hasn't been touched yet (unless it's required)
+		if (!this.touchedFields.has(fieldName) && !input.required) {
+			return true;
+		}
+
+		// Skip validation if field is empty and not required
+		if (!value && !input.required) {
+			this.clearValidation(fieldWrapper);
+			return true;
+		}
+
+		// Check required
+		if (input.required && !value) {
+			this.showError(fieldWrapper, 'This field is required');
+			return false;
+		}
+
+		// Check HTML5 validity first
+		if (input.checkValidity && !input.checkValidity()) {
+			this.showError(fieldWrapper, input.validationMessage);
+			return false;
+		}
+
+		// Custom pattern validation from data attribute
+		const pattern = fieldWrapper.dataset.pattern;
+		if (pattern && value) {
+			const regex = new RegExp(pattern);
+			if (!regex.test(value)) {
+				const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
+				this.showError(fieldWrapper, message);
+				return false;
+			}
+		}
+
+		// Type-specific validation
+		const validateType = fieldWrapper.dataset.validate || input.type;
+		if (validateType && this.validators[validateType]) {
+			const validator = this.validators[validateType];
+
+			if (validator.pattern && !validator.pattern.test(value)) {
+				this.showError(fieldWrapper, validator.message);
+				return false;
+			}
+
+			if (validator.test) {
+				const result = validator.test(value, fieldWrapper);
+				if (result !== true) {
+					this.showError(fieldWrapper, result);
+					return false;
+				}
+			}
+		}
+
+		// All validations passed
+		this.showSuccess(fieldWrapper);
+		this.notify('field-validated', input);
+		return true;
+	}
+
+
+
+	/**
+	 * Show success state (green checkmark)
+	 */
+	showSuccess(fieldWrapper, textMessage = '') {
+		if (!fieldWrapper) return;
+
+		// Find validation elements (they might be in field-input-wrapper or field-content)
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove error state
+		fieldWrapper.classList.remove('has-error');
+		input?.classList.remove('error');
+
+		// Add success state
+		fieldWrapper.classList.add('has-success');
+
+		// Show checkmark (if element exists)
+		if (success) {
+			success.hidden = false;
+		}
+		if (error) {
+			error.hidden = true;
+		}
+
+		// Hide error message
+		if (message) {
+			if (textMessage === '') {
+				message.hidden = true;
+				message.textContent = '';
+			} else {
+				message.hidden = false;
+				message.textContent = textMessage;
+			}
+		}
+	}
+
+	/**
+	 * Show error state (red message below field)
+	 */
+	showError(fieldWrapper, errorMessage) {
+		if (!fieldWrapper) return;
+
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove success state
+		fieldWrapper.classList.remove('has-success');
+
+		// Add error state
+		fieldWrapper.classList.add('has-error');
+		input?.classList.add('error');
+
+		// Hide checkmark (if element exists)
+		if (success) {
+			success.hidden = true;
+		}
+		//show x
+		if (error) {
+			error.hidden = false;
+		}
+
+		// Show error message
+		if (message) {
+			message.hidden = false;
+			message.textContent = errorMessage;
+		}
+	}
+
+	/**
+	 * Clear validation state
+	 */
+	clearValidation(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		const icon = fieldWrapper.querySelector('.validation-icon');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		fieldWrapper.classList.remove('has-error', 'has-success');
+		input?.classList.remove('error');
+
+		if (icon) {
+			icon.hidden = true;
+		}
+
+		if (message) {
+			message.hidden = true;
+			message.textContent = '';
+		}
+	}
+
+	/* ========== Auto-save functionality ========== */
+	/**
+	 * Get appropriate delay based on field type and context
+	 */
+	getDelayForField(field) {
+		// Text fields get longer delay for typing
+		if (field.type === 'text' || field.type === 'textarea') {
+			return this.autoSaveDefaults.typingDelay;
+		}
+
+		// Checkboxes, radios, selects get shorter delay
+		if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) {
+			return 1000;
+		}
+
+		// Default delay
+		return this.autoSaveDefaults.delay;
+	}
+	scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
+		if (!formConfig.options.autosave) {
+			return;
+		}
+		document.addEventListener('input', this.saveCheck, {passive: true});
+		const saveKey = `autosave_${formConfig.id}`;
+
+		this.debouncer.schedule(
+			saveKey,
+			() => this.autosave(formConfig),
+			delay
+		);
+	}
+
+	//Extend delay if user is currently typing
+	saveCheck(e) {
+		let form = e.target.closest('form[data-id]');
+		if (!form) {
+			return;
+		}
+
+		this.scheduleSave(this.forms.get(form.dataset.id));
+	}
+
+	async autosave(formConfig) {
+		if (!formConfig.autosave) return;
+		const formData = this.collectFormData(formConfig.element);
+
+		this.showFormStatus(formConfig.id, 'saving');
+
+		// DataStore will now automatically:
+		// - Convert Sets/Maps to Arrays/Objects
+		// - Strip DOM references
+		// - Validate serializability
+		await this.store.save({
+			formId: formConfig.id,
+			data: formData,
+			status: 'draft',
+			timestamp: Date.now()
+		}).then(() => {
+			this.showFormStatus(formConfig.id, 'autosaved');
+		}).catch(error => {
+			console.error('Autosave failed:', error);
+			this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
+		});
+
+		// Get only changed fields
+		const changes = this.getChangedFields(formConfig.data, formData);
+		if (Object.keys(changes).length === 0) return;
+
+		// Update stored data
+		formConfig.data = formData;
+		this.forms.set(formConfig.id, formConfig);
+		document.removeEventListener('input', this.handleInput);
+
+		for (let [key, value] of Object.entries(formData)) {
+			// Complex fields need full data
+			if (typeof value === 'object') {
+				changes[key] = value;
+			}
+		}
+
+		// Notify
+		this.notify('form-autosave', {
+			formId: formConfig.id,
+			changes: changes,
+			fullData: formData,
+			config: formConfig
+		});
+	}
+
+	/**
+	 * Check if form has unsaved changes
+	 */
+	hasUnsavedChanges(formId) {
+		const formConfig = this.forms.get(formId);
+		if (!formConfig) return false;
+
+		// Check if there are pending operations
+		if (formConfig.operations?.size > 0) return true;
+
+		// Check if current data differs from snapshot
+		const currentData = this.collectFormData(formConfig.element);
+		const changes = this.getChangedFields(formConfig.data, currentData);
+
+		return Object.keys(changes).length > 0;
+	}
+
+	showFormStatus(formID, status, message='') {
+		let form = this.forms.get(formID);
+		if (!form?.options.formStatus) {
+			return;
+		}
+
+		if (form.status === status){
+			return;
+		}
+
+		form.status = status;
+
+		const statusWrap = form.element.querySelector('.fstatus');
+		statusWrap.hidden = false;
+		const statusElement = statusWrap.querySelector('.message');
+		statusElement.textContent = '';
+		statusWrap.querySelector('.icon')?.remove();
+		statusWrap.querySelector('.actions')?.remove(); // Clear old actions
+
+		const messages = {
+			'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'
+		};
+
+		const icons = {
+			'autosaved': 'check-circle',
+			'submitted': 'check-circle',
+			'restored': 'history',
+			'error': 'close-circle',
+			'offline': 'cloud-slash',
+			'pending': 'exclamation-mark'
+		}
+
+		let icon = window.getIcon(icons[status]);
+		if (icon) {
+			statusWrap.prepend(icon);
+		}
+
+		if (message === '') {
+			message = messages[status] || status;
+		}
+		statusElement.textContent = message;
+		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
+
+		// Add action buttons for certain statuses
+		if (status === 'restored') {
+			const actions = document.createElement('div');
+			actions.className = 'actions';
+			actions.innerHTML = `
+            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
+            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
+        `;
+			statusWrap.appendChild(actions);
+
+			// Auto-dismiss after 10 seconds
+			setTimeout(() => statusWrap.hidden = true, 10000);
+		}
+
+		// Auto-hide success messages
+		if (status === 'submitted') {
+			setTimeout(() => statusWrap.hidden = true, 3000);
+		}
+	}
+
+	cleanupSpecialFields() {
+		this.specialFields.forEach(field => {
+			if (field.type === 'quill' && field.instance) {
+				// Remove Quill toolbar
+				const toolbar = field.instance.container.previousSibling;
+				if (toolbar?.classList.contains('ql-toolbar')) {
+					toolbar.remove();
+				}
+			}
+		});
+
+		this.uploader?.destroy();
+
+		this.specialFields.clear();
+	}
+
+	/* ========== Form Data Methods ========== */
+
+	collectFormData(form) {
+		//Table forms are handled separately
+		if (form.classList.contains('table') && form.tagName === 'FORM') {
+			return {};
+		}
+
+		const formData = new FormData(form);
+		let data = {};
+		const repeaterData = {};
+		const postData = {};
+
+		for (let [key, value] of formData.entries()) {
+			if (this.ignore.includes(key) || key.endsWith('_temp')) continue;
+
+			const processor = this.getFieldProcessor(key);
+			processor(key, value, data, repeaterData, postData, form);
+		}
+		if (Object.keys(postData).length !== 0) {
+			data = this.mergeRepeaterData(data, repeaterData);
+			return this.mergePostData(data, postData);
+		}
+		return this.mergeRepeaterData(data, repeaterData);
+	}
+
+	getFieldProcessor(key) {
+		if (key.includes('::')) return this.processGroupField;
+		if (key.includes(':')) return this.processRepeaterField;
+		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
+		return this.processRegularField;
+	}
+
+	mergeRepeaterData(data, repeaterData) {
+		Object.keys(repeaterData).forEach(fieldName => {
+			// Clean up empty rows and convert to array format
+			const cleanedRows = {};
+			Object.keys(repeaterData[fieldName]).forEach(index => {
+				const rowData = repeaterData[fieldName][index];
+				if (Object.keys(rowData).length > 0) {
+					cleanedRows[index] = rowData;
+				}
+			});
+
+			// Convert to sequential array
+			data[fieldName] = Object.values(cleanedRows);
+		});
+		return data;
+	}
+
+	mergePostData(data, postData) {
+		for (let [postId, fields] of Object.entries(postData)) {
+			data[postId] = fields;
+		}
+		return data;
+	}
+
+	processRepeaterField(key, value, data, repeaterData, postData, form) {
+		let [fieldName, index, subField] = key.split(':');
+
+		const isArray = subField.endsWith('[]');
+		subField = subField.replace('[]', '');
+
+		//Ensure this repeater and row is in repeaterData
+		if (!repeaterData[fieldName]) {
+			repeaterData[fieldName] = {};
+		}
+		if (!repeaterData[fieldName][index]) {
+			repeaterData[fieldName][index] = {};
+		}
+
+		if (isArray || repeaterData[fieldName][index][subField]) {
+			// Initialize as array if not already
+			if (!repeaterData[fieldName][index][subField]) {
+				repeaterData[fieldName][index][subField] = [];
+			} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
+				repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
+			}
+			repeaterData[fieldName][index][subField].push(value);
+		} else {
+			// Single value field
+			repeaterData[fieldName][index][subField] = value;
+		}
+	}
+	processGroupField(key, value, data, repeaterData, postData, form) {
+		const keys = key.split('::');
+		const rootGroup = keys[0];
+
+		// Initialize root group if it doesn't exist
+		if (!data[rootGroup]) {
+			data[rootGroup] = {};
+		}
+
+		// Build nested structure step by step
+		let current = data[rootGroup];
+		for (let i = 1; i < keys.length - 1; i++) {
+			const groupKey = keys[i];
+			if (!current[groupKey]) {
+				current[groupKey] = {};
+			}
+			current = current[groupKey];
+		}
+
+		// Set the final field value
+		const fieldKey = keys[keys.length - 1];
+
+		// Handle array values (checkboxes, multi-selects)
+		if (current[fieldKey] !== undefined) {
+			if (!Array.isArray(current[fieldKey])) {
+				current[fieldKey] = [current[fieldKey]];
+			}
+			current[fieldKey].push(value);
+		} else {
+			current[fieldKey] = value;
+		}
+	}
+	processLocationField(key, value, data, repeaterData, postData, form) {
+		let [fieldKey, v ] = key.split('[');
+		v = v.replace(']','');
+		if (!Object.hasOwn(data, fieldKey)) {
+			data[fieldKey] = {};
+
+			if (!Object.hasOwn(data, 'sendAll')) {
+				data['sendAll'] = [fieldKey];
+			} else if (!data['sendAll'].includes(fieldKey)) {
+				data['sendAll'].push(fieldKey);
+			}
+		}
+		data[fieldKey][v] = value;
+	}
+
+	processRegularField(key, value, data, repeaterData, postData, form) {
+		//handle array values (like checkboxes/selects)
+		key = key.replace('[]','');
+		if (data[key]) {
+			if (!Array.isArray(data[key])) {
+				data[key] = [data[key]];
+			}
+			data[key].push(value);
+		} else {
+			data[key] = value;
+		}
+	}
+
+	/**
+	 * Get field value (handles different input types)
+	 */
+	getFieldValue(input) {
+		if (!input) return '';
+
+		if (input.type === 'checkbox') {
+			return input.checked ? input.value || '1' : '';
+		} else if (input.type === 'radio') {
+			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
+			return checked ? checked.value : '';
+		} else if (input.type === 'select-multiple') {
+			return Array.from(input.selectedOptions).map(o => o.value);
+		}
+
+		return input.value?.trim() || '';
+	}
+
+	getChangedFields(original, current) {
+		return window.getDifferences?.map(original, current) || {};
+	}
+
+	/*******************************************************
+	 Field Summary
+	*******************************************************/
+	/**
+	 * Show a comprehensive summary of form submission
+	 */
+	showSummary(formId, clear = 'form') {
+		const formConfig = this.forms.get(formId);
+		if (!formConfig) return;
+
+		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
+		const summary = window.getTemplate('formSummary');
+		if (!summary) return;
+		const wrapper = summary.querySelector('.result');
+
+		// Fields to skip in summary
+		const skipFields = ['sendAll', ...this.ignore];
+
+		// Process each field in the form data
+		for (const [key, value] of Object.entries(formConfig.data)) {
+			// Skip ignored fields and empty values
+			if (skipFields.includes(key) || this.isEmptyValue(value)) {
+				continue;
+			}
+
+			// Get field info from form
+			const fieldInfo = this.getFieldInfo(form, key);
+
+			if (!fieldInfo.label) continue; // Skip if no label found
+
+			let field = wrapper.cloneNode(true);
+			let title = field.querySelector('h3');
+			let p = field.querySelector('p');
+
+			title.textContent = fieldInfo.label;
+
+
+			let formatted = this.formatFieldValue(value, fieldInfo.type, form);
+			if (this.isHtmlContent(formatted)) {
+				p.innerHTML = formatted;
+			} else {
+				p.textContent = formatted;
+			}
+
+			summary.append(field);
+		}
+		let uploads = form.querySelectorAll('[data-upload-field]');
+		if (uploads) {
+			uploads.forEach(upload => {
+				let label = upload.querySelector('h2').textContent;
+
+				let imgs = upload.querySelectorAll('.item-grid.preview img');
+				if (imgs) {
+					let field = wrapper.cloneNode(true);
+					let title = field.querySelector('h3');
+					let p = field.querySelector('p');
+					p.remove();
+
+					title.textContent = label;
+					imgs.forEach(img => {
+						img = img.cloneNode(true);
+						field.append(img);
+					});
+					summary.append(field);
+				}
+			});
+		}
+
+		// Remove template
+		wrapper.remove();
+
+		// Insert summary and hide form
+		clear = (clear !== 'form') ? form.closest(clear)??form : form;
+
+		clear.after(summary);
+		window.fade(clear, false);
+	}
+
+	/**
+	 * Check if a value is empty (null, undefined, empty string, empty array, empty object)
+	 */
+	isEmptyValue(value) {
+		if (value === null || value === undefined || value === '') {
+			return true;
+		}
+		if (Array.isArray(value) && value.length === 0) {
+			return true;
+		}
+		return typeof value === 'object' && Object.keys(value).length === 0;
+
+	}
+
+	/**
+	 * Get field information (label, type, etc.) from the form
+	 * Handles special field name patterns ([], ::, :, etc.)
+	 */
+	getFieldInfo(form, fieldName) {
+		// Try to find label by 'for' attribute (exact match)
+		let label = form.querySelector(`label[for="${fieldName}"]`);
+		let input = form.querySelector(`[name=${fieldName}]`);
+		// Try to find the input field - check multiple patterns
+		if (!input) {
+			// Try exact match first
+			input = form.querySelector(`[name="${fieldName}"]`);
+		}
+
+		if (!input) {
+			// Try with [] suffix (for checkboxes, multi-selects)
+			input = form.querySelector(`[name="${fieldName}[]"]`);
+		}
+
+		if (!input) {
+			// Try as fieldset legend (for checkbox/radio groups)
+			const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
+			if (fieldset) {
+				label = fieldset.querySelector('legend');
+				input = fieldset.querySelector('input, select, textarea');
+			}
+		}
+
+		// Get label from input if not found yet
+		if (!label && input) {
+			// Try closest field wrapper first
+			const field = input.closest('.field, fieldset');
+			if (field) {
+				label = field.querySelector('label, legend, h2');
+			}
+		}
+
+		// Get field wrapper - always use base name (no special characters)
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+
+		// Determine field type
+		let fieldType = 'text';
+		if (fieldWrapper?.dataset.type) {
+			fieldType = fieldWrapper.dataset.type;
+		} else if (input) {
+			// Infer from input type
+			if (input.type === 'checkbox' && input.name.endsWith('[]')) {
+				fieldType = 'checkbox'; // checkbox group
+			} else if (input.type === 'checkbox') {
+				fieldType = 'true_false'; // single checkbox
+			} else if (input.tagName === 'SELECT' && input.multiple) {
+				fieldType = 'select'; // multi-select
+			} else {
+				fieldType = input.type || 'text';
+			}
+		}
+
+		return {
+			label: label?.textContent.replace('*', '').trim() || null,
+			type: fieldType,
+			wrapper: fieldWrapper,
+			input: input
+		};
+	}
+
+	/**
+	 * Check if content should be treated as HTML
+	 */
+	isHtmlContent(content) {
+		return typeof content === 'string' && (
+			content.includes('<br>') ||
+			content.includes('<p>') ||
+			content.includes('<ul>') ||
+			content.includes('<ol>') ||
+			content.includes('<a ') ||
+			content.includes('<strong>') ||
+			content.includes('<em>') ||
+			content.includes('<div')
+		);
+	}
+
+	/**
+	 * Format field value based on type
+	 */
+	formatFieldValue(value, type, form) {
+		switch (type) {
+			case 'textarea':
+			case 'wysiwyg':
+				// Handle rich text - check if it's actual HTML content from Quill
+				return this.formatTextareaValue(value, type);
+
+			case 'true_false':
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+			case 'checkbox':
+				// Handle both single checkbox and checkbox groups
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
+
+			case 'select':
+				// Handle both single and multi-select
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				// Get label from select option
+				return this.getSelectLabel(value, form, type);
+			case 'date':
+			case 'datetime':
+			case 'time':
+				return window.formatDate ? window.formatDate(value) : value;
+
+			case 'radio':
+				// Get label from select option or radio label
+				return this.getSelectLabel(value, form, type);
+
+			case 'repeater':
+				return this.formatRepeaterValue(value);
+
+			case 'group':
+				return this.formatGroupValue(value);
+
+			case 'location':
+				return this.formatLocationValue(value);
+
+			case 'upload':
+				return this.formatFileValue(value);
+
+			case 'number':
+				return this.formatNumber(value);
+
+			case 'email':
+				return `<a href="mailto:${value}">${value}</a>`;
+
+			case 'url':
+				return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
+
+			case 'phone':
+				return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
+
+			default:
+				// Handle arrays (multi-select, checkbox group)
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return value;
+		}
+	}
+
+	/**
+	 * Format repeater field value
+	 */
+	formatRepeaterValue(rows) {
+		if (!Array.isArray(rows) || rows.length === 0) {
+			return '<em>No entries</em>';
+		}
+
+		let html = '<div class="repeater-summary">';
+		rows.forEach((row, index) => {
+			html += `<div class="repeater-row">`;
+			html += `<strong>Entry ${index + 1}:</strong><ul>`;
+			for (const [key, value] of Object.entries(row)) {
+				if (!this.isEmptyValue(value)) {
+					const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+			html += `</ul></div>`;
+		});
+		html += '</div>';
+		return html;
+	}
+
+	/**
+	 * Format group field value
+	 */
+	formatGroupValue(groupData) {
+		if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
+			return '<em>No data</em>';
+		}
+
+		let html = '<div class="group-summary"><ul>';
+		for (const [key, value] of Object.entries(groupData)) {
+			if (!this.isEmptyValue(value)) {
+				const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+				// Handle nested groups
+				if (typeof value === 'object' && !Array.isArray(value)) {
+					html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
+				} else {
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+		}
+		html += '</ul></div>';
+		return html;
+	}
+
+	/**
+	 * Format location field value
+	 */
+	formatLocationValue(location) {
+		if (typeof location !== 'object') return location;
+
+		const parts = [];
+		const fields = ['address', 'city', 'state', 'zip', 'country'];
+
+		fields.forEach(field => {
+			if (location[field]) {
+				parts.push(location[field]);
+			}
+		});
+
+		return parts.join(', ');
+	}
+
+	/**
+	 * Format file/image value
+	 */
+	formatFileValue(value) {
+		if (typeof value === 'string') {
+			// Single file - could be URL or filename
+			if (value.startsWith('http')) {
+				return `<a href="${value}" target="_blank">View file</a>`;
+			}
+			return value;
+		}
+
+		if (Array.isArray(value)) {
+			return value.map(file => {
+				if (typeof file === 'string') {
+					return `<a href="${file}" target="_blank">View file</a>`;
+				}
+				return file.name || 'File';
+			}).join(', ');
+		}
+
+		return 'File uploaded';
+	}
+
+	/**
+	 * Format number with proper locale formatting
+	 */
+	formatNumber(value) {
+		const num = parseFloat(value);
+		if (isNaN(num)) return value;
+
+		// Check if it's likely currency (has 2 decimal places)
+		if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
+			return new Intl.NumberFormat('en-CA', {
+				style: 'currency',
+				currency: 'USD'
+			}).format(num);
+		}
+
+		return new Intl.NumberFormat('en-CA').format(num);
+	}
+
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	formatArrayValue(arr, form = null, fieldInfo = null) {
+		if (arr.length === 0) return '<em>None selected</em>';
+
+		// If we have field info, try to get proper labels
+		if (form && fieldInfo && fieldInfo.input) {
+			const labeled = arr.map(val => {
+				return this.getSelectLabel(val, form, fieldInfo.type);
+			});
+			return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
+		}
+
+		// Fallback to raw values
+		return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
+	}
+
+	/**
+	 * Get label for select/radio option
+	 */
+	/**
+	 * Get label for select/radio/checkbox option
+	 */
+	getSelectLabel(value, form, type) {
+		if (type === 'select') {
+			const option = form.querySelector(`option[value="${value}"]`);
+			return option?.textContent || value;
+		}
+
+		if (type === 'radio') {
+			const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
+			const label = radio?.nextElementSibling;
+			return label?.textContent || value;
+		}
+
+		if (type === 'checkbox') {
+			// Try to find the checkbox with this value
+			const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
+			if (checkbox) {
+				// Look for associated label
+				const label = form.querySelector(`label[for="${checkbox.id}"]`);
+				if (label) {
+					return label.textContent.trim();
+				}
+				// Try next sibling
+				const nextLabel = checkbox.nextElementSibling;
+				if (nextLabel?.tagName === 'LABEL') {
+					return nextLabel.textContent.trim();
+				}
+			}
+		}
+
+		return value;
+	}
+
+	/**
+	 * Format textarea value - handles both rich text and plain text
+	 */
+	formatTextareaValue(value, type) {
+		if (!value) return '<em>Empty</em>';
+
+		// If it's explicitly a wysiwyg type or contains HTML tags, use as-is
+		if (type === 'wysiwyg' || this.containsHtml(value)) {
+			// Quill content already has proper HTML structure
+			return value;
+		}
+
+		// Plain textarea - preserve formatting
+		return this.formatPlainText(value);
+	}
+
+	/**
+	 * Check if string contains HTML content (more reliable than just checking for '<')
+	 */
+	containsHtml(str) {
+		// Check for common HTML tags that Quill uses
+		const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
+		return htmlPattern.test(str);
+	}
+
+	/**
+	 * Format plain text content - preserves whitespace and converts newlines
+	 */
+	formatPlainText(text) {
+		if (!text) return '';
+
+		// First, escape any HTML entities that might be in the text
+		text = text
+			.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;');
+
+		// Convert double newlines to paragraphs for better readability
+		const paragraphs = text.split(/\n\n+/);
+
+		if (paragraphs.length > 1) {
+			// Multiple paragraphs
+			return paragraphs
+				.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
+				.join('');
+		}
+
+		// Single paragraph - just convert newlines to breaks
+		return text.replace(/\n/g, '<br>');
+	}
+
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/**
+	 * Cleanup when form is closed/destroyed
+	 */
+	cleanupForm(formId) {
+		const formConfig = this.forms.get(formId);
+		if (!formConfig) return;
+
+		// Check for unsaved changes
+		if (this.hasUnsavedChanges(formId)) {
+			this.autosave(formConfig);
+		}
+
+		// Clean up special fields
+		this.cleanupSpecialFields();
+
+		// Remove form config
+		this.forms.delete(formId);
+	}
+
+	/**
+	 * Cleanup
+	 */
+	destroy() {
+		// Remove global handlers
+		if (this.globalHandlersAdded) {
+			document.removeEventListener('change', this.changeHandler);
+			document.removeEventListener('blur', this.blurHandler, true);
+			document.removeEventListener('input', this.inputHandler, true);
+		}
+		this.forms.forEach((formConfig) => {
+			let element = formConfig.element;
+			if (element) {
+				element.removeEventListener('submit', this.submitHandler);
+			}
+		});
+
+		// Clear maps
+		this.specialFields.clear();
+		this.forms.clear();
+		this.activeRepeaters.clear();
+
+		if (this.forms) {
+			this.forms.clear();
+		}
+	}
+}
+
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.jvbForm = FormController;
+		}
+	});
+
+});
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 32c07c8..4d38f4e 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -240,6 +240,8 @@
 		if (this.selectedItems.has(id)) return;
 
 		this.selectedItems.add(id);
+		let item = this.getItem(id);
+		if (item) item.element.classList.add('selected');
 		if (updateCheckbox) this.setCheckboxState(id, true);
 		if (updateUI) {
 			this.updateSelectionUI();
@@ -251,6 +253,8 @@
 		if (!this.selectedItems.has(id)) return;
 
 		this.selectedItems.delete(id);
+		let item = this.getItem(id);
+		if (item) item.element.classList.remove('selected');
 		if (updateCheckbox) this.setCheckboxState(id, false);
 		if (updateUI) {
 			this.updateSelectionUI();
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index cf48f19..373a61c 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -1,562 +1,240 @@
-/**********************************************
- PopulateForm extracts saved data and populates the form field accordingly
- **********************************************/
 class PopulateForm {
-	constructor(form, itemDataOrFields = {}, legacyImages = {}, options = {}) {
-		// Support both old signature (fields, images) and new signature (item object)
-		this.item = this.normalizeItemData(itemDataOrFields, legacyImages);
+	constructor() {
+		this.templates = window.jvbTemplates;
+		this.formHelper = window.jvbForm;
+
+		this.defineTemplates();
+
+		this.data = null;
+		this.form = null;
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} form
+	 * @param {object} data
+	 * 	@param {object} data.fields
+	 * 	@param {object} data.images
+	 * 	@param {object} data.taxonomies
+	 */
+	populate (form, data = {})
+	{
+		this.data = data;
 		this.form = form;
-		this.options = options;
+		if (!this.formHelper) {
+			this.formHelper = window.jvbForm;
+		}
+		// If still not available, queue for retry
+		if (!this.formHelper) {
+			requestAnimationFrame(() => {
+				this.populate(form, data);
+			});
+			return;
+		}
 
-		// Populate all fields
-		for (let [fieldName, fieldValue] of Object.entries(this.item.fields)) {
-			let wrapper = form.querySelector(`[data-field="${fieldName}"]`);
-			if (wrapper) {
-				this.populateField(wrapper, fieldName, fieldValue);
+		if (!Object.hasOwn(this.data, 'fields') || Object.keys(this.data.fields).length === 0) return;
+
+		for (let [name, value] of Object.entries(this.data.fields)) {
+			let field = form.querySelector(`[data-field="${name}"]`);
+			if (field) {
+				this.populateField(field, name, value);
 			}
 		}
 	}
 
 	/**
-	 * Normalize data to consistent structure
-	 * Supports both new format (item object) and legacy format (fields, images)
+	 *
+	 * @param {HTMLElement} field
+	 * @param {string} name
+	 * @param {mixed} value
 	 */
-	normalizeItemData(itemDataOrFields, legacyImages) {
-		// Check if this is the new format (has a fields property) or legacy format
-		if (itemDataOrFields && typeof itemDataOrFields === 'object' && 'fields' in itemDataOrFields) {
-			// New format - already structured
-			return {
-				fields: itemDataOrFields.fields || {},
-				images: itemDataOrFields.images || {},
-				taxonomies: itemDataOrFields.taxonomies || {}
-			};
+	populateField(field, name, value) {
+
+		let type = this.formHelper.getFieldType(field);
+		if (!type || this.isEmptyValue(name) || this.isEmptyValue(value)) return;
+
+		const handlers = {
+			'repeater': this.populateRepeater.bind(this),
+			'tag-list': this.populateTagList.bind(this),
+			'location': this.populateLocation.bind(this),
+			'selector': this.populateTaxonomy.bind(this),
+			'user': 	this.populateUser.bind(this),
+			'upload': 	this.populateUpload.bind(this),
+			'set': 		this.populateMultiValue.bind(this),
+			'checkbox': this.populateMultiValue.bind(this),
+			'select': 	this.populateSingleValue.bind(this),
+			'radio':	this.populateSingleValue.bind(this),
+			'true-false': this.populateBoolean.bind(this),
+			'date': 	this.populateDate.bind(this),
+			'time': 	this.populateDate.bind(this),
+			'datetime': this.populateDate.bind(this),
+			'number': 	this.populateNumber.bind(this),
+			'textarea': this.populateTextarea.bind(this)
+		};
+
+		if (Object.hasOwn(handlers, type)) {
+			handlers[type](field, name, value);
 		} else {
-			// Legacy format - fields and images passed separately
-			return {
-				fields: itemDataOrFields || {},
-				images: legacyImages || {},
-				taxonomies: {}
-			};
+			this.populateText(field, name, value);
 		}
 	}
 
-	/**
-	 * Check if a field is a taxonomy field
-	 */
-	isTaxonomyField(fieldName) {
-		return Object.hasOwn(this.item.taxonomies, fieldName) &&
-			Object.keys(this.item.taxonomies[fieldName]).length > 0;
+	populateRepeater(field, name, value) {
+		if (!value || !Array.isArray(value)) return;
+
+		const container = field.querySelector('.repeater-items');
+		let template = field.querySelector('template')?.className??false;
+		if (!container || !template) return;
+
+		window.removeChildren(container);
+
+		value.forEach((data, index) => {
+			data.index = index;
+			const row = this.templates.create(template, data);
+			let fields = row.querySelectorAll('.field');
+			this.populate(fields, data);
+			container.append(row);
+		});
 	}
+	populateTagList(field, name, value) {
+		if (!value || !Array.isArray(value)) return;
 
-	/**
-	 * Check if a value references image data
-	 */
-	isImageField(value) {
-		if (!this.item.images || Object.keys(this.item.images).length === 0) {
-			return false;
-		}
+		const container = field.querySelector('.tag-items');
+		let template = field.querySelector('template')?.className??false;
+		if (!container || !template) return;
 
-		const ids = this.splitIDs(value);
-		return ids.some(id => Object.keys(this.item.images).includes(String(id)));
+		window.removeChildren(container);
+
+		value.forEach((data, index) => {
+			data.index = index;
+			const row = this.templates.create(template, data);
+			let fields = row.querySelectorAll('.field');
+			this.populate(fields, data);
+			container.append(row);
+		});
 	}
-
-	/**
-	 * Split comma-separated IDs into array of integers
-	 */
-	splitIDs(value) {
-		return String(value).split(',')
-			.map(v => parseInt(v.trim()))
-			.filter(v => !isNaN(v) && v > 0);
-	}
-
-	/**
-	 * Populate a single field with its value
-	 */
-	populateField(fieldWrapper, fieldName, fieldValue, options = {}) {
-		if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
-			return;
-		}
-
-		// Determine field type from classes or data attributes
-		const fieldType = this.getFieldType(fieldWrapper);
-
-		switch (fieldType) {
-			case 'upload':
-			case 'gallery':
-			case 'image':
-				this.populateUploadField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'repeater':
-				this.populateRepeaterField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'selector':
-				this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'user':
-				this.populateUserField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'location':
-				this.populateLocationField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'set':
-			case 'checkbox':
-				this.populateSetField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'select':
-			case 'radio':
-				this.populateSelectField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'true_false':
-				this.populateBooleanField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'date':
-			case 'time':
-			case 'datetime':
-				this.populateDateField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'number':
-				this.populateNumberField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'textarea':
-				if (fieldWrapper.querySelector('.editor-container')) {
-					this.populateEditorField(fieldWrapper, fieldName, fieldValue);
-				} else {
-					this.populateTextareaField(fieldWrapper, fieldName, fieldValue);
-				}
-				break;
-
-			case 'text':
-			case 'email':
-			case 'url':
-			case 'tel':
-			case 'phone':
-			default:
-				this.populateTextField(fieldWrapper, fieldName, fieldValue);
-				break;
-		}
-	}
-
-	/**
-	 * Populate taxonomy fields with visual display
-	 */
-	populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
-		// Handle different value formats
-		let termIds = [];
-
-		if (Array.isArray(fieldValue)) {
-			termIds = fieldValue.map(v => String(v));
-		} else if (typeof fieldValue === 'string') {
-			try {
-				const parsed = JSON.parse(fieldValue);
-				termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
-			} catch (e) {
-				termIds = fieldValue.split(',').map(v => v.trim()).filter(v => v);
+	populateLocation(field, name, value) {
+		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
+		subFields.forEach(subField => {
+			if (Object.hasOwn(value, subField)) {
+				let input = field.querySelector(`[data-location-field="${subField}"]`);
+				if (input) input.value = String(value[subField]||'');
 			}
-		} else if (fieldValue) {
-			termIds = [String(fieldValue)];
-		}
+		});
+	}
+	populateTaxonomy(field, name, value) {
+		let termIds = this.splitIDs(value);
+		if (termIds.length === 0) return;
 
-		if (termIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
+		const hiddenInput = field.querySelector(`input[type="hidden"][name="${name}"]`);
 		if (hiddenInput) {
 			hiddenInput.value = termIds.join(',');
-
-			if (window.jvbTaxonomy) {
+			if (window.jvbSelector) {
 				requestAnimationFrame(() => {
-					window.jvbTaxonomy.updateFieldFromInput(hiddenInput);
+					window.jvbSelector.updateFieldFromInput(hiddenInput);
 				});
 			}
-
 		}
 	}
-
-	/**
-	 * Populate upload fields (images, videos, files)
-	 */
-	populateUploadField(fieldWrapper, fieldName, fieldValue) {
-		// Check if this is a timeline gallery
-		const isTimeline = fieldWrapper.dataset.subtype === 'timeline' || fieldName === 'timeline';
-
-		if (isTimeline) {
-			this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue);
+	populateUser(field, name, value) {
+		this.populateTaxonomyField(field, name, value);
+	}
+	populateUpload(field, name, value) {
+		if (name === 'timeline' || field.dataset.subtype && field.dataset.subtype === 'timeline') {
+			this.populateTimelineGallery(field,name,value);
 			return;
 		}
 
-		if (!fieldValue) {
-			return;
-		}
-
-		// Handle comma-separated IDs or single ID
-		const itemIds = this.splitIDs(fieldValue);
-		if (itemIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
+		if (this.isEmptyValue(value)) return;
+		const ids = this.splitIDs(value);
+		if (ids.length === 0) return;
+		const hiddenInput = field.querySelector(`input[type="hidden"]`);
 		if (hiddenInput) {
-			hiddenInput.value = itemIds.join(',');
+			hiddenInput.value = ids.join(',');
 		}
 
-		// Update display grid
-		const grid = fieldWrapper.querySelector('.item-grid');
-		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
-
-		// Clear existing items first
+		const grid = field.querySelector('.item-grid');
+		let uploadContainer = field.querySelector('.file-upload-container');
+		uploadContainer.hidden = ids.length > 0;
+		field.querySelector('.progress')?.remove();
 		if (grid) {
 			window.removeChildren(grid);
-		}
-
-		fieldWrapper.querySelector('.progress')?.remove();
-
-		if (grid) {
-			itemIds.forEach(itemId => {
-				const template = window.getTemplate('uploadItem');
-				if (!template) {
-					console.warn('uploadItem template not found');
-					return;
-				}
-
-				this.populateUploadItem(template, itemId);
-				grid.append(template);
+			ids.forEach(id => {
+				let data = this.data.images[id]??{};
+				data.id = id;
+				grid.append(this.templates.create('uploadItem', data));
 			});
-
-			// Hide upload container if items exist
-			if (itemIds.length > 0 && uploadContainer) {
-				uploadContainer.hidden = true;
-			}
 		}
 	}
+		populateTimelineGallery(field,name, value) {
+			if (!value || !Array.isArray(value) || value.length === 0) return;
 
-	/**
-	 * Populate a single upload item
-	 */
-	populateUploadItem(template, itemId) {
-		let input = template.querySelector('input[name="select-item"]');
-		let label = template.querySelector('label[for="select-item"]');
+			let grid = field.querySelector('.item-grid');
+			let uploadContainer = field.querySelector('.file-upload-container');
+			uploadContainer.hidden = value.length > 0;
+			if (grid) {
+				window.removeChildren(grid);
 
-		template.dataset.id = itemId;
-		input.name = `select-item-${itemId}`;
-		input.id = input.name;
-		label.htmlFor = input.name;
-
-		const img = template.querySelector('img');
-		template.querySelector('video')?.remove();
-
-		// Populate with data from item.images
-		if (this.item.images[itemId]) {
-			const data = this.item.images[itemId];
-			if (img) {
-				img.src = data.medium || data.small || data.large || '';
-				img.alt = data['image-alt-text'] || data.alt || '';
-			}
-
-			// Populate metadata fields
-			const titleInput = template.querySelector('[name="image-title"]');
-			const altInput = template.querySelector('[name="image-alt-text"]');
-			const captionInput = template.querySelector('[name="image-caption"]');
-
-			if (titleInput) titleInput.value = data['image-title'] || data.title || '';
-			if (altInput) altInput.value = data['image-alt-text'] || data.alt || '';
-			if (captionInput) captionInput.value = data['image-caption'] || data.caption || '';
-		} else {
-			console.warn(`No image data found for ID: ${itemId}`);
-		}
-
-		// Remove hint if present
-		template.querySelector('details .upload-meta > .hint')?.remove();
-	}
-
-	/**
-	 * Populate timeline gallery - FIXED iteration
-	 */
-	populateTimelineGallery(fieldWrapper, fieldName, fieldValue) {
-		if (!fieldValue || !Array.isArray(fieldValue)) {
-			console.warn('Timeline field value must be an array');
-			return;
-		}
-
-		if (fieldValue.length === 0) {
-			return;
-		}
-
-		const grid = fieldWrapper.querySelector('.item-grid');
-		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
-
-		// Clear existing items
-		if (grid) {
-			window.removeChildren(grid);
-		}
-
-		fieldWrapper.querySelector('.progress')?.remove();
-
-		if (!grid) return;
-
-		// FIX: Iterate directly over array, not Object.entries
-		for (let itemData of fieldValue) {
-			const template = window.getTemplate('timelineItem');
-			if (!template) {
-				console.warn('timelineItem template not found');
-				continue;
-			}
-
-			const imageId = itemData.post_thumbnail;
-			const postId = itemData.id;
-
-			// Set template data attributes
-			template.dataset.id = imageId;
-			template.dataset.postId = postId;
-
-			// Update selection controls
-			let input = template.querySelector('input[name="select-item"]');
-			let label = template.querySelector('label[for="select-item"]');
-			if (input && label) {
-				input.name = `select-item-${imageId}`;
-				input.id = input.name;
-				label.htmlFor = input.name;
-			}
-
-			// Remove unnecessary elements
-			template.querySelector('video')?.remove();
-			template.querySelector('.select-item span')?.remove();
-
-			// Populate main image
-			const img = template.querySelector('img');
-			const imgData = this.item.images[imageId];
-			if (img && imgData) {
-				img.src = imgData.medium || imgData.small || imgData.large || '';
-				img.title = imgData['image-title'] || '';
-				img.alt = imgData['image-alt-text'] || '';
-			}
-
-			// Populate all fields within the template
-			const fields = template.querySelectorAll('.field');
-			fields.forEach(field => {
-				if (field.classList.contains('group')) {
-					return;
-				}
-
-				const input = field.querySelector('input:not([type="file"]), textarea');
-				if (!input) return;
-
-				const label = field.querySelector('label');
-				const fieldName = input.name.replace('upload_data::', '').replace(/^\[.*?\]/, '');
-
-				// Get value from itemData or imgData
-				let value = itemData[fieldName];
-				if (value === undefined && imgData) {
-					value = imgData[fieldName];
-				}
-
-				// Populate the field using our standard method
-				if (value !== undefined && value !== null) {
-					this.populateField(field, fieldName, value);
-				}
-
-				// Update field identifiers to include post ID
-				const newName = `[${postId}]${fieldName}`;
-				const newId = newName;
-				input.name = newName;
-				input.id = newId;
-				if (label) label.htmlFor = newId;
-			});
-
-			grid.append(template);
-		}
-
-		// Hide upload container if items exist
-		if (fieldValue.length > 0 && uploadContainer) {
-			uploadContainer.hidden = true;
-		}
-	}
-
-	populateTextField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
-		if (input && input.type !== 'file') {
-			input.value = String(fieldValue || '');
-
-			if (input.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = input.value.length;
-				}
-			}
-		}
-	}
-
-	populateTextareaField(fieldWrapper, fieldName, fieldValue) {
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
-			fieldWrapper.querySelector('textarea:not([data-editor="true"])');
-
-		if (textarea) {
-			textarea.value = String(fieldValue || '');
-			textarea.dispatchEvent(new Event('change', { bubbles: true }));
-
-			if (textarea.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = textarea.value.length;
-					const limit = parseInt(textarea.dataset.limit, 10);
-					fieldWrapper.classList.toggle('reached', textarea.value.length >= limit);
-				}
-			}
-		}
-	}
-
-	populateEditorField(fieldWrapper, fieldName, fieldValue) {
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"][data-editor="true"]`);
-		if (!textarea) return;
-
-		textarea.value = String(fieldValue || '');
-		const editorContainer = fieldWrapper.querySelector('.editor');
-		const content = fieldValue || '<p><br></p>';
-
-		if (editorContainer) {
-			let quillInstance = editorContainer.__quill;
-
-			if (!quillInstance && window.Quill) {
-				for (let instance of (window.Quill.instances || [])) {
-					if (instance.container === editorContainer) {
-						quillInstance = instance;
-						break;
+				field.querySelector('.progress')?.remove();
+				for (let data of value) {
+					let point = this.templates.create('timelineItem', data);
+					if (point) {
+						grid.append(point);
 					}
 				}
 			}
-
-			if (quillInstance) {
-				quillInstance.root.innerHTML = content;
-				editorContainer.__quill = quillInstance;
-			} else {
-				editorContainer.innerHTML = content;
+		}
+	populateMultiValue(field, name, value) {
+		if (typeof value === 'string') {
+			try {
+				value = JSON.parse(value);
+			} catch (e) {
+				value = value.split(',').map(v => v.trim());
 			}
 		}
-
-		textarea.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	getFieldType(fieldWrapper) {
-		if (fieldWrapper.dataset.fieldType) return fieldWrapper.dataset.fieldType;
-		if (fieldWrapper.dataset.type) return fieldWrapper.dataset.type;
-
-		const typeClasses = [
-			'upload', 'repeater', 'taxonomy', 'user', 'location',
-			'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
-			'time', 'datetime', 'editor', 'number', 'text', 'textarea',
-			'email', 'url', 'tel', 'phone'
-		];
-
-		for (const type of typeClasses) {
-			if (fieldWrapper.classList.contains(type)) {
-				return type;
+		if (!Array.isArray(value)) {
+			value = [String(value)];
+		}
+		let select = field.querySelector(`select[name="${name}"]`);
+		if (select && select.multiple) {
+			for (let option of select.options) {
+				option.selected = value.includes(option.value);
 			}
+			return;
 		}
+		field.querySelectorAll(`[type="checkbox"][name=${name}]`).forEach(checkbox => {
+			checkbox.checked = value.includes(checkbox.value);
+		});
 
-		const input = fieldWrapper.querySelector('input, select, textarea');
-		if (input) {
-			if (input.tagName === 'TEXTAREA') {
-				return input.dataset.editor === 'true' ? 'editor' : 'textarea';
-			}
-			if (input.type) {
-				return input.type === 'checkbox' && !fieldWrapper.classList.contains('true_false') ? 'set' : input.type;
-			}
-		}
-
-		return 'text';
 	}
-
-	/**
-	 * Populate number fields
-	 */
-	populateNumberField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="number"]`);
-		if (input) {
-			input.value = Number(fieldValue) || 0;
-		}
-	}
-
-	/**
-	 * Populate boolean/true_false fields
-	 */
-	populateBooleanField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="checkbox"]`);
-		if (input) {
-			input.checked = Boolean(fieldValue);
-		}
-	}
-
-	/**
-	 * Populate select/radio fields
-	 */
-	populateSelectField(fieldWrapper, fieldName, fieldValue) {
-		const value = String(fieldValue || '');
+	populateSingleValue(field, name, value) {
+		value = String(value || '');
 
 		// Try select first
-		const select = fieldWrapper.querySelector(`select[name="${fieldName}"]`);
+		let select = field.querySelector(`select[name="${name}"]`);
 		if (select) {
 			select.value = value;
 			return;
 		}
-
-		// Try radio buttons
-		const radio = fieldWrapper.querySelector(`input[type="radio"][name="${fieldName}"][value="${value}"]`);
-		if (radio) {
-			radio.checked = true;
+		let input = field.querySelector(`[name="${name}"][value="${value}"]`);
+		if (input) {
+			input.checked = true;
 		}
 	}
-
-	/**
-	 * Populate set/checkbox fields (multiple selections)
-	 */
-	populateSetField(fieldWrapper, fieldName, fieldValue) {
-		// Parse value if it's a string
-		let values = fieldValue;
-		if (typeof fieldValue === 'string') {
-			try {
-				values = JSON.parse(fieldValue);
-			} catch (e) {
-				values = fieldValue.split(',').map(v => v.trim());
-			}
+	populateBoolean(field, name, value) {
+		const input = field.querySelector(`[name="${name}"], input[type="checkbox"]`);
+		if (input) {
+			input.checked = Boolean(value);
 		}
-
-		if (!Array.isArray(values)) {
-			values = [String(values)];
-		}
-
-		// Update checkboxes
-		fieldWrapper.querySelectorAll(`input[type="checkbox"][name*="${fieldName}"]`).forEach(checkbox => {
-			checkbox.checked = values.includes(checkbox.value);
-		});
 	}
-
-	/**
-	 * Populate date/time fields
-	 */
-	populateDateField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input`);
-		if (input && fieldValue) {
-			// Handle different date formats
-			let dateValue = fieldValue;
-			if (typeof fieldValue === 'object' && fieldValue.date) {
-				dateValue = fieldValue.date;
+	populateDate(field, name, value) {
+		const input = field.querySelector(`[name="${name}"], input`);
+		if (input) {
+			if (typeof value === 'object' && Object.hasOwn(value, 'date')) {
+				value = value.date;
 			}
-
-			// Convert to appropriate format for input type
 			try {
-				const date = new Date(dateValue);
+				const date = new Date(value);
 				if (!isNaN(date.getTime())) {
 					switch (input.type) {
 						case 'date':
@@ -569,131 +247,109 @@
 							input.value = date.toISOString().slice(0, 16);
 							break;
 						default:
-							input.value = dateValue;
+							input.value = value;
 					}
 				}
 			} catch (e) {
-				input.value = dateValue;
+				input.value = value;
 			}
 		}
 	}
-
-	/**
-	 * Populate location fields
-	 */
-	populateLocationField(fieldWrapper, fieldName, fieldValue) {
-		if (!fieldValue || typeof fieldValue !== 'object') {
+	populateNumber(field, name, value) {
+		const input = field.querySelector(`[name="${name}"], input[type="number"]`);
+		if (input) {
+			input.value = Number(value) || 0;
+		}
+	}
+	populateTextarea(field, name, value) {
+		let textarea = field.querySelector('textarea');
+		if (!textarea.dataset.editor) {
+			this.populateText(field, name, value)
 			return;
 		}
-
-		// Location fields typically have sub-fields
-		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
-
-		subFields.forEach(subField => {
-			if (fieldValue[subField] !== undefined) {
-				const input = fieldWrapper.querySelector(`[name="${fieldName}_${subField}"], [name="${subField}"]`);
-				if (input) {
-					input.value = String(fieldValue[subField] || '');
-				}
-			}
+		textarea.value = String(value || '');
+		textarea.dispatchEvent(new Event('change', {bubbles: true}));
+	}
+	populateText(field, name, value) {
+		let input = field.querySelector(`[name="${name}"], input, textarea`);
+		if (input && input.type !== 'file') {
+			input.value = String(value || '');
+		}
+	}
+	/********************************************************************
+	UTILITY
+	 ********************************************************************/
+	getFormHelper() {
+		window.requestAnimationFrame(()=> {
+			this.formHelper = window.jvbForm;
 		});
 	}
 
-
-	/**
-	 * Populate user fields (similar to taxonomy)
-	 */
-	populateUserField(fieldWrapper, fieldName, fieldValue) {
-		// Similar logic to taxonomy fields
-		this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
+	splitIDs(value) {
+		return String(value).split(',')
+			.map(v=>parseInt(v.trim()))
+			.filter(v=>!isNaN(v) && v>0);
+	}
+	isEmptyValue(value) {
+		if (value === null || value === undefined || value === '') return true;
+		if (Array.isArray(value) && value.length === 0) return true;
+		return typeof value === 'object' && Object.keys(value).length === 0;
 	}
 
-	/**
-	 * Populate repeater fields
-	 */
-	populateRepeaterField(fieldWrapper, fieldName, fieldValue) {
-		if (!fieldValue || !Array.isArray(fieldValue)) {
-			return;
-		}
+	defineTemplates() {
+		const T = this.templates;
+		const p = this;
 
-		const container = fieldWrapper.querySelector('.repeater-items');
-		const template = fieldWrapper.querySelector('template');
+		T.define('timelineItem', {
+			refs: {
+				select: '[name="select-item"]',
+				video: 'video',
+				file: '.select-item span',
+				img: 'img',
+			},
+			manyRefs: {
+				fields: '.field',
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.itemId = data.id;
 
-		if (!container || !template) {
-			console.warn(`Repeater field ${fieldName}: missing container or template`);
-			return;
-		}
+				if (refs.select) {
+					window.prefixInput(refs.select, `${data.id}-`);
+				}
+				if (refs.video) refs.video.remove();
+				if (refs.file) refs.file.remove();
 
-		// Clear existing rows
-		window.removeChildren(container);
-
-		// Create rows for each data item
-		fieldValue.forEach((rowData, index) => {
-			if (!rowData || typeof rowData !== 'object') {
-				return;
-			}
-
-			const row = window.getTemplate(template.className);
-			if (!row) {
-				console.warn(`Repeater field ${fieldName}: template not found`);
-				return;
-			}
-
-			// Set row ID and update row number
-			row.id = `${fieldWrapper.closest('form').id}-${fieldName}-row-${index}`;
-			row.dataset.index = index;
-
-			const rowNumber = row.querySelector('.row-number');
-			if (rowNumber) {
-				rowNumber.textContent = `#${index + 1}`;
-			}
-
-			// Update field names and populate values
-			row.querySelectorAll('input, select, textarea').forEach(field => {
-				const originalName = field.name;
-				const newName = `${fieldName}:${index}:${originalName}`;
-				const newId = `${fieldName}-${index}-${originalName}-${field.value}`;
-
-				// Update field identifiers
-				field.name = newName;
-				field.id = newId;
-
-				// Update label
-				const label = field.nextElementSibling;
-				if (label && label.tagName === 'LABEL') {
-					label.htmlFor = newId;
+				let imgData = p.data.images[data['post_thumbnail']]??false;
+				if (refs.img && imgData) {
+					refs.img.src = imgData.medium || imgData.small || imgData.large || '';
+					refs.img.title = imgData['image-title']??'';
+					refs.img.alt = imgData['image-alt-text']??'';
 				}
 
-				// Populate field value
-				if (rowData[originalName] !== undefined) {
-					this.populateRepeaterFieldValue(field, originalName, rowData[originalName]);
-				}
-			});
+				if (manyRefs.fields) {
+					for (let field of manyRefs.fields) {
+						if (field.dataset.fieldType === 'group') continue;
 
-			container.appendChild(row);
+						let name = field.dataset.field;
+						let value = data[name]??'';
+						if (!p.isEmptyValue(value)) {
+							p.populateField(field, name, value);
+						}
+						const input = field.querySelector('input:not([type="file"]), textarea');
+						if (!input) continue;
+						window.prefixInput(input, `[${data.id}]`);
+					}
+
+				}
+			}
 		});
 	}
-
-	/**
-	 * Populate individual repeater field value
-	 */
-	populateRepeaterFieldValue(field, fieldName, fieldValue) {
-		switch (field.type) {
-			case 'checkbox':
-				field.checked = Boolean(fieldValue);
-				break;
-			case 'radio':
-				field.checked = field.value === String(fieldValue);
-				break;
-			case 'select-one':
-			case 'select-multiple':
-				field.value = String(fieldValue || '');
-				break;
-			default:
-				field.value = String(fieldValue || '');
-		}
-	}
 }
 
-// Make available globally
-window.jvbPopulate = PopulateForm;
+document.addEventListener('DOMContentLoaded', function() {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.jvbPopulate = new PopulateForm();
+		}
+	});
+});
diff --git a/assets/js/concise/PopulateFormOld.js b/assets/js/concise/PopulateFormOld.js
new file mode 100644
index 0000000..b2a5f22
--- /dev/null
+++ b/assets/js/concise/PopulateFormOld.js
@@ -0,0 +1,683 @@
+/**********************************************
+ PopulateForm extracts saved data and populates the form field accordingly
+ **********************************************/
+class PopulateForm {
+	constructor(form, itemDataOrFields = {}, legacyImages = {}, options = {}) {
+		// Support both old signature (fields, images) and new signature (item object)
+		this.item = this.normalizeItemData(itemDataOrFields, legacyImages);
+		this.form = form;
+		this.options = options;
+
+		// Populate all fields
+		for (let [fieldName, fieldValue] of Object.entries(this.item.fields)) {
+			let wrapper = form.querySelector(`[data-field="${fieldName}"]`);
+			if (wrapper) {
+				this.populateField(wrapper, fieldName, fieldValue);
+			}
+		}
+	}
+
+	/**
+	 * Normalize data to consistent structure
+	 * Supports both new format (item object) and legacy format (fields, images)
+	 */
+	normalizeItemData(itemDataOrFields, legacyImages) {
+		// Check if this is the new format (has a fields property) or legacy format
+		if (itemDataOrFields && typeof itemDataOrFields === 'object' && 'fields' in itemDataOrFields) {
+			// New format - already structured
+			return {
+				fields: itemDataOrFields.fields || {},
+				images: itemDataOrFields.images || {},
+				taxonomies: itemDataOrFields.taxonomies || {}
+			};
+		} else {
+			// Legacy format - fields and images passed separately
+			return {
+				fields: itemDataOrFields || {},
+				images: legacyImages || {},
+				taxonomies: {}
+			};
+		}
+	}
+
+	/**
+	 * Check if a field is a taxonomy field
+	 */
+	isTaxonomyField(fieldName) {
+		return Object.hasOwn(this.item.taxonomies, fieldName) &&
+			Object.keys(this.item.taxonomies[fieldName]).length > 0;
+	}
+
+	/**
+	 * Check if a value references image data
+	 */
+	isImageField(value) {
+		if (!this.item.images || Object.keys(this.item.images).length === 0) {
+			return false;
+		}
+
+		const ids = this.splitIDs(value);
+		return ids.some(id => Object.keys(this.item.images).includes(String(id)));
+	}
+
+	/**
+	 * Split comma-separated IDs into array of integers
+	 */
+	splitIDs(value) {
+		return String(value).split(',')
+			.map(v => parseInt(v.trim()))
+			.filter(v => !isNaN(v) && v > 0);
+	}
+
+	/**
+	 * Populate a single field with its value
+	 */
+	populateField(fieldWrapper, fieldName, fieldValue, options = {}) {
+		if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
+			return;
+		}
+
+		// Determine field type from classes or data attributes
+		const fieldType = this.getFieldType(fieldWrapper);
+
+		switch (fieldType) {
+			case 'upload':
+			case 'gallery':
+			case 'image':
+				this.populateUploadField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'repeater':
+				this.populateRepeaterField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'selector':
+				this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'user':
+				this.populateUserField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'location':
+				this.populateLocationField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'set':
+			case 'checkbox':
+				this.populateSetField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'select':
+			case 'radio':
+				this.populateSelectField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'true_false':
+				this.populateBooleanField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'date':
+			case 'time':
+			case 'datetime':
+				this.populateDateField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'number':
+				this.populateNumberField(fieldWrapper, fieldName, fieldValue);
+				break;
+
+			case 'textarea':
+				if (fieldWrapper.querySelector('.editor-container')) {
+					this.populateEditorField(fieldWrapper, fieldName, fieldValue);
+				} else {
+					this.populateTextareaField(fieldWrapper, fieldName, fieldValue);
+				}
+				break;
+
+			case 'text':
+			case 'email':
+			case 'url':
+			case 'tel':
+			case 'phone':
+			default:
+				this.populateTextField(fieldWrapper, fieldName, fieldValue);
+				break;
+		}
+	}
+
+	/**
+	 * Populate taxonomy fields with visual display
+	 */
+	populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
+		// Handle different value formats
+		let termIds = [];
+
+		if (Array.isArray(fieldValue)) {
+			termIds = fieldValue.map(v => String(v));
+		} else if (typeof fieldValue === 'string') {
+			try {
+				const parsed = JSON.parse(fieldValue);
+				termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
+			} catch (e) {
+				termIds = fieldValue.split(',').map(v => v.trim()).filter(v => v);
+			}
+		} else if (fieldValue) {
+			termIds = [String(fieldValue)];
+		}
+
+		if (termIds.length === 0) {
+			return;
+		}
+
+		// Update hidden input
+		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
+		if (hiddenInput) {
+			hiddenInput.value = termIds.join(',');
+
+			if (window.jvbSelector) {
+				requestAnimationFrame(() => {
+					window.jvbSelector.updateFieldFromInput(hiddenInput);
+				});
+			}
+
+		}
+	}
+
+	/**
+	 * Populate upload fields (images, videos, files)
+	 */
+	populateUploadField(fieldWrapper, fieldName, fieldValue) {
+		// Check if this is a timeline gallery
+		const isTimeline = fieldWrapper.dataset.subtype === 'timeline' || fieldName === 'timeline';
+
+		if (isTimeline) {
+			this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue);
+			return;
+		}
+
+		if (!fieldValue) {
+			return;
+		}
+
+		// Handle comma-separated IDs or single ID
+		const itemIds = this.splitIDs(fieldValue);
+		if (itemIds.length === 0) {
+			return;
+		}
+
+		// Update hidden input
+		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"]`);
+		if (hiddenInput) {
+			hiddenInput.value = itemIds.join(',');
+		}
+
+		// Update display grid
+		const grid = fieldWrapper.querySelector('.item-grid');
+		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
+
+		// Clear existing items first
+		if (grid) {
+			window.removeChildren(grid);
+		}
+
+		fieldWrapper.querySelector('.progress')?.remove();
+
+		if (grid) {
+			itemIds.forEach(itemId => {
+				const template = window.getTemplate('uploadItem');
+				if (!template) {
+					console.warn('uploadItem template not found');
+					return;
+				}
+
+				this.populateUploadItem(template, itemId);
+				grid.append(template);
+			});
+
+			// Hide upload container if items exist
+			if (itemIds.length > 0 && uploadContainer) {
+				uploadContainer.hidden = true;
+			}
+		}
+	}
+
+	/**
+	 * Populate a single upload item
+	 */
+	populateUploadItem(template, itemId) {
+		let input = template.querySelector('input[name="select-item"]');
+
+		template.dataset.id = itemId;
+		window.prefixInput(input, `select-item-${itemId}`, true);
+
+		const img = template.querySelector('img');
+		template.querySelector('video')?.remove();
+
+		// Populate with data from item.images
+		if (this.item.images[itemId]) {
+			const data = this.item.images[itemId];
+			if (img) {
+				img.src = data.medium || data.small || data.large || '';
+				img.alt = data['image-alt-text'] || data.alt || '';
+			}
+
+			// Populate metadata fields
+			const titleInput = template.querySelector('[name="image-title"]');
+			const altInput = template.querySelector('[name="image-alt-text"]');
+			const captionInput = template.querySelector('[name="image-caption"]');
+
+			if (titleInput) titleInput.value = data['image-title'] || data.title || '';
+			if (altInput) altInput.value = data['image-alt-text'] || data.alt || '';
+			if (captionInput) captionInput.value = data['image-caption'] || data.caption || '';
+		} else {
+			console.warn(`No image data found for ID: ${itemId}`);
+		}
+
+		// Remove hint if present
+		template.querySelector('details .upload-meta > .hint')?.remove();
+	}
+
+	/**
+	 * Populate timeline gallery - FIXED iteration
+	 */
+	populateTimelineGallery(fieldWrapper, fieldName, fieldValue) {
+		if (!fieldValue || !Array.isArray(fieldValue)) {
+			console.warn('Timeline field value must be an array');
+			return;
+		}
+
+		if (fieldValue.length === 0) {
+			return;
+		}
+
+		const grid = fieldWrapper.querySelector('.item-grid');
+		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
+
+		// Clear existing items
+		if (grid) {
+			window.removeChildren(grid);
+		}
+
+		fieldWrapper.querySelector('.progress')?.remove();
+
+		if (!grid) return;
+
+		// FIX: Iterate directly over array, not Object.entries
+		for (let itemData of fieldValue) {
+			const template = window.getTemplate('timelineItem');
+			if (!template) {
+				console.warn('timelineItem template not found');
+				continue;
+			}
+
+			const imageId = itemData.post_thumbnail;
+			const postId = itemData.id;
+
+			// Set template data attributes
+			template.dataset.id = imageId;
+			template.dataset.postId = postId;
+
+			// Update selection controls
+			let input = template.querySelector('input[name="select-item"]');
+			if (input) {
+				window.prefixInput(input, `select-item-${imageId}`, true);
+			}
+
+			// Remove unnecessary elements
+			template.querySelector('video')?.remove();
+			template.querySelector('.select-item span')?.remove();
+
+			// Populate main image
+			const img = template.querySelector('img');
+			const imgData = this.item.images[imageId];
+			if (img && imgData) {
+				img.src = imgData.medium || imgData.small || imgData.large || '';
+				img.title = imgData['image-title'] || '';
+				img.alt = imgData['image-alt-text'] || '';
+			}
+
+			// Populate all fields within the template
+			const fields = template.querySelectorAll('.field');
+			fields.forEach(field => {
+				if (field.classList.contains('group')) {
+					return;
+				}
+
+				const input = field.querySelector('input:not([type="file"]), textarea');
+				if (!input) return;
+				const fieldName = input.name.replace('upload_data::', '').replace(/^\[.*?\]/, '');
+
+				// Get value from itemData or imgData
+				let value = itemData[fieldName];
+				if (value === undefined && imgData) {
+					value = imgData[fieldName];
+				}
+
+				// Populate the field using our standard method
+				if (value !== undefined && value !== null) {
+					this.populateField(field, fieldName, value);
+				}
+
+				// Update field identifiers to include post ID
+				const newName = `[${postId}]${fieldName}`;
+				window.prefixInput(input, newName, true);
+			});
+
+			grid.append(template);
+		}
+
+		// Hide upload container if items exist
+		if (fieldValue.length > 0 && uploadContainer) {
+			uploadContainer.hidden = true;
+		}
+	}
+
+	populateTextField(fieldWrapper, fieldName, fieldValue) {
+		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
+		if (input && input.type !== 'file') {
+			input.value = String(fieldValue || '');
+
+			if (input.dataset.limit) {
+				const counter = fieldWrapper.querySelector('.char-count .current');
+				if (counter) {
+					counter.textContent = input.value.length;
+				}
+			}
+		}
+	}
+
+	populateTextareaField(fieldWrapper, fieldName, fieldValue) {
+		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
+			fieldWrapper.querySelector('textarea:not([data-editor="true"])');
+
+		if (textarea) {
+			textarea.value = String(fieldValue || '');
+			textarea.dispatchEvent(new Event('change', { bubbles: true }));
+
+			if (textarea.dataset.limit) {
+				const counter = fieldWrapper.querySelector('.char-count .current');
+				if (counter) {
+					counter.textContent = textarea.value.length;
+					const limit = parseInt(textarea.dataset.limit, 10);
+					fieldWrapper.classList.toggle('reached', textarea.value.length >= limit);
+				}
+			}
+		}
+	}
+
+	populateEditorField(fieldWrapper, fieldName, fieldValue) {
+		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"][data-editor="true"]`);
+		if (!textarea) return;
+
+		textarea.value = String(fieldValue || '');
+		const editorContainer = fieldWrapper.querySelector('.editor');
+		const content = fieldValue || '<p><br></p>';
+
+		if (editorContainer) {
+			let quillInstance = editorContainer.__quill;
+
+			if (!quillInstance && window.Quill) {
+				for (let instance of (window.Quill.instances || [])) {
+					if (instance.container === editorContainer) {
+						quillInstance = instance;
+						break;
+					}
+				}
+			}
+
+			if (quillInstance) {
+				quillInstance.root.innerHTML = content;
+				editorContainer.__quill = quillInstance;
+			} else {
+				editorContainer.innerHTML = content;
+			}
+		}
+
+		textarea.dispatchEvent(new Event('change', { bubbles: true }));
+	}
+
+	getFieldType(fieldWrapper) {
+		if (fieldWrapper.dataset.fieldType) return fieldWrapper.dataset.fieldType;
+		if (fieldWrapper.dataset.type) return fieldWrapper.dataset.type;
+
+		const typeClasses = [
+			'upload', 'repeater', 'taxonomy', 'user', 'location',
+			'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
+			'time', 'datetime', 'editor', 'number', 'text', 'textarea',
+			'email', 'url', 'tel', 'phone'
+		];
+
+		for (const type of typeClasses) {
+			if (fieldWrapper.classList.contains(type)) {
+				return type;
+			}
+		}
+
+		const input = fieldWrapper.querySelector('input, select, textarea');
+		if (input) {
+			if (input.tagName === 'TEXTAREA') {
+				return input.dataset.editor === 'true' ? 'editor' : 'textarea';
+			}
+			if (input.type) {
+				return input.type === 'checkbox' && !fieldWrapper.classList.contains('true_false') ? 'set' : input.type;
+			}
+		}
+
+		return 'text';
+	}
+
+	/**
+	 * Populate number fields
+	 */
+	populateNumberField(fieldWrapper, fieldName, fieldValue) {
+		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="number"]`);
+		if (input) {
+			input.value = Number(fieldValue) || 0;
+		}
+	}
+
+	/**
+	 * Populate boolean/true_false fields
+	 */
+	populateBooleanField(fieldWrapper, fieldName, fieldValue) {
+		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="checkbox"]`);
+		if (input) {
+			input.checked = Boolean(fieldValue);
+		}
+	}
+
+	/**
+	 * Populate select/radio fields
+	 */
+	populateSelectField(fieldWrapper, fieldName, fieldValue) {
+		const value = String(fieldValue || '');
+
+		// Try select first
+		const select = fieldWrapper.querySelector(`select[name="${fieldName}"]`);
+		if (select) {
+			select.value = value;
+			return;
+		}
+
+		// Try radio buttons
+		const radio = fieldWrapper.querySelector(`input[type="radio"][name="${fieldName}"][value="${value}"]`);
+		if (radio) {
+			radio.checked = true;
+		}
+	}
+
+	/**
+	 * Populate set/checkbox fields (multiple selections)
+	 */
+	populateSetField(fieldWrapper, fieldName, fieldValue) {
+		// Parse value if it's a string
+		let values = fieldValue;
+		if (typeof fieldValue === 'string') {
+			try {
+				values = JSON.parse(fieldValue);
+			} catch (e) {
+				values = fieldValue.split(',').map(v => v.trim());
+			}
+		}
+
+		if (!Array.isArray(values)) {
+			values = [String(values)];
+		}
+
+		// Update checkboxes
+		fieldWrapper.querySelectorAll(`input[type="checkbox"][name*="${fieldName}"]`).forEach(checkbox => {
+			checkbox.checked = values.includes(checkbox.value);
+		});
+	}
+
+	/**
+	 * Populate date/time fields
+	 */
+	populateDateField(fieldWrapper, fieldName, fieldValue) {
+		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input`);
+		if (input && fieldValue) {
+			// Handle different date formats
+			let dateValue = fieldValue;
+			if (typeof fieldValue === 'object' && fieldValue.date) {
+				dateValue = fieldValue.date;
+			}
+
+			// Convert to appropriate format for input type
+			try {
+				const date = new Date(dateValue);
+				if (!isNaN(date.getTime())) {
+					switch (input.type) {
+						case 'date':
+							input.value = date.toISOString().split('T')[0];
+							break;
+						case 'time':
+							input.value = date.toTimeString().slice(0, 5);
+							break;
+						case 'datetime-local':
+							input.value = date.toISOString().slice(0, 16);
+							break;
+						default:
+							input.value = dateValue;
+					}
+				}
+			} catch (e) {
+				input.value = dateValue;
+			}
+		}
+	}
+
+	/**
+	 * Populate location fields
+	 */
+	populateLocationField(fieldWrapper, fieldName, fieldValue) {
+		if (!fieldValue || typeof fieldValue !== 'object') {
+			return;
+		}
+
+		// Location fields typically have sub-fields
+		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
+
+		subFields.forEach(subField => {
+			if (fieldValue[subField] !== undefined) {
+				const input = fieldWrapper.querySelector(`[name="${fieldName}_${subField}"], [name="${subField}"]`);
+				if (input) {
+					input.value = String(fieldValue[subField] || '');
+				}
+			}
+		});
+	}
+
+
+	/**
+	 * Populate user fields (similar to taxonomy)
+	 */
+	populateUserField(fieldWrapper, fieldName, fieldValue) {
+		// Similar logic to taxonomy fields
+		this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
+	}
+
+	/**
+	 * Populate repeater fields
+	 */
+	populateRepeaterField(fieldWrapper, fieldName, fieldValue) {
+		if (!fieldValue || !Array.isArray(fieldValue)) {
+			return;
+		}
+
+		const container = fieldWrapper.querySelector('.repeater-items');
+		const template = fieldWrapper.querySelector('template');
+
+		if (!container || !template) {
+			console.warn(`Repeater field ${fieldName}: missing container or template`);
+			return;
+		}
+
+		// Clear existing rows
+		window.removeChildren(container);
+
+		// Create rows for each data item
+		fieldValue.forEach((rowData, index) => {
+			if (!rowData || typeof rowData !== 'object') {
+				return;
+			}
+
+			const row = window.getTemplate(template.className);
+			if (!row) {
+				console.warn(`Repeater field ${fieldName}: template not found`);
+				return;
+			}
+
+			// Set row ID and update row number
+			row.id = `${fieldWrapper.closest('form').id}-${fieldName}-row-${index}`;
+			row.dataset.index = index;
+
+			const rowNumber = row.querySelector('.row-number');
+			if (rowNumber) {
+				rowNumber.textContent = `#${index + 1}`;
+			}
+
+			// Update field names and populate values
+			row.querySelectorAll('input, select, textarea').forEach((field, index) => {
+				const originalName = field.name;
+				const newName = `${fieldName}:${index}:${originalName}`;
+				const prefix = `${fieldName}-${index}-`;
+
+				// Update field identifiers
+				field.name = newName;
+				window.prefixInput(field, prefix);
+
+				// Populate field value
+				if (rowData[originalName] !== undefined) {
+					this.populateRepeaterFieldValue(field, originalName, rowData[originalName]);
+				}
+			});
+
+			container.appendChild(row);
+		});
+	}
+
+	/**
+	 * Populate individual repeater field value
+	 */
+	populateRepeaterFieldValue(field, fieldName, fieldValue) {
+		switch (field.type) {
+			case 'checkbox':
+				field.checked = Boolean(fieldValue);
+				break;
+			case 'radio':
+				field.checked = field.value === String(fieldValue);
+				break;
+			case 'select-one':
+			case 'select-multiple':
+				field.value = String(fieldValue || '');
+				break;
+			default:
+				field.value = String(fieldValue || '');
+		}
+	}
+}
+
+// Make available globally
+window.jvbPopulate = PopulateForm;
+
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index db25262..a44d3a1 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -15,6 +15,8 @@
 		this.api = jvbSettings.api;
 		this.endpoint = 'queue';
 
+		this.queueItems = new Map();
+
 		this.init();
 	}
 	init() {
@@ -31,6 +33,7 @@
 				name: 'Queue Panel',
 			});
 		}
+		this.defineTemplates();
 	}
 
 	initElements() {
@@ -138,6 +141,17 @@
 		if (!this.ui.panel) this.canUpdateUI = false;
 	}
 
+	defineTemplates() {
+		const T = window.jvbTemplates;
+
+		T.define('emptyState');
+		T.define('queueItem', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+			}
+		});
+	}
+
 
 	initListeners() {
 		this.activityListeners = null;
@@ -173,9 +187,12 @@
 			if (!window.targetCheck(e, this.selectors.panel+', '+this.selectors.toggle.button)) return;
 			const refresh = window.targetCheck(e, this.selectors.refresh.button);
 			if (refresh) {
+				this.ui.refresh.button.classList.add('fetching');
 				this.store.clearCache();
 				this.store.clearFilters();
-				this.store.fetch();
+				this.store.fetch().finally(() => {
+					this.ui.refresh.button.classList.remove('fetching');
+				});
 				return;
 			}
 
@@ -366,9 +383,12 @@
 		}
 		if (statusOrId.length ===0) return;
 		if (!['cancel', 'dismiss', 'retry'].includes(action)) return;
+
 		const shouldRemove = ['cancel', 'dismiss'].includes(action);
 		if (shouldRemove) {
-			statusOrId.forEach(id => this.removeOperationUI(id));
+			statusOrId.forEach(id => {
+				this.removeOperationUI(id)
+			});
 		}
 
 		try {
@@ -396,7 +416,10 @@
 			}
 			statusOrId.forEach(id => {
 				let item = this.getQueue(id);
-				this.notify(`${action}-operation`, item);
+				if (item) {
+					this.notify(`${action}-operation`, item);
+				}
+
 				if (shouldRemove) {
 					this.clearQueue(id);
 				} else {
@@ -633,8 +656,10 @@
 		if (!this.isPolling) return;
 
 		try {
+			this.ui.refresh.button.classList.add('fetching');
 			this.store.clearCache();
 			await this.store.fetch();
+			this.ui.refresh.button.classList.remove('fetching');
 			if (!this.maybeStartPolling()) {
 				this.stopPolling();
 				this.updatePanel('synced');
@@ -649,6 +674,10 @@
 	}
 
 	startCountdown(count, onComplete) {
+		if (!this.ui.refresh.countdown) {
+			console.warn('Countdown element not found');
+			return;
+		}
 		this.ui.refresh.countdown.classList.add('counting');
 		this.ui.refresh.countdown.textContent = count;
 
@@ -727,7 +756,7 @@
 
 		if (sortedOps.length === 0) {
 			window.removeChildren(this.ui.items.container);
-			const empty = window.getTemplate('emptyQueue');
+			const empty = window.jvbTemplates.create('emptyQueue');
 			this.ui.items.container.append(empty);
 			this.a11y.announce('No items in queue');
 			return;
@@ -761,9 +790,7 @@
 	}
 
 	createOperationElement(op) {
-		const el = window.getTemplate('queueItem');
-		el.dataset.id = op.id;
-
+		const el = window.jvbTemplates.create('queueItem', op);
 		const item = {
 			element: el,
 			ui: window.uiFromSelectors(this.selectors.item, el)
diff --git a/assets/js/concise/SchemaManager.js b/assets/js/concise/SchemaManager.js
index bad3fa4..4d2ae40 100644
--- a/assets/js/concise/SchemaManager.js
+++ b/assets/js/concise/SchemaManager.js
@@ -321,46 +321,23 @@
 		form.dataset.currentType = newType;
 
 		// Use PopulateForm to properly populate all fields including repeaters
-		if (window.jvbPopulateForm) {
-			const populator = new window.jvbPopulateForm();
-			const preservedFields = [];
+		if (window.jvbPopulate) {
+			this.populate = window.jvbPopulate.populate;
 
 			// Populate each field that exists in both schemas
 			Object.keys(currentData).forEach(fieldName => {
 				const fieldWrapper = form.querySelector(`[data-field="${fieldName}"]`);
 				if (fieldWrapper) {
-					const fieldType = this.getFieldType(fieldWrapper);
 					const fieldValue = currentData[fieldName];
-
-					// Use PopulateForm's methods for complex fields
-					if (fieldType === 'repeater' && Array.isArray(fieldValue)) {
-						populator.populateRepeaterField(fieldWrapper, fieldName, fieldValue);
-						preservedFields.push(fieldName);
-					} else if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
-						// Simple field - populate directly
-						const field = fieldWrapper.querySelector(`[name="${fieldName}"]`) ||
-							fieldWrapper.querySelector(`[name^="${fieldName}"]`);
-						if (field) {
-							this.populateSimpleField(field, fieldValue);
-							preservedFields.push(fieldName);
-						}
-					}
+					this.populate(fieldWrapper, fieldName, fieldValue);
 				}
 			});
 
 			// Announce changes
-			if (preservedFields.length > 0) {
-				const message = `Schema type changed to ${newType}. Preserved ${preservedFields.length} field value(s).`;
-				console.log(message);
 
-				if (this.a11y && typeof this.a11y.announce === 'function') {
-					this.a11y.announce(message);
-				}
-			} else {
-				const message = `Schema type changed to ${newType}.`;
-				if (this.a11y && typeof this.a11y.announce === 'function') {
-					this.a11y.announce(message);
-				}
+			const message = `Schema type changed to ${newType}.`;
+			if (this.a11y && typeof this.a11y.announce === 'function') {
+				this.a11y.announce(message);
 			}
 		}
 	}
@@ -416,25 +393,6 @@
 	}
 
 	/**
-	 * Populate a simple field with value
-	 */
-	populateSimpleField(field, value) {
-		if (field.type === 'checkbox') {
-			field.checked = value === '1' || value === 'true' || value === true;
-		} else if (field.tagName === 'SELECT') {
-			setTimeout(() => {
-				field.value = value;
-			}, 10);
-		} else {
-			field.value = value;
-		}
-
-		// Visual feedback
-		field.classList.add('value-preserved');
-		setTimeout(() => field.classList.remove('value-preserved'), 2000);
-	}
-
-	/**
 	 * Add CSS for preserved field indication
 	 */
 	addPreservedFieldStyles() {
diff --git a/assets/js/concise/Tabs.js b/assets/js/concise/Tabs.js
index 4dfce20..b93c083 100644
--- a/assets/js/concise/Tabs.js
+++ b/assets/js/concise/Tabs.js
@@ -2,227 +2,275 @@
  * Handles the tabs functionality, given the html requirements are met
  */
 class TabsContainer{
-    constructor(container, callbacks = {}, parent = null){
-        this.tabs = container.querySelector('.tabs');
-        this.a11y = window.jvbA11y;
+	constructor() {
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
 
-		this.updateURL = true;
+		this.subscribers = new Set();
+		this.tabs = new Map();
 
-        this.parent = parent;
-        this.childTabs = new Map();
-		if ('updateURL' in callbacks && callbacks.updateURL === false) {
-			this.updateURL = false;
-		}
-        this.callbacks = callbacks;
-
-
-        this.activeTab = (this.updateURL) ? this.getInitialTabFromHash() :
-            container.querySelector('button.tab.active')?.dataset.tab;
-
-        this.container = container;
-
-        if (this.tabs) {
-            this.tabs.addEventListener('click', e => {
-                const tab = e.target.closest('[data-tab]');
-                if (tab) {
-					let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
-                    this.switchTab(tab.dataset.tab, update);
-                }
-            });
-        }
-
-        // Check for and initialize child tab containers
-        this.initializeChildTabs();
-
-        // Find and set up select dropdown listeners
-        this.selectDropdown = document.querySelector('select.tab-list');
-        if (this.selectDropdown) {
-            this.selectDropdown.addEventListener('change', e => {
-
-				let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
-                this.switchTab(e.target.value, update);
-            });
-        }
-
-		let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
-
-		if (!this.activeTab){
-			this.activeTab = document.querySelector('button.tab')?.dataset.tab;
-		}
-
-		this.switchTab(this.activeTab, update);
-    }
-
-    /**
-     * Auto-detect and initialize child tab containers
-     */
-    initializeChildTabs() {
-
-        this.tabs.querySelectorAll('button').forEach(tab => {
-            let hasChildren = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
-
-            if(hasChildren && hasChildren.querySelector('.tabs')){
-                let container = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
-				let tabs = new window.jvbTabs(container, {updateURL: false}, this);
-                this.childTabs.set(tab.dataset.tab, tabs);
-            }
-        });
-        // Find all tab content panels in this container
-        // const tabPanels = this.container.querySelectorAll('.tab-content');
-        // tabPanels.forEach(panel => {
-        //     const tabId = panel.dataset.tab;
-        //     if (!tabId) return;
-        //
-        //     // Look for nested tab containers within this panel
-        //     const nestedTabContainers = panel.querySelectorAll('.tabs, [data-tabs]');
-        //
-        //     nestedTabContainers.forEach(nestedContainer => {
-        //         // Create a new TabsContainer for this nested container
-        //         const childContainer = new TabsContainer(nestedContainer, {}, this);
-        //
-        //         // Store the child container
-        //         this.childTabs.set(tabId, childContainer);
-        //     });
-        // });
-    }
-
-    /**
-     * Get initial tab from URL hash
-     * @returns {string|false} Tab identifier from hash or null if not present
-     */
-    getInitialTabFromHash() {
-        if (!window.location.hash) return false;
-
-        const hash = window.location.hash.substring(1); // Remove the # character
-        const pathParts = hash.split('/');
-
-        // If this is a root container, check first path part
-        if (!this.parent) {
-            const rootTabId = pathParts[0];
-            // Check if this hash corresponds to a valid tab
-            const matchingTab = this.tabs.querySelector(`[data-tab="${rootTabId}"]`);
-            if (matchingTab) {
-                return rootTabId;
-            }
-        }
-        // If this is a child container, check if our parent's active tab matches the path
-        else if (this.parent && pathParts.length > 1) {
-            const parentIdx = this.getParentDepth();
-            if (parentIdx < pathParts.length) {
-                const childTabId = pathParts[parentIdx];
-                const matchingTab = this.tabs.querySelector(`[data-tab="${childTabId}"]`);
-                if (matchingTab) {
-                    return childTabId;
-                }
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Calculate the depth of this tabs container in the hierarchy
-     * @returns {number} The depth (0 for root, 1 for first level child, etc.)
-     */
-    getParentDepth() {
-        let depth = 0;
-        let parent = this.parent;
-
-        while (parent) {
-            depth++;
-            parent = parent.parent;
-        }
-
-        return depth;
-    }
-
-    /**
-     * Build the full path for this tab including all parent tabs
-     * @param {string} tabId - The current tab ID
-     * @returns {string} The full path including parent tabs
-     */
-    getFullTabPath(tabId) {
-        if (!this.parent) {
-            return tabId;
-        }
-
-        // Get parent's active tab path and append this tab
-        const parentPath = this.parent.getFullTabPath(this.parent.activeTab);
-        return `${parentPath}/${tabId}`;
-    }
-
-    /**
-     * Switch between tabs
-     * @param {string} tab - Tab to switch to ('items' or 'lists')
-     * @param {boolean} updateHistory - Whether to push the state to the url
-     */
-	switchTab(tab, updateHistory = false) {
-		document.activeElement?.blur();
-
-		// Update tab buttons
-		this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => {
-			tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab);
-			tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab);
-		});
-
-		// Update tab panels
-		this.container.querySelectorAll('.tab-content').forEach(content => {
-			content.classList.toggle('active', content.dataset.tab === tab);
-			content.setAttribute('aria-hidden', content.dataset.tab !== tab);
-			content.hidden = content.dataset.tab !== tab;
-		});
-
-		// Update state
-		this.activeTab = tab;
-		if (this.callbacks[tab]) {
-			this.callbacks[tab]();
-		}
-
-		// Activate first child tab if this tab has children
-		const childContainer = this.childTabs.get(tab);
-		if (childContainer) {
-			const firstTab = childContainer.container.querySelector('button.tab')?.dataset.tab;
-			if (firstTab) {
-				childContainer.switchTab(firstTab, false);
-			}
-		}
-
-		// Update URL hash with full path (only from root container)
-		if (updateHistory) {
-			if (!this.parent) {
-				window.history.pushState({ tab: tab }, '', `#${tab}`);
-			} else {
-				// This is a child container, notify parent to update URL
-				this.parent.updateUrlFromChild();
-			}
-		}
-
-		// Update select dropdown if it exists
-		if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) {
-			this.selectDropdown.value = tab;
-		}
-
-		// Announce to screen readers
-		this.a11y.announce(`Switched to ${tab} tab`);
+		this.hasHash = false;
+		this.init();
+	}
+	init() {
+		this.initElements();
+		this.initListeners();
 	}
 
-    /**
-     * Update URL when a child tab changes
-     */
-    updateUrlFromChild() {
-		console.log('Updating URL');
-		if (('updateURL' in this.callbacks) ? this.callbacks.updateURL : true) {
-			if (!this.parent) {
-				// Only the root container should update the URL
-				const fullPath = this.getFullTabPath(this.activeTab);
-				window.history.pushState({ tab: fullPath }, '', `#${fullPath}`);
-			} else {
-				// Propagate up to the root
-				this.parent.updateUrlFromChild();
+	initElements() {
+		this.selectors = {
+			nav: '.tabs',
+			tab: '[data-tab]',
+			active: 'button.tab.active',
+			section: '.tab-content',
+			button: 'button.tab',
+			select: 'select.tab-list',
+		};
+	}
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+	}
+		handleClick(e) {
+			let config = this.getConfig(e.target);
+			if (!config) return;
+
+			const tab = e.target.closest(this.selectors.tab);
+			if (tab) {
+				this.switchTab(tab.dataset.tab, config);
 			}
 		}
-    }
+
+		handleChange(e) {
+			let config = this.getConfig(e.target);
+			if (!config) return;
+			if (!config) return;
+			this.switchTab(e.target.value, config);
+		}
+
+	/**
+	 *
+	 * @param {HTMLElement} container
+	 * @param {object} options
+	 */
+	registerTab(container, options = {}) {
+		if (!container) return false;
+
+		let	ui = window.uiFromSelectors(this.selectors, container);
+		if (!ui.nav || !ui.section) {
+			console.error('No tab navigation or section found');
+			return false;
+		}
+		let tabsId = window.generateID('tab');
+
+		container.dataset.tabsId = tabsId;
+		ui.buttons = Array.from(container.querySelectorAll(this.selectors.button));
+		ui.sections = Array.from(container.querySelectorAll(this.selectors.section));
+		ui.sections.forEach(section => {
+			if (section.querySelector('.tabs')) {
+				options.hasChildren = true;
+				this.registerTab(section, {parent: tabsId});
+			}
+		});
+
+		if (ui.select) {
+			ui.options = Array.from(ui.select.querySelectorAll('option'));
+		}
+
+		let config = {
+			id: tabsId,
+			ui: ui,
+			updateURL: options.updateURL ?? true
+		};
+
+		//Add listeners
+		ui.nav.addEventListener('click', this.clickHandler);
+		ui.select?.addEventListener('change', this.changeHandler);
+		this.tabs.set(tabsId, config);
+		this.determineActiveTab(config);
+
+		return config;
+	}
+
+	determineActiveTab(config) {
+		if (this.getInitialTabFromHash()) {
+			let updated = this.tabs.get(config.id);
+			if (updated.activeTab && config.activeTab && updated.activeTab === config.activeTab) return;
+		}
+		let tab = config.ui.buttons[0].dataset.tab??false;
+		if (tab) {
+			this.switchTab(tab, config);
+		}
+	}
+	getInitialTabFromHash() {
+		if (this.hasHash || !window.location.search) return false;
+		const params = new URLSearchParams(window.location.search);
+		const hash = params.get('tab');
+		if (!hash) return false;
+
+		const parts = hash.split('|');
+
+		parts.forEach(part => {
+			// Find the config that has a button matching this part
+			const conf = Array.from(this.tabs.values()).find(tab =>
+				tab.ui.buttons.some(btn => btn.dataset.tab === part)
+			);
+
+			if (conf) {
+				this.switchTab(part, conf);
+			}
+		});
+
+		this.hasHash = true;
+		return true;
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} container
+	 */
+	removeTab(container) {
+		if (!container || !container.dataset.tabsId) return;
+		let config = this.tabs.get(container.dataset.tabsId);
+		if (!config) return;
+		config.ui.nav.removeEventListener('click', this.clickHandler);
+		config.ui.select?.removeEventListener('change', this.changeHandler);
+		this.tabs.delete(container.dataset.tabsId);
+	}
+
+	/**
+	 *
+	 * @param {string} tab
+	 * @param {string|Object} config Either the key of the tabs instance, or a tab config object
+	 */
+	switchTab(tab, config) {
+		config = (typeof config === 'string') ? this.tabs.get(config) : config;
+		if (!config) return;
+		let activeTab = config.ui.sections.filter(section => section.classList.contains('active'));
+
+		if (Object.hasOwn(config, 'preCheck') && !config.preCheck(activeTab[0], config)) return;
+		if (document.activeElement && this.isInTabs(document.activeElement, config)) document.activeElement.blur();
+
+		config.ui.buttons.forEach((btn, index) => {
+			btn.classList.remove('active', 'previous', 'next');
+			let isActive = btn.dataset.tab === tab;
+			btn.setAttribute('aria-selected', isActive);
+			if (isActive) {
+				btn.classList.add('active');
+				let prv = Math.max(index - 1, 0);
+				let next = Math.min(index+1, config.ui.buttons.length -1);
+				if (prv !== index) {
+					config.ui.buttons[prv]?.classList.add('previous');
+				}
+				if (next !== index) {
+					config.ui.buttons[next]?.classList.add('next');
+				}
+			}
+		});
+
+		config.ui.sections.forEach((section, index) => {
+			let isActive = section.dataset.tab === tab;
+			section.classList.toggle('active', isActive);
+			section.setAttribute('aria-hidden', !isActive);
+			section.hidden = !isActive;
+		});
+
+		this.notify('tab-switched', {
+			previous: config.activeTab,
+			current: tab,
+			config: config
+		});
+		config.activeTab = tab;
+
+		this.tabs.set(config.id, config);
+		if (config?.hasChildren) this.updateChildTabs(config.id);
+		if (config?.updateURL) this.updateURL(config);
+		if (config.ui.select) this.maybeUpdateSelect(tab, config);
+
+		this.a11y.announce(`Switched to ${tab} tab`);
+	}
+	updateChildTabs(id) {
+		Array.from(this.tabs.values()).filter(conf => conf.parent === id).forEach(inst => {
+			let firstBtn = inst.ui.buttons[0].dataset.tab??false;
+			if (firstBtn) {
+				this.switchTab(firstBtn, inst);
+			}
+		});
+	}
+
+	updateURL(config) {
+		if (!config.updateURL) return;
+		let hash = this.checkAncestorsHash(config);
+		if (hash) {
+			window.history.pushState({tab:config.activeTab},'',`?tab=${hash}`);
+		}
+	}
+	checkAncestorsHash(conf) {
+		const parts = [];
+		let current = conf;
+
+		while (current) {
+			parts.unshift(current.activeTab);
+			current = current.parent ? this.tabs.get(current.parent) : null;
+		}
+
+		return parts.join('|');
+	}
+	maybeUpdateSelect(tab, config) {
+		if (!config.ui.select || !Object.hasOwn(config, 'options')) return;
+		config.options.forEach(option => {
+			if (option.value === tab) {
+				config.ui.select.value = tab;
+				return;
+			}
+		});
+
+	}
+
+
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/**************************************************************
+	 UTILITY
+	**************************************************************/
+	/**
+	 *
+	 * @param {HTMLElement} target
+	 * @returns {object|boolean}
+	 */
+	getConfig(target) {
+		const instance = target.closest('[data-tabs-id]');
+		if (!instance) return false;
+		const config = this.tabs.get(instance.dataset.tabsId);
+		if (!config) return false;
+		return config;
+	}
+	isInTabs(target, config) {
+		return config.ui.sections.some(section => section.contains(target));
+	}
+	/**************************************************************
+	 CLEANUP
+	**************************************************************/
+	destroy() {
+		this.subscribers.clear();
+
+		Array.from(this.tabs.values()).forEach(tab => {
+			tab.ui.nav.removeEventListener('click', this.clickHandler);
+			tab.ui.select?.removeEventListener('change', this.changeHandler);
+		});
+	}
 }
 
-window.jvbTabs = TabsContainer;
-
+document.addEventListener('DOMContentLoaded', function() {
+	window.jvbTabs = new TabsContainer();
+});
diff --git a/assets/js/concise/TabsOLD.js b/assets/js/concise/TabsOLD.js
new file mode 100644
index 0000000..daadd0a
--- /dev/null
+++ b/assets/js/concise/TabsOLD.js
@@ -0,0 +1,233 @@
+/**
+ * Handles the tabs functionality, given the html requirements are met
+ */
+class TabsContainer{
+    constructor(container, callbacks = {}, parent = null){
+        this.tabs = container.querySelector('.tabs');
+        this.a11y = window.jvbA11y;
+
+		this.updateURL = true;
+
+        this.parent = parent;
+        this.childTabs = new Map();
+		if ('updateURL' in callbacks && callbacks.updateURL === false) {
+			this.updateURL = false;
+		}
+        this.callbacks = callbacks;
+
+
+        this.activeTab = (this.updateURL) ? this.getInitialTabFromHash() :
+            container.querySelector('button.tab.active')?.dataset.tab;
+
+        this.container = container;
+
+        if (this.tabs) {
+            this.tabs.addEventListener('click', e => {
+                const tab = e.target.closest('[data-tab]');
+                if (tab) {
+					let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
+                    this.switchTab(tab.dataset.tab, update);
+                }
+            });
+        }
+
+        // Check for and initialize child tab containers
+        this.initializeChildTabs();
+
+        // Find and set up select dropdown listeners
+        this.selectDropdown = document.querySelector('select.tab-list');
+        if (this.selectDropdown) {
+            this.selectDropdown.addEventListener('change', e => {
+
+				let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
+                this.switchTab(e.target.value, update);
+            });
+        }
+
+		let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true;
+
+		if (!this.activeTab){
+			this.activeTab = document.querySelector('button.tab')?.dataset.tab;
+		}
+
+		this.switchTab(this.activeTab, update);
+    }
+
+    /**
+     * Auto-detect and initialize child tab containers
+     */
+    initializeChildTabs() {
+
+        this.tabs.querySelectorAll('button').forEach(tab => {
+            let hasChildren = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
+
+            if(hasChildren && hasChildren.querySelector('.tabs')){
+                let container = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
+				let tabs = new window.jvbTabs(container, {updateURL: false}, this);
+                this.childTabs.set(tab.dataset.tab, tabs);
+            }
+        });
+        // Find all tab content panels in this container
+        // const tabPanels = this.container.querySelectorAll('.tab-content');
+        // tabPanels.forEach(panel => {
+        //     const tabId = panel.dataset.tab;
+        //     if (!tabId) return;
+        //
+        //     // Look for nested tab containers within this panel
+        //     const nestedTabContainers = panel.querySelectorAll('.tabs, [data-tabs]');
+        //
+        //     nestedTabContainers.forEach(nestedContainer => {
+        //         // Create a new TabsContainer for this nested container
+        //         const childContainer = new TabsContainer(nestedContainer, {}, this);
+        //
+        //         // Store the child container
+        //         this.childTabs.set(tabId, childContainer);
+        //     });
+        // });
+    }
+
+    /**
+     * Get initial tab from URL hash
+     * @returns {string|false} Tab identifier from hash or null if not present
+     */
+    getInitialTabFromHash() {
+        if (!window.location.hash) return false;
+
+        const hash = window.location.hash.substring(1); // Remove the # character
+        const pathParts = hash.split('/');
+
+        // If this is a root container, check first path part
+        if (!this.parent) {
+            const rootTabId = pathParts[0];
+            // Check if this hash corresponds to a valid tab
+            const matchingTab = this.tabs.querySelector(`[data-tab="${rootTabId}"]`);
+            if (matchingTab) {
+                return rootTabId;
+            }
+        }
+        // If this is a child container, check if our parent's active tab matches the path
+        else if (this.parent && pathParts.length > 1) {
+            const parentIdx = this.getParentDepth();
+            if (parentIdx < pathParts.length) {
+                const childTabId = pathParts[parentIdx];
+                const matchingTab = this.tabs.querySelector(`[data-tab="${childTabId}"]`);
+                if (matchingTab) {
+                    return childTabId;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Calculate the depth of this tabs container in the hierarchy
+     * @returns {number} The depth (0 for root, 1 for first level child, etc.)
+     */
+    getParentDepth() {
+        let depth = 0;
+        let parent = this.parent;
+
+        while (parent) {
+            depth++;
+            parent = parent.parent;
+        }
+
+        return depth;
+    }
+
+    /**
+     * Build the full path for this tab including all parent tabs
+     * @param {string} tabId - The current tab ID
+     * @returns {string} The full path including parent tabs
+     */
+    getFullTabPath(tabId) {
+        if (!this.parent) {
+            return tabId;
+        }
+
+        // Get parent's active tab path and append this tab
+        const parentPath = this.parent.getFullTabPath(this.parent.activeTab);
+        return `${parentPath}|${tabId}`;
+    }
+
+    /**
+     * Switch between tabs
+     * @param {string} tab - Tab to switch to ('items' or 'lists')
+     * @param {boolean} updateHistory - Whether to push the state to the url
+     */
+	switchTab(tab, updateHistory = false) {
+		document.activeElement?.blur();
+
+		// Update tab buttons
+		this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => {
+			tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab);
+			tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab);
+		});
+
+		// Update tab panels
+		this.container.querySelectorAll('.tab-content').forEach(content => {
+			content.classList.toggle('active', content.dataset.tab === tab);
+			content.setAttribute('aria-hidden', content.dataset.tab !== tab);
+			content.hidden = content.dataset.tab !== tab;
+		});
+
+		// Update state
+		this.activeTab = tab;
+		if (this.callbacks[tab]) {
+			this.callbacks[tab]();
+		}
+
+		// Activate first child tab if this tab has children
+		const childContainer = this.childTabs.get(tab);
+		if (childContainer) {
+			const firstTab = childContainer.container.querySelector('button.tab')?.dataset.tab;
+			if (firstTab) {
+				childContainer.switchTab(firstTab, false);
+			}
+		}
+
+		// Update URL hash with full path (only from root container)
+		if (updateHistory) {
+			if (!this.parent) {
+				window.history.pushState({ tab: tab }, '', `#${tab}`);
+			} else {
+				// This is a child container, notify parent to update URL
+				this.parent.updateUrlFromChild();
+			}
+		}
+
+		// Update select dropdown if it exists
+		if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) {
+			this.selectDropdown.value = tab;
+		}
+
+		//TODO: Initialize notification system
+		// this.notify('tab-switched', {
+		// 	current: this.activeTab
+		// });
+
+		// Announce to screen readers
+		this.a11y.announce(`Switched to ${tab} tab`);
+	}
+
+    /**
+     * Update URL when a child tab changes
+     */
+    updateUrlFromChild() {
+		console.log('Updating URL');
+		if (('updateURL' in this.callbacks) ? this.callbacks.updateURL : true) {
+			if (!this.parent) {
+				// Only the root container should update the URL
+				const fullPath = this.getFullTabPath(this.activeTab);
+				window.history.pushState({ tab: fullPath }, '', `#${fullPath}`);
+			} else {
+				// Propagate up to the root
+				this.parent.updateUrlFromChild();
+			}
+		}
+    }
+}
+
+window.jvbTabs = TabsContainer;
+
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 5244f02..bdbe06b 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -14,6 +14,7 @@
 
 		this.activeField = null;
 		this.isInitializing = true;
+		this.lazyInit = false;
 		this.messageText = {}
 		this.init();
 	}
@@ -21,6 +22,7 @@
 	init() {
 		this.initStore();
 		this.initElements();
+		this.defineTemplates();
 		this.initModal();
 		this.scanExistingFields();
 		this.initListeners();
@@ -62,6 +64,88 @@
 		this.store.subscribe(this.handleStoreEvent.bind(this));
 	}
 
+	defineTemplates() {
+		const T = window.jvbTemplates;
+		const terms = this;
+
+		T.define('emptyState');
+		T.define('selectedTerm', {
+			refs: {
+				name: '.item-name',
+				btn: 'button',
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.dataset.taxonomy = data.taxonomy;
+				if (refs.name) refs.name.textContent = data.path;
+				if (refs.button) refs.button.title = `Remove ${data.name}`;
+			}
+		});
+		T.define('termListItem', {
+			refs: {
+				checkbox: 'input',
+				label: 'label',
+				name: 'span, .term-name'
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+
+				let field = terms.currentField();
+				let isSelected = terms.selectedTerms.get(terms.activeField).has(data.id);
+				let limitReached = field.limit > 0 && terms.selectedTerms.get(terms.activeField).size >= field.limit;
+
+				if (refs.checkbox) {
+					refs.checkbox.dataset.id = data.id;
+					refs.checkbox.id = `${field.element.id}-${data.id}`;
+					refs.checkbox.name = `${field.element.id}-${field.taxonomy}-select`;
+					refs.checkbox.value = data.id;
+					refs.checkbox.disabled = !isSelected && limitReached;
+					refs.checkbox.checked = isSelected;
+				}
+				if (refs.label) {
+					refs.label.htmlFor = `${field.element.id}-${data.id}`;
+					refs.label.title = data.path??data.name;
+					refs.label.dataset.path = data.path;
+				}
+				if (refs.name) {
+					refs.name.textContent = data.show ? data.path : data.name;
+				}
+
+				if (data.hasChildren) {
+					let temp = {
+						plural: field.plural,
+						name: data.name
+					};
+					const toggle = window.jvbTemplates.create('termChildrenToggle', temp);
+					el.append(toggle);
+				}
+			}
+		});
+
+		T.define('termChildrenToggle', {
+			setup({el, refs, manyRefs, data}) {
+				el.ariaLabel = `View ${data.plural} nested under ${data.name}`;
+			}
+		});
+
+		T.define('termBreadcrumb', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.name;
+				el.title = data.name;
+			}
+		});
+
+		T.define('autocompleteItem', {
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.textContent = data.path||data.name;
+				el.title = `Select ${data.name}`;
+			}
+		});
+
+
+	}
 	/******************************************************************
 	 ELEMENTS
 	 ******************************************************************/
@@ -148,19 +232,23 @@
 	}
 
 	handleClick(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
 		const fieldId = this.getFieldId(e.target) || this.activeField;
 		const field = this.fields.get(fieldId);
 		if (!fieldId || !field) return;
 
-		const autoComplete = window.targetCheck(e, '.item.autocomplete');
+		const autocomplete = window.targetCheck(e, '.item.autocomplete');
 
-		if (autoComplete) {
-			let termId = parseInt(autoComplete.dataset.id);
+		if (autocomplete) {
+			let termId = parseInt(autocomplete.dataset.id);
 			this.addSelected(termId, fieldId);
-			this.scheduleHideDropdown(fieldId);
+			this.scheduleHideDropdown(fieldId, 6000);
 			if (field.ui.search) {
 				field.ui.search.value = '';
 			}
+			return;
 		}
 
 		const toggleButton = window.targetCheck(e, this.selectors.field.toggle);
@@ -207,12 +295,14 @@
 		if (pathLevel) {
 			const termId = parseInt(pathLevel.dataset.id)??0;
 			this.navigateTo(termId);
+			return;
 		}
 
 		const dropdown = window.targetCheck(e, this.selectors.field.dropdown);
 		if (dropdown) {
 			// reset the timer for hiding the dropdown
 			this.scheduleHideDropdown(fieldId);
+			return;
 		}
 
 		const clearSearch = window.targetCheck(e, this.selectors.search.clear);
@@ -240,7 +330,7 @@
 
 	}
 	handleChange(e) {
-		if (!this.container.contains(e.target)) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
 			return;
 		}
 		if (!['checkbox', 'button'].includes(e.target.type)) return;
@@ -257,6 +347,9 @@
 	}
 	//For search in modal or field autocomplete
 	handleInput(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
 		let fieldId = this.getFieldId(e.target)??this.activeField;
 		if (!fieldId) return;
 		const field = this.fields.get(fieldId);
@@ -315,9 +408,13 @@
 	}
 
 	handleFocus(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
 		const fieldId = this.getFieldId(e.target);
+		if (!fieldId) return;
 		const field = this.fields.get(fieldId);
-		if (!fieldId || !field) return;
+		if (!field) return;
 		if (!field.hasAutocomplete && !field.hasSearch) return;
 
 		window.debouncer.cancel(`${fieldId}-search-results`);
@@ -329,16 +426,20 @@
 
 	//Hide autocomplete dropdown on blur
 	handleBlur(e) {
+		if (!this.container.contains(e.target) && !e.target.closest('[data-type="selector"], [data-field-type="selector"]')) {
+			return;
+		}
 		const fieldId = this.getFieldId(e.target);
+		if (!fieldId) return;
 		const field = this.fields.get(fieldId);
-		if (!fieldId || ! field) return;
+		if (!field) return;
 		if (!field.hasAutocomplete || this.container.open) return;
 		if (e.relatedTarget && field.ui.dropdown.wrapper?.contains(e.relatedTarget)) return;
 
 		this.scheduleHideDropdown(fieldId);
 	}
 
-	scheduleHideDropdown(fieldId){
+	scheduleHideDropdown(fieldId, delay = 1500){
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 
@@ -352,7 +453,7 @@
 					field.ui.dropdown.wrapper.hidden = true;
 				}
 			},
-			1500
+			delay
 		);
 	}
 
@@ -455,6 +556,10 @@
 	closeModal() {
 		const field = this.fields.get(this.activeField);
 		if (!field) return;
+
+
+		this.updateFieldValue(this.activeField);
+
 		this.observer.unobserve(this.ui.terms.sentinel);
 		window.removeChildren(this.ui.terms.list);
 
@@ -522,24 +627,26 @@
 		if (!field) return;
 		if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
 
-		const item = window.getTemplate('selectedTerm');
-		if (!item) return;
+		this.ui.selected.append(this.getSelectedTermUI(term));
+	}
 
-		item.dataset.id = termId;
-		item.dataset.taxonomy = field.taxonomy;
-		item.querySelector('.item-name').textContent = term.path;
-		item.querySelector('button').title = `Remove ${term.name}`;
-
-		this.ui.selected.append(item);
+	getSelectedTermUI(term, showPath = true) {
+		return window.jvbTemplates.create('selectedTerm', term);
 	}
 	/******************************************************************
 	 FIELDS
 	 ******************************************************************/
 	scanExistingFields(container = document.body) {
-		container.querySelectorAll('[data-type="selector"]').forEach(
+		container.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach(
 			selector => {
 				try {
-					this.registerField(selector);
+					if (selector.dataset.lazy) {
+						this.lazyInit = true;
+					} else {
+						// Register field if not already registered
+						// registerField will check if already registered and return early if so
+						this.registerField(selector);
+					}
 				} catch (error) {
 					this.error.log(error, {
 						component: 'TaxonomySelector',
@@ -549,12 +656,41 @@
 				}
 			}
 		);
+		if (this.lazyInit) {
+			this.initObserver(container);
+		}
+	}
+
+	unregisterFields(container) {
+		container.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach(
+			selector=> {
+				this.fields.delete(selector.dataset.fieldId);
+			}
+		);
+	}
+	initObserver(container){
+		this.lazyObserver = new IntersectionObserver((entries) => {
+			entries.forEach(entry => {
+				if (entry.isIntersecting && entry.target.dataset.lazy) {
+					delete entry.target.dataset.lazy;
+					this.registerField(entry.target);
+					this.lazyObserver.unobserve(entry.target);
+				}
+			});
+		}, {rootMargin: '50px'});
+
+		container.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach(field => {
+			this.lazyObserver.observe(field);
+		});
 	}
 
 	registerField(element, options = {}) {
+		if (element.dataset.fieldId && this.fields.has(element.dataset.fieldId)) {
+			return element.dataset.fieldId; // Already registered
+		}
+
 		let input = element.querySelector('input[type="hidden"]');
 		if (!input && !Object.hasOwn(element.dataset, 'filter')) {
-			console.warn('TaxonomySelector: No hidden input found for field', element);
 			return;
 		}
 
@@ -578,7 +714,6 @@
 				autocomplete: Object.hasOwn(button.dataset, 'autocomplete'),
 				creatable: Object.hasOwn(button.dataset, 'creatable')
 			};
-			if (Object.keys(options).length === 0) return;
 		} else if (Object.hasOwn(options, 'toggle')) {
 			button = document.querySelector(options.toggle);
 			selectors.toggle = options.toggle;
@@ -629,7 +764,18 @@
 		if (this.isInitializing) {
 			this.batchFetch.add(config.taxonomy);
 		}
-		this.updateFieldUI(fieldId);
+
+		if (element.offsetParent !== null) {
+			this.updateFieldUI(fieldId);
+		} else {
+			// Defer until visible
+			requestIdleCallback(() => {
+				if (element.offsetParent !== null) {
+					this.updateFieldUI(fieldId);
+				}
+			}, {timeout: 2000});
+
+		}
 
 		return fieldId;
 	}
@@ -695,7 +841,8 @@
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 		let selected = Array.from(this.selectedTerms.get(fieldId));
-		field.ui.value.value = selected.join(',');
+		field.ui.value.value = selected.join(',')??'';
+		field.ui.value.dispatchEvent(new Event('change', { bubbles: true }));
 	}
 
 	checkLimits(fieldId) {
@@ -757,6 +904,7 @@
 			if (this.ui.terms.sentinel) {
 				this.observer.unobserve(this.ui.terms.sentinel);
 			}
+			return;
 		}
 
 		this.setCreateButton(true);
@@ -772,76 +920,20 @@
 		const currentParent = this.store.filters.parent??0;
 		this.ui.nav.back.hidden = currentParent === 0;
 
-		const fragment = document.createDocumentFragment();
-		terms.forEach(term => {
-			const element = this.createTermElement({
-				show: showPath,
-				... term
-			});
-			if (element) {
-				fragment.append(element);
-			}
-		});
+		window.chunkIt(
+			terms,
+			(term) => this.createTermElement({show:showPath, ... term}),
+			(fragment) => this.ui.terms.list.append(fragment),
+			10
+		).then(()=>{});
 
 		if (terms.length > 0) {
 			this.setMessage(false);
 		}
-
-		this.ui.terms.list.append(fragment);
 	}
 	createTermElement(term) {
 		if (!term || !term.name) return null;
-
-		const item = window.getTemplate('termListItem');
-		item.dataset.id = term.id;
-
-		const isSelected = this.selectedTerms.get(this.activeField).has(term.id);
-		let [
-			checkbox,
-			label,
-			nameSpan
-		] = [
-			item.querySelector('input'),
-			item.querySelector('label'),
-			item.querySelector('span, .term-name')
-		];
-
-		let field = this.currentField();
-		let limitReached = field.limit > 0 && this.selectedTerms.get(this.activeField).size >= field.limit;
-		if (checkbox && label && nameSpan) {
-			[
-				checkbox.dataset.id,
-				checkbox.id,
-				checkbox.name,
-				checkbox.value,
-				checkbox.disabled,
-				checkbox.checked,
-				label.htmlFor,
-				label.title,
-				label.dataset.path,
-				nameSpan.textContent
-			] = [
-				term.id,
-				`${field.element.id}-${term.id}`,
-				`${field.element.id}-${field.taxonomy}-select`,
-				term.id,
-				!isSelected && limitReached,
-				isSelected,
-				`${field.element.id}-${term.id}`,
-				term.path??term.name,
-				term.path,
-				term.show ? term.path : term.name
-			];
-			if (term.hasChildren) {
-				const toggle = window.getTemplate('termChildrenToggle');
-				if (toggle) {
-					toggle.ariaLabel = `View ${field.plural} nested under ${term.name}`;
-					item.append(toggle);
-				}
-			}
-		}
-
-		return item;
+		return window.jvbTemplates.create('termListItem', term);
 	}
 
 	showAutocompleteTerms() {
@@ -856,12 +948,12 @@
 		if (terms.length === 0) {
 			this.setMessage(true, `No ${field.plural} found.`, false);
 		} else {
-			terms.forEach(term => {
-				const item = this.createAutocompleteTerm(term);
-				if (item) {
-					dropdown.append(item);
-				}
-			});
+			window.chunkIt(
+				terms,
+				(term) => this.createAutocompleteTerm(term),
+				(fragment) => dropdown.append(fragment)
+			).then(()=>{});
+
 			this.setMessage(false);
 		}
 		this.setCreateButton(true);
@@ -870,14 +962,9 @@
 			field.ui.dropdown.wrapper.hidden = false;
 		}
 	}
+
 	createAutocompleteTerm(term) {
-		const item = window.getTemplate('autocompleteItem');
-		if (!item) return;
-
-		item.dataset.id = term.id;
-		item.textContent = term.path || term.name;
-
-		return item;
+		return window.jvbTemplates.create('autocompleteItem', term);
 	}
 	/******************************************************************
 	 UI
@@ -886,16 +973,12 @@
 		const term = this.store.get(termId);
 		const field = this.fields.get(fieldId);
 		if (!term || !field) return;
+
 		//if the term already exists in the selected items, bail early
 		if (field.ui.selected && field.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
 
-		const item = window.getTemplate('selectedTerm');
-		if (!item) return;
 
-		item.dataset.id = termId;
-		item.dataset.taxonomy = field.taxonomy;
-		item.querySelector('.item-name').textContent = term.path;
-		item.querySelector('button').title = `Remove ${term.name}`;
+		let item = this.getSelectedTermUI(term);
 
 		if (field.ui.selected) {
 			field.ui.selected.append(item);
@@ -926,13 +1009,7 @@
 			// Add new breadcrumb
 			const term = this.store.get(termId);
 			if (!term) return;
-
-			const crumb = window.getTemplate('termBreadcrumb');
-			if (!crumb) return;
-
-			crumb.dataset.id = termId;
-			crumb.textContent = term.name;
-			crumb.title = term.name;
+			const crumb = window.jvbTemplates.create('termBreadcrumb', term);
 
 			nav.append(crumb);
 		}
@@ -955,6 +1032,13 @@
 	/******************************************************************
 	 UTILITY
 	 ******************************************************************/
+	checkRendered(collection, term) {
+		if (!collection) return;
+		if (!Object.hasOwn(collection, term.taxonomy)) {
+			collection[term.taxonomy] = new Map();
+		}
+		return collection[term.taxonomy].has(term.id);
+	}
 	currentField() {
 		return this.fields.get(this.activeField)??false;
 	}
@@ -1222,6 +1306,7 @@
 			this.observer?.unobserve(this.ui.terms.sentinel);
 		}
 		this.observer?.disconnect();
+		this.lazyObserver?.disconnect();
 
 		// Remove event listeners
 		document.removeEventListener('click', this.clickHandler);
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index ebc4209..f2e9188 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -3,6 +3,7 @@
 		this.a11y = window.jvbA11y;
 		this.queue = window.jvbQueue;
 		this.error = window.jvbError;
+		this.templates = window.jvbTemplates;
 
 		this.subscribers = new Set();
 
@@ -21,6 +22,232 @@
 		this.previewUrls = new Set();
 		this.initElements();
 		this.initListeners();
+		this.defineTemplates();
+	}
+
+	defineTemplates() {
+		const T = this.templates;
+		const images = this;
+
+		T.define('uploadItem', {
+			refs: {
+				select: '[name="select-item"]',
+				featured: '[name="featured"]',
+				img: 'img',
+				video: 'video',
+				file: 'label > span',
+				details: 'details',
+			},
+			manyRefs: {
+				inputs: 'input',
+			},
+			setup({el, refs, manyRefs, data}) {
+				const isNewUpload = Object.hasOwn(data, 'file');
+				let mimeType;
+				let url;
+				let alt;
+				let previewUrl = false;
+				if (isNewUpload) {
+					el.dataset.uploadId = data.uploadId;
+					mimeType = images.getSubtypeFromMime(data.file.type)||'image';
+					url = (mimeType !== 'document') ? images.createPreviewUrl(data.file) : false;
+					previewUrl = url;
+					alt = data.file.name||'';
+				} else {
+					el.dataset.id = data.id;
+					mimeType = images.getSubtypeFromURL(data.medium??data.src);
+					url = data.medium??data.src;
+					alt = data['image-alt-text']??'';
+				}
+
+
+				el.dataset.subtype = mimeType;
+
+				if (refs.featured) {
+					refs.featured.value = data.uploadId;
+				}
+				switch (mimeType) {
+					case 'image':
+						if (refs.img) {
+							refs.img.src = url;
+							refs.img.alt = alt;
+
+							if (previewUrl) refs.img.dataset.previewUrl = previewUrl;
+						}
+						if (refs.video) refs.video.remove();
+						if (refs.file) refs.file.remove();
+						break;
+					case 'video':
+						if (refs.video) {
+							refs.video.src = url;
+							refs.video.alt = alt;
+							if (previewUrl) refs.video.dataset.previewUrl = previewUrl;
+						}
+						if (refs.img) refs.img.remove();
+						if (refs.file) refs.file.remove();
+						break;
+					case 'document':
+						if (refs.preview) {
+							let ext = data.file.name.split('.').pop()?.toLowerCase()??'';
+							let map = {
+								'pdf': 'file-pdf', 'csv': 'file-csv',
+								'doc': 'file-doc', 'docx': 'file-doc',
+								'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+							};
+							let icon = window.getIcon(map[ext]??'file');
+							refs.preview.innerText = data.file.name??data.title;
+							refs.preview.prepend(icon);
+						}
+						if (refs.img) refs.img.remove();
+						if (refs.video) refs.video.remove();
+						break;
+				}
+				if (refs.details) {
+					refs.details.append(T.create('uploadMeta'));
+				}
+
+
+				el.draggable = el.dataset.mode !== 'single';
+
+				if (manyRefs.inputs) {
+					for (let input of manyRefs.inputs) {
+						window.prefixInput(input, `${data.uploadId}-`);
+					}
+				}
+			}
+		});
+
+		T.define('uploadMeta', {
+			refs: {
+				alt: '[name="alt_text"]',
+				title: '[name="image-title"]',
+				description: '[name="image-caption"]',
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (Object.hasOwn(data, 'alt') && refs.alt) {
+					refs.alt.value = data.alt;
+				}
+				if (Object.hasOwn(data, 'title') && refs.title) {
+					refs.title.value = data.title;
+				}
+				if (Object.hasOwn(data, 'description') && refs.description) {
+					refs.description.value = data.description;
+				}
+			}
+		});
+
+		T.define('imageGroup', {
+			refs: {
+				selectAll: '[data-select-all]',
+				fields: '.fields',
+				details: 'details',
+				grid: '.item-grid',
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.groupId = data.groupId;
+				if (refs.selectAll) {
+					window.prefixInput(refs.selectAll, `select-all-${data.groupId}`, true);
+				}
+				let fields = T.create('groupMetadata', {groupId: data.groupId});
+				if (fields) {
+					refs.fields.append(fields);
+				} else {
+					refs.details.remove();
+				}
+				if (refs.grid) {
+					refs.grid.dataset.groupId = data.groupId;
+				}
+			}
+		});
+
+		T.define('groupMetadata', {
+			manyRefs: {
+				inputs: 'input,textarea,select'
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (refs.inputs) {
+					refs.inputs.forEach(input => {
+						window.prefixInput(input, `${data.groupId}-`);
+					});
+				}
+			}
+		});
+
+		T.define('restoreNotification', {
+			refs: {
+				details: '.details',
+				wrap: '.wrap',
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (refs.details) {
+					let source = data.bySource.size > 1 ? ` across ${data.bySource.size} pages` : '';
+					let upload = data.pendingUploads.length > 1 ? 'uploads' : 'upload';
+					refs.details.textContent = `${data.pendingUploads.length} ${upload} can be recovered${source}`;
+				}
+				if (!refs.wrap) {
+					console.warn('No wrap element in template');
+					return;
+				}
+				let i = 1;
+				for (const [src, uploads] of data.bySource) {
+					let data = {
+						index: i,
+						isCurrent: src === window.location.href,
+						src: src,
+						uploads: uploads
+					};
+					refs.wrap.append(T.create('restoreField', data));
+					i++;
+				}
+			}
+		});
+
+		T.define('restoreField', {
+			refs: {
+				h3: 'h3',
+				a: 'h3 a',
+				grid: '.item-grid'
+			},
+			async setup({el, refs, manyRefs, data}) {
+				let fieldId = images.registerField(el, false, `recovery_${data.index}`);
+				if (data.isCurrent) {
+					el.open = true;
+
+					refs.a?.remove();
+					if (refs.h3) {
+						refs.h3.textContent = 'From this page:';
+					}
+
+				} else {
+					if (refs.a) {
+						refs.a.href = data.src;
+						refs.a.title = 'Navigate to page and restore';
+						refs.a.textContent = data.src;
+					}
+				}
+
+				let filtered = [... new Set(data.uploads.map(upload => upload.group??'preview'))];
+				for (let groupId of filtered) {
+					let group = (groupId === 'preview') ? true : images.stores.groups.get(groupId);
+					if (!group) continue;
+
+					let element = await images.createGroupElement(groupId, fieldId);
+					let groupGrid = element.querySelector('.item-grid');
+					let groupUploads = data.uploads.filter(upload => upload.group === (groupId === 'preview') ? null : groupId);
+
+					for (const [key,  value] of Object.entries(group.fields??{})) {
+						let field = element.querySelector(`input[name*="${key}"]`);
+						if (field) field.value = value;
+					}
+
+					for (let upload of groupUploads) {
+						let item = await images.createUpload(upload.id, images.formatFile(upload), fieldId);
+						groupGrid.append(item);
+					}
+					refs.grid.append(element);
+				}
+			}
+		});
 	}
 
 	initStores() {
@@ -62,7 +289,7 @@
 				const data = operation.data instanceof FormData
 					? this.stores.uploads.formDataToObject(operation.data)
 					: operation.data;
-				console.log(data);
+
 				let uploads = data['upload_ids'];
 				if (!uploads || uploads.length === 0) return;
 				if (event === 'cancel-operation') return this.handleOperationCancelled(uploads);
@@ -895,17 +1122,6 @@
 		const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']});
 		if (pendingUploads.length === 0) return;
 
-		let notification = window.getTemplate('restoreNotification');
-		if (!notification) {
-			this.error.log(
-				'No restore notification',
-				{
-					component: 'UploadManager',
-					src: window.location.href
-				}
-			);
-			return;
-		}
 		// Group by source page
 		const bySource = new Map();
 		pendingUploads.forEach(upload => {
@@ -914,75 +1130,21 @@
 			bySource.get(src).push(upload);
 		});
 
-		const currentSrc = window.location.href;
+		let data = {
+			bySource: bySource,
+			pendingUploads: pendingUploads
+		};
 
-
-		let source = bySource.size > 1 ? ` across ${bySource.size} pages` : '';
-		let upload = pendingUploads.length > 1 ? 'uploads' : 'upload';
-		let message = `${pendingUploads.length} ${upload} can be recovered${source}`;
-
-		let details = notification.querySelector('.details');
-		if (details) {
-			details.textContent = message;
-		}
-
-		let i = 1;
-		for (const [src, uploads] of bySource) {
-			let template = window.getTemplate('restoreField');
-			if (!template) continue;
-			let fieldId = this.registerField(template,false, 'recovery_'+i);
-			let field = this.fields.get(fieldId);
-			i++;
-			let isCurrent = src === currentSrc;
-			let [
-				h3,
-				a,
-				grid
-			] = [
-				template.querySelector('h3'),
-				template.querySelector('h3 a'),
-				template.querySelector('.item-grid')
-			];
-
-			template.open = isCurrent;
-			if (!isCurrent) {
-				[a.href, a.title,a.textContent] =
-					[src, 'Navigate to Page and Restore', src];
-			} else {
-				a.remove();
-				h3.textContent = 'From this page:';
-			}
-
-			let filteredGroupIds = [...new Set(uploads.map(upload => upload.group??'preview'))];
-
-			for (let groupId of filteredGroupIds) {
-				let group = (groupId === 'preview') ? true : this.stores.groups.get(groupId);
-				if (!group) continue;
-
-				let groupElement = await this.createGroupElement(groupId,field.id);
-				let groupGrid = groupElement.querySelector('.item-grid');
-				let theseUploads = uploads.filter(upload => upload.group === (groupId === 'preview') ? null : groupId);
-				for (const [key, value] of Object.entries(group.fields ?? {})) {
-					let field = groupElement.querySelector(`input[name*="${key}"]`);
-					if (field) field.value = value;
-				}
-				for (let upload of theseUploads) {
-					let item = await this.createUpload(upload.id, this.formatFile(upload), field.id);
-					groupGrid.append(item);
-				}
-
-				grid.append(groupElement);
-			}
-			notification.querySelector('.wrap').append(template);
-		}
-		document.body.append(notification);
-		notification = document.querySelector('dialog.restore-uploads');
+		document.body.append(this.templates.create('restoreNotification', data));
+		let notification = document.querySelector('dialog.restore-uploads');
 		this.restoreModal = new window.jvbModal(notification);
 		this.restoreSelection = new window.jvbHandleSelection(notification,
 			{
 				wrapper: {
-					wrapper: '.wrap'
+					wrapper: '.restore-field',
+					id: 'selection'
 				},
+				items: '.item-grid.restore',
 				selectAll: {
 					bulkControls: '.selection-actions',
 					checkbox: '#select-all-restore',
@@ -1116,82 +1278,27 @@
 	 UPLOAD METHODS
 	*******************************************************************************/
 	async createUpload(uploadId, file, fieldId) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) return null;
-
 		let field = this.fields.get(fieldId);
 		if (!field) return null;
 
-		image.dataset.uploadId = uploadId;
-		let mimeType = this.getSubtypeFromMime(file.type)||'image';
-		image.dataset.subtype = mimeType;
-
-		let [featured, img, video, preview, details] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-
-		if (featured) featured.value = uploadId;
-		switch (mimeType) {
-			case 'image':
-				if (img) {
-					const previewUrl = this.createPreviewUrl(file);
-					img.src = previewUrl;
-					img.alt = file.name || '';
-					img.dataset.previewUrl = previewUrl;
-				}
-				video?.remove();
-				preview?.remove();
-				break;
-			case 'video':
-				if (video){
-					const previewUrl = this.createPreviewUrl(file);
-					video.src = previewUrl;
-					video.dataset.previewUrl = previewUrl;
-				}
-				img?.remove();
-				preview?.remove();
-				break;
-			case 'document':
-				let ext = file.name.split('.').pop()?.toLowerCase()??'';
-				let map = {
-					'pdf': 'file-pdf', 'csv': 'file-csv',
-					'doc': 'file-doc', 'docx': 'file-doc',
-					'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
-				};
-				let icon = window.getIcon(map[ext]??'file');
-				if (preview) {
-					preview.innerText = file.name;
-					preview.prepend(icon);
-				}
-				img?.remove();
-				video?.remove();
-				break;
-		}
-
-		if (details) {
-			let template = window.getTemplate('uploadMeta');
-			if (template) details.append(template);
-		}
-
-		image.draggable = field.config.type !== 'single'??false;
-
-		image.querySelectorAll('input').forEach(input  => {
-			let id = input.id;
-			if (id) {
-				let newId = id + uploadId;
-				let label = input.parentNode.querySelector(`label[for="${id}"]`);
-				input.id = newId;
-				if (label) label.htmlFor = newId;
-			}
-		});
-
-		return image;
+		let data = {
+			uploadId: uploadId,
+			file: file,
+			field: field,
+		};
+		return this.templates.create('uploadItem', data);
 	}
 
+	getSubtypeFromURL(url) {
+		const imgs = ['.webp', '.jpg', '.jpeg', '.png', '.gif', '.svg'];
+		const videos = ['.mp4', '.ogg', '.mov', '.webm', '.avi'];
+
+		const path = url.split('?')[0].toLowerCase();
+
+		if (imgs.some(ext => path.endsWith(ext))) return 'image';
+		if (videos.some(ext => path.endsWith(ext))) return 'video';
+		return 'document';
+	}
 	getSubtypeFromMime(mimeType) {
 		if (mimeType.startsWith('image/')) return 'image';
 		if (mimeType.startsWith('video/')) return 'video';
@@ -1299,7 +1406,12 @@
 		const element = this.createGroupElement(groupId, fieldId);
 		if (!element) return null;
 
-		field.groupUI.grid.append(element);
+		const emptyGroup = field.groupUI.empty;
+		if (emptyGroup?.nextSibling) {
+			field.groupUI.grid.insertBefore(element, emptyGroup.nextSibling);
+		} else {
+			field.groupUI.grid.append(element);
+		}
 
 		// Create Sortable for this group's grid
 		const grid = element.querySelector('.item-grid');
@@ -1318,49 +1430,12 @@
 	}
 
 	createGroupElement(groupId, fieldId = null) {
-		let element = window.getTemplate('imageGroup');
-		if (!element) return;
 
-		element.dataset.groupId = groupId;
-		if (fieldId) {
-			element.dataset.fieldId = fieldId;
+		let data = {
+			groupId: groupId,
+			fieldId: fieldId,
 		}
-
-		const selectAll = element.querySelector('[data-select-all]');
-		if (selectAll) {
-			const newId = `select-all-${groupId}`;
-			const label = element.querySelector(`label[for="${selectAll.id}"]`);
-			selectAll.id = newId;
-			selectAll.name = newId;
-			if (label) label.htmlFor = newId;
-		}
-
-		let fields = window.getTemplate('groupMetadata');
-		let container = element.querySelector('.fields');
-		if (fields && container) {
-			container.append(fields);
-
-			let title = container.querySelector('[name="post_title"]');
-			let excerpt = container.querySelector('[name="post_excerpt"]');
-
-			if (title) {
-				title.dataset.groupId = groupId;
-				title.id = `${groupId}_title`;
-				title.name = `${groupId}[post_title]`;
-			}
-			if (excerpt) {
-				title.dataset.groupId = groupId;
-				excerpt.id = `${groupId}_excerpt`;
-				excerpt.name = `${groupId}[post_excerpt]`;
-			}
-		} else {
-			element.querySelector('details')?.remove();
-		}
-
-		const grid = element.querySelector('.item-grid');
-		if (grid) {
-			grid.dataset.groupId = groupId;
-		}
+		let element = this.templates.create('imageGroup', data);
 
 		this.groups.set(groupId, {
 			element: element,
@@ -1573,7 +1648,6 @@
 
 			handler.subscribe((event, data) => {
 				this.selected.set(fieldId, data.selectedItems);
-				this.syncSortableSelection(fieldId);
 			});
 
 			this.selectionHandlers.set(key, handler);
@@ -1624,8 +1698,6 @@
 			selectedClass: 'selected',
 			avoidImplicitDeselect: true,
 			group: { name: fieldId, pull: true, put: true },
-			ghostClass: 'ghost',
-			chosenClass: 'chosen',
 			dragClass: 'dragging',
 
 			onStart: (evt) => {
@@ -1643,9 +1715,6 @@
 						handler.select(uploadId);
 					}
 				}
-
-				// Sync all selections to Sortable
-				this.syncSortableSelection(fieldId);
 			},
 			onEnd: (evt) => this.sortableDrop(evt, fieldId),
 		});
@@ -1699,34 +1768,13 @@
 
 		const targetGroupId = dropTarget.dataset.groupId || null;
 
-		await Promise.all(
-			uploadIds.map(uploadId => this.addToGroup(uploadId, targetGroupId))
-		);
-
-		// After all moves complete, sync order from DOM
-		await this.handleReorder(fieldId, targetGroupId);
-
-		this.selectionHandlers.get(fieldId)?.clearSelection();
-	}
-
-	syncSortableSelection(fieldId) {
-		const selectedItems = this.selected.get(fieldId) || new Set();
-
-		for (const [uploadId, uploadData] of this.uploads) {
-			const upload = this.stores.uploads.get(uploadId);
-			if (!upload || upload.field !== fieldId) continue;
-
-			const element = uploadData.element;
-			if (!element) continue;
-
-			const shouldBeSelected = selectedItems.has(uploadId);
-
-			if (shouldBeSelected && !element.classList.contains('selected')) {
-				Sortable.utils.select(element);
-			} else if (!shouldBeSelected && element.classList.contains('selected')) {
-				Sortable.utils.deselect(element);
-			}
+		// Process sequentially to avoid race conditions
+		for (const uploadId of uploadIds) {
+			await this.addToGroup(uploadId, targetGroupId);
 		}
+
+		await this.handleReorder(fieldId, targetGroupId);
+		this.selectionHandlers.get(fieldId)?.clearSelection();
 	}
 
 	handleReorder(fieldId, groupId = null) {
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index d80a661..2381056 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -152,6 +152,118 @@
 	return false;
 }
 
+
+
+
+/**
+ * Load and instantiate HTML templates as lightweight components
+ */
+class TemplateRegistry {
+	constructor() {
+		this.templates = new Map();     // name -> <template>
+		this.definitions = new Map();   // name -> component definition
+	}
+
+	/**
+	 * Collect all <template class="name"> elements
+	 */
+	registerAll(root = document) {
+		root.querySelectorAll('template').forEach(tpl => {
+			tpl.classList.forEach(name => {
+				if (!this.templates.has(name)) {
+					this.templates.set(name, tpl);
+				}
+			});
+		});
+	}
+
+	/**
+	 * Define component behavior
+	 */
+	define(name, definition = {}, context = null) {
+		this.definitions.set(name, {
+			refs: definition.refs || null,
+			manyRefs: definition.manyRefs || null,
+			setup: definition.setup || null,
+			context: context
+		});
+	}
+
+	/**
+	 * Create a component instance
+	 */
+	create(name, data = {}) {
+		const tpl = this.templates.get(name);
+		if (!tpl) {
+			console.warn(`[TemplateRegistry] Template "${name}" not found`);
+			return null;
+		}
+
+		const element = tpl.content.cloneNode(true).firstElementChild;
+		if (!element) return null;
+
+		const def = this.definitions.get(name);
+		const refs = def?.refs
+			? this.#collectRefs(element, def.refs)
+			: {};
+		const manyRefs = def?.manyRefs
+			? this.#collectRefs(element, def.manyRefs, false)
+			: {};
+
+		def?.setup?.({
+			el: element,
+			refs,
+			manyRefs,
+			data
+		});
+
+		return element;
+	}
+
+	/**
+	 * Resolve refs declared in component definition
+	 */
+	#collectRefs(root, refMap, single = true) {
+		const refs = {};
+
+		for (const [key, value] of Object.entries(refMap)) {
+			let selector;
+			let required = false;
+
+			if (typeof value === 'string') {
+				selector = value;
+			} else {
+				selector = value.selector;
+				required = !!value.required;
+			}
+
+			const found = (single) ? root.querySelector(selector) : root.querySelectorAll(selector);
+
+			if (required) {
+				if (single && !found) {
+					console.warn(`[TemplateRegistry] Required ref "${key}" not found: ${selector}`);
+				}
+
+				if (!single && found.length === 0) {
+					console.warn(`[TemplateRegistry] Required manyRef "${key}" not found: ${selector}`);
+				}
+			}
+
+			refs[key] = single ? found : Array.from(found);
+		}
+
+		return refs;
+	}
+
+}
+
+
+window.jvbTemplates = new TemplateRegistry();
+
+document.addEventListener('DOMContentLoaded', () => {
+	window.jvbTemplates.registerAll();
+});
+
 /**
  * Gets a clone of an icon element if it exists for efficient DOM manipulation
  * @param icon
@@ -283,6 +395,46 @@
 	};
 }
 
+window.chunkIt = async function(items, renderCallback, placementCallback, size = 10) {
+	const chunks = [];
+	for (let i = 0; i <items.length; i += size) {
+		chunks.push(items.slice(i, i + size));
+	}
+
+	for (const chunk of chunks) {
+		const fragment = document.createDocumentFragment();
+		chunk.forEach(item => {
+			const element = renderCallback(item);
+			if (element) fragment.append(element);
+		});
+
+		placementCallback(fragment);
+		await new Promise(resolve => requestAnimationFrame(resolve));
+	}
+}
+
+window.prefixInput = function(input, prefix, replace = false) {
+	let newId = replace ? prefix : `${prefix}${input.name}`;
+	if (input.labels.length > 0) {
+		input.labels?.forEach(label => {
+			label.htmlFor = newId;
+		});
+	} else {
+		if (input.nextElementSibling?.tagName === 'LABEL') {
+			input.nextElementSibling.htmlFor = newId;
+		}else if (input.previousElementSibling?.tagName === 'LABEL') {
+			input.previousElementSibling.htmlFor = newId;
+		} else {
+			let label = input.parentElement.querySelector(`label[for="${input.id}"]`);
+			if (label) {
+				label.htmlFor = newId;
+			}
+		}
+	}
+
+	input.id = newId;
+}
+
 /**
  * Makes first letter uppercase
  * @param string
@@ -713,23 +865,23 @@
  * @param {HTMLElement|null} parent
  * @returns {object}
  */
-window.uiFromSelectors = function(selectors, parent = null) {
+window.uiFromSelectors = function(selectors, parent = null, all = false) {
 	let ui = {};
 	for (let [key, selector] of Object.entries(selectors)) {
 		if (typeof selector === 'object') {
 			ui[key] = window.uiFromSelectors(selector, parent);
 		}else {
 			if (!parent) {
-				ui[key] = document.querySelector(selector);
+				ui[key] = (all) ? document.querySelectorAll(selector) : document.querySelector(selector);
 			} else {
-				ui[key] = parent.querySelector(selector);
+				ui[key] = (all) ? parent.querySelectorAll(selector) : parent.querySelector(selector);
 			}
-
 		}
 	}
 	return ui;
 }
 
+
 class DebouncedActions {
 	constructor() {
 		this.timeouts = new Map();
diff --git a/assets/js/concise/quill.js b/assets/js/concise/quill.js
index 0670b00..cab9cf5 100644
--- a/assets/js/concise/quill.js
+++ b/assets/js/concise/quill.js
@@ -1,5 +1,6 @@
 window.jvbQuill = function(form) {
 	const textareas = form.querySelectorAll('textarea[data-editor=true]');
+	const instances = [];
 
 	textareas.forEach(textarea => {
 		let container, editor, toolbar;
@@ -240,6 +241,8 @@
 			}
 		});
 
+		instances.push(quill);
+
 		quill.on('selection-change', function(range) {
 			const alignmentTools = toolbar.querySelector('.ql-align');
 			if (alignmentTools) {
@@ -260,4 +263,5 @@
 			textarea.dispatchEvent(new Event('change', { bubbles: true }));
 		});
 	});
+	return instances;
 };
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 912812f..76dbecb 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.items={list:new Map,grid:new Map,table:new Map},this.init())}init(){this.initElements(),this.initListeners();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()}))})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,i)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.formController.cleanupForm(t),this.ui.modals[e].form.reset(),"date"===e&&this.handleCustomDateSelection()}})))}initStore(e){let t={...this.defaults,...e};const i=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0});this.store=i[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:".crud #select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache()}))}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,i])=>{const s=`tax_${t}`,l=this.cache.get(s);l&&(i.value=l,e[s]=l)}));let i=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===i&&(this.ui.table.nav.checked=!0);let s={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(s))if(t.element){let i=this.cache.get(e)??t.default;t.element.open="open"===i,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler)}handleChange(e){if(window.targetCheck(e,this.selectors.filters.search))return;const t=window.targetCheck(e,this.selectors.bulk.action);if(t)return void this.handleBulkAction(t);let i=window.targetCheck(e,"[data-filter]");if(i)return void this.handleFilterChange(i);let s=window.targetCheck(e,"input[data-view]");if(s)this.handleViewChange(s);else if("table"===this.view){if(window.targetCheck(e,"[data-id]"))return void this.handleTableChange(e);if(window.targetCheck(e,"details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);let t=window.targetCheck(e,this.selectors.table.nav);t&&(this.tabNav=t.checked,this.cache.set("tabNav",t.checked?"vertical":"horizontal"))}}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],i=t.dataset.taxonomy,s=t.dataset.single,l=t.dataset.plural;return window.jvbSelector.openEmpty(i,s,l,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){if(!e.termIds.length||!this.selected.size)return;const t={},i=`tax_${e.taxonomy}`;this.selected.forEach((s=>{const l=this.store.get(parseInt(s));if(!l)return;const a=(l.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),o=[...new Set([...a,...e.termIds])];t[s]={[i]:o.join(","),content:this.content}})),Object.keys(t).length>0&&this.savePosts(t,`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`),this.selectionHandler.clearSelection()}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,i]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,i);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),i=`${e}-${t}-01`,s=new Date(e,parseInt(t),0).getDate(),l=`${e}-${t}-${String(s).padStart(2,"0")}`;this.setFilter("dateFrom",i),this.setFilter("dateTo",l),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){window.targetCheck(e,".clear-search")&&this.deleteFilter("search","");let t=window.targetCheck(e,"[data-action]");if(t){e.preventDefault();let i=t.dataset.id;switch(t.dataset.action){case"edit":this.openEditModal(i);break;case"delete":if(confirm("Delete this item? This cannot be undone")){let e={};e[i]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[i]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.selected.size>0&&confirm(`Delete  ${this.selected.size} items?`)&&(this.selected.forEach((e=>this.store.delete(e))),this.selectionHandler.clearSelection());break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}if(window.targetCheck(e,".apply-date-filter"))return this.handleCustomDateSelection(),void this.modals.date.handleClose();window.targetCheck(e,this.selectors.buttons.create)&&(this.ui.modals.create.form.reset(),this.formController.registerForm(this.ui.modals.create.form),this.modals.create.handleOpen())}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),i=`${this.content}-search`;0!==t.length?window.debouncer.schedule(i,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),i=e.target.closest("tr");if(!t||!i)return;const s=t.dataset.field,l=e.shiftKey;let a=this.findNextEditableRow(i,l);a||(a=this.wrapToRow(i,l)),a&&this.focusFieldInRow(a,s,l)}}findNextEditableRow(e,t=!1){let i=t?e.previousElementSibling:e.nextElementSibling;for(;i&&!this.isEditableRow(i);)i=t?i.previousElementSibling:i.nextElementSibling;return i}wrapToRow(e,t=!1){if(this.isTimeline){const i=e.closest("tbody");if(!i)return null;const s=Array.from(i.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?s[s.length-1]:s[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.id)}focusFieldInRow(e,t,i=!1){const s=e.querySelector(`[data-field="${t}"]`);if(!s)return;const l=this.findFocusableInput(s);if(l){l.focus(),l.select&&"text"===l.type&&l.select();const e=i?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const i of t){const t=e.querySelector(i);if(t)return t}return null}toggleTimelineListeners(e=!0){if(!this.isTimeline||"table"!==this.view)return;if(this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]),!e)return;this.timelineSortables=[];this.ui.table.form.querySelectorAll("tbody.item").forEach((e=>{const t=new Sortable(e,{animation:150,handle:".drag-handle",draggable:"tr.timeline-point",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",group:{name:`timeline-${e.dataset.id}`,pull:!1,put:!1},onEnd:e=>this.handleTimelineReorder(e)});this.timelineSortables.push(t)}))}handleTimelineReorder(e){const{item:t,oldIndex:i,newIndex:s,from:l}=e;if(i===s)return;const a=parseInt(l.dataset.id),o=this.store.get(a);if(!o?.fields?.timeline)return;const n=Array.from(l.querySelectorAll("tr.timeline-point")).map((e=>e.dataset.imageId)).filter(Boolean),r={};n.forEach(((e,t)=>{o.fields.timeline[e]&&(r[e]={...o.fields.timeline[e],order:t})})),o.fields.timeline=r,this.store.save(o),this.savePosts({[a]:{timeline:r,content:this.content}},"Reordering timeline...",!0),this.a11y?.announce(`Moved to position ${s+1}`)}openEditModal(e){let t=this.store.get(parseInt(e));t&&(this.activeItem=t.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,this.ui.modals.edit.h2.textContent=`Editing ${""===t.fields.post_title?this.singular:t.fields.post_title}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.ui.modals.edit.form.reset(),new window.jvbPopulate(this.ui.modals.edit.form,t),this.formController.registerForm(this.ui.modals.edit.form),this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),this.modals.bulkEdit.handleOpen(),this.selected.forEach((e=>{let t=window.getTemplate("bulkItem");if(!t)return;let i=this.store.get(parseInt(e));if(!i)return;let[s,l,a]=[t.querySelector("input"),t.querySelector("img"),t.querySelector("label")];s&&(s.id=`bulk_${i.id}`,s.value=i.id,s.checked=!0,s.name="selected[]");let o=i.images[i.fields.post_thumbnail]??{};l&&(l.src=o.medium??"",l.alt=o.alt??""),a.title=i.fields.post_title??"",this.ui.modals.bulkEdit.selected.append(t)})),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.formController.registerForm(this.ui.modals.bulkEdit.form)}handleFormChange(e,t){let i=t.fullData.post_title,s=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.activeItem]=s,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&this.removeItems([this.activeItem]),void this.savePosts(l,i,"form-submit"===e);let a=[],o=t.config.element;if(o===this.ui.modals.edit.form)l[this.activeItem]=s,i=`Saving ${i} Changes`,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&a.push(this.activeItem);else if(o===this.ui.modals.bulkEdit.form){let e=0;o.querySelectorAll(".selected input:checked").forEach((t=>{l[t.value]=s,s.post_status&&this.shouldRemoveItemUI(s.post_status)&&a.push(t.value),e++})),i=`Updating ${e} ${this.plural} Changes`}else o===this.ui.modals.create.form&&"form-submit"===e&&(l[o.dataset.formId]=s,i=`Saving ${i} Changes`);a.length>0&&this.removeItems(a),this.selectionHandler.clearSelection(),0!==Object.keys(l).length&&this.savePosts(l,i,"form-submit"===e)}savePosts(e,t,i=!1){if(0===Object.keys(e).length)return;let s={};for(let t in e)t&&"null"!==t&&"undefined"!==t?(e[t].content||(e[t].content=this.content),s[t]=e[t]):console.warn("Skipping save for invalid ID:",t);let l={endpoint:this.endpoint,headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:s},delay:i,popup:"Saving changes",title:t};this.queue.addToQueue(l)}handleTableChange(e){const t=this.isTimeline?e.target.closest("tbody[data-id]"):e.target.closest("tr[data-id]");if(!t)return;const i=e.target,s=i.closest("[data-field]")?.dataset.field;if(!s)return;const l=parseInt(t.dataset.id),a=this.store.get(l);if(!a)return;const o=this.getInputValue(i);if(this.isTimeline){const e=i.closest("tr.timeline-point");if(e){const t=e.dataset.imageId;a.fields.timeline??={},a.fields.timeline[t]??={},a.fields.timeline[t][s]=o}else a.fields[s]=o}else a.fields[s]=o;this.store.save(a),this.savePosts({[l]:a.fields},"Saving changes...",!0)}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i={};if(this.selected.forEach((t=>{i[t]={post_status:e,content:this.content}})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(Object.keys(i)),this.selectionHandler.clearSelection(),0!==Object.keys(i).length&&this.savePosts(i,`${t} ${Object.keys(i).length} ${this.plural}...`)}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]');"trash"===this.status&&t?(window.removeChildren(this.ui.bulk.action),e=window.getTemplate("trashOptions")):"trash"===this.status||t||(window.removeChildren(this.ui.bulk.action),e=window.getTemplate("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e?this.formController.registerForm(this.ui.table.form,{autosave:!1,formStatus:!1,isTable:!0}):this.formController.cleanupForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler),this.isTimeline&&!e&&this.toggleTimelineListeners(e)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderGridItem(e);t.appendChild(i)})),this.ui.grid.appendChild(t)}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderListItem(e);t.append(i)})),this.ui.grid.append(t)}renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid);let t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderTableItem(e);i&&t.append(i)})),this.ui.table.body?this.ui.table.body.append(t):this.ui.table.table.insertBefore(t,this.ui.table.foot),this.isTimeline&&this.toggleTimelineListeners(!0)}setupItemElement(e,t,i){return this.items[this.view].has(t.id)?this.items[this.view].get(t.id):(e.dataset.id=t.id,t._pending&&e.classList.add("pending"),this.setupItemSelection(e,t),this.setupItemThumbnail(e,t),this.setupItemActions(e,t),this.items[this.view].set(t.id,e),e)}setupItemSelection(e,t){const i=e.querySelector(".select-item"),s=e.querySelector(".select-item + label, .select-item-label");i&&(i.id=`select-${t.id}`,i.value=t.id,i.checked=this.selected.has(parseInt(t.id)),s&&(s.htmlFor=`select-${t.id}`))}setupItemThumbnail(e,t){const i=e.querySelector("img");if(!i)return;const s=t.images?.[t.fields?.post_thumbnail]??{};i.src=s.medium??"",i.alt=s.alt??""}setupItemActions(e,t){e.querySelectorAll("[data-action]").forEach((e=>{e.dataset.id=t.id}))}renderGridItem(e){if(this.items.grid.has(e.id))return this.items.grid.get(e.id);const t=window.getTemplate("gridView");return t?this.setupItemElement(t,e,"grid"):null}renderListItem(e){if(this.items.list.has(e.id))return this.items.list.get(e.id);let t=window.getTemplate("listView");return t?(t=this.setupItemElement(t,e,"list"),t.querySelectorAll("[data-attr]").forEach((t=>{const i=e[t.dataset.attr];i&&""!==i?t.textContent=i:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{const i=e.fields?.[t.dataset.field];i&&""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()})),this.items.list.set(e.id,t),t):null}renderTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);let t=window.getTemplate("tableView");if(!t)return null;t=this.setupItemElement(t,e,"table");const i=t.querySelector(`input[name="post_status"][value="${e.status}"]`);if(i&&(i.checked=!0),this.isTimeline){const i=t.querySelector("tr.shared");i&&(new window.jvbPopulate(i,e),this.cleanupTableRow(i));const s=t.querySelector("tr.timeline-point");s&&e.fields?.timeline&&(Object.entries(e.fields.timeline).forEach((([i,l],a)=>{const o=s.cloneNode(!0);o.dataset.index=a,o.dataset.imageId=i,new window.jvbPopulate(o,{fields:l,images:e.images,taxonomies:e.taxonomies}),this.cleanupTableRow(o);const n=e.images?.[l.post_thumbnail];n&&o.querySelector(".field.upload")?.setAttribute("title",n["image-title"]||""),t.insertBefore(o,s)})),s.remove())}else void 0!==this.ui.table.form?.dataset.edit?new window.jvbPopulate(t,e):this.populateTableReadOnly(t,e),this.cleanupTableRow(t);return this.items.table.set(e.id,t),t}populateTableReadOnly(e,t){for(const[i,s]of Object.entries(t)){const t=e.querySelector(`[data-field="${i}"]`);if(!t)continue;const l=t.querySelector("p");l&&(l.textContent="date"===t.dataset.fieldType?window.formatTimeAgo(s):s)}}cleanupTableRow(e){e.querySelectorAll("td[data-field]").forEach((e=>{e.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach((e=>{e.closest(".radio-options")||e.remove()})),"true_false"===e.dataset.fieldType&&e.querySelector(".toggle-label")?.remove(),["checkbox","radio","select"].includes(e.dataset.fieldType)&&e.querySelector(".label")?.remove()}))}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){let t=0;e.forEach((e=>{setTimeout((()=>{let t=this.items[this.view].get(e);t&&window.fade(t,!1)}),t),t+=50}))}setFilters(e){for(let[t,i]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,i);let s=this.findFilterEl(t);this.setElValue(s,i)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t);let i=this.findFilterEl(e,t);this.setElValue(i,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let i=this.findFilterEl(e,t);this.setElValue(i,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),i=this.ui.filters.taxonomies?.[t];return i||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let i=this.ui.filters[e];if("object"==typeof i){if(!Object.hasOwn(this.ui.filters[e],t))return!1;i=this.ui.filters[e][t]}return i}destroy(){this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput);for(let e of Object.values(this.items))e.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector(".crud[data-content]:not([data-ignore])"),this.container&&(this.content=this.container.dataset.content,this.endpoint=this.container.dataset.endpoint??"content",this.singular=this.container.dataset.singular,this.plural=this.container.dataset.plural,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.populate=window.jvbPopulate,this.cache=new window.jvbCache(this.content),this.activeItem=null,this.isTimeline=!1,this.isPopulating=!1,this.changes=new Map,this.items=new Map,this.init())}init(){this.initElements(),this.initListeners(),this.defineTemplates();let e=this.initSettings();this.initStore(e),this.checkHideFilters(),this.initIntegrations(),this.initUploader(),this.initModals()}defineTemplates(){const e=window.jvbTemplates,t=this,i=(e,i,s)=>{e.dataset.itemId=s.id,window.prefixInput(i.checkbox,`select-${s.id}`,!0),i.checkbox.value=s.id,i.checkbox.checked=t.selected.has(parseInt(s.id)),i.selectLabel&&(i.selectLabel.htmlFor=`select-${s.id}`),i.edit&&(i.edit.dataset.id=s.id),i.trash&&(i.trash.dataset.id=s.id)},s=function(e,t,i){if(i?.fields?.post_thumbnail){const e=i.images[i.fields.post_thumbnail]??{};t.img.src=e.medium??"",t.img.alt=e.alt??i.fields.post_title??""}};e.define("gridView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l)}}),e.define("listView",{refs:{img:"img",checkbox:".select-item",selectLabel:"label.select-item-label",edit:'[data-action="edit"]',trash:'[data-action="trash"]'},manyRefs:{attrs:"[data-attr]",fields:"[data-field]"},setup({el:e,refs:t,manyRefs:a,data:l}){i(e,t,l),s(0,t,l),a?.attrs?.forEach((e=>{const t=l[e.dataset.attr];t&&""!==t?e.textContent=t:e.remove()})),a?.fields?.forEach((e=>{const t=l.fields?.[e.dataset.field];t&&""!==t?"DIV"===e.tagName?e.innerHTML=t:e.textContent=t:e.remove()}))}});let a={};this.isTimeline&&(a.sharedRow="tr.shared",a.point="tr.timeline-point"),e.define("tableView",{refs:{checkbox:".select-item",selectLabel:"label.select-item-label",...a},manyRefs:{inputs:"input,select,textarea",status:'input[name="post_status"]',selectors:'[data-type="selector"]',fields:"[data-field]"},setup({el:e,refs:s,manyRefs:a,data:l}){if(i(e,s,l),a?.inputs?.forEach((e=>{window.prefixInput(e,`${l.id}-`)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.isTimeline)s.sharedRow&&(s.sharedRow.querySelectorAll("input,select,textarea").forEach((e=>{window.prefixInput(e,`${l.id}-`)})),t.populate.populate(s.sharedRow,l),s.sharedRow.querySelectorAll('input[name="post_status"]').forEach((e=>{e.value===l.status&&(e.checked=!0)}))),s.point&&l.fields?.timeline&&(Object.entries(l.fields.timeline).forEach((([i,a],n)=>{const o=s.point.cloneNode(!0);o.dataset.index=`${n}`,o.dataset.itemId=a.id,o.querySelectorAll("input,select,textarea").forEach((e=>{window.prefixInput(e,`${a.id}-`)})),t.populate.populate(o,{fields:a,images:l.images,taxonomies:l.taxonomies});const d=l.images?.[a.post_thumbnail];d&&o.querySelector(".field.upload")?.setAttribute("title",d["image-title"]??""),e.insertBefore(o,s.point)})),s.point.remove());else if(void 0!==t.ui.table.form?.dataset.edit)a?.inputs?.forEach((e=>{window.prefixInput(e,`${l.id}-`)})),a?.status?.forEach((e=>{e.value===l.status&&(e.checked=!0)})),t.populate.populate(e,l);else{const e=Object.hasOwn(l,"fields")?l.fields:l;a?.fields?.forEach((t=>{if(Object.hasOwn(e,t.dataset.field)&&""!==e[t.dataset.field]){let i=e[t.dataset.field],s=e.children[0];s&&(s.textContent="date"===t.dataset.field?window.formatTimeAgo(i):i)}}))}a?.selectors?.forEach((e=>e.setAttribute("data-lazy","")))}}),e.define("emptyState"),e.define("bulkItem",{refs:{checkbox:"input",img:"img",label:"label"},setup({el:e,refs:t,manyRefs:i,data:s}){t.checkbox&&(t.checkbox.id=`bulk_${s.id}`,t.checkbox.value=s.id,t.checkbox.checked=!0,t.checkbox.name="selected[]");let a=s?.images[s?.fields?.post_thumnbail]??{};t.img&&Object.keys(a).length>0&&(t.img.src=a.medium??"",t.img.alt=a.alt??""),t.label&&(t.label.title=item.fields.post_title)}}),e.define("trashOptions"),e.define("notTrashOptions"),e.define("contentTable")}initElements(){this.allowedFilters=["status","orderby","order","search","date-filter","dateFrom","dateTo"],this.selectors={buttons:{create:".create-item",clearFilters:'[data-action="clear-filters"]'},views:{grid:'input[data-view="grid"]',list:'input[data-view="list"]',table:'input[data-view="table"]'},modals:{create:{modal:"dialog.create",form:"dialog.create form",h2:"dialog.create h2"},edit:{modal:"dialog.edit",form:"dialog.edit form",h2:"dialog.edit h2"},bulkEdit:{modal:"dialog.bulkEdit",selected:"dialog.bulkEdit .selected",h2:"dialog.bulkEdit h2 span",form:"dialog.bulkEdit form"},date:{modal:"dialog.date-range",start:"dialog.date-range .date-start",end:"dialog.date-range .date-end",month:"dialog.date-range .month-select"}},grid:`.${this.content}.item-grid`,table:{nav:"#vertical",form:"form.table",table:"form.table table",body:"form.table body",head:"form.table thead",foot:"form.table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{action:".bulk-action-select",count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"},filters:{container:"details.all-filters",search:'.all-filters input[type="search"]',status:{all:'[name="status"]#all',publish:'[name="status"]#publish',draft:'[name="status"]#draft',trash:'[name="status"]#trash'},orderby:{date:'[name="orderby"]#date',alphabetical:'[name="orderby"]#alphabetical'},order:{asc:'[name="order"][value="asc"]',desc:'[name="order"][value="desc"]'},date:'[data-filter="date"]'},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.selectors);const e=document.querySelectorAll('[data-filter="taxonomies"]');e.length>0&&(this.ui.filters.taxonomies={},e.forEach((e=>{const t=e.dataset.taxonomy;this.ui.filters.taxonomies[t]=e,this.allowedFilters.push(`tax_${t}`)}))),this.isTimeline=!!document.querySelector("[data-timeline]")}initUploader(){this.ui.uploader&&(window.jvbUploads.scanFields(this.ui.uploader),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&t===this.ui.uploader.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()}))})))}initModals(){this.modals={};for(let[e,t]of Object.entries(this.ui.modals))t.modal&&(this.modals[e]=new window.jvbModal(t.modal),this.modals[e].subscribe(((t,i)=>{if("modal-close"===t){const t=this.ui.modals[e].form.dataset.formId;t&&this.forms.clearForm(t),this.ui.modals[e].form.reset(),"date"===e&&this.handleCustomDateSelection()}})))}initStore(e){let t={...this.defaults,...e};const i=window.jvbStore.register(this.content,[{storeName:this.content,keyPath:"id",endpoint:this.endpoint??"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:t,ignore:["content","user"],TTL:36e5,showLoading:!0},{storeName:"changes",keyPath:"id"}]);this.changesStore=i.changes,this.store=i[this.content],this.store.subscribe(((e,t)=>{if("data-loaded"===e)this.render(),this.selectionHandler.collectItems()})),this.changesStore.subscribe(((e,t)=>{if("data-ready"===e){let e=this.changesStore.getAll();e.length>0&&(e.forEach((e=>{this.changes.set(e.id,e)})),this.savePosts("",!1).then((()=>{})))}}))}initIntegrations(){this.selected=new Set,this.selectionHandler=new window.jvbHandleSelection(this.container,{selectAll:{checkbox:"#select-all",label:".bulk-select label",span:".bulk-select label span"},wrapper:{wrapper:".wrap"},item:{idAttribute:"itemId"}}),this.selectionHandler.subscribe(((e,t)=>{this.selected=new Set([...t.selectedItems].map((e=>parseInt(e)))),this.ui.bulk.control.hidden=0===this.selected.size,this.ui.bulk.count.hidden=0===this.selected.size,this.ui.bulk.count.textContent=`${this.selected.size} ${this.plural} selected`})),this.forms=window.jvbForm,this.queue.subscribe(((e,t)=>{if(["image_upload","video_upload","document_upload"].includes(t.type)&&"operation-status"===e&&"completed"===t.status&&this.store.clearCache(),"operation-status"===e&&"completed"===t.status&&"content"===t.endpoint&&Object.keys(t.data?.posts??{}).length>0){this.store.clearCache();let e=Object.keys(t.data.posts),i=this.changesStore.getMany(e);this.changesStore.deleteMany(e);for(let s of e){let e=i.filter((e=>e.id===s))[0]??!1,a=t.data.posts[s],l={};for(let[t,i]of Object.entries(a))e&&!Object.hasOwn(e,t)||(e[t]===i&&delete e[t],l[t]=i);Object.keys(l).length>0&&(l.id=s,l.content=this.content,this.changes.set(s,l))}Object.values(this.changes).length>0&&this.scheduleBackup()}}))}initSettings(){this.defaults={content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"date",order:"desc",search:""};let e={},t=this.container.dataset.view??"grid";this.view=this.cache.get("view")??t,this.view!==t&&(this.ui.views[this.view].checked=!0),this.status=this.cache.get("status")??this.defaults.status,this.status!==this.defaults.status&&(this.ui.filters.status[this.status].checked=!0,e.status=this.status),this.orderby=this.cache.get("orderby")??this.defaults.orderby,this.orderby!==this.defaults.orderby&&(this.ui.filters.orderby[this.orderby].checked=!0,e.orderBy=this.orderby),this.order=this.cache.get("order")??this.defaults.order,this.order!==this.defaults.order&&(this.ui.filters.order[this.order].checked=!0,e.order=this.order),this.ui.filters.taxonomies&&Object.entries(this.ui.filters.taxonomies).forEach((([t,i])=>{const s=`tax_${t}`,a=this.cache.get(s);a&&(i.value=a,e[s]=a)}));let i=this.cache.get("tabNav")??"horizontal";this.ui.table.nav&&"vertical"===i&&(this.ui.table.nav.checked=!0);let s={showFilters:{element:this.ui.filters.container,default:"closed"},showUploader:{element:this.ui.uploader,default:"open"}};for(let[e,t]of Object.entries(s))if(t.element){let i=this.cache.get(e)??t.default;t.element.open="open"===i,t.element.addEventListener("toggle",(()=>{this.cache.set(e,t.element.open?"open":"closed")}))}return e}initListeners(){this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleModalSubmit.bind(this),document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler),this.ui.filters.search&&this.ui.filters.search.addEventListener("input",this.inputHandler);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.addEventListener("submit",this.submitHandler)}handleModalSubmit(e){e.preventDefault();const t=e.target.closest("dialog");if(!t)return;let i=`Saving changes for multiple ${this.plural}`;t.classList.contains("edit")?i="Saving your edits...":t.classList.contains("create")&&(i=`Creating your new ${this.singular}`),this.savePosts(i,!1)}handleChange(e){const t=e.target.closest("[data-item-id]"),i=e.target.matches("[data-filter]"),s=e.target.matches(".bulk-action-select"),a=e.target.matches("[data-view]");if(t||i||s||a)if(this.isPopulating||!t||e.target.closest("[data-ignore], .select-item")){if(a)return this.items.clear(),void this.handleViewChange(e.target);if(s)this.handleBulkAction(e.target);else if(i)this.handleFilterChange(e.target);else if("table"===this.view){if(e.target.matches("details.multi-select"))return void this.toggleColumn(e.target.id,e.target.checked);e.target.matches(this.selectors.table.nav)&&(this.tabNav=e.target.checked,this.cache.set("tabNav",e.target.checked?"vertical":"horizontal"))}}else this.handleItemUpdate(e)}handleBulkAction(e){if(e.value.startsWith("tax-")){const t=e.options[e.selectedIndex],i=t.dataset.taxonomy,s=t.dataset.single,a=t.dataset.plural;return window.jvbSelector.openEmpty(i,s,a,(e=>this.handleBulkTaxonomy(e))),void(e.value="")}switch(e.value){case"edit":this.openBulkEditModal();break;case"publish":case"trash":case"delete":this.setBulkStatus(e.value);break;case"draft":case"restore":this.setBulkStatus("draft")}}handleBulkTaxonomy(e){if(!e.termIds.length||!this.selected.size)return;const t={},i=`tax_${e.taxonomy}`;this.selected.forEach((s=>{const a=this.store.get(parseInt(s));if(!a)return;const l=(a.taxonomies?.[e.taxonomy]||[]).map((e=>e.id)),n=[...new Set([...l,...e.termIds])];t[s]={[i]:n.join(","),content:this.content}})),Object.keys(t).length>0&&this.savePosts(`Adding ${e.terms.length} ${e.taxonomy} to ${this.selected.size} ${this.plural}...`,!1).then((()=>{})),this.selectionHandler.clearSelection()}handleItemUpdate(e){let t=window.targetCheck(e,"[data-item-id]");t&&(t.dataset.itemId.split(",").forEach((t=>{let i=this.forms.getField(e.target).dataset.field,s=this.forms.getFieldValue(e.target);this.updateItem(t,i,s)})),this.savePosts("",!0).then((()=>{})))}updateItem(e,t,i){this.changes.has(e)||this.changes.set(e,{id:e,content:this.content}),this.changes.get(e)[t]=i,this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule(`changes-${this.content}`,(async()=>{this.changes.size>0&&(await this.changesStore.saveMany(this.changes),this.changes.clear())}),2e3)}handleFilterChange(e){let t=e.dataset.filter;return"date"===t&&"custom"===e.value?(e.value="",void this.modals.date.handleOpen()):"date"===t&&""!==e.value?(this.setFilter("date-filter",e.value),this.deleteFilter("dateFrom"),this.deleteFilter("dateTo"),void this.checkHideFilters()):("taxonomies"===t&&(t=`tax_${e.dataset.taxonomy}`),void this.setFilter(t,e.value))}checkHideFilters(){const e=this.store.filters,t=Object.entries(e).some((([e,t])=>!["content","user","page"].includes(e)&&(this.defaults[e]!==t&&""!==t&&null!==t)));this.ui.buttons.clearFilters.hidden=!t}clearAllFilters(){let e=this.store.filters;this.store.clearFilters();for(let[t,i]of Object.entries(e))this.cache.remove(t),this.deleteFilter(t,i);this.a11y.announce("All filters cleared")}handleCustomDateSelection(){if(this.ui.modals.date.month&&this.ui.modals.date.month.value){const[e,t]=this.ui.modals.date.month.value.split("-"),i=`${e}-${t}-01`,s=new Date(e,parseInt(t),0).getDate(),a=`${e}-${t}-${String(s).padStart(2,"0")}`;this.setFilter("dateFrom",i),this.setFilter("dateTo",a),this.deleteFilter("date-filter"),this.ui.modals.date.month.value=""}else this.ui.modals.date.start&&this.ui.modals.date.start.value&&this.ui.modals.date.end&&this.ui.modals.date.end.value&&(this.setFilter("dateFrom",this.ui.modals.date.start.value),this.setFilter("dateTo",this.ui.modals.date.end.value),this.deleteFilter("date-filter"),this.ui.modals.date.start.value="",this.ui.modals.date.end.value="");this.checkHideFilters()}handleViewChange(e){this.view=e.dataset.view,this.cache.set("view",this.view),this.render()}handleClick(e){if(e.target.matches(".clear-search"))return void this.deleteFilter("search","");const t=e.target.closest("[data-action]");return t?(e.preventDefault(),void this.handleActionButton(t)):e.target.matches(".apply-date-filter")?(this.handleCustomDateSelection(),void this.modals.date.handleClose()):void(e.target.matches(this.selectors.buttons.create)&&this.openCreateModal())}openCreateModal(){this.ui.modals.create.form.reset(),this.forms.registerForm(this.ui.modals.create.form,{cache:!1}),this.ui.modals.create.modal.dataset.itemId=window.generateID("new"),this.modals.create.handleOpen()}handleActionButton(e){const t=e.dataset.id;switch(e.dataset.action){case"edit":this.openEditModal(t);break;case"delete":confirm("Delete this item? This cannot be undone")&&(this.updateItem(t,"post_status","delete"),window.fade(e.closest(".item"),!1),this.savePosts(`Permanently deleting ${this.singular}...`).then((()=>{})),this.store.delete(t));break;case"trash":this.updateItem(t,"post_status","trash"),window.fade(e.closest(".item"),!1),this.savePosts(`Sending ${this.singular} to trash...`).then((()=>{}));break;case"bulk-edit":this.selected.size>0&&this.openBulkEditModal();break;case"bulk-delete":this.handleBulkDelete();break;case"refresh":this.store.clearCache(),this.store.fetch();break;case"clear-filters":this.clearAllFilters()}}handleBulkDelete(){let e="trash"===this.store.filters.status;if(this.selected.size>0&&confirm(`${e?"Permanently delete":"Send"} ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}${e?"":"to trash"}?`)){this.selected.forEach((t=>{this.store.delete(t),this.updateItem(t,"post_status",e?"delete":"trash")}));let t=e?`Permanently deleting ${this.selected.size} ${1===this.selected.size?this.singular:this.plural}`:`Sending ${this.selected.size} ${1===this.selected.size?this.singular:this.plural} to trash`;this.savePosts(t).then((()=>{})),this.selectionHandler.clearSelection()}}handleInput(e){e.preventDefault(),e.stopPropagation();let t=e.target.value.trim(),i=`${this.content}-search`;0!==t.length?window.debouncer.schedule(i,(()=>{this.a11y.announce(`Searching for "${t}"...`),this.store.setFilters({search:t,page:1})}),300):this.deleteFilter("search","")}handleKeys(e){if(this.tabNav&&"Tab"===e.key){e.preventDefault();const t=e.target.closest("[data-field]"),i=e.target.closest("tr");if(!t||!i)return;const s=t.dataset.field,a=e.shiftKey;let l=this.findNextEditableRow(i,a);l||(l=this.wrapToRow(i,a)),l&&this.focusFieldInRow(l,s,a)}}findNextEditableRow(e,t=!1){let i=t?e.previousElementSibling:e.nextElementSibling;for(;i&&!this.isEditableRow(i);)i=t?i.previousElementSibling:i.nextElementSibling;return i}wrapToRow(e,t=!1){if(this.isTimeline){const i=e.closest("tbody");if(!i)return null;const s=Array.from(i.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?s[s.length-1]:s[0]}{if(!this.ui.table.body)return null;const e=Array.from(this.ui.table.body.querySelectorAll("tr")).filter((e=>this.isEditableRow(e)));return t?e[e.length-1]:e[0]}}isEditableRow(e){return!e.closest("thead")&&!e.closest("tfoot")&&(this.isTimeline?e.classList.contains("shared")||e.classList.contains("timeline-point"):!!e.dataset.itemId)}focusFieldInRow(e,t,i=!1){const s=e.querySelector(`[data-field="${t}"]`);if(!s)return;const a=this.findFocusableInput(s);if(a){a.focus(),a.select&&"text"===a.type&&a.select();const e=i?"next":"previous";this.a11y?.announce(`Moved to ${t} in ${e} row`)}}findFocusableInput(e){const t=['input:not([type="hidden"]):not([disabled])',"textarea:not([disabled])","select:not([disabled])","button:not([disabled])"];for(const i of t){const t=e.querySelector(i);if(t)return t}return null}openEditModal(e){let t=this.store.get(parseInt(e));t&&(this.activeItem=t.id,this.ui.modals.edit.modal.dataset.itemId=e,this.ui.modals.edit.modal.dataset.content=this.content,this.ui.modals.edit.h2.textContent=`Editing ${""===t.fields.post_title?this.singular:t.fields.post_title}`,this.ui.modals.edit.form.dataset.formId=`edit-${e}`,this.ui.modals.edit.form.reset(),this.forms.registerForm(this.ui.modals.edit.form,{cache:!1}),this.isPopulating=!0,this.populate.populate(this.ui.modals.edit.form,t),this.isPopulating=!1,this.modals.edit.handleOpen())}openBulkEditModal(){window.removeChildren(this.ui.modals.bulkEdit.selected),this.ui.modals.edit.form.reset(),window.chunkIt(this.selected,(t=>{let i=this.store.get(parseInt(t));if(i)return e.push(i.id),window.jvbTemplates.create("bulkItem",i)}),(e=>this.ui.modals.bulkEdit.selected.append(e))).then((()=>{}));let e=Array.from(this.selected).map((e=>this.store.get(parseInt(e)))).filter(Boolean);this.ui.modals.bulkEdit.modal.dataset.itemId=e.join(","),this.ui.modals.bulkEdit.h2&&(this.ui.modals.bulkEdit.h2.textContent=this.selected.size),this.modals.bulkEdit.handleOpen(),this.forms.registerForm(this.ui.modals.bulkEdit.form,{cache:!1})}async savePosts(e="",t=!1){const i=Array.from(this.changes.values()),s=await this.changesStore.getAll(),a=window.deepMerge(s,i);if(0===a.length)return;""===e&&(e=`Saving ${a.length} ${1===a.length?this.singular:this.plural}`);let l={},n=[];a.forEach((e=>{let t=e.id;const{id:i,...s}=e;l[t]=s,e.post_status&&this.shouldRemoveItemUI(e.post_status)&&n.push(t)})),n.length>0&&this.removeItems(n);let o={endpoint:this.endpoint,headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:l},delay:t,popup:"Saving changes",title:e};this.queue.addToQueue(o)}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i=[];if(this.selected.forEach((t=>{i.push(t),this.updateItem(t,"post_status",e)})),"delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";this.shouldRemoveItemUI(e)&&this.removeItems(i),this.selectionHandler.clearSelection(),this.savePosts(`${t} ${i.length} ${1===i.length?this.singular:this.plural}...`).then((()=>{}))}render(){const e=this.store.getFiltered();if(0!==e.length){switch(this.view){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e).then((()=>{}));break;case"list":this.renderList(e)}this.updateUI()}else this.renderEmpty()}updateUI(){if(this.ui.bulk.action){let e=!1,t=this.ui.bulk.action.querySelector('[value="edit"]');"trash"===this.status&&t?(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("trashOptions")):"trash"===this.status||t||(window.removeChildren(this.ui.bulk.action),e=window.jvbTemplates.create("notTrashOptions")),e&&e.querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulk.action.append(e)})),this.ui.bulk.action.value=""}this.selected.size>0&&this.selectionHandler.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.jvbTemplates.create("emptyState");e&&(this.ui.grid.append(e),this.a11y.announceItems(0,!1,!1))}toggleTable(e=!0){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.form){let e=window.jvbTemplates.create("contentTable");this.container.append(e),this.ui.table=window.uiFromSelectors(this.selectors.table),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.form&&(this.ui.table.form.hidden=!e,e||this.forms.clearForm(this.ui.table.form.dataset.formId),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.keyHandler=this.handleKeys.bind(this),e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler)}renderGrid(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view"),window.chunkIt(e,(e=>this.renderGridItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}renderList(e){window.removeChildren(this.ui.grid),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),window.chunkIt(e,(e=>this.renderListItem(e)),(e=>this.ui.grid.append(e))).then((()=>{}))}async renderTable(e){this.toggleTable(),window.removeChildren(this.ui.grid),await window.chunkIt(e,(e=>this.renderTableItem(e)),(e=>{this.ui.table.body?this.ui.table.body.append(e):this.ui.table.table.insertBefore(e,this.ui.table.foot)}),5),requestAnimationFrame((()=>{window.jvbSelector?.scanExistingFields(this.ui.table.table)}))}renderGridItem(e){let t=window.jvbTemplates.create("gridView",e);return this.items.set(e.id,t),t}renderListItem(e){let t=window.jvbTemplates.create("listView",e);return this.items.set(e.id,t),t}renderTableItem(e){let t=window.jvbTemplates.create("tableView",e);return this.items.set(e.id,t),t}toggleColumn(e,t){this.ui.table.table.querySelectorAll(`.${e}`).forEach((e=>{e.hidden=!t}))}shouldRemoveItemUI(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.store.filters.status}removeItems(e){e.forEach((e=>{if(this.items.has(e)){let t=this.items.get(e);t&&window.fade(t,!1)}}))}setFilters(e){for(let[t,i]of Object.entries(e)){if(!this.allowedFilters.includes(t)){delete e[t];continue}this.cache.set(t,i);let s=this.findFilterEl(t);this.setElValue(s,i)}this.store.setFilters(e)}setFilter(e,t){if(!this.allowedFilters.includes(e))return;this.cache.set(e,t);let i=this.findFilterEl(e,t);this.setElValue(i,t),this.store.setFilter(e,t)}deleteFilter(e,t){if(!this.allowedFilters.includes(e))return;if(Object.hasOwn(this.defaults,e))return void this.setFilter(e,this.defaults[e]);let i=this.findFilterEl(e,t);this.setElValue(i,!1),this.cache.remove(e),this.setFilter(e,"")}setElValue(e,t){if(e){if(!t)return["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=""),["text","search"].includes(e.type)&&(e.value=""),void("radio"===e.type&&(e.checked=!1));["SELECT","TEXTAREA"].includes(e.tagName)&&(e.value=t),["text","search"].includes(e.type)&&(e.value=t),"radio"===e.type&&(e.checked=!0)}}findFilterEl(e,t){if(["date-filter","dateFrom","dateTo"].includes(e)){switch(e){case"date-filter":e="month";break;case"dateFrom":e="start";break;case"dateTo":e="end"}return this.ui.modals.date[e]}if(e.includes("tax_")){const t=e.replace("tax_",""),i=this.ui.filters.taxonomies?.[t];return i||(console.warn("Taxonomy filter element not found:",t),null)}if(!Object.hasOwn(this.ui.filters,e))return console.warn("Filter el not found: ",e),!1;let i=this.ui.filters[e];if("object"==typeof i){if(!Object.hasOwn(this.ui.filters[e],t))return!1;i=this.ui.filters[e][t]}return i}destroy(){window.debouncer.cancel(`changes-${this.content}`),this.changes.size>0&&(this.changesStore.saveMany(this.changes).then((()=>{})),this.changes.clear()),this.timelineSortables&&(this.timelineSortables.forEach((e=>e.destroy())),this.timelineSortables=[]);for(let[e,t]of Object.entries(this.ui.modals))t.form&&t.form.removeEventListener("submit",this.submitHandler);document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.ui.filters.search&&this.ui.filters.search.removeEventListener("input",this.handleInput)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 73152e3..63037d0 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=r(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=t.items||[],o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const d={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,d),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(d)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items)return this.applyOrdering(r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]),t);const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=i.filter((e=>{for(const[s,r]of Object.entries(t.filters))if(!t.ignoreFilters.has(s)&&null!=r&&""!==r&&"all"!==r)if("string"==typeof r&&r.includes(",")){if(!r.split(",").map((e=>e.trim())).includes(String(e[s])))return!1}else if(String(e[s])!==String(r))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(o,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;if(t.filters.orderby||t.filters.order){const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.fields?.post_title||e.title||e.name||"").toLowerCase(),a=(t.fields?.post_title||t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||0),a=new Date(t.modified||0);break;default:i=new Date(e.date||0),a=new Date(t.date||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0}))}return e}searchObject(e,t){if(!e||"object"!=typeof e)return!1;for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),this.notify(e,"data-loaded",{cached:!0,items:this.getFiltered(e)});const a=await this.shouldFetchWithFilters(e,t,i);r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=r(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=t.items||[],o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const d={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,d),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(d)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items)return this.applyOrdering(r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]),t);const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];for(const[e,s]of Object.entries(t.filters))if(!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;if(t.filters.orderby||t.filters.order){const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.fields?.post_title||e.title||e.name||"").toLowerCase(),a=(t.fields?.post_title||t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||0),a=new Date(t.modified||0);break;default:i=new Date(e.date||0),a=new Date(t.date||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0}))}return e}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),this.notify(e,"data-loaded",{cached:!0,items:this.getFiltered(e)});const a=await this.shouldFetchWithFilters(e,t,i);r.config.endpoint&&a?await this.fetch(e):r.config.endpoint&&this.notify(e,"data-loaded")}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 6fee3ca..e487647 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e},this.isRestoring=!1;const t=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0});this.store=t.forms,this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=window.crudManager&&window.crudManager.isTimeline,this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){const e=await this.store.getAll(),t=window.location.pathname;e.filter((e=>{if("draft"!==e.status)return!1;const r=e.data?._wp_http_referer;return r===t})).forEach((e=>{const t=this.findFormElement(e);if(!t)return;let r=this.forms.get(e.formId);t.dataset.formId||(r=this.registerForm(t)),this.isRestoring=!0,new this.populateForm(t,e.data),setTimeout((()=>{this.isRestoring=!1}),0),this.showFormStatus(e.formId,"restored"),window.jvbA11y&&window.jvbA11y.announce("Your previous entry has been restored")}))}findFormElement(e){if(e.data?.form_id){const t=document.querySelector(`[name="form_id"][value="${e.data.form_id}"]`)?.closest("form");if(t)return t}if(e.data?.form_type){const t=document.querySelector(`[name="form_type"][value="${e.data.form_type}"]`)?.closest("form");if(t)return t}return document.querySelector(`[data-form-id="${e.formId}"]`)}showPendingNotification(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);if(!r)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,r.insertBefore(s,r.firstChild),s.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),s.remove()}))}async restorePendingForm(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);r&&(new this.populateForm(r,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,autoUpload:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.data&&this.showPendingNotification(r,e.data)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),this.initTagListFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e,t),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const d=s.switchTab.bind(s);s.switchTab=(e,t)=>{d(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initTagListFields(e,t){e.querySelectorAll(".field.tag-list").forEach((e=>{const r=e.querySelector(".tag-input-row"),s=e.querySelector(".add-tag-item"),a=e.querySelector(".tag-items"),i=e.querySelector(".tag-template"),n=e.dataset.field,o=e.dataset.tagFormat||"first_field";if(!(r&&s&&a&&i))return;const l=()=>Array.from(r.querySelectorAll("input, select, textarea")).filter((e=>!e.closest("button"))),d=()=>{const r=l(),s={};let d=!1;if(r.forEach((e=>{const t=e.name.replace("new_",""),r=this.getFieldValue(e);r&&(d=!0),s[t]=r})),!d)return window.jvbA11y&&window.jvbA11y.announce("Please fill in at least one field","error"),void r[0].focus();const c=r.find((e=>{const t="required"in e.dataset&&"1"===e.dataset.required,r=this.getFieldValue(e);return t&&!r}));if(c){const e=c.closest(".field"),t=e?.querySelector("label")?.textContent||"This field";return this.showError(e,`${t} is required.`),void c.focus()}for(let t of r){let r=e.closest(".field");if(!this.validateField(t,r))return void t.focus()}const u=a.children.length,h=i.content.cloneNode(!0).firstElementChild;h.dataset.index=u;const m=h.querySelector(".tag-label");m&&(m.textContent=this.getTagDisplayText(s,o)),h.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;e.name=`${n}:${u}:${t}`,e.value=s[t]||""})),a.appendChild(h),r.forEach((e=>{"checkbox"===e.type||"radio"===e.type?e.checked=!1:e.value="";let t=e.closest(".field");this.clearValidation(t)})),r.length>0&&r[0].focus(),t&&this.scheduleSave(t,{type:"tag_list",action:"add",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce("Item added")};s.addEventListener("click",d);const c=l();c.length>0&&(c[c.length-1].addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),d())})),c.slice(0,-1).forEach(((e,t)=>{e.addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),c[t+1].focus())}))}))),a.addEventListener("click",(e=>{if(e.target.closest(".remove-tag")){const r=e.target.closest(".tag-item"),s=r.querySelector(".tag-label")?.textContent||"Item";r.remove(),this.reindexTagList(a,n),t&&this.scheduleSave(t,{type:"tag_list",action:"remove",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce(`${s} removed`)}}))}))}reindexTagList(e,t){Array.from(e.children).forEach(((e,r)=>{e.dataset.index=r,e.querySelectorAll('input[type="hidden"]').forEach((e=>{const s=e.dataset.field;e.name=`${t}:${r}:${s}`}))}))}getTagDisplayText(e,t){const r=Object.values(e).filter((e=>e));if(0===r.length)return"New Item";switch(t){case"first_field":return r[0];case"all_fields":return r.join(", ");default:if(t.includes("{")){let r=t;for(const[t,s]of Object.entries(e))r=r.replace(`{${t}}`,s);return r}return e[t]||r[0]}}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s===a;case"!=":return s!==a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.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)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:s,config:r})}}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 r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,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"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}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 r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,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}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]"),r=t.dataset.action,s=t.closest("form");switch(r){case"clear-form":s?.dataset.formId&&(this.store.delete(s.dataset.formId),s.reset(),s.querySelector(".fstatus").hidden=!0),window.jvbA11y&&window.jvbA11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":s.querySelector(".fstatus").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form")||this.isRestoring)return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,(()=>this.validateField.bind(this)),500)}initValidators(){return{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 r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s?.options.formStatus)return;if(s.status===t)return;s.status=t;const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove(),a.querySelector(".actions")?.remove();const n={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"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);if(o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"restored"===t){const e=document.createElement("div");e.className="actions",e.innerHTML='\n            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>\n            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>\n        ',a.appendChild(e),setTimeout((()=>a.hidden=!0),1e4)}"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return 0!==Object.keys(a).length?(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a)):this.mergeRepeaterData(r,s)}collectTimeline(e){let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)](.+)$/);if(a){const[,t,o]=a;if(r[t]||(r[t]={id:parseInt(t)},s.push(t)),"post_thumbnail"===o)r[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,r[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[r,s]of Object.entries(t))e[r]=s;return e}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const d=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),d||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const d=n[n.length-1];void 0!==l[d]?(Array.isArray(l[d])||(l[d]=[l[d]]),l[d].push(t)):l[d]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary");if(!a)return;const i=a.querySelector(".result"),n=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(n.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;let o=i.cloneNode(!0),l=o.querySelector("h3"),d=o.querySelector("p");l.textContent=r.label;let c=this.formatFieldValue(t,r.type,s);this.isHtmlContent(c)?d.innerHTML=c:d.textContent=c,a.append(o)}let o=s.querySelectorAll("[data-upload-field]");o&&o.forEach((e=>{let t=e.querySelector("h2").textContent,r=e.querySelectorAll(".item-grid.preview img");if(r){let e=i.cloneNode(!0),s=e.querySelector("h3");e.querySelector("p").remove(),s.textContent=t,r.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),a.append(e)}})),i.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=e.querySelector(`[name=${t}]`);if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend, h2"))}let a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`),i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":e;case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"upload":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=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.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:".success",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:".tag-input-row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",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-loaded"===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.remove(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" data-form-id="${e}">Restore</button>\n        <button class="discard" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0,new this.populate(i,t),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.remove(e),this.a11y.announce("Previous changes discared"),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.getFieldValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required&&!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.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(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)}))}let s=this.getForm(e.target);s&&s.options.cache&&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),t.options.cache&&this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t||!t.options.cache)return;let s=this.getField(e.target);s&&(this.showFormStatus(t,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${s.dataset.field}`,(()=>this.validateField.bind(this)),500))}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0){e.preventDefault();const s=await this.store.get(t.id);this.notify("form-submit",{config:t,data:s?.changes||{}})}if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary(t.id,{config:t,data: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),this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{if(this.changes.size>0){await this.store.saveMany(this.changes);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}}),2e3)}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:!1,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",formStatus:t.showStatus??!0,showSummary:!1,cache:t.cache??!0},ui:window.uiFromSelectors(this.selectors.forms,e)};return i.showSummary&&!this.summaryTemplate&&this.defineSummaryTemplate(),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.delete(item.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",...e.ignore];for(let[e,i]of Object.entries(a.changes)){if(r.includes(e)||this.isEmptyValue(i))continue;let a=Array.from(this.inputs.values()).find((t=>t.field?.dataset.field===e));if(!a)continue;let n=s.result.cloneNode(!0),l=n.querySelector("h3"),o=n.querySelector("p");l.textContent=a.label.textContent,"string"==typeof i?o.textContent=i:Array.isArray(i)||"object"==typeof i&&(o.textContent=`${i.address}`),t.append(n)}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");if(a){let e=s.result.cloneNode(!0),r=field.querySelector("h3"),n=field.querySelector("p");n?.remove(),r&&(r.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]":()=>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.increase.contains(e.target)?s++:t.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.input.value?0:parseFloat(t.input.value);t.input.value=a+i*s,a=parseFloat(t.input.value),t.input.min&&a<t.input.min?(t.input.value=t.input.min,t.decrease.disabled=!0):t.input.max&&a>t.input.max?(t.input.value=t.input.max,t.increase.disabled=!0):(t.decrease.disabled&&(t.decrease.disabled=!1),t.increase.disabled&&(t.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.addButton)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,`${e.dataset.fieldName}:${r}:`)}))}}),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)}addRepeaterRow(e){e.append(this.templates.create(e.dataset.repeaterId)),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;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((t=>{window.prefixInput(t,`${e.dataset.fieldName}:${r}:`)})),t.label&&(t.label.textContent=a.label)}}),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){e.target.matches(this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):e.target.matches(this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.remove))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1;for(let e of t.ui.inputs){this.validateField(e);const t=e.name.replace("new_",""),s=this.getFieldValue(e);s&&(a=!0),i[t]=s,["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e)}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(format.includes("{")){let e=t.format;for(const[t,s]of Object.entries(i))e=e.replace(`{${t}}`,s)}else s=i[t.format]??Object.values(i)[0]}let r=this.templates.create(e.dataset.tagListId,{label:s});const n=t.ui.items?.children?.length??0;r?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.element.field}:${n}:${s}`,e.value=i[s]||""})),t.ui.items.append(r),t.ui.inputs[0]?.focus(),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");e.remove(),this.reindexList(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.getFieldValue(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.autoUpload)}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){Array.from(e.children).forEach(((t,s)=>{t.dataset.index=`${s}`,Array.from(t.children).forEach((t=>{"hidden"===t.type&&window.prefixInput(t,`${e.dataset.field}:${s}:${t.dataset.field}`)}))}))}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))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&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){this.templates.create("formSummary",e)}getForm(e){let t=e.closest("[data-form-id]").dataset.formId;if(!t)return!1;let s=this.forms.get(t);return s||!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;default:return e.value}}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}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)}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
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index cee4f65..f45f6bc 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e,t={}){this.container=e,this.selectors=window.deepMerge({selectAll:{checkbox:"[data-select-all]",label:".selected label",span:".selected label span",target:"data-selects",count:".selected-count, .selected .info",bulkControls:".bulk-actions"},items:".item-grid",wrapper:{wrapper:":has(.item-grid, [data-select-all])",id:"selection"},item:{item:".item",idAttribute:"id",checkbox:'[name="select-item"]'},wrappers:{}},t),this.a11y=window.jvbA11y,this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.items=new Map,this.initElements(),this.initListeners(),this.collectItems()}removeDataReferences(){let e=JSON.parse(JSON.stringify(this.selectors));return delete e.item.idAttribute,delete e.wrapper.id,delete e.selectAll.target,e}initElements(){this.index=0;let e=this.removeDataReferences();this.ui=window.uiFromSelectors(e,this.container),this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach((e=>{this.addWrapper(e)}))}addWrapper(e){let t=this.selectors.wrapper.id;Object.hasOwn(e.dataset,t)||(e.setAttribute(`data-${t}`,this.index),this.index++);let s=this.removeDataReferences().selectAll;this.ui.wrappers[e.dataset[t]]={element:e,items:e.querySelector(this.selectors.items),selectAll:window.uiFromSelectors(s,e)}}removeWrapper(e){delete this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),document.addEventListener("keydown",this.keyHandler)}handleChange(e){if(this.container.contains(e.target))if(e.target.matches(this.selectors.selectAll.checkbox))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.item.checkbox)){const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=this.getItemWrapper(e.target);if(!s)return;this.lastClicked=s.element;const i=this.getItemId(t);if(!i)return;e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s.element,this.updateSelectionUI()}}handleClick(e){if(!e.shiftKey)return;const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=e.target.matches(this.selectors.item.checkbox),i=e.target.closest("label[for]");if(!s&&!i)return;const l=this.getItemWrapper(t);if(!l)return;if(!this.lastSelected||!this.lastSelectedWrapper||l.element!==this.lastSelectedWrapper)return;e.preventDefault();const c=this.getItemId(t),r=this.getWrapperChildren(l),n=r.findIndex((e=>e===this.lastSelected)),a=r.findIndex((e=>e===c));if(-1===n||-1===a)return;const[h,d]=[Math.min(n,a),Math.max(n,a)];r.slice(h,d+1).forEach((e=>{this.select(e,!0,!1)})),this.lastSelected=c,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:l})}getWrapperChildren(e){return Array.from(e.items.children).map((e=>this.getItemId(e)))}getItemWrapper(e){if(!e)return null;let t=e.closest(this.selectors.wrapper.wrapper);return t?this.getWrapper(t):null}getWrapper(e){return this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]??null}handleKeys(e){"Escape"===e.key&&this.selectedItems.size>0&&(e.preventDefault(),Object.keys(this.ui.wrappers).length>1&&this.lastClicked?this.clearWrapperSelection(this.lastClicked):this.clearSelection())}handleSelectAll(e){const t=this.getItemWrapper(e);if(!t)return;const s=this.getWrapperChildren(t);e.checked?(s.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t.element):(s.forEach((e=>this.deselect(e,!0,!1))),this.lastSelectedWrapper=null),t.selectAll.span&&(t.selectAll.span.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:s,selectedItems:new Set(this.selectedItems)})}getItemId(e){if(!(!e instanceof Element)||(e=e.element??!1))return e.dataset[`${this.selectors.item.idAttribute}`]}select(e,t=!0,s=!0){this.selectedItems.has(e)||(this.selectedItems.add(e),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)}))}deselect(e,t=!0,s=!0){this.selectedItems.has(e)&&(this.selectedItems.delete(e),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)}))}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.deselect(e,!0,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null;for(let e of Object.values(this.ui.wrappers))e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All");this.a11y.announce("Selection cleared"),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}clearWrapperSelection(e){(e=this.getWrapper(e))&&(this.getWrapperChildren(e).forEach((e=>this.deselect(e,!0,!1))),e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All"),this.a11y.announce("Selection cleared in group"),this.updateSelectionUI(),this.notify("wrapper-selection-cleared",{selectedItems:this.selectedItems}))}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.getItem(e);s&&s.checkbox&&s.checkbox.checked!==t&&(s.checkbox.checked=t)}updateSelectionUI(){if(!this.lastClicked)return;const e=this.getWrapper(this.lastClicked);if(!e||!e.selectAll)return;const t=this.selectedItems.size;let s=e.selectAll.bulkControls;s&&(s.hidden=0===t);let i=e.selectAll.count;if(i){const e=1===t?"item":"items";i.textContent=0===t?"":`${t} ${e} selected`,i.hidden=0===t}}collectItems(){this.container.querySelectorAll(this.selectors.item.item).forEach((e=>{this.setItem(e,!0)}))}getItem(e){return this.items.has(e)?this.items.get(e):this.setItem(e)}setItem(e,t=!1){let s=t?e:this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${e}"]`);return s?(e=this.getItemId(s),this.items.has(e)||this.items.set(e,{element:s,checkbox:s.querySelector(this.selectors.item.checkbox)}),this.items.get(e)):null}camelToKebab(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}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.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
+window.jvbHandleSelection=class{constructor(e,t={}){this.container=e,this.selectors=window.deepMerge({selectAll:{checkbox:"[data-select-all]",label:".selected label",span:".selected label span",target:"data-selects",count:".selected-count, .selected .info",bulkControls:".bulk-actions"},items:".item-grid",wrapper:{wrapper:":has(.item-grid, [data-select-all])",id:"selection"},item:{item:".item",idAttribute:"id",checkbox:'[name="select-item"]'},wrappers:{}},t),this.a11y=window.jvbA11y,this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.items=new Map,this.initElements(),this.initListeners(),this.collectItems()}removeDataReferences(){let e=JSON.parse(JSON.stringify(this.selectors));return delete e.item.idAttribute,delete e.wrapper.id,delete e.selectAll.target,e}initElements(){this.index=0;let e=this.removeDataReferences();this.ui=window.uiFromSelectors(e,this.container),this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach((e=>{this.addWrapper(e)}))}addWrapper(e){let t=this.selectors.wrapper.id;Object.hasOwn(e.dataset,t)||(e.setAttribute(`data-${t}`,this.index),this.index++);let s=this.removeDataReferences().selectAll;this.ui.wrappers[e.dataset[t]]={element:e,items:e.querySelector(this.selectors.items),selectAll:window.uiFromSelectors(s,e)}}removeWrapper(e){delete this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),document.addEventListener("keydown",this.keyHandler)}handleChange(e){if(this.container.contains(e.target))if(e.target.matches(this.selectors.selectAll.checkbox))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.item.checkbox)){const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=this.getItemWrapper(e.target);if(!s)return;this.lastClicked=s.element;const i=this.getItemId(t);if(!i)return;e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s.element,this.updateSelectionUI()}}handleClick(e){if(!e.shiftKey)return;const t=e.target.closest(this.selectors.item.item);if(!t)return;const s=e.target.matches(this.selectors.item.checkbox),i=e.target.closest("label[for]");if(!s&&!i)return;const l=this.getItemWrapper(t);if(!l)return;if(!this.lastSelected||!this.lastSelectedWrapper||l.element!==this.lastSelectedWrapper)return;e.preventDefault();const c=this.getItemId(t),r=this.getWrapperChildren(l),n=r.findIndex((e=>e===this.lastSelected)),a=r.findIndex((e=>e===c));if(-1===n||-1===a)return;const[h,d]=[Math.min(n,a),Math.max(n,a)];r.slice(h,d+1).forEach((e=>{this.select(e,!0,!1)})),this.lastSelected=c,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:l})}getWrapperChildren(e){return Array.from(e.items.children).map((e=>this.getItemId(e)))}getItemWrapper(e){if(!e)return null;let t=e.closest(this.selectors.wrapper.wrapper);return t?this.getWrapper(t):null}getWrapper(e){return this.ui.wrappers[e.dataset[this.selectors.wrapper.id]]??null}handleKeys(e){"Escape"===e.key&&this.selectedItems.size>0&&(e.preventDefault(),Object.keys(this.ui.wrappers).length>1&&this.lastClicked?this.clearWrapperSelection(this.lastClicked):this.clearSelection())}handleSelectAll(e){const t=this.getItemWrapper(e);if(!t)return;const s=this.getWrapperChildren(t);e.checked?(s.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t.element):(s.forEach((e=>this.deselect(e,!0,!1))),this.lastSelectedWrapper=null),t.selectAll.span&&(t.selectAll.span.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:s,selectedItems:new Set(this.selectedItems)})}getItemId(e){if(!(!e instanceof Element)||(e=e.element??!1))return e.dataset[`${this.selectors.item.idAttribute}`]}select(e,t=!0,s=!0){if(this.selectedItems.has(e))return;this.selectedItems.add(e);let i=this.getItem(e);i&&i.element.classList.add("selected"),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)})}deselect(e,t=!0,s=!0){if(!this.selectedItems.has(e))return;this.selectedItems.delete(e);let i=this.getItem(e);i&&i.element.classList.remove("selected"),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)})}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.deselect(e,!0,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null;for(let e of Object.values(this.ui.wrappers))e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All");this.a11y.announce("Selection cleared"),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}clearWrapperSelection(e){(e=this.getWrapper(e))&&(this.getWrapperChildren(e).forEach((e=>this.deselect(e,!0,!1))),e.selectAll.checkbox&&(e.selectAll.checkbox.checked=!1),e.selectAll.span&&(e.selectAll.span.textContent="Select All"),this.a11y.announce("Selection cleared in group"),this.updateSelectionUI(),this.notify("wrapper-selection-cleared",{selectedItems:this.selectedItems}))}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.getItem(e);s&&s.checkbox&&s.checkbox.checked!==t&&(s.checkbox.checked=t)}updateSelectionUI(){if(!this.lastClicked)return;const e=this.getWrapper(this.lastClicked);if(!e||!e.selectAll)return;const t=this.selectedItems.size;let s=e.selectAll.bulkControls;s&&(s.hidden=0===t);let i=e.selectAll.count;if(i){const e=1===t?"item":"items";i.textContent=0===t?"":`${t} ${e} selected`,i.hidden=0===t}}collectItems(){this.container.querySelectorAll(this.selectors.item.item).forEach((e=>{this.setItem(e,!0)}))}getItem(e){return this.items.has(e)?this.items.get(e):this.setItem(e)}setItem(e,t=!1){let s=t?e:this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${e}"]`);return s?(e=this.getItemId(s),this.items.has(e)||this.items.set(e,{element:s,checkbox:s.querySelector(this.selectors.item.checkbox)}),this.items.get(e)):null}camelToKebab(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}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.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index f05859a..42f7c96 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-window.jvbPopulate=class{constructor(e,t={},a={},i={}){this.item=this.normalizeItemData(t,a),this.form=e,this.options=i;for(let[t,a]of Object.entries(this.item.fields)){let i=e.querySelector(`[data-field="${t}"]`);i&&this.populateField(i,t,a)}}normalizeItemData(e,t){return e&&"object"==typeof e&&"fields"in e?{fields:e.fields||{},images:e.images||{},taxonomies:e.taxonomies||{}}:{fields:e||{},images:t||{},taxonomies:{}}}isTaxonomyField(e){return Object.hasOwn(this.item.taxonomies,e)&&Object.keys(this.item.taxonomies[e]).length>0}isImageField(e){return!(!this.item.images||0===Object.keys(this.item.images).length)&&this.splitIDs(e).some((e=>Object.keys(this.item.images).includes(String(e))))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}populateField(e,t,a,i={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(e,t,a);break;case"repeater":this.populateRepeaterField(e,t,a);break;case"selector":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}populateTaxonomyField(e,t,a){let i=[];if(Array.isArray(a))i=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);i=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){i=a.split(",").map((e=>e.trim())).filter((e=>e))}else a&&(i=[String(a)]);if(0===i.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=i.join(","),window.jvbTaxonomy&&requestAnimationFrame((()=>{window.jvbTaxonomy.updateFieldFromInput(r)})))}populateUploadField(e,t,a){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a);if(!a)return;const i=this.splitIDs(a);if(0===i.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=i.join(","));const l=e.querySelector(".item-grid"),o=e.querySelector(".file-upload-container");l&&window.removeChildren(l),e.querySelector(".progress")?.remove(),l&&(i.forEach((e=>{const t=window.getTemplate("uploadItem");t?(this.populateUploadItem(t,e),l.append(t)):console.warn("uploadItem template not found")})),i.length>0&&o&&(o.hidden=!0))}populateUploadItem(e,t){let a=e.querySelector('input[name="select-item"]'),i=e.querySelector('label[for="select-item"]');e.dataset.id=t,a.name=`select-item-${t}`,a.id=a.name,i.htmlFor=a.name;const r=e.querySelector("img");if(e.querySelector("video")?.remove(),this.item.images[t]){const a=this.item.images[t];r&&(r.src=a.medium||a.small||a.large||"",r.alt=a["image-alt-text"]||a.alt||"");const i=e.querySelector('[name="image-title"]'),l=e.querySelector('[name="image-alt-text"]'),o=e.querySelector('[name="image-caption"]');i&&(i.value=a["image-title"]||a.title||""),l&&(l.value=a["image-alt-text"]||a.alt||""),o&&(o.value=a["image-caption"]||a.caption||"")}else console.warn(`No image data found for ID: ${t}`);e.querySelector("details .upload-meta > .hint")?.remove()}populateTimelineGallery(e,t,a){if(!a||!Array.isArray(a))return void console.warn("Timeline field value must be an array");if(0===a.length)return;const i=e.querySelector(".item-grid"),r=e.querySelector(".file-upload-container");if(i&&window.removeChildren(i),e.querySelector(".progress")?.remove(),i){for(let e of a){const t=window.getTemplate("timelineItem");if(!t){console.warn("timelineItem template not found");continue}const a=e.post_thumbnail,r=e.id;t.dataset.id=a,t.dataset.postId=r;let l=t.querySelector('input[name="select-item"]'),o=t.querySelector('label[for="select-item"]');l&&o&&(l.name=`select-item-${a}`,l.id=l.name,o.htmlFor=l.name),t.querySelector("video")?.remove(),t.querySelector(".select-item span")?.remove();const n=t.querySelector("img"),s=this.item.images[a];n&&s&&(n.src=s.medium||s.small||s.large||"",n.title=s["image-title"]||"",n.alt=s["image-alt-text"]||""),t.querySelectorAll(".field").forEach((t=>{if(t.classList.contains("group"))return;const a=t.querySelector('input:not([type="file"]), textarea');if(!a)return;const i=t.querySelector("label"),l=a.name.replace("upload_data::","").replace(/^\[.*?\]/,"");let o=e[l];void 0===o&&s&&(o=s[l]),null!=o&&this.populateField(t,l,o);const n=`[${r}]${l}`,c=n;a.name=n,a.id=c,i&&(i.htmlFor=c)})),i.append(t)}a.length>0&&r&&(r.hidden=!0)}}populateTextField(e,t,a){const i=e.querySelector(`[name="${t}"], input, textarea`);if(i&&"file"!==i.type&&(i.value=String(a||""),i.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=i.value.length)}}populateTextareaField(e,t,a){const i=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(i&&(i.value=String(a||""),i.dispatchEvent(new Event("change",{bubbles:!0})),i.dataset.limit)){const t=e.querySelector(".char-count .current");if(t){t.textContent=i.value.length;const a=parseInt(i.dataset.limit,10);e.classList.toggle("reached",i.value.length>=a)}}}populateEditorField(e,t,a){const i=e.querySelector(`textarea[name="${t}"][data-editor="true"]`);if(!i)return;i.value=String(a||"");const r=e.querySelector(".editor"),l=a||"<p><br></p>";if(r){let e=r.__quill;if(!e&&window.Quill)for(let t of window.Quill.instances||[])if(t.container===r){e=t;break}e?(e.root.innerHTML=l,r.__quill=e):r.innerHTML=l}i.dispatchEvent(new Event("change",{bubbles:!0}))}getFieldType(e){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateNumberField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="number"]`);i&&(i.value=Number(a)||0)}populateBooleanField(e,t,a){const i=e.querySelector(`[name="${t}"], input[type="checkbox"]`);i&&(i.checked=Boolean(a))}populateSelectField(e,t,a){const i=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=i);const l=e.querySelector(`input[type="radio"][name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateSetField(e,t,a){let i=a;if("string"==typeof a)try{i=JSON.parse(a)}catch(e){i=a.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateDateField(e,t,a){const i=e.querySelector(`[name="${t}"], input`);if(i&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(i.type){case"date":i.value=t.toISOString().split("T")[0];break;case"time":i.value=t.toTimeString().slice(0,5);break;case"datetime-local":i.value=t.toISOString().slice(0,16);break;default:i.value=e}}catch(t){i.value=e}}}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((i=>{if(void 0!==a[i]){const r=e.querySelector(`[name="${t}_${i}"], [name="${i}"]`);r&&(r.value=String(a[i]||""))}}))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateRepeaterField(e,t,a){if(!a||!Array.isArray(a))return;const i=e.querySelector(".repeater-items"),r=e.querySelector("template");i&&r?(window.removeChildren(i),a.forEach(((a,l)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(r.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${l}`,o.dataset.index=l;const n=o.querySelector(".row-number");n&&(n.textContent=`#${l+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const i=e.name,r=`${t}:${l}:${i}`,o=`${t}-${l}-${i}-${e.value}`;e.name=r,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[i]&&this.populateRepeaterFieldValue(e,i,a[i])})),i.appendChild(o)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
\ No newline at end of file
+(()=>{class e{constructor(){this.templates=window.jvbTemplates,this.formHelper=window.jvbForm,this.defineTemplates(),this.data=null,this.form=null}populate(e,t={}){if(this.data=t,this.form=e,this.formHelper||(this.formHelper=window.jvbForm),this.formHelper){if(Object.hasOwn(this.data,"fields")&&0!==Object.keys(this.data.fields).length)for(let[t,i]of Object.entries(this.data.fields)){let l=e.querySelector(`[data-field="${t}"]`);l&&this.populateField(l,t,i)}}else requestAnimationFrame((()=>{this.populate(e,t)}))}populateField(e,t,i){let l=this.formHelper.getFieldType(e);if(!l||this.isEmptyValue(t)||this.isEmptyValue(i))return;const a={repeater:this.populateRepeater.bind(this),"tag-list":this.populateTagList.bind(this),location:this.populateLocation.bind(this),selector:this.populateTaxonomy.bind(this),user:this.populateUser.bind(this),upload:this.populateUpload.bind(this),set:this.populateMultiValue.bind(this),checkbox:this.populateMultiValue.bind(this),select:this.populateSingleValue.bind(this),radio:this.populateSingleValue.bind(this),"true-false":this.populateBoolean.bind(this),date:this.populateDate.bind(this),time:this.populateDate.bind(this),datetime:this.populateDate.bind(this),number:this.populateNumber.bind(this),textarea:this.populateTextarea.bind(this)};Object.hasOwn(a,l)?a[l](e,t,i):this.populateText(e,t,i)}populateRepeater(e,t,i){if(!i||!Array.isArray(i))return;const l=e.querySelector(".repeater-items");let a=e.querySelector("template")?.className??!1;l&&a&&(window.removeChildren(l),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(a,e);let r=i.querySelectorAll(".field");this.populate(r,e),l.append(i)})))}populateTagList(e,t,i){if(!i||!Array.isArray(i))return;const l=e.querySelector(".tag-items");let a=e.querySelector("template")?.className??!1;l&&a&&(window.removeChildren(l),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(a,e);let r=i.querySelectorAll(".field");this.populate(r,e),l.append(i)})))}populateLocation(e,t,i){["address","lat","lng","street","city","province","postal_code","country"].forEach((t=>{if(Object.hasOwn(i,t)){let l=e.querySelector(`[data-location-field="${t}"]`);l&&(l.value=String(i[t]||""))}}))}populateTaxonomy(e,t,i){let l=this.splitIDs(i);if(0===l.length)return;const a=e.querySelector(`input[type="hidden"][name="${t}"]`);a&&(a.value=l.join(","),window.jvbSelector&&requestAnimationFrame((()=>{window.jvbSelector.updateFieldFromInput(a)})))}populateUser(e,t,i){this.populateTaxonomyField(e,t,i)}populateUpload(e,t,i){if("timeline"===t||e.dataset.subtype&&"timeline"===e.dataset.subtype)return void this.populateTimelineGallery(e,t,i);if(this.isEmptyValue(i))return;const l=this.splitIDs(i);if(0===l.length)return;const a=e.querySelector('input[type="hidden"]');a&&(a.value=l.join(","));const r=e.querySelector(".item-grid");e.querySelector(".file-upload-container").hidden=l.length>0,e.querySelector(".progress")?.remove(),r&&(window.removeChildren(r),l.forEach((e=>{let t=this.data.images[e]??{};t.id=e,r.append(this.templates.create("uploadItem",t))})))}populateTimelineGallery(e,t,i){if(!i||!Array.isArray(i)||0===i.length)return;let l=e.querySelector(".item-grid");if(e.querySelector(".file-upload-container").hidden=i.length>0,l){window.removeChildren(l),e.querySelector(".progress")?.remove();for(let e of i){let t=this.templates.create("timelineItem",e);t&&l.append(t)}}}populateMultiValue(e,t,i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){i=i.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]);let l=e.querySelector(`select[name="${t}"]`);if(l&&l.multiple)for(let e of l.options)e.selected=i.includes(e.value);else e.querySelectorAll(`[type="checkbox"][name=${t}]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateSingleValue(e,t,i){i=String(i||"");let l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=i);let a=e.querySelector(`[name="${t}"][value="${i}"]`);a&&(a.checked=!0)}populateBoolean(e,t,i){const l=e.querySelector(`[name="${t}"], input[type="checkbox"]`);l&&(l.checked=Boolean(i))}populateDate(e,t,i){const l=e.querySelector(`[name="${t}"], input`);if(l){"object"==typeof i&&Object.hasOwn(i,"date")&&(i=i.date);try{const e=new Date(i);if(!isNaN(e.getTime()))switch(l.type){case"date":l.value=e.toISOString().split("T")[0];break;case"time":l.value=e.toTimeString().slice(0,5);break;case"datetime-local":l.value=e.toISOString().slice(0,16);break;default:l.value=i}}catch(e){l.value=i}}}populateNumber(e,t,i){const l=e.querySelector(`[name="${t}"], input[type="number"]`);l&&(l.value=Number(i)||0)}populateTextarea(e,t,i){let l=e.querySelector("textarea");l.dataset.editor?(l.value=String(i||""),l.dispatchEvent(new Event("change",{bubbles:!0}))):this.populateText(e,t,i)}populateText(e,t,i){let l=e.querySelector(`[name="${t}"], input, textarea`);l&&"file"!==l.type&&(l.value=String(i||""))}getFormHelper(){window.requestAnimationFrame((()=>{this.formHelper=window.jvbForm}))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}defineTemplates(){const e=this.templates,t=this;e.define("timelineItem",{refs:{select:'[name="select-item"]',video:"video",file:".select-item span",img:"img"},manyRefs:{fields:".field"},setup({el:e,refs:i,manyRefs:l,data:a}){e.dataset.itemId=a.id,i.select&&window.prefixInput(i.select,`${a.id}-`),i.video&&i.video.remove(),i.file&&i.file.remove();let r=t.data.images[a.post_thumbnail]??!1;if(i.img&&r&&(i.img.src=r.medium||r.small||r.large||"",i.img.title=r["image-title"]??"",i.img.alt=r["image-alt-text"]??""),l.fields)for(let e of l.fields){if("group"===e.dataset.fieldType)continue;let i=e.dataset.field,l=a[i]??"";t.isEmptyValue(l)||t.populateField(e,i,l);const r=e.querySelector('input:not([type="file"]), textarea');r&&window.prefixInput(r,`[${a.id}]`)}}})}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbPopulate=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 725c753..fd05a2c 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.api=jvbSettings.api,this.endpoint="queue",this.init()}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"}))}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .refresh .refreshNow",countdown:"#queue .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),window.addEventListener("beforeunload",this.unloadHandler)}handleOnline(){this.updatePanel("synced"),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleBeforeUnload(e){if(this.getQueueByStatus(this.pendingStatuses).length>0)return e.preventDefault(),e.returnValue="",""}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.store.clearCache(),this.store.clearFilters(),void this.store.fetch();if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then((()=>{}));if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then((()=>{}));const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1});this.store=e.queue,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.previousItem&&t.previousItem.status!==t.item.status&&this.updateOperationStatus(t.item.id,t.item.status),this.maybeStartPolling()}}))}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),created_at:(new Date).toISOString(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout((()=>this.ui.popup.popup.hidden=!0),2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then((()=>{})),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map((e=>e.id)):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationUI(e)));try{const i=await fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:e,user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);if(this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}})),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.opActions(e,t))),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0!==e.length){this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.toggleQueue(this.maybeStartPolling())}else this.stopActivityTracking()}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(s=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),t=e.data):(t=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json"),null==t)return;const i=await fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),n=await i.json();if(s&&(e.data={}),!i.ok||!n.success)throw new Error(n.message||`HTTP ${i.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status??"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort(((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0)))}sortOperations(e){const t={processing:0,uploading:1,pending:2,queued:3,localProcessing:4,failed:5,completed:6,failed_permanent:7};return e.sort(((e,s)=>{const i=(t[e.status]??99)-(t[s.status]??99);if(0!==i)return i;const n=e.updated_at??e.timestamp??0,a=s.updated_at??s.timestamp??0;return new Date(a)-new Date(n)}))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortOperations(e)}getQueueByStatus(e){"string"==typeof e&&(e=[e]);let t=[...new Set([...Array.from(this.store.filterByIndex({status:e})),...Array.from(this.queue.values()).filter((t=>e.includes(t.status)))])];return this.sortOperations(t)}updateOperationStatus(e,t){let s=this.getQueue(e);s&&this.statuses.includes(t)&&(s.status=t,this.notify("operation-status",s),this.setQueue(s))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus([...this.pendingStatuses,...this.workingStatuses]).length>0?(this.startPolling(),!0):(this.updatePanel("synced"),!1)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.runPollCycle())}async runPollCycle(){if(this.isPolling){try{if(this.store.clearCache(),await this.store.fetch(),!this.maybeStartPolling())return this.stopPolling(),void this.updatePanel("synced")}catch(e){console.error("Polling error:",e)}this.startCountdown(5,(()=>this.runPollCycle()))}}startCountdown(e,t){this.ui.refresh.countdown.classList.add("counting"),this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((()=>{--e>0?this.ui.refresh.countdown.textContent=e:(this.stopCountdown(),t&&t())}),1e3)}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null),this.ui.refresh.countdown.classList.remove("counting"),this.ui.refresh.countdown.textContent=""}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter((e=>"failed"===e.status)).length,this.ui.actions.clear.disabled=0===e.filter((e=>"completed"===e.status)).length;const t=e.filter((e=>[...this.pendingStatuses,...this.workingStatuses].includes(e.status))).length;this.ui.toggle.count.hidden=0===t,this.ui.toggle.count.textContent=t;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter((e=>e.status===t)).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e),s=this.sortOperations(t);if(0===s.length){window.removeChildren(this.ui.items.container);const e=window.getTemplate("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}this.ui.items.container.querySelector(".empty-group")?.remove();const i=new Set(s.map((e=>e.id)));this.items.forEach(((e,t)=>{i.has(t)||(e.element?.remove(),this.items.delete(t))})),s.forEach(((e,t)=>{let s=this.items.get(e.id);s||(s=this.createOperationElement(e)),s?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(s.element))}))}createOperationElement(e){const t=window.getTemplate("queueItem");t.dataset.id=e.id;const s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));s.status;const a="completed"===s.status&&(s.completed_at||s.updated_at);if(t.ui.completed.wrap.hidden=!a,a){const e=s.completed_at??s.updated_at;t.ui.completed.label.textContent="Completed: ",t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status))}getProgress(e){if(e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(...this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.user=window.auth.getUser(),this.canUpdateUI=!0,this.isProcessing=!1,this.isPolling=!1,this.queue=new Map,this.items=new Map,this.subscribers=new Set,this.api=jvbSettings.api,this.endpoint="queue",this.queueItems=new Map,this.init()}init(){this.headers={"X-WP-Nonce":window.auth.getNonce()},this.initElements(),this.initListeners(),this.initStore(),this.canUpdateUI&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle.button,name:"Queue Panel"})),this.defineTemplates()}initElements(){this.panelStatuses=["syncing","synced","pending","offline"],this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.pendingStatuses=["queued","localProcessing","uploading"],this.workingStatuses=["pending","processing"],this.completedStatuses=["completed","failed","failed_permanent"],this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:{button:"button.qtoggle",indicator:".qtoggle .indicator",count:".qtoggle .count"},refresh:{button:"#queue .refresh .refreshNow",countdown:"#queue .refresh .countdown"},popup:{popup:"#queue .popup",message:"#queue .popup span"},items:{container:"#queue .qitems"},actions:{retry:"#queue .retry-all",clear:"#queue .dismiss-all"},filters:{filter:"#queue [data-filter]",all:{label:'#queue [for="qfilter-all"]',radio:'#queue [data-filter="all"]',count:'#queue [data-filter="all"] .count'},queued:{label:'#queue [for="qfilter-queued"]',input:'#queue [data-filter="queued"]',count:'#queue [for="qfilter-queued"] .count'},localProcessing:{label:'#queue [for="qfilter-localProcessing"]',input:'#queue [data-filter="localProcessing"]',count:'#queue [for="qfilter-localProcessing"] .count'},uploading:{label:'#queue [for="qfilter-uploading"]',input:'#queue [data-filter="uploading"]',count:'#queue [for="qfilter-uploading"] .count'},pending:{label:'#queue [for="qfilter-pending"]',input:'#queue [data-filter="pending"]',count:'#queue [for="qfilter-pending"] .count'},processing:{label:'#queue [for="qfilter-processing"]',input:'#queue [data-filter="processing"]',count:'#queue [for="qfilter-processing"] .count'},completed:{label:'#queue [for="qfilter-completed"]',input:'#queue [data-filter="completed"]',count:'#queue [for="qfilter-completed"] .count'},failed:{label:'#queue [for="qfilter-failed"]',input:'#queue [data-filter="failed"]',count:'#queue [for="qfilter-failed"] .count'}},item:{type:".type",status:".status",details:".info .details",icon:".status .icon",startedAt:".started time",completed:{wrap:".completed",label:".completed span",time:".completed time"},progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},actions:{cancel:"button.cancel",retry:"button.retry",dismiss:"button.dismiss"}}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}defineTemplates(){const e=window.jvbTemplates;e.define("emptyState"),e.define("queueItem",{setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id}})}initListeners(){this.activityListeners=null,this.clickHandler=this.handleClick.bind(this),this.onlineHandler=this.handleOnline.bind(this),this.offlineHandler=this.handleOffline.bind(this),this.unloadHandler=this.handleBeforeUnload.bind(this),document.addEventListener("click",this.clickHandler),window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),window.addEventListener("beforeunload",this.unloadHandler)}handleOnline(){this.updatePanel("synced"),this.getQueueByStatus(this.pendingStatuses).length>0&&this.processQueue()}handleOffline(){this.updatePanel("offline")}handleBeforeUnload(e){if(this.getQueueByStatus(this.pendingStatuses).length>0)return e.preventDefault(),e.returnValue="",""}handleClick(e){if(!window.targetCheck(e,this.selectors.panel+", "+this.selectors.toggle.button))return;if(window.targetCheck(e,this.selectors.refresh.button))return this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),this.store.clearFilters(),void this.store.fetch().finally((()=>{this.ui.refresh.button.classList.remove("fetching")}));if(window.targetCheck(e,this.selectors.actions.clear))return void this.opActions("completed","dismiss").then((()=>{}));if(window.targetCheck(e,this.selectors.actions.retry))return void this.opActions("failed","retry").then((()=>{}));const t=window.targetCheck(e,"[data-action]");if(t){const e=t.closest("[data-id]")?.dataset.id;return void(e&&this.opActions(e,t.dataset.action))}const s=window.targetCheck(e,this.selectors.filters.filter);s&&this.setFilter(s.dataset.filter)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t.input?.dataset.filter===e&&(t.input.checked=!0)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}trackActivity(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),1750)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}initStore(){if(!this.user)return;const e=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1});this.store=e.queue,this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-save":this.maybeStartPolling(),this.updateUI();break;case"item-saved":t.previousItem&&t.previousItem.status!==t.item.status&&this.updateOperationStatus(t.item.id,t.item.status),this.maybeStartPolling()}}))}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},delay:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),created_at:(new Date).toISOString(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return null;if(t.popup&&this.ui.popup?.message&&(this.ui.popup.message.textContent=t.popup,this.ui.popup.popup.hidden=!1,setTimeout((()=>this.ui.popup.popup.hidden=!0),2e3)),!t.delay)return this.queue.set(t.id,t),this.processOperation(t).then((()=>{})),this.store.clearCache(),this.maybeStartPolling(),this.toggleQueue(),t.id;const s=Array.from(this.getAllQueue()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.trackActivity(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.trackActivity(),t.id}async opActions(e,t){if(this.statuses.includes(e)?e=this.getQueueByStatus(e).map((e=>e.id)):"string"==typeof e&&(e=[e]),0===e.length)return;if(!["cancel","dismiss","retry"].includes(t))return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>{this.removeOperationUI(e)}));try{const i=await fetch(`${this.api}${this.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({action:t,ids:e,user:this.user})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);if(i&&this.notify(`${t}-operation`,i),s)this.clearQueue(e);else{let t=this.getQueue(e);t.status="queued",this.setQueue(t),this.updateOperationStatus(t.id,t.status)}})),"retry"===t&&this.trackActivity(),this.updateUI(),n}catch(s){return await window.jvbError.log(s,{component:"Queue",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.opActions(e,t))),{success:!1,error:s.message}}}async processQueue(){if(this.isProcessing)return;const e=this.getQueueByStatus("queued");if(0!==e.length){this.setProcessing();for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.toggleQueue(this.maybeStartPolling())}else this.stopActivityTracking()}async processOperation(e){try{this.queue.has(e.id)||this.queue.set(e.id,e);let t,s=!1;if(e.data?._isFormData&&!e.data instanceof FormData&&(s=!0,e.data=await this.store.objectToFormData(e.data)),this.updateOperationStatus(e.id,"uploading"),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),t=e.data):(t=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json"),null==t)return;const i=await fetch(`${this.api}${e.endpoint}`,{method:e.method,headers:e.headers,body:t}),n=await i.json();if(s&&(e.data={}),!i.ok||!n.success)throw new Error(n.message||`HTTP ${i.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status??"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing`),this.setQueue(e)}catch(t){console.error("Operation failed: ",t),e.retries++,e.lastError=t.message,e.retries>=3?e.status="failed_permanent":e.status="failed",this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(e.status=t.status||"pending",e.serverData=t,this.mergeOp(s,e)):(this.clearQueue(e.id),this.setQueue(t),t)}mergeOp(e,t){return e.data=window.deepMerge(e.data,t.data),e.status=t.status,Object.hasOwn(t,"serverData")&&(e.serverData=t.serverData),this.updateOperationStatus(e.id,e.status),this.removeOperationUI(t.id),this.clearQueue(t.id),e}sortByDate(e){return e.sort(((e,t)=>(e.updated_at??e.timestamp??0)-(t.updated_at??t.timestamp??0)))}sortOperations(e){const t={processing:0,uploading:1,pending:2,queued:3,localProcessing:4,failed:5,completed:6,failed_permanent:7};return e.sort(((e,s)=>{const i=(t[e.status]??99)-(t[s.status]??99);if(0!==i)return i;const n=e.updated_at??e.timestamp??0,a=s.updated_at??s.timestamp??0;return new Date(a)-new Date(n)}))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortOperations(e)}getQueueByStatus(e){"string"==typeof e&&(e=[e]);let t=[...new Set([...Array.from(this.store.filterByIndex({status:e})),...Array.from(this.queue.values()).filter((t=>e.includes(t.status)))])];return this.sortOperations(t)}updateOperationStatus(e,t){let s=this.getQueue(e);s&&this.statuses.includes(t)&&(s.status=t,this.notify("operation-status",s),this.setQueue(s))}setQueue(e){this.store.save(e),this.queue.set(e.id,e)}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.get(e)}clearQueue(e){this.queue.delete(e),this.store.delete(e)}maybeStartPolling(){return this.getQueueByStatus([...this.pendingStatuses,...this.workingStatuses]).length>0?(this.startPolling(),!0):(this.updatePanel("synced"),!1)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.runPollCycle())}async runPollCycle(){if(this.isPolling){try{if(this.ui.refresh.button.classList.add("fetching"),this.store.clearCache(),await this.store.fetch(),this.ui.refresh.button.classList.remove("fetching"),!this.maybeStartPolling())return this.stopPolling(),void this.updatePanel("synced")}catch(e){console.error("Polling error:",e)}this.startCountdown(5,(()=>this.runPollCycle()))}}startCountdown(e,t){this.ui.refresh.countdown?(this.ui.refresh.countdown.classList.add("counting"),this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((()=>{--e>0?this.ui.refresh.countdown.textContent=e:(this.stopCountdown(),t&&t())}),1e3)):console.warn("Countdown element not found")}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null),this.ui.refresh.countdown.classList.remove("counting"),this.ui.refresh.countdown.textContent=""}updateUI(){this.canUpdateUI&&window.debouncer.schedule("queue-ui",this.handleUpdateUI.bind(this))}handleUpdateUI(){const e=this.getAllQueue();this.ui.actions.retry.disabled=0===e.filter((e=>"failed"===e.status)).length,this.ui.actions.clear.disabled=0===e.filter((e=>"completed"===e.status)).length;const t=e.filter((e=>[...this.pendingStatuses,...this.workingStatuses].includes(e.status))).length;this.ui.toggle.count.hidden=0===t,this.ui.toggle.count.textContent=t;for(let t of this.statuses){if("failed_permanent"===t)continue;let s=e.filter((e=>e.status===t)).length;this.ui.filters[t].label.hidden=0===s,this.ui.filters[t].input.dataset.count=`${s}`,this.ui.filters[t].count.textContent=s>0?s:""}this.renderOperations()}renderOperations(){if(!this.ui.items.container)return;const e=this.store.filters?.status??"all",t="all"===e?this.getAllQueue():this.getQueueByStatus(e),s=this.sortOperations(t);if(0===s.length){window.removeChildren(this.ui.items.container);const e=window.jvbTemplates.create("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}this.ui.items.container.querySelector(".empty-group")?.remove();const i=new Set(s.map((e=>e.id)));this.items.forEach(((e,t)=>{i.has(t)||(e.element?.remove(),this.items.delete(t))})),s.forEach(((e,t)=>{let s=this.items.get(e.id);s||(s=this.createOperationElement(e)),s?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(s.element))}))}createOperationElement(e){const t=window.jvbTemplates.create("queueItem",e),s={element:t,ui:window.uiFromSelectors(this.selectors.item,t)};return this.items.set(e.id,s),s}updateOperationUI(e){let t=this.items.has(e)?this.items.get(e):this.createOperationElement(e);if(!t)return;let s=this.getQueue(e),i=t.element;i.classList.remove(this.statuses),i.classList.add(s.status);let n=this.getProgress(s);t.ui.type&&t.ui.type.textContent!==s.title&&(t.ui.type.textContent=s.title),t.ui.status&&(t.ui.status.title=this.statusLabel(s.status)),t.ui.icon&&(t.ui.icon.className=`icon icon-${this.icons[s.status]}`),t.ui.details&&(t.ui.details.textContent=this.itemMessage(s)),t.ui.startedAt&&(t.ui.startedAt.setAttribute("datetime",s.created_at),t.ui.startedAt.textContent=window.formatTimeAgo(s.created_at));s.status;const a="completed"===s.status&&(s.completed_at||s.updated_at);if(t.ui.completed.wrap.hidden=!a,a){const e=s.completed_at??s.updated_at;t.ui.completed.label.textContent="Completed: ",t.ui.completed.time.setAttribute("datetime",e),t.ui.completed.time.textContent=window.formatTimeAgo(e)}window.showProgress(t.ui.progress,n,100,this.statusLabel(s.status)),t.ui.actions.cancel&&(t.ui.actions.cancel.hidden=this.completedStatuses.includes(s.status)),t.ui.actions.retry&&(s.retries>=3&&(t.ui.actions.retry.disabled=!0),t.ui.actions.retry.hidden="failed"!==s.status),t.ui.actions.dismiss&&(t.ui.actions.dismiss.hidden=this.pendingStatuses.includes(s.status))}getProgress(e){if(e.progress)return e.progress;if(!this.statuses.includes(e.status))return 0;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]??0}removeOperationUI(e){let t=this.items.get(e);t&&window.fade(t.element,!1)}updatePanel(e="syncing"){this.panelStatuses.includes(e)&&(this.ui.panel.classList.remove(...this.panelStatuses),this.ui.panel.classList.add(e))}statusLabel(e){if(!this.statuses.includes(e))return"";return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed",failed_permanent:"Failed permanently"}[e]}itemMessage(e){if(Object.hasOwn(e,"message")&&""!==e.message)return e.message;if(Object.hasOwn(e,"error_message")&&e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}toggleQueue(e=!0){this.ui.panel.hidden=!e,this.ui.toggle.button.hidden=!e}setProcessing(e=!0){this.isProcessing=e,this.ui.toggle.button.classList.toggle("saving",e)}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.isPolling&&this.stopPolling(),this.stopActivityTracking(),document.removeEventListener("click",this.clickHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/quill.min.js b/assets/js/min/quill.min.js
index 2c1bcb9..d030666 100644
--- a/assets/js/min/quill.min.js
+++ b/assets/js/min/quill.min.js
@@ -1 +1 @@
-window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const l=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${window.getIcon("image")}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${l}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const l=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const l=e.querySelector(".remove");l&&l.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),l=new FormData;l.append("image",e),objectID&&l.append("post_id",objectID);try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:l});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});l.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),l.on("text-change",(()=>{t.value=l.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
\ No newline at end of file
+window.jvbQuill=function(t){const n=t.querySelectorAll("textarea[data-editor=true]"),e=[];return n.forEach((t=>{let n,i,l;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),i=n.querySelector(".editor"),l=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",i=document.createElement("div"),i.className="editor",l=document.createElement("div"),l.className="toolbar";const e=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${window.getIcon("image")}\n                </button>`:"";l.id=`toolbar-${t.id}`,l.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${e}\n                </span>\n            `,n.appendChild(l),n.appendChild(i),t.parentNode.insertBefore(n,t),t.style.display="none",i.innerHTML=t.value}const o=new Quill(i,{theme:"snow",modules:{toolbar:{container:l,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const l=e.querySelector(".remove");l&&l.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),l=new FormData;l.append("image",e),objectID&&l.append("post_id",objectID);try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:l});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});e.push(o),o.on("selection-change",(function(t){const n=l.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))})),e};
\ No newline at end of file
diff --git a/assets/js/min/schema.min.js b/assets/js/min/schema.min.js
index af3dd28..449035b 100644
--- a/assets/js/min/schema.min.js
+++ b/assets/js/min/schema.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.formController=null,this.tabsInstance=null,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.init()}init(){if(window.jvbForm&&!window.formController?(this.formController=new window.jvbForm,window.formController=this.formController):window.formController&&(this.formController=window.formController),window.jvbTabs){const e=document.querySelector(".jvb-seo-admin");e&&(this.tabsInstance=new window.jvbTabs(e))}this.formController&&this.formController.subscribe(((e,t)=>{"form-submit"===e&&this.handleFormSubmit(t)})),this.queue&&this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"seo"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initializeForms(),this.addPreservedFieldStyles()}initializeForms(){document.querySelectorAll('form[data-save="seo"]').forEach((e=>{this.formController&&this.formController.registerForm(e,{endpoint:"seo",autosave:!1,formStatus:!1}),this.initializeTypeSwitch(e);const t=e.querySelector('[data-action="reset"]');t&&t.addEventListener("click",(()=>this.handleReset(e)))}))}handleFormSubmit(e){const t=e.config.element.dataset.content,n=e.fullData,o={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"save",...n},popup:"Saving SEO configuration",title:`Saving ${t} settings`};this.queue.addToQueue(o)}async handleReset(e){const t=e.dataset.content;if(!confirm("Reset to default settings? This cannot be undone."))return;const n={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"reset"},popup:"Resetting configuration",title:`Resetting ${t} to defaults`};this.queue.addToQueue(n)}handleQueueSuccess(e,t){console.log("SEO save successful:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Configuration saved successfully"),"reset"===t.operation?.data?.action&&t.response?.schema&&this.reloadFormData(t.operation.data.context,t.response)}handleQueueFailure(e,t){console.error("SEO operation failed permanently:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(`Error: ${t.error_message||"Operation failed"}`)}reloadFormData(e,t){const n=document.querySelector(`form[data-content="${e}"]`);if(!n)return;const o=t.schema||{};Object.keys(o).forEach((e=>{const t=n.querySelector(`[name="${e}"]`);t&&("checkbox"===t.type?t.checked=!!o[e]:t.value=o[e]||"")})),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Form reset to defaults")}initializeTypeSwitch(e){const t=e.querySelector('select[name="type"]');t&&(t.addEventListener("change",(n=>{const o=e.dataset.currentType||t.dataset.initialValue,a=n.target.value;o!==a&&this.confirmTypeChange(e,t,o,a)})),t.dataset.initialValue=t.value,e.dataset.currentType=t.value)}confirmTypeChange(e,t,n,o){const a={},s=new FormData(e);for(let[e,t]of s.entries())"type"!==e&&t&&""!==t&&(a[e]=t);const r=window.getTemplate(`seo-${o}`);if(!r)return console.error("No template found for type:",o),void(t.value=n);const i=e=>e.split(":")[0],l=new Set(Object.keys(a).map(i)),c=r.querySelectorAll("[data-field]"),u=new Set(Array.from(c).map((e=>e.dataset.field)));if(0===u.size){const e=r.querySelectorAll("[name]");Array.from(e).forEach((e=>{u.add(i(e.getAttribute("name")))}))}const d=[...l].filter((e=>u.has(e))),h=[...l].filter((e=>!u.has(e)));let p=`Change schema type from ${n} to ${o}?\n\n`;d.length>0&&(p+=`✓ ${d.length} field value(s) will be preserved:\n`,p+=d.map((e=>`  • ${e}`)).join("\n"),p+="\n\n"),h.length>0&&(p+=`⚠ ${h.length} field value(s) will be lost:\n`,p+=h.map((e=>`  • ${e}`)).join("\n")),confirm(p)?this.handleTypeChange(e,t,o):(t.value=n,this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Type change cancelled"))}handleTypeChange(e,t,n){const o=e.dataset.currentType||t.dataset.initialValue,a=this.collectFormData(e),s=window.getTemplate(`seo-${n}`);if(!s)return void console.error("No template found for type:",n);const r=e.querySelector(".seo-"+o);if(r&&(r.parentNode.insertBefore(s,r),r.remove()),e.dataset.currentType=n,window.jvbPopulateForm){const t=new window.jvbPopulateForm,o=[];if(Object.keys(a).forEach((n=>{const s=e.querySelector(`[data-field="${n}"]`);if(s){const e=this.getFieldType(s),r=a[n];if("repeater"===e&&Array.isArray(r))t.populateRepeaterField(s,n,r),o.push(n);else if(null!=r&&""!==r){const e=s.querySelector(`[name="${n}"]`)||s.querySelector(`[name^="${n}"]`);e&&(this.populateSimpleField(e,r),o.push(n))}}})),o.length>0){const e=`Schema type changed to ${n}. Preserved ${o.length} field value(s).`;console.log(e),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(e)}else{const e=`Schema type changed to ${n}.`;this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(e)}}}collectFormData(e){const t={},n=new FormData(e);for(let[e,o]of n.entries())if("type"!==e&&"context"!==e)if(e.includes(":")){const n=e.split(":"),a=n[0],s=parseInt(n[1]),r=n[2];t[a]||(t[a]=[]),t[a][s]||(t[a][s]={}),t[a][s][r]=o}else t[e]=o;return t}getFieldType(e){return e.classList.contains("repeater")?"repeater":"text"}populateSimpleField(e,t){"checkbox"===e.type?e.checked="1"===t||"true"===t||!0===t:"SELECT"===e.tagName?setTimeout((()=>{e.value=t}),10):e.value=t,e.classList.add("value-preserved"),setTimeout((()=>e.classList.remove("value-preserved")),2e3)}addPreservedFieldStyles(){const e=document.createElement("style");e.textContent="\n            .value-preserved {\n                background-color: #e7f5e7 !important;\n                transition: background-color 0.3s ease;\n            }\n        ",document.head.appendChild(e)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSchema=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.formController=null,this.tabsInstance=null,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.init()}init(){if(window.jvbForm&&!window.formController?(this.formController=new window.jvbForm,window.formController=this.formController):window.formController&&(this.formController=window.formController),window.jvbTabs){const e=document.querySelector(".jvb-seo-admin");e&&(this.tabsInstance=new window.jvbTabs(e))}this.formController&&this.formController.subscribe(((e,t)=>{"form-submit"===e&&this.handleFormSubmit(t)})),this.queue&&this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"seo"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initializeForms(),this.addPreservedFieldStyles()}initializeForms(){document.querySelectorAll('form[data-save="seo"]').forEach((e=>{this.formController&&this.formController.registerForm(e,{endpoint:"seo",autosave:!1,formStatus:!1}),this.initializeTypeSwitch(e);const t=e.querySelector('[data-action="reset"]');t&&t.addEventListener("click",(()=>this.handleReset(e)))}))}handleFormSubmit(e){const t=e.config.element.dataset.content,n=e.fullData,o={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"save",...n},popup:"Saving SEO configuration",title:`Saving ${t} settings`};this.queue.addToQueue(o)}async handleReset(e){const t=e.dataset.content;if(!confirm("Reset to default settings? This cannot be undone."))return;const n={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"reset"},popup:"Resetting configuration",title:`Resetting ${t} to defaults`};this.queue.addToQueue(n)}handleQueueSuccess(e,t){console.log("SEO save successful:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Configuration saved successfully"),"reset"===t.operation?.data?.action&&t.response?.schema&&this.reloadFormData(t.operation.data.context,t.response)}handleQueueFailure(e,t){console.error("SEO operation failed permanently:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(`Error: ${t.error_message||"Operation failed"}`)}reloadFormData(e,t){const n=document.querySelector(`form[data-content="${e}"]`);if(!n)return;const o=t.schema||{};Object.keys(o).forEach((e=>{const t=n.querySelector(`[name="${e}"]`);t&&("checkbox"===t.type?t.checked=!!o[e]:t.value=o[e]||"")})),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Form reset to defaults")}initializeTypeSwitch(e){const t=e.querySelector('select[name="type"]');t&&(t.addEventListener("change",(n=>{const o=e.dataset.currentType||t.dataset.initialValue,a=n.target.value;o!==a&&this.confirmTypeChange(e,t,o,a)})),t.dataset.initialValue=t.value,e.dataset.currentType=t.value)}confirmTypeChange(e,t,n,o){const a={},i=new FormData(e);for(let[e,t]of i.entries())"type"!==e&&t&&""!==t&&(a[e]=t);const r=window.getTemplate(`seo-${o}`);if(!r)return console.error("No template found for type:",o),void(t.value=n);const s=e=>e.split(":")[0],l=new Set(Object.keys(a).map(s)),c=r.querySelectorAll("[data-field]"),u=new Set(Array.from(c).map((e=>e.dataset.field)));if(0===u.size){const e=r.querySelectorAll("[name]");Array.from(e).forEach((e=>{u.add(s(e.getAttribute("name")))}))}const d=[...l].filter((e=>u.has(e))),h=[...l].filter((e=>!u.has(e)));let f=`Change schema type from ${n} to ${o}?\n\n`;d.length>0&&(f+=`✓ ${d.length} field value(s) will be preserved:\n`,f+=d.map((e=>`  • ${e}`)).join("\n"),f+="\n\n"),h.length>0&&(f+=`⚠ ${h.length} field value(s) will be lost:\n`,f+=h.map((e=>`  • ${e}`)).join("\n")),confirm(f)?this.handleTypeChange(e,t,o):(t.value=n,this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Type change cancelled"))}handleTypeChange(e,t,n){const o=e.dataset.currentType||t.dataset.initialValue,a=this.collectFormData(e),i=window.getTemplate(`seo-${n}`);if(!i)return void console.error("No template found for type:",n);const r=e.querySelector(".seo-"+o);if(r&&(r.parentNode.insertBefore(i,r),r.remove()),e.dataset.currentType=n,window.jvbPopulate){this.populate=window.jvbPopulate.populate,Object.keys(a).forEach((t=>{const n=e.querySelector(`[data-field="${t}"]`);if(n){const e=a[t];this.populate(n,t,e)}}));const t=`Schema type changed to ${n}.`;this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(t)}}collectFormData(e){const t={},n=new FormData(e);for(let[e,o]of n.entries())if("type"!==e&&"context"!==e)if(e.includes(":")){const n=e.split(":"),a=n[0],i=parseInt(n[1]),r=n[2];t[a]||(t[a]=[]),t[a][i]||(t[a][i]={}),t[a][i][r]=o}else t[e]=o;return t}getFieldType(e){return e.classList.contains("repeater")?"repeater":"text"}addPreservedFieldStyles(){const e=document.createElement("style");e.textContent="\n            .value-preserved {\n                background-color: #e7f5e7 !important;\n                transition: background-color 0.3s ease;\n            }\n        ",document.head.appendChild(e)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSchema=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index a36e129..4d9f4bd 100644
--- a/assets/js/min/selector.min.js
+++ b/assets/js/min/selector.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}initElements(){this.selectors={search:{input:'[type="search"]',clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:'button.taxonomy-toggle, [data-filter="taxonomy"]',value:'input[type="hidden"]',selected:".selected-items",dropdown:{list:".search-results",wrapper:".auto-wrapper"},create:{button:".auto-wrapper .submit-term",span:".auto-wrapper button span"},search:"input[data-autocomplete]",message:{message:"p.message",text:"p.message span"}}},this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.nextPage()}))}),{root:this.ui.terms.sentinel,threshold:.5}),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("input",this.inputHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0)}handleClick(e){const t=this.getFieldId(e.target)||this.activeField,s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,".item.autocomplete");if(i){let e=parseInt(i.dataset.id);this.addSelected(e,t),this.scheduleHideDropdown(t),s.ui.search&&(s.ui.search.value="")}if(window.targetCheck(e,this.selectors.field.toggle))return e.preventDefault(),void this.openModal(t);const r=window.targetCheck(e,".remove-term");if(r){const e=r.closest("[data-id]").dataset.id??!1;return void(t&&e&&this.removeSelected(parseInt(e),t))}if(e.target.matches(".modal-close"))return this.updateFieldValue(t),void this.modal?.handleClose();if(window.targetCheck(e,this.selectors.nav.back))return void this.navigateToParent();if(window.targetCheck(e,this.selectors.nav.child)){const t=e.target.closest("li"),s=parseInt(t.dataset.id);return void(s&&this.navigateTo(s))}const a=window.targetCheck(e,this.selectors.nav.pathLevel);if(a){const e=parseInt(a.dataset.id)??0;this.navigateTo(e)}window.targetCheck(e,this.selectors.field.dropdown)&&this.scheduleHideDropdown(t);if(window.targetCheck(e,this.selectors.search.clear)){const e=this.currentField();e&&e.ui.search&&(e.ui.search.value="",this.store.setFilters({search:"",page:1,parent:this.store.filters.parent||0})),this.ui.search.input&&(this.ui.search.input.value="")}if(this.creator){window.targetCheck(e,this.selectors.create.button)&&this.maybeCreateTerm(e).then((()=>{}))}}handleChange(e){if(!this.container.contains(e.target))return;if(!["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.dataset.id);let s=this.getFieldId(e.target);e.target.checked?this.addSelected(t,s):this.removeSelected(t,s)}handleInput(e){let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const s=this.fields.get(t);if(!s)return;if(["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation(),this.container.open||this.setField(t);let i=e.target.value.trim();this.setMessage(!0,`Searching for "${i}" in ${s.plural??"items"}`),window.debouncer.schedule(`${t}-search`,(async()=>{this.container.open&&window.removeChildren(this.ui.terms.list),await this.store.setFilters({taxonomy:s.taxonomy,search:i,page:1,parent:i?0:this.store.filters.parent||0})}),100)}setField(e){const t=this.fields.get(e);t?(this.activeField=e,this.setMessage(!0,`Loading ${t.plural}...`),this.resetFilters({taxonomy:t.taxonomy})):console.error("No field found...")}resetFilters(e){Object.hasOwn(e,"taxonomy")&&(e={page:1,search:"",parent:0,...e},this.store.setFilters(e))}handleFocus(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&(s.hasAutocomplete||s.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||this.setField(t))}handleBlur(e){const t=this.getFieldId(e.target),s=this.fields.get(t);t&&s&&s.hasAutocomplete&&!this.container.open&&(e.relatedTarget&&s.ui.dropdown.wrapper?.contains(e.relatedTarget)||this.scheduleHideDropdown(t))}scheduleHideDropdown(e){const t=this.fields.get(e);t&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.container.open||(this.activeField=null),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!0)}),1500)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,open:null}),this.modal.subscribe(((e,t)=>{if("modal-close"===e)this.closeModal()}))}toggleModal(e,t=!0){this.fields.get(e)&&(t?this.openModal(e):this.closeModal())}openModal(e){const t=this.fields.get(e);if(!t)return;this.setField(e),this.ui.modal.title.textContent=t.isFilter?`Filter by ${t.singular}`:`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.creator&&this.creator.handleOpen(t);let s=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}openEmpty(e,t,s,i){this.emptyCallback=i;const r=`empty-${e}-${Date.now()}`;this.fields.has(r)||(this.fields.set(r,{id:r,taxonomy:e,singular:t,plural:s,canSearch:!0,canCreate:!1,hasAutocomplete:!1,isFilter:!1,isEmpty:!0,limit:0,ui:{},element:null,value:null,toggle:null,checked:!0}),this.selectedTerms.set(r,new Set)),this.setField(r),this.ui.modal.title.textContent=`Add to ${s}`,this.ui.search?.container&&(this.ui.search.container.hidden=!1),window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen()}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;if(this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),e.isEmpty&&this.emptyCallback){const t=Array.from(this.selectedTerms.get(this.activeField)||[]),s=t.map((e=>this.store.get(e))).filter(Boolean);this.emptyCallback({taxonomy:e.taxonomy,termIds:t,terms:s}),this.fields.delete(this.activeField),this.selectedTerms.delete(this.activeField),this.emptyCallback=null,this.bulkAssignmentTaxonomy=null}else this.notify("selected-terms",{terms:this.selectedTerms.get(this.activeField),taxonomy:e.taxonomy});this.activeField=null;let t=`Closed ${e.singular} selector.`;this.a11y.announce(t)}navigateToParent(){const e=this.store.filters.parent;if(0===e)return;let t=this.store.get(parseInt(e));if(!t)return void this.navigateTo(0);let s=t.parent;this.navigateTo(parseInt(s))}navigateTo(e=0){e=parseInt(e)??0,this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.terms.list),this.updateBreadcrumbs(e)}nextPage(){let e=this.store.filters.page,t=Math.min(e++,this.store.lastResponse.total);this.store.setFilters({page:t})}prevPage(){let e=this.store.filters.page,t=Math.max(e-1,1);this.store.setFilters({page:t})}addTermToModal(e){const t=this.store.get(e);if(!t)return;const s=this.currentField();if(!s)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;const i=window.getTemplate("selectedTerm");i&&(i.dataset.id=e,i.dataset.taxonomy=s.taxonomy,i.querySelector(".item-name").textContent=t.path,i.querySelector("button").title=`Remove ${t.name}`,this.ui.selected.append(i))}scanExistingFields(e=document.body){e.querySelectorAll('[data-type="selector"]').forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let s=e.querySelector('input[type="hidden"]');if(!s&&!Object.hasOwn(e.dataset,"filter"))return void console.warn("TaxonomySelector: No hidden input found for field",e);"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const i=e.dataset.fieldId;let r=this.selectors.field;const a=Object.hasOwn(e.dataset,"filter")&&"taxonomy"===e.dataset.filter;let n=a?e:e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!n)return;if(t={taxonomy:n.dataset.taxonomy,single:n.dataset.single,plural:n.dataset.plural,search:Object.hasOwn(n.dataset,"search"),autocomplete:Object.hasOwn(n.dataset,"autocomplete"),creatable:Object.hasOwn(n.dataset,"creatable")},0===Object.keys(t).length)return}else Object.hasOwn(t,"toggle")&&(n=document.querySelector(t.toggle),r.toggle=t.toggle);const o={id:i,value:s,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:t.search??!1,limit:t.limit??0,hasAutocomplete:t.autocomplete??!1,canCreate:t.creatable??!1,isRequired:t.required??!1,isFilter:a,toggle:n,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(a&&!o.ui.toggle&&(o.ui.toggle=e),o.taxonomy)return o.singular&&o.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),o.singular=o.taxonomy.replace("jvb_",""),o.plural=o.singular+"s"),this.fields.set(i,o),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(o.taxonomy),this.updateFieldUI(i),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!e)return;let s=this.fields.get(e);if(!s)return;if(!t&&!s.isFilter)return;let i=new Set;t&&t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>i.add(e))),this.selectedTerms.set(e,i)}addSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;const r=this.selectedTerms.get(t);0!==s.limit&&r.size>=s.limit||(r.add(parseInt(e)),this.container.open||s.isFilter||this.updateFieldValue(t),this.addTermToDisplay(e,t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;this.selectedTerms.get(t).delete(parseInt(e));const r=!!s.ui.selected&&s.ui.selected.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=!!this.ui.selected&&this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove();let s=this.ui.terms.list.querySelector(`[type=checkbox][data-id="${e}"]`);s&&(s.checked=!1)}this.container.open||s.isFilter||this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let s=Array.from(this.selectedTerms.get(e));t.ui.value.value=s.join(",")}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||!t.isFilter||0===t.limit)return;const s=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(s)}updateFieldFromInput(e){const t=this.getFieldId(e);if(!t)return;this.fields.get(t)&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let s=this.selectedTerms.get(e)??new Set;t&&!t.isFilter&&0!==s.size&&Array.from(s).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const s=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.toggle&&(e.toggle.disabled=!s&&!e.canCreate,e.toggle.title=s?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0)}))}showModalTerms(e=!1){const t=this.currentField(),s=this.store.getFiltered();0===s.length&&((this.store.filters.page??1)&&window.removeChildren(this.ui.terms.list),this.setMessage(!0,""===this.store.filters.search?`No matching ${t.plural}.`:`No ${t.plural} found.`,!1),this.ui.terms.sentinel&&this.observer.unobserve(this.ui.terms.sentinel)),this.setCreateButton(!0),this.ui.terms.sentinel&&(this.store.lastResponse?.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel));const i=this.store.filters.parent??0;this.ui.nav.back.hidden=0===i;const r=document.createDocumentFragment();s.forEach((t=>{const s=this.createTermElement({show:e,...t});s&&r.append(s)})),s.length>0&&this.setMessage(!1),this.ui.terms.list.append(r)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem");t.dataset.id=e.id;const s=this.selectedTerms.get(this.activeField).has(e.id);let[i,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("span, .term-name")],n=this.currentField(),o=n.limit>0&&this.selectedTerms.get(this.activeField).size>=n.limit;if(i&&r&&a&&([i.dataset.id,i.id,i.name,i.value,i.disabled,i.checked,r.htmlFor,r.title,r.dataset.path,a.textContent]=[e.id,`${n.element.id}-${e.id}`,`${n.element.id}-${n.taxonomy}-select`,e.id,!s&&o,s,`${n.element.id}-${e.id}`,e.path??e.name,e.path,e.show?e.path:e.name],e.hasChildren)){const s=window.getTemplate("termChildrenToggle");s&&(s.ariaLabel=`View ${n.plural} nested under ${e.name}`,t.append(s))}return t}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e)return;const s=e.ui.dropdown.list;s&&(window.removeChildren(s),0===t.length?this.setMessage(!0,`No ${e.plural} found.`,!1):(t.forEach((e=>{const t=this.createAutocompleteTerm(e);t&&s.append(t)})),this.setMessage(!1)),this.setCreateButton(!0),e.ui.dropdown.wrapper&&(e.ui.dropdown.wrapper.hidden=!1))}createAutocompleteTerm(e){const t=window.getTemplate("autocompleteItem");if(t)return t.dataset.id=e.id,t.textContent=e.path||e.name,t}addTermToDisplay(e,t){const s=this.store.get(e),i=this.fields.get(t);if(!s||!i)return;if(i.ui.selected&&i.ui.selected.querySelector(`[data-id="${e}"]`))return;const r=window.getTemplate("selectedTerm");if(r&&(r.dataset.id=e,r.dataset.taxonomy=i.taxonomy,r.querySelector(".item-name").textContent=s.path,r.querySelector("button").title=`Remove ${s.name}`,i.ui.selected&&i.ui.selected.append(r),this.container.open)){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const s=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(s){let e=s.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const s=this.store.get(e);if(!s)return;const i=window.getTemplate("termBreadcrumb");if(!i)return;i.dataset.id=e,i.textContent=s.name,i.title=s.name,t.append(i)}}updateSelectionCount(){if(!this.container.open)return;const e=this.fields.get(this.activeField);if(e&&this.ui.modal.count){const t=this.selectedTerms.get(this.activeField).size;this.ui.modal.count.textContent=e.limit>0?`${t} of ${e.limit} ${e.plural} selected`:`${t} ${e.plural} selected`}}currentField(){return this.fields.get(this.activeField)??!1}currentTerms(){return this.store.getFiltered()}needsCreator(){return Array.from(this.fields.values()).some((e=>e.canCreate||e.hasAutocomplete))}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?.dataset.fieldId||null}setCheckboxes(e){this.ui.terms.list.querySelectorAll("input[type=checkbox]").forEach((t=>{t.checked||(t.disabled=e)}))}handleStoreEvent(e,t){const s={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(t),"fetch-error":()=>this.handleFetchError()};try{s[e]?.(t)}catch(t){console.error(`Error handling store event "${e}":`,t),this.setMessage(!0,"An error occurred loading data",!1)}}handleDataLoaded(){const e=this.store.filters.taxonomy;if(e){e.split(",").map((e=>e.trim())).forEach((e=>this.updateFieldsForTaxonomy(e)))}this.container.open?this.showResults():this.activeField?this.showResults(!0):this.setMessage(!1)}showResults(e=!1){this.setMessage(!1);const t=this.store.getFiltered(),s=this.store.filters,i=s.search&&s.search.length>0;this.notify("terms-loaded",{terms:t,filters:s}),e?this.showAutocompleteTerms():this.showModalTerms(i),this.a11y.announce(t.length)}handleFiltersChanged(e){}handleFetchError(e){const t=this.currentField(),s=t?`Failed to load ${t.plural}`:"Failed to load data";this.setMessage(!0,s,!1),console.error("Store fetch error:",e)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);this.batchFetch.clear();try{await this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}catch(e){console.error("Failed to batch fetch taxonomies:",e)}}preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}setCreateButton(e=!0){const t=this.currentField();if(!t||!t.canCreate||!this.creator)return;const s=this.container.open?this.ui:t.ui;if(!s.create?.button||!s.create?.span)return;const i=s.create.button,r=s.create.span,a=this.container.open?s.search.input:s.search;if(!a)return;let n=(this.currentTerms()??[]).map((e=>e.name)),o=a.value;const l=e&&o.length>=2&&!n.includes(o);i.hidden=!l,l&&(r.textContent=a.value??"")}async maybeCreateTerm(e){const t=this.currentField();if(!t)return;window.debouncer.cancel(`${t.id}-search-results`);let s={taxonomy:t.taxonomy,parent:this.store.filters.parent??0};if(this.container.open&&""===this.ui.search.input.value?(s.parent=this.creator.ui.parent.value??s.parent,s.name=this.creator.ui.name.value??!1):s.name=this.container.open?this.ui.search.input.value:t.ui.search.value,void 0!==s.parent&&s.name){this.setMessage(!0,`Creating "${s.name}"...`),this.setCreateButton(!1),this.container.open?window.removeChildren(this.ui.terms.list):(t.ui.search.disabled=!0,window.removeChildren(t.ui.dropdown.list),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!1));let e=await this.creator.handleTermCreation(s);e&&this.addSelected(e.id,t.id),this.container.open||(t.ui.search.disabled=!1,t.ui.search.value=""),this.scheduleHideDropdown(t.id),this.setMessage(!1)}}setMessage(e=!0,t="",s=!0){const i=this.currentField();if(!i)return;const r=this.container.open||i.isFilter?this.ui:i.isFilter?null:i.ui;if(!r?.message?.message)return;t=""===t?`No ${i.plural??"items"} found.`:t;const a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}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("Subscriber error:",e)}}))}destroy(){this.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("dialog#jvb-selector"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.fields=new Map,this.selectedTerms=new Map,this.batchFetch=new Set,this.activeField=null,this.isInitializing=!0,this.lazyInit=!1,this.messageText={},this.init())}init(){this.initStore(),this.initElements(),this.defineTemplates(),this.initModal(),this.scanExistingFields(),this.initListeners(),this.needsCreator()&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.isInitializing=!1,this.batchFetchTaxonomies().then((()=>{}))}initStore(){const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.store.subscribe(this.handleStoreEvent.bind(this))}defineTemplates(){const e=window.jvbTemplates,t=this;e.define("emptyState"),e.define("selectedTerm",{refs:{name:".item-name",btn:"button"},setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id,e.dataset.taxonomy=i.taxonomy,t.name&&(t.name.textContent=i.path),t.button&&(t.button.title=`Remove ${i.name}`)}}),e.define("termListItem",{refs:{checkbox:"input",label:"label",name:"span, .term-name"},setup({el:e,refs:s,manyRefs:i,data:r}){e.dataset.id=r.id;let a=t.currentField(),n=t.selectedTerms.get(t.activeField).has(r.id),o=a.limit>0&&t.selectedTerms.get(t.activeField).size>=a.limit;if(s.checkbox&&(s.checkbox.dataset.id=r.id,s.checkbox.id=`${a.element.id}-${r.id}`,s.checkbox.name=`${a.element.id}-${a.taxonomy}-select`,s.checkbox.value=r.id,s.checkbox.disabled=!n&&o,s.checkbox.checked=n),s.label&&(s.label.htmlFor=`${a.element.id}-${r.id}`,s.label.title=r.path??r.name,s.label.dataset.path=r.path),s.name&&(s.name.textContent=r.show?r.path:r.name),r.hasChildren){let t={plural:a.plural,name:r.name};const s=window.jvbTemplates.create("termChildrenToggle",t);e.append(s)}}}),e.define("termChildrenToggle",{setup({el:e,refs:t,manyRefs:s,data:i}){e.ariaLabel=`View ${i.plural} nested under ${i.name}`}}),e.define("termBreadcrumb",{setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id,e.textContent=i.name,e.title=i.name}}),e.define("autocompleteItem",{setup({el:e,refs:t,manyRefs:s,data:i}){e.dataset.id=i.id,e.textContent=i.path||i.name,e.title=`Select ${i.name}`}})}initElements(){this.selectors={search:{input:'[type="search"]',clear:".clear-search",container:".search-wrapper",results:".search-results"},create:{button:"button.submit-term",span:".submit-term span"},terms:{list:".items-container",wrap:".items-wrap",sentinel:".scroll-sentinel"},nav:{nav:"nav.term-navigation",back:".back-to-parent",child:".toggle-children",pathLevel:".path-level"},message:{message:"p.message",text:"p.message span"},selected:".selected-items",modal:{title:"#modal-title",content:".modal-content",count:".selection-count"},favourites:".favourite-terms",field:{toggle:'button.taxonomy-toggle, [data-filter="taxonomy"]',value:'input[type="hidden"]',selected:".selected-items",dropdown:{list:".search-results",wrapper:".auto-wrapper"},create:{button:".auto-wrapper .submit-term",span:".auto-wrapper button span"},search:"input[data-autocomplete]",message:{message:"p.message",text:"p.message span"}}},this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.nextPage()}))}),{root:this.ui.terms.sentinel,threshold:.5}),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("input",this.inputHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0)}handleClick(e){if(!this.container.contains(e.target)&&!e.target.closest('[data-type="selector"], [data-field-type="selector"]'))return;const t=this.getFieldId(e.target)||this.activeField,s=this.fields.get(t);if(!t||!s)return;const i=window.targetCheck(e,".item.autocomplete");if(i){let e=parseInt(i.dataset.id);return this.addSelected(e,t),this.scheduleHideDropdown(t,6e3),void(s.ui.search&&(s.ui.search.value=""))}if(window.targetCheck(e,this.selectors.field.toggle))return e.preventDefault(),void this.openModal(t);const r=window.targetCheck(e,".remove-term");if(r){const e=r.closest("[data-id]").dataset.id??!1;return void(t&&e&&this.removeSelected(parseInt(e),t))}if(e.target.matches(".modal-close"))return this.updateFieldValue(t),void this.modal?.handleClose();if(window.targetCheck(e,this.selectors.nav.back))return void this.navigateToParent();if(window.targetCheck(e,this.selectors.nav.child)){const t=e.target.closest("li"),s=parseInt(t.dataset.id);return void(s&&this.navigateTo(s))}const a=window.targetCheck(e,this.selectors.nav.pathLevel);if(a){const e=parseInt(a.dataset.id)??0;return void this.navigateTo(e)}if(window.targetCheck(e,this.selectors.field.dropdown))return void this.scheduleHideDropdown(t);if(window.targetCheck(e,this.selectors.search.clear)){const e=this.currentField();e&&e.ui.search&&(e.ui.search.value="",this.store.setFilters({search:"",page:1,parent:this.store.filters.parent||0})),this.ui.search.input&&(this.ui.search.input.value="")}if(this.creator){window.targetCheck(e,this.selectors.create.button)&&this.maybeCreateTerm(e).then((()=>{}))}}handleChange(e){if(!this.container.contains(e.target)&&!e.target.closest('[data-type="selector"], [data-field-type="selector"]'))return;if(!["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.dataset.id);let s=this.getFieldId(e.target);e.target.checked?this.addSelected(t,s):this.removeSelected(t,s)}handleInput(e){if(!this.container.contains(e.target)&&!e.target.closest('[data-type="selector"], [data-field-type="selector"]'))return;let t=this.getFieldId(e.target)??this.activeField;if(!t)return;const s=this.fields.get(t);if(!s)return;if(["checkbox","button"].includes(e.target.type))return;e.preventDefault(),e.stopPropagation(),this.container.open||this.setField(t);let i=e.target.value.trim();this.setMessage(!0,`Searching for "${i}" in ${s.plural??"items"}`),window.debouncer.schedule(`${t}-search`,(async()=>{this.container.open&&window.removeChildren(this.ui.terms.list),await this.store.setFilters({taxonomy:s.taxonomy,search:i,page:1,parent:i?0:this.store.filters.parent||0})}),100)}setField(e){const t=this.fields.get(e);t?(this.activeField=e,this.setMessage(!0,`Loading ${t.plural}...`),this.resetFilters({taxonomy:t.taxonomy})):console.error("No field found...")}resetFilters(e){Object.hasOwn(e,"taxonomy")&&(e={page:1,search:"",parent:0,...e},this.store.setFilters(e))}handleFocus(e){if(!this.container.contains(e.target)&&!e.target.closest('[data-type="selector"], [data-field-type="selector"]'))return;const t=this.getFieldId(e.target);if(!t)return;const s=this.fields.get(t);s&&(s.hasAutocomplete||s.hasSearch)&&(window.debouncer.cancel(`${t}-search-results`),this.container.open||this.setField(t))}handleBlur(e){if(!this.container.contains(e.target)&&!e.target.closest('[data-type="selector"], [data-field-type="selector"]'))return;const t=this.getFieldId(e.target);if(!t)return;const s=this.fields.get(t);s&&s.hasAutocomplete&&!this.container.open&&(e.relatedTarget&&s.ui.dropdown.wrapper?.contains(e.relatedTarget)||this.scheduleHideDropdown(t))}scheduleHideDropdown(e,t=1500){const s=this.fields.get(e);s&&window.debouncer.schedule(`${e}-search-results`,(()=>{this.container.open||(this.activeField=null),s.ui.dropdown.wrapper&&(s.ui.dropdown.wrapper.hidden=!0)}),t)}initModal(){this.modalID="dialog#jvb-selector",this.container=document.querySelector(this.modalID),this.modal=new window.jvbModal(this.container,{handleForm:!1,open:null}),this.modal.subscribe(((e,t)=>{if("modal-close"===e)this.closeModal()}))}toggleModal(e,t=!0){this.fields.get(e)&&(t?this.openModal(e):this.closeModal())}openModal(e){const t=this.fields.get(e);if(!t)return;this.setField(e),this.ui.modal.title.textContent=t.isFilter?`Filter by ${t.singular}`:`Select ${t.plural}`,this.ui.search.container&&(this.ui.search.container.hidden=!t.canSearch),this.creator&&this.creator.handleOpen(t);let s=`Opened ${t.singular} selection. Choose from checkboxes, or search to filter results.`;window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen(),this.a11y.announce(s)}openEmpty(e,t,s,i){this.emptyCallback=i;const r=`empty-${e}-${Date.now()}`;this.fields.has(r)||(this.fields.set(r,{id:r,taxonomy:e,singular:t,plural:s,canSearch:!0,canCreate:!1,hasAutocomplete:!1,isFilter:!1,isEmpty:!0,limit:0,ui:{},element:null,value:null,toggle:null,checked:!0}),this.selectedTerms.set(r,new Set)),this.setField(r),this.ui.modal.title.textContent=`Add to ${s}`,this.ui.search?.container&&(this.ui.search.container.hidden=!1),window.removeChildren(this.ui.selected),window.removeChildren(this.ui.terms.list),this.modal.handleOpen()}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;if(this.updateFieldValue(this.activeField),this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),e.isEmpty&&this.emptyCallback){const t=Array.from(this.selectedTerms.get(this.activeField)||[]),s=t.map((e=>this.store.get(e))).filter(Boolean);this.emptyCallback({taxonomy:e.taxonomy,termIds:t,terms:s}),this.fields.delete(this.activeField),this.selectedTerms.delete(this.activeField),this.emptyCallback=null,this.bulkAssignmentTaxonomy=null}else this.notify("selected-terms",{terms:this.selectedTerms.get(this.activeField),taxonomy:e.taxonomy});this.activeField=null;let t=`Closed ${e.singular} selector.`;this.a11y.announce(t)}navigateToParent(){const e=this.store.filters.parent;if(0===e)return;let t=this.store.get(parseInt(e));if(!t)return void this.navigateTo(0);let s=t.parent;this.navigateTo(parseInt(s))}navigateTo(e=0){e=parseInt(e)??0,this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.terms.list),this.updateBreadcrumbs(e)}nextPage(){let e=this.store.filters.page,t=Math.min(e++,this.store.lastResponse.total);this.store.setFilters({page:t})}prevPage(){let e=this.store.filters.page,t=Math.max(e-1,1);this.store.setFilters({page:t})}addTermToModal(e){const t=this.store.get(e);if(!t)return;this.currentField()&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||this.ui.selected.append(this.getSelectedTermUI(t)))}getSelectedTermUI(e,t=!0){return window.jvbTemplates.create("selectedTerm",e)}scanExistingFields(e=document.body){e.querySelectorAll('[data-type="selector"], [data-field-type="selector"]').forEach((e=>{try{e.dataset.lazy?this.lazyInit=!0:this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}})),this.lazyInit&&this.initObserver(e)}unregisterFields(e){e.querySelectorAll('[data-type="selector"],[data-field-type="selector"]').forEach((e=>{this.fields.delete(e.dataset.fieldId)}))}initObserver(e){this.lazyObserver=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&e.target.dataset.lazy&&(delete e.target.dataset.lazy,this.registerField(e.target),this.lazyObserver.unobserve(e.target))}))}),{rootMargin:"50px"}),e.querySelectorAll('[data-type="selector"][data-lazy], [data-field-type="selector"][data-lazy]').forEach((e=>{this.lazyObserver.observe(e)}))}registerField(e,t={}){if(e.dataset.fieldId&&this.fields.has(e.dataset.fieldId))return e.dataset.fieldId;let s=e.querySelector('input[type="hidden"]');if(!s&&!Object.hasOwn(e.dataset,"filter"))return;"fieldId"in e.dataset||(e.dataset.fieldId=window.generateID("selector"));const i=e.dataset.fieldId;let r=this.selectors.field;const a=Object.hasOwn(e.dataset,"filter")&&"taxonomy"===e.dataset.filter;let n=a?e:e.querySelector("button.taxonomy-toggle");if(0===Object.keys(t).length){if(!n)return;t={taxonomy:n.dataset.taxonomy,single:n.dataset.single,plural:n.dataset.plural,search:Object.hasOwn(n.dataset,"search"),autocomplete:Object.hasOwn(n.dataset,"autocomplete"),creatable:Object.hasOwn(n.dataset,"creatable")}}else Object.hasOwn(t,"toggle")&&(n=document.querySelector(t.toggle),r.toggle=t.toggle);const o={id:i,value:s,element:e,taxonomy:t.taxonomy??!1,singular:t.single??"",plural:t.plural??"",name:e.dataset.field,canSearch:t.search??!1,limit:t.limit??0,hasAutocomplete:t.autocomplete??!1,canCreate:t.creatable??!1,isRequired:t.required??!1,isFilter:a,toggle:n,create:{button:null,span:null},selectors:r,ui:window.uiFromSelectors(r,e),checked:!1};if(a&&!o.ui.toggle&&(o.ui.toggle=e),o.taxonomy)return o.singular&&o.plural||(console.warn("TaxonomySelector: Field missing singular/plural labels",e),o.singular=o.taxonomy.replace("jvb_",""),o.plural=o.singular+"s"),this.fields.set(i,o),this.setSelectedFromValue(i,s),this.isInitializing&&this.batchFetch.add(o.taxonomy),null!==e.offsetParent?this.updateFieldUI(i):requestIdleCallback((()=>{null!==e.offsetParent&&this.updateFieldUI(i)}),{timeout:2e3}),i;console.error("TaxonomySelector: Field missing taxonomy",e)}setSelectedFromValue(e,t){if(!e)return;let s=this.fields.get(e);if(!s)return;if(!t&&!s.isFilter)return;let i=new Set;t&&t.value.trim().split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>i.add(e))),this.selectedTerms.set(e,i)}addSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;const r=this.selectedTerms.get(t);0!==s.limit&&r.size>=s.limit||(r.add(parseInt(e)),this.container.open||s.isFilter||this.updateFieldValue(t),this.addTermToDisplay(e,t),this.checkLimits(t))}removeSelected(e,t=null){t||(t=this.activeField);const s=this.fields.get(t),i=this.store.get(e);if(!s||!i)return;this.selectedTerms.get(t).delete(parseInt(e));const r=!!s.ui.selected&&s.ui.selected.querySelector(`[data-id="${e}"]`);if(r&&r.remove(),this.container.open){let t=!!this.ui.selected&&this.ui.selected.querySelector(`[data-id="${e}"]`);t&&t.remove();let s=this.ui.terms.list.querySelector(`[type=checkbox][data-id="${e}"]`);s&&(s.checked=!1)}this.container.open||s.isFilter||this.updateFieldValue(t),this.checkLimits(t)}updateFieldValue(e){const t=this.fields.get(e);if(!t)return;let s=Array.from(this.selectedTerms.get(e));t.ui.value.value=s.join(",")??"",t.ui.value.dispatchEvent(new Event("change",{bubbles:!0}))}checkLimits(e){if(!this.container.open)return;const t=this.fields.get(e);if(!t||!t.isFilter||0===t.limit)return;const s=this.selectedTerms.get(e).size>=t.limit;this.setCheckboxes(s)}updateFieldFromInput(e){const t=this.getFieldId(e);if(!t)return;this.fields.get(t)&&(this.setSelectedFromValue(t,e),this.updateFieldUI(t))}updateFieldUI(e){const t=this.fields.get(e);let s=this.selectedTerms.get(e)??new Set;t&&!t.isFilter&&0!==s.size&&Array.from(s).forEach((t=>{this.addTermToDisplay(t,e)}))}updateFieldsForTaxonomy(e){let t=Array.from(this.fields.values()).filter((t=>!t.checked&&t.taxonomy===e));const s=Array.from(this.store.data.values()).some((t=>t.taxonomy===e));t.forEach((e=>{e.toggle&&(e.toggle.disabled=!s&&!e.canCreate,e.toggle.title=s?`Select ${e.plural}`:`No ${e.singular} available`,e.checked=!0)}))}showModalTerms(e=!1){const t=this.currentField(),s=this.store.getFiltered();if(0===s.length)return(this.store.filters.page??1)&&window.removeChildren(this.ui.terms.list),this.setMessage(!0,""===this.store.filters.search?`No matching ${t.plural}.`:`No ${t.plural} found.`,!1),void(this.ui.terms.sentinel&&this.observer.unobserve(this.ui.terms.sentinel));this.setCreateButton(!0),this.ui.terms.sentinel&&(this.store.lastResponse?.has_more?this.observer.observe(this.ui.terms.sentinel):this.observer.unobserve(this.ui.terms.sentinel));const i=this.store.filters.parent??0;this.ui.nav.back.hidden=0===i,window.chunkIt(s,(t=>this.createTermElement({show:e,...t})),(e=>this.ui.terms.list.append(e)),10).then((()=>{})),s.length>0&&this.setMessage(!1)}createTermElement(e){return e&&e.name?window.jvbTemplates.create("termListItem",e):null}showAutocompleteTerms(){const e=this.currentField(),t=this.currentTerms();if(!e)return;const s=e.ui.dropdown.list;s&&(window.removeChildren(s),0===t.length?this.setMessage(!0,`No ${e.plural} found.`,!1):(window.chunkIt(t,(e=>this.createAutocompleteTerm(e)),(e=>s.append(e))).then((()=>{})),this.setMessage(!1)),this.setCreateButton(!0),e.ui.dropdown.wrapper&&(e.ui.dropdown.wrapper.hidden=!1))}createAutocompleteTerm(e){return window.jvbTemplates.create("autocompleteItem",e)}addTermToDisplay(e,t){const s=this.store.get(e),i=this.fields.get(t);if(!s||!i)return;if(i.ui.selected&&i.ui.selected.querySelector(`[data-id="${e}"]`))return;let r=this.getSelectedTermUI(s);if(i.ui.selected&&i.ui.selected.append(r),this.container.open){this.addTermToModal(e);const t=this.ui.terms.list.querySelector(`input[value="${e}"]`);t&&(t.checked=!0)}}updateBreadcrumbs(e){const t=this.ui.nav.nav;if(!t)return;const s=Array.from(t.children).find((t=>parseInt(t.dataset.id)===e));if(s){let e=s.nextElementSibling;for(;e;){const t=e;e=e.nextElementSibling,t.remove()}}else{const s=this.store.get(e);if(!s)return;const i=window.jvbTemplates.create("termBreadcrumb",s);t.append(i)}}updateSelectionCount(){if(!this.container.open)return;const e=this.fields.get(this.activeField);if(e&&this.ui.modal.count){const t=this.selectedTerms.get(this.activeField).size;this.ui.modal.count.textContent=e.limit>0?`${t} of ${e.limit} ${e.plural} selected`:`${t} ${e.plural} selected`}}checkRendered(e,t){if(e)return Object.hasOwn(e,t.taxonomy)||(e[t.taxonomy]=new Map),e[t.taxonomy].has(t.id)}currentField(){return this.fields.get(this.activeField)??!1}currentTerms(){return this.store.getFiltered()}needsCreator(){return Array.from(this.fields.values()).some((e=>e.canCreate||e.hasAutocomplete))}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?.dataset.fieldId||null}setCheckboxes(e){this.ui.terms.list.querySelectorAll("input[type=checkbox]").forEach((t=>{t.checked||(t.disabled=e)}))}handleStoreEvent(e,t){const s={"data-loaded":()=>this.handleDataLoaded(),"filters-changed":()=>this.handleFiltersChanged(t),"fetch-error":()=>this.handleFetchError()};try{s[e]?.(t)}catch(t){console.error(`Error handling store event "${e}":`,t),this.setMessage(!0,"An error occurred loading data",!1)}}handleDataLoaded(){const e=this.store.filters.taxonomy;if(e){e.split(",").map((e=>e.trim())).forEach((e=>this.updateFieldsForTaxonomy(e)))}this.container.open?this.showResults():this.activeField?this.showResults(!0):this.setMessage(!1)}showResults(e=!1){this.setMessage(!1);const t=this.store.getFiltered(),s=this.store.filters,i=s.search&&s.search.length>0;this.notify("terms-loaded",{terms:t,filters:s}),e?this.showAutocompleteTerms():this.showModalTerms(i),this.a11y.announce(t.length)}handleFiltersChanged(e){}handleFetchError(e){const t=this.currentField(),s=t?`Failed to load ${t.plural}`:"Failed to load data";this.setMessage(!0,s,!1),console.error("Store fetch error:",e)}async batchFetchTaxonomies(){if(0===this.batchFetch.size)return;const e=Array.from(this.batchFetch);this.batchFetch.clear();try{await this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}catch(e){console.error("Failed to batch fetch taxonomies:",e)}}preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}setCreateButton(e=!0){const t=this.currentField();if(!t||!t.canCreate||!this.creator)return;const s=this.container.open?this.ui:t.ui;if(!s.create?.button||!s.create?.span)return;const i=s.create.button,r=s.create.span,a=this.container.open?s.search.input:s.search;if(!a)return;let n=(this.currentTerms()??[]).map((e=>e.name)),o=a.value;const l=e&&o.length>=2&&!n.includes(o);i.hidden=!l,l&&(r.textContent=a.value??"")}async maybeCreateTerm(e){const t=this.currentField();if(!t)return;window.debouncer.cancel(`${t.id}-search-results`);let s={taxonomy:t.taxonomy,parent:this.store.filters.parent??0};if(this.container.open&&""===this.ui.search.input.value?(s.parent=this.creator.ui.parent.value??s.parent,s.name=this.creator.ui.name.value??!1):s.name=this.container.open?this.ui.search.input.value:t.ui.search.value,void 0!==s.parent&&s.name){this.setMessage(!0,`Creating "${s.name}"...`),this.setCreateButton(!1),this.container.open?window.removeChildren(this.ui.terms.list):(t.ui.search.disabled=!0,window.removeChildren(t.ui.dropdown.list),t.ui.dropdown.wrapper&&(t.ui.dropdown.wrapper.hidden=!1));let e=await this.creator.handleTermCreation(s);e&&this.addSelected(e.id,t.id),this.container.open||(t.ui.search.disabled=!1,t.ui.search.value=""),this.scheduleHideDropdown(t.id),this.setMessage(!1)}}setMessage(e=!0,t="",s=!0){const i=this.currentField();if(!i)return;const r=this.container.open||i.isFilter?this.ui:i.isFilter?null:i.ui;if(!r?.message?.message)return;t=""===t?`No ${i.plural??"items"} found.`:t;const a=r.message.message,n=r.message.text;a.hidden=!e,e?t&&n&&(s&&window.typeLoop&&n?(this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id]),this.messageText[i.id]=window.typeLoop(n,t)):n.textContent=t):this.messageText[i.id]&&(this.messageText[i.id](),delete this.messageText[i.id])}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("Subscriber error:",e)}}))}destroy(){this.fields.forEach(((e,t)=>{window.debouncer.cancel(`${t}-search`),window.debouncer.cancel(`${t}-search-results`)})),Object.keys(this.messageText).forEach((e=>{this.messageText[e]&&this.messageText[e]()})),this.messageText={},this.ui.terms?.sentinel&&this.observer?.unobserve(this.ui.terms.sentinel),this.observer?.disconnect(),this.lazyObserver?.disconnect(),document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("input",this.inputHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear(),this.batchFetch.clear(),this.creator&&(this.creator.destroy(),this.creator=null),this.store&&(this.store=null)}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/tabs.min.js b/assets/js/min/tabs.min.js
index 4abd03c..421c197 100644
--- a/assets/js/min/tabs.min.js
+++ b/assets/js/min/tabs.min.js
@@ -1 +1 @@
-window.jvbTabs=class{constructor(t,a={},e=null){this.tabs=t.querySelector(".tabs"),this.a11y=window.jvbA11y,this.updateURL=!0,this.parent=e,this.childTabs=new Map,"updateURL"in a&&!1===a.updateURL&&(this.updateURL=!1),this.callbacks=a,this.activeTab=this.updateURL?this.getInitialTabFromHash():t.querySelector("button.tab.active")?.dataset.tab,this.container=t,this.tabs&&this.tabs.addEventListener("click",(t=>{const a=t.target.closest("[data-tab]");if(a){let t=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(a.dataset.tab,t)}})),this.initializeChildTabs(),this.selectDropdown=document.querySelector("select.tab-list"),this.selectDropdown&&this.selectDropdown.addEventListener("change",(t=>{let a=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(t.target.value,a)}));let s=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.activeTab||(this.activeTab=document.querySelector("button.tab")?.dataset.tab),this.switchTab(this.activeTab,s)}initializeChildTabs(){this.tabs.querySelectorAll("button").forEach((t=>{let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`);if(a&&a.querySelector(".tabs")){let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`),e=new window.jvbTabs(a,{updateURL:!1},this);this.childTabs.set(t.dataset.tab,e)}}))}getInitialTabFromHash(){if(!window.location.hash)return!1;const t=window.location.hash.substring(1).split("/");if(this.parent){if(this.parent&&t.length>1){const a=this.getParentDepth();if(a<t.length){const e=t[a];if(this.tabs.querySelector(`[data-tab="${e}"]`))return e}}}else{const a=t[0];if(this.tabs.querySelector(`[data-tab="${a}"]`))return a}return null}getParentDepth(){let t=0,a=this.parent;for(;a;)t++,a=a.parent;return t}getFullTabPath(t){return this.parent?`${this.parent.getFullTabPath(this.parent.activeTab)}/${t}`:t}switchTab(t,a=!1){document.activeElement?.blur(),this.tabs.querySelectorAll("[data-tab]").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-selected",a.dataset.tab===t)})),this.container.querySelectorAll(".tab-content").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-hidden",a.dataset.tab!==t),a.hidden=a.dataset.tab!==t})),this.activeTab=t,this.callbacks[t]&&this.callbacks[t]();const e=this.childTabs.get(t);if(e){const t=e.container.querySelector("button.tab")?.dataset.tab;t&&e.switchTab(t,!1)}a&&(this.parent?this.parent.updateUrlFromChild():window.history.pushState({tab:t},"",`#${t}`)),this.selectDropdown&&this.selectDropdown.querySelector(`option[value="${t}"]`)&&(this.selectDropdown.value=t),this.a11y.announce(`Switched to ${t} tab`)}updateUrlFromChild(){if(console.log("Updating URL"),!("updateURL"in this.callbacks)||this.callbacks.updateURL)if(this.parent)this.parent.updateUrlFromChild();else{const t=this.getFullTabPath(this.activeTab);window.history.pushState({tab:t},"",`#${t}`)}}};
\ No newline at end of file
+(()=>{class t{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.subscribers=new Set,this.tabs=new Map,this.hasHash=!1,this.init()}init(){this.initElements(),this.initListeners()}initElements(){this.selectors={nav:".tabs",tab:"[data-tab]",active:"button.tab.active",section:".tab-content",button:"button.tab",select:"select.tab-list"}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(t){let e=this.getConfig(t.target);if(!e)return;const s=t.target.closest(this.selectors.tab);s&&this.switchTab(s.dataset.tab,e)}handleChange(t){let e=this.getConfig(t.target);e&&e&&this.switchTab(t.target.value,e)}registerTab(t,e={}){if(!t)return!1;let s=window.uiFromSelectors(this.selectors,t);if(!s.nav||!s.section)return console.error("No tab navigation or section found"),!1;let i=window.generateID("tab");t.dataset.tabsId=i,s.buttons=Array.from(t.querySelectorAll(this.selectors.button)),s.sections=Array.from(t.querySelectorAll(this.selectors.section)),s.sections.forEach((t=>{t.querySelector(".tabs")&&(e.hasChildren=!0,this.registerTab(t,{parent:i}))})),s.select&&(s.options=Array.from(s.select.querySelectorAll("option")));let a={id:i,ui:s,updateURL:e.updateURL??!0};return s.nav.addEventListener("click",this.clickHandler),s.select?.addEventListener("change",this.changeHandler),this.tabs.set(i,a),this.determineActiveTab(a),a}determineActiveTab(t){if(this.getInitialTabFromHash()){let e=this.tabs.get(t.id);if(e.activeTab&&t.activeTab&&e.activeTab===t.activeTab)return}let e=t.ui.buttons[0].dataset.tab??!1;e&&this.switchTab(e,t)}getInitialTabFromHash(){if(this.hasHash||!window.location.search)return!1;const t=new URLSearchParams(window.location.search).get("tab");if(!t)return!1;return t.split("|").forEach((t=>{const e=Array.from(this.tabs.values()).find((e=>e.ui.buttons.some((e=>e.dataset.tab===t))));e&&this.switchTab(t,e)})),this.hasHash=!0,!0}removeTab(t){if(!t||!t.dataset.tabsId)return;let e=this.tabs.get(t.dataset.tabsId);e&&(e.ui.nav.removeEventListener("click",this.clickHandler),e.ui.select?.removeEventListener("change",this.changeHandler),this.tabs.delete(t.dataset.tabsId))}switchTab(t,e){if(!(e="string"==typeof e?this.tabs.get(e):e))return;let s=e.ui.sections.filter((t=>t.classList.contains("active")));Object.hasOwn(e,"preCheck")&&!e.preCheck(s[0],e)||(document.activeElement&&this.isInTabs(document.activeElement,e)&&document.activeElement.blur(),e.ui.buttons.forEach(((s,i)=>{s.classList.remove("active","previous","next");let a=s.dataset.tab===t;if(s.setAttribute("aria-selected",a),a){s.classList.add("active");let t=Math.max(i-1,0),a=Math.min(i+1,e.ui.buttons.length-1);t!==i&&e.ui.buttons[t]?.classList.add("previous"),a!==i&&e.ui.buttons[a]?.classList.add("next")}})),e.ui.sections.forEach(((e,s)=>{let i=e.dataset.tab===t;e.classList.toggle("active",i),e.setAttribute("aria-hidden",!i),e.hidden=!i})),this.notify("tab-switched",{previous:e.activeTab,current:t,config:e}),e.activeTab=t,this.tabs.set(e.id,e),e?.hasChildren&&this.updateChildTabs(e.id),e?.updateURL&&this.updateURL(e),e.ui.select&&this.maybeUpdateSelect(t,e),this.a11y.announce(`Switched to ${t} tab`))}updateChildTabs(t){Array.from(this.tabs.values()).filter((e=>e.parent===t)).forEach((t=>{let e=t.ui.buttons[0].dataset.tab??!1;e&&this.switchTab(e,t)}))}updateURL(t){if(!t.updateURL)return;let e=this.checkAncestorsHash(t);e&&window.history.pushState({tab:t.activeTab},"",`?tab=${e}`)}checkAncestorsHash(t){const e=[];let s=t;for(;s;)e.unshift(s.activeTab),s=s.parent?this.tabs.get(s.parent):null;return e.join("|")}maybeUpdateSelect(t,e){e.ui.select&&Object.hasOwn(e,"options")&&e.options.forEach((s=>{s.value!==t||(e.ui.select.value=t)}))}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}getConfig(t){const e=t.closest("[data-tabs-id]");if(!e)return!1;const s=this.tabs.get(e.dataset.tabsId);return s||!1}isInTabs(t,e){return e.ui.sections.some((e=>e.contains(t)))}destroy(){this.subscribers.clear(),Array.from(this.tabs.values()).forEach((t=>{t.ui.nav.removeEventListener("click",this.clickHandler),t.ui.select?.removeEventListener("change",this.changeHandler)}))}}document.addEventListener("DOMContentLoaded",(function(){window.jvbTabs=new t}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 565cbd3..7f8dabc 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners()}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){const s=t.data instanceof FormData?this.stores.uploads.formDataToObject(t.data):t.data;console.log(s);let r=s.upload_ids;if(!r||0===r.length)return;if("cancel-operation"===e)return this.handleOperationCancelled(r);this.setBulkUpload(r,"status",t.status).then((()=>{})),"completed"===t.status&&r.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name,r=e.value,i=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${i}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[i]=r,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{this.updateHandlerItems(r)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const t=this.groups.get(e.id)?.element,s={images:[],fields:this.collectGroupFieldsFromDOM(t,e.id)},a=this.getGroupUploadsInOrder(e);for(const t of a){const r=this.formatFile(t);if(r){o.push(r);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),n=l?.element?.querySelector(`input[name="${e.id}_featured"]`);n?.checked&&(s.fields.featured=t.id),s.images.push(a),i.push(t.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const r=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>r.includes(e)))||e.value&&(s[r]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection(t,{wrapper:{wrapper:".wrap"},selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(r)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortable(e,i,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.dataset.groupId=e,t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(t.dataset.groupId=e,s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),this.getSelectionHandler(t)?.addWrapper(s),s}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}r.ui.checkbox&&(r.ui.checkbox.checked=!1);const o=this.selectionHandlers.get(s.field);if(o&&o.isSelected(e)&&o.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,await this.stores.groups.save(i))}else s.group=null;let a=t?this.groups.get(t)?.ui.grid:i.ui.grid;a&&(a.append(r.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(r.field)){const t=this.getGroupKey(r.field,e),i=this.selectionHandlers.get(t);i?.destroy&&i.destroy(),this.selectionHandlers.get(r.field)?.removeWrapper(s.element);const o=this.sortables.get(t);o?.destroy&&o.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let r=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),this.syncSortableSelection(e)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}this.syncSortableSelection(e)},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),await this.handleReorder(t,i),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.stores.groups.get(t);e&&(e.uploads=r,this.stores.groups.save(e).then((()=>{})))}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}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("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.templates=window.jvbTemplates,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners(),this.defineTemplates()}defineTemplates(){const e=this.templates,t=this;e.define("uploadItem",{refs:{select:'[name="select-item"]',featured:'[name="featured"]',img:"img",video:"video",file:"label > span",details:"details"},manyRefs:{inputs:"input"},setup({el:s,refs:r,manyRefs:i,data:o}){let a,l,n,d=!1;switch(Object.hasOwn(o,"file")?(s.dataset.uploadId=o.uploadId,a=t.getSubtypeFromMime(o.file.type)||"image",l="document"!==a&&t.createPreviewUrl(o.file),d=l,n=o.file.name||""):(s.dataset.id=o.id,a=t.getSubtypeFromURL(o.medium??o.src),l=o.medium??o.src,n=o["image-alt-text"]??""),s.dataset.subtype=a,r.featured&&(r.featured.value=o.uploadId),a){case"image":r.img&&(r.img.src=l,r.img.alt=n,d&&(r.img.dataset.previewUrl=d)),r.video&&r.video.remove(),r.file&&r.file.remove();break;case"video":r.video&&(r.video.src=l,r.video.alt=n,d&&(r.video.dataset.previewUrl=d)),r.img&&r.img.remove(),r.file&&r.file.remove();break;case"document":if(r.preview){let e=o.file.name.split(".").pop()?.toLowerCase()??"",t={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},s=window.getIcon(t[e]??"file");r.preview.innerText=o.file.name??o.title,r.preview.prepend(s)}r.img&&r.img.remove(),r.video&&r.video.remove()}if(r.details&&r.details.append(e.create("uploadMeta")),s.draggable="single"!==s.dataset.mode,i.inputs)for(let e of i.inputs)window.prefixInput(e,`${o.uploadId}-`)}}),e.define("uploadMeta",{refs:{alt:'[name="alt_text"]',title:'[name="image-title"]',description:'[name="image-caption"]'},setup({el:e,refs:t,manyRefs:s,data:r}){Object.hasOwn(r,"alt")&&t.alt&&(t.alt.value=r.alt),Object.hasOwn(r,"title")&&t.title&&(t.title.value=r.title),Object.hasOwn(r,"description")&&t.description&&(t.description.value=r.description)}}),e.define("imageGroup",{refs:{selectAll:"[data-select-all]",fields:".fields",details:"details",grid:".item-grid"},setup({el:t,refs:s,manyRefs:r,data:i}){t.dataset.groupId=i.groupId,s.selectAll&&window.prefixInput(s.selectAll,`select-all-${i.groupId}`,!0);let o=e.create("groupMetadata",{groupId:i.groupId});o?s.fields.append(o):s.details.remove(),s.grid&&(s.grid.dataset.groupId=i.groupId)}}),e.define("groupMetadata",{manyRefs:{inputs:"input,textarea,select"},setup({el:e,refs:t,manyRefs:s,data:r}){t.inputs&&t.inputs.forEach((e=>{window.prefixInput(e,`${r.groupId}-`)}))}}),e.define("restoreNotification",{refs:{details:".details",wrap:".wrap"},setup({el:t,refs:s,manyRefs:r,data:i}){if(s.details){let e=i.bySource.size>1?` across ${i.bySource.size} pages`:"",t=i.pendingUploads.length>1?"uploads":"upload";s.details.textContent=`${i.pendingUploads.length} ${t} can be recovered${e}`}if(!s.wrap)return void console.warn("No wrap element in template");let o=1;for(const[t,r]of i.bySource){let i={index:o,isCurrent:t===window.location.href,src:t,uploads:r};s.wrap.append(e.create("restoreField",i)),o++}}}),e.define("restoreField",{refs:{h3:"h3",a:"h3 a",grid:".item-grid"},async setup({el:e,refs:s,manyRefs:r,data:i}){let o=t.registerField(e,!1,`recovery_${i.index}`);i.isCurrent?(e.open=!0,s.a?.remove(),s.h3&&(s.h3.textContent="From this page:")):s.a&&(s.a.href=i.src,s.a.title="Navigate to page and restore",s.a.textContent=i.src);let a=[...new Set(i.uploads.map((e=>e.group??"preview")))];for(let e of a){let r="preview"===e||t.stores.groups.get(e);if(!r)continue;let a=await t.createGroupElement(e,o),l=a.querySelector(".item-grid"),n=i.uploads.filter((t=>t.group===("preview"===e)?null:e));for(const[e,t]of Object.entries(r.fields??{})){let s=a.querySelector(`input[name*="${e}"]`);s&&(s.value=t)}for(let e of n){let s=await t.createUpload(e.id,t.formatFile(e),o);l.append(s)}s.grid.append(a)}}})}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(("operation-status"===e||"cancel-operation"===e)&&["image_upload","video_upload","document_upload"].includes(t.type)){let s=(t.data instanceof FormData?this.stores.uploads.formDataToObject(t.data):t.data).upload_ids;if(!s||0===s.length)return;if("cancel-operation"===e)return this.handleOperationCancelled(s);this.setBulkUpload(s,"status",t.status).then((()=>{})),"completed"===t.status&&s.forEach((e=>{this.removeUpload(e).then((()=>{}))}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery().then((()=>{})))}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:"[data-select-all]",actions:".selection-actions",count:".selected .info",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:".item.upload",checkbox:'[name="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.dataset.groupId;if(!t)return;const s=e.name,r=e.value,i=s.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");window.debouncer.schedule(`group-meta-${t}-${i}`,(async()=>{const e=this.stores.groups.get(t);e&&(e.fields||(e.fields={}),e.fields[i]=r,await this.setGroup(t,e))}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{this.updateHandlerItems(r)})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,p;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,p=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,p=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let c=this.sendToQueue(e,s,u,p);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return c?(r.operationId=c,await this.setBulkUpload(i,"operationId",c),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",c),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),c}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const t=this.groups.get(e.id)?.element,s={images:[],fields:this.collectGroupFieldsFromDOM(t,e.id)},a=this.getGroupUploadsInOrder(e);for(const t of a){const r=this.formatFile(t);if(r){o.push(r);const a={upload_id:t.id,index:i.length},l=this.uploads.get(t.id),n=l?.element?.querySelector(`input[name="${e.id}_featured"]`);n?.checked&&(s.fields.featured=t.id),s.images.push(a),i.push(t.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}getGroupUploadsInOrder(e){return e.uploads&&0!==e.uploads.length?e.uploads.map((e=>this.stores.uploads.get(e))).filter(Boolean):[]}collectGroupFieldsFromDOM(e,t){if(!e)return{};const s={};return e.querySelectorAll("input, textarea, select").forEach((e=>{const r=e.name.replace(`${t}[`,"").replace(`${t}_`,"").replace("]","");["featured","select-all"].some((e=>r.includes(e)))||e.value&&(s[r]=e.value)})),s}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;const t=new Map;e.forEach((e=>{const s=e.src||"unknown";t.has(s)||t.set(s,[]),t.get(s).push(e)}));let s={bySource:t,pendingUploads:e};document.body.append(this.templates.create("restoreNotification",s));let r=document.querySelector("dialog.restore-uploads");this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection(r,{wrapper:{wrapper:".restore-field",id:"selection"},items:".item-grid.restore",selectAll:{bulkControls:".selection-actions",checkbox:"#select-all-restore",count:".selection-count"}}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=this.fields.get(s);if(!r)return null;let i={uploadId:e,file:t,field:r};return this.templates.create("uploadItem",i)}getSubtypeFromURL(e){const t=e.split("?")[0].toLowerCase();return[".webp",".jpg",".jpeg",".png",".gif",".svg"].some((e=>t.endsWith(e)))?"image":[".mp4",".ogg",".mov",".webm",".avi"].some((e=>t.endsWith(e)))?"video":"document"}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>{if("string"==typeof e&&(e=await this.stores.uploads.get(e)),e)return"status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e)}));await Promise.all(r)}async setUploadStatus(e,t){"string"==typeof e&&(e=await this.stores.uploads.get(e)),e&&e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length?await this.removeGroup(s.id,!1):await this.stores.groups.save(s)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;const i=s.groupUI.empty;i?.nextSibling?s.groupUI.grid.insertBefore(r,i.nextSibling):s.groupUI.grid.append(r);const o=r.querySelector(".item-grid");o&&(o.dataset.groupId=t,this.createSortable(e,o,t));let a=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...a,id:t,field:e}),t}createGroupElement(e,t=null){let s={groupId:e,fieldId:t},r=this.templates.create("imageGroup",s);return this.groups.set(e,{element:r,ui:window.uiFromSelectors(this.selectors.group,r)}),this.getSelectionHandler(t)?.addWrapper(r),r}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.removeGroup(t.id,!1):await this.stores.groups.save(t))}r.ui.checkbox&&(r.ui.checkbox.checked=!1);const o=this.selectionHandlers.get(s.field);if(o&&o.isSelected(e)&&o.deselect(e),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,await this.stores.groups.save(i))}else s.group=null;let a=t?this.groups.get(t)?.ui.grid:i.ui.grid;a&&(a.append(r.element),t&&await this.handleReorder(s.field,t)),await this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));if(this.fields.get(r.field)){const t=this.getGroupKey(r.field,e),i=this.selectionHandlers.get(t);i?.destroy&&i.destroy(),this.selectionHandlers.get(r.field)?.removeWrapper(s.element);const o=this.sortables.get(t);o?.destroy&&o.destroy(),this.sortables.delete(t)}s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){0!==e.length&&e.forEach((e=>{this.removeUpload(e)}))}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;if("post_group"!==s.config.destination)return;let r=new window.jvbHandleSelection(s.element,{selectAll:{checkbox:this.selectors.fields.selectAll,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions},item:{item:this.selectors.items.item,checkbox:this.selectors.items.checkbox,idAttribute:"uploadId"},wrapper:{wrapper:".preview-wrap, .upload-group",id:"groupId"}});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}updateHandlerItems(e){let t=this.getSelectionHandler(e);t&&t.collectItems()}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},dragClass:"dragging",onStart:t=>{const s=t.item,r=s?.dataset.uploadId,i=this.selected.get(e);if(r&&(!i||!i.has(r))){const t=this.selectionHandlers.get(e);t&&t.select(r)}},onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;for(const e of r)await this.addToGroup(e,i);await this.handleReorder(t,i),this.selectionHandlers.get(t)?.clearSelection()}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.children).filter((e=>e.matches(this.selectors.items.item)&&!e.classList.contains("ghost"))).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.stores.groups.get(t);e&&(e.uploads=r,this.stores.groups.save(e).then((()=>{})))}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}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("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.restoreModal&&this.cleanupRestore(),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index 8263ddb..a31af92 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t,e="default"){const n=t instanceof Date?t:new Date(t),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===e)return n.toLocaleDateString();const t={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return e.replace(/[YyFMmnjDlHishgAa]/g,(e=>t[e]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.generateID=function(t="jvb"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(t,e,n,i="",o=""){const r=e<n;t.progress&&r&&window.fade(t.progress,!0);const a=n>0?e/n*100:0;t.fill&&(t.fill.style.width=`${a}%`),t.details&&(t.details.textContent=i),t.count&&(t.count.textContent=`${e}/${n}`),t.icon&&(t.icon.className=""===o?"icon":"icon icon-"+o),t.progress&&e===n&&window.fade(t.progress,!1)},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return new Promise((i=>{t._typeInterval&&(clearInterval(t._typeInterval),delete t._typeInterval);let o=0;t.textContent="",t._typeInterval=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(t._typeInterval),delete t._typeInterval,i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{t._eraseInterval&&(clearInterval(t._eraseInterval),delete t._eraseInterval);let i=t.textContent,o=i.length;t._eraseInterval=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(t._eraseInterval),delete t._eraseInterval,n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){const a=t.id||t.dataset.typeKey||`type-${Date.now()}`;t.dataset.typeKey||(t.dataset.typeKey=a),t._stopTyping&&t._stopTyping();let s=!0;const l=function(){s=!1,t._typeInterval&&(clearInterval(t._typeInterval),delete t._typeInterval),t._eraseInterval&&(clearInterval(t._eraseInterval),delete t._eraseInterval)};return t._stopTyping=l,async function(){for(;s&&(await window.typeText(t,e,n),s)&&(await new Promise((t=>setTimeout(t,o))),s)&&(await window.eraseText(t,i),s);)await new Promise((t=>setTimeout(t,r)))}(),l},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function s(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function l(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),s(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||t.scrollTop||0)}),20)})),a(),s(n)})();
\ No newline at end of file
+(()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=!1){let i=n?t:`${t}${e.name}`;if(e.labels.length>0)e.labels?.forEach((e=>{e.htmlFor=i}));else if("LABEL"===e.nextElementSibling?.tagName)e.nextElementSibling.htmlFor=i;else if("LABEL"===e.previousElementSibling?.tagName)e.previousElementSibling.htmlFor=i;else{let t=e.parentElement.querySelector(`label[for="${e.id}"]`);t&&(t.htmlFor=i)}e.id=i},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||e.scrollTop||0)}),20)})),a(),s(n)})();
\ No newline at end of file
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 45073e4..1e9f0a8 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'b17f126267647a51eaa7');
+<?php return array('dependencies' => array(), 'version' => 'eb66f05b2fcc7e099499');
diff --git a/build/feed/view.js b/build/feed/view.js
index 09d0f45..0371212 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters"}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.content=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.content=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.content=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.content=!1),this.contentTypes=this.ui.content?Array.from(this.ui.content).map((e=>e.value)):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach((e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach((e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t)}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach((e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()})),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter((t=>t.dataset.taxonomy===e))[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter((e=>e!==t)),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach((t=>{t&&t.forEach((t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)}))}))}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach((e=>{this.createTermElement(e)})),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find((t=>t.dataset.taxonomy===e));return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);if(!t)return;if(this.ui.selected.querySelector(`[data-id="${e}"]`))return;let i=this.getTaxonomyIcon(t.taxonomy),s=window.getTemplate("feedTerm");if(!s)return;let[r,o]=[s.querySelector(".icon"),s.querySelector("span")];r&&o&&(s.dataset.id=t.id,s.dataset.taxonomy=t.taxonomy,r.className=`icon icon-${i}`,o.textContent=t.name,this.ui.selected.append(s))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach((i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)}));let i=!1;return e.forEach(((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}})),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])}));for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach((e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)}));const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()}))}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=e.feed,this.store.subscribe(((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))}))}isFirstPage(){return 1===this.store.filters.page}renderItems(){let e=this.store.getFiltered();if(this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length)this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage());else{const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.append(r)}var o;r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(o=this.store.lastResponse?.has_more)&&void 0!==o&&o))};i(0)}this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid);let e=window.getTemplate("emptyState");e&&this.ui.grid.append(e)}createItemElement(e){let t=window.getTemplate(`feedItem${window.uppercaseFirst(e.content)}`);const i=Object.hasOwn(t.dataset,"timeline");for(let[s,r]of Object.entries(e.fields)){if(i&&["timeline","number"].includes(s))continue;let o=t.querySelector(`[data-field="${s}"]`);o&&(""!==r?this.isImageField(e,r)?this.formatImageField(o,r,e):this.isTaxonomyField(e,s)?this.formatTaxonomyField(o,e,s,r):this.isTimeField(o)?this.formatTimeField(o,r):this.formatField(o,r):o.remove())}let s=t.querySelector("a");var r;return s&&""!==e.url&&([s.href,s.title]=[e.url,`View ${null!==(r=e.fields.post_title)&&void 0!==r?r:"Item"}`]),i&&this.addTimelineElements(e,t),t}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(n))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.fields.number} Tx`),s&&(s.textContent=e.fields.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()}),100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters"}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.content=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.content=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.content=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.content=!1),this.contentTypes=this.ui.content?Array.from(this.ui.content).map((e=>e.value)):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach((e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}})),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach((e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t)}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach((e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()})),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter((t=>t.dataset.taxonomy===e))[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter((e=>e!==t)),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach((t=>{t&&t.forEach((t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)}))}))}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach((e=>{this.createTermElement(e)})),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find((t=>t.dataset.taxonomy===e));return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach((i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)}));let i=!1;return e.forEach(((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}})),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach((t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])}));for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach((e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)}));const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()}))}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=e.feed,this.store.subscribe(((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))}))}isFirstPage(){return 1===this.store.filters.page}renderItems(){let e=this.store.getFiltered();this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,(e=>this.createItemElement(e)),(t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)}),5).then((()=>{})),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>e))}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some((t=>Object.keys(e.images).map((e=>parseInt(e))).includes(parseInt(t))))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach((s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)})),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");l&&([l.href,l.title,l.textContent]=[r.url,`See more ${r.title}`,r.title],e.append(n))}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=t}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.fields.number} Tx`),s&&(s.textContent=e.fields.number),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=s.name)}}),e.define("emptyState"),this.contentTypes.forEach((i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/build/forms/view.asset.php b/build/forms/view.asset.php
index 5ac10d4..9fe42fd 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '2321e606c22ff3ca6cff');
+<?php return array('dependencies' => array(), 'version' => '2afaae229d42019919a2');
diff --git a/build/forms/view.js b/build/forms/view.js
index 6d7c3b2..beaacce 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){const{formId:r,config:e,fullData:t}=o,n=e.element,s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));if(window.jvbUploads)try{(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r})=>{s.append(`${r}[]`,o)}))}catch(o){console.error("Error getting files:",o)}this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{await this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
+(()=>{class o{constructor(){this.controller=window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1,showSummary:!0})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r).then((()=>{}))}))}async handleFormSubmission(o){const{config:r,data:e}=o,t=r.element,n=new FormData;for(const[o,r]of Object.entries(e))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>n.append(`${o}[]`,r))):"object"==typeof r&&null!==r?n.append(o,JSON.stringify(r)):n.append(o,r));if(window.jvbUploads)try{(await window.jvbUploads.getFilesForForm(t)).forEach((({file:o,fieldName:r})=>{n.append(`${r}[]`,o)}))}catch(o){console.error("Error getting files:",o)}this.controller.showFormStatus(r.id,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",body:n}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r.id,"error"),void this.controller.handleFormError(t,e);if(this.controller.showFormStatus(r.id,"submitted"),this.controller.showSummary(r.id,".jvb-form-block"),window.jvbUploads){const o=t.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r.id,"error"),this.controller.handleFormError(t,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{await this.controller.store.delete(r.id)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index c16416b..7fc2674 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -32,6 +32,7 @@
     public function __construct()
     {
         $this->cache = CacheManager::for('timelines', WEEK_IN_SECONDS)->connectTo('post', 'timeline');
+		$this->cache->clear();
         add_action('init', [ $this, 'registerBlock' ]);
 		add_action('wp_footer', 'jvbRenderGallery');
     }
@@ -153,7 +154,7 @@
 				<div class="info">
 					<header>
 						<h2><?=jvbIcon(jvbLogoIcon())?><?= $title?></h2>
-						<?= array_key_exists('post_date', $fields) && $fields['date'] !== '' ? '<time>'.date('F Y', strtotime($fields['post_date'])).'</time>' : '' ?>
+						<?= array_key_exists('post_date', $fields) && $fields['post_date'] !== '' ? '<time>'.date('F Y', strtotime($fields['post_date'])).'</time>' : '' ?>
 						<?= array_key_exists('timeline', $fields) && $fields['timeline'] !== '' ? $this->outputTimelineTax($ID) : '' ?>
 						<?= array_key_exists('post_content', $fields) && $fields['post_content'] !== '' ? '<div class="content">'.wptexturize(wp_kses_post( wpautop($fields['post_content']))).'</div>' : '' ?>
 					</header>
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index 0c5f1b0..047a948 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -1,9 +1,7 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\managers\CacheManager;
 use Exception;
-use JVBase\utility\Features;
 use WP_Error;
 use WP_REST_Response;
 use WP_REST_Request;
@@ -49,17 +47,13 @@
 
 	protected ?CacheManager $cache = null;
 	protected int $ttl = 300;
-	protected string $cacheGroup = 'queue';
 	// Cache keys for different data types
 	private const CACHE_QUEUE_STATUS = 'status';
 	private const CACHE_HAS_ITEMS = 'has_items';
 	private const CACHE_OPERATION_PREFIX = 'op_';
 	private const CACHE_USER_QUEUE_PREFIX = 'user_queue_';
-	private const CACHE_METRICS_PREFIX = 'metrics_';
 	private const CACHE_QUEUE_SIZE = 'queue_size';
 
-	// Prepared statement cache
-	protected array $preparedStatements = [];
 
     public function __construct()
     {
@@ -117,7 +111,7 @@
      *
      * @return bool|WP_REST_Response
      */
-    public function adminActionFilter(WP_REST_Response $response, WP_REST_Request $request, string $action)
+    public function adminActionFilter(WP_REST_Response $response, WP_REST_Request $request, string $action):WP_REST_Response|bool
     {
         switch ($action) {
             case 'unlock-operation-queue':
@@ -152,8 +146,8 @@
 		$result = $this->wpdb->get_row($this->wpdb->prepare("
         SELECT
             COUNT(*) as total,
-            SUM(CASE WHEN status IN ('pending', 'processing') THEN 1 ELSE 0 END) as active,
-            SUM(CASE WHEN status = 'scheduled' AND scheduled_at <= %s THEN 1 ELSE 0 END) as ready_scheduled
+            SUM(IF(status IN ('pending', 'processing'), 1, 0)) as active,
+            SUM(IF(status = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
         FROM $table
         WHERE status IN ('pending', 'processing', 'scheduled')
     ", $current_time));
@@ -215,17 +209,6 @@
 //		$this->pruneOldMetrics(); // Optional: clean old metrics
 	}
 
-	protected function pruneOldMetrics(): void
-	{
-		$metricsTable = $this->wpdb->prefix . $this->metricsTable;
-
-		// Keep only last 90 days of metrics
-		$this->wpdb->query($this->wpdb->prepare(
-			"DELETE FROM $metricsTable WHERE date < %s",
-			date('Y-m-d', strtotime('-90 days'))
-		));
-	}
-
 	protected function isHeavyOperation(string $type): bool
 	{
 		$heavyOperations = [
@@ -255,24 +238,18 @@
      * Check if the queue is currently locked for processing
      * @return bool
      */
-    protected function isQueueLocked():bool
-    {
-        $lock = get_transient(BASE . 'queue_lock');
+	protected function isQueueLocked(): bool
+	{
+		$lock_name = BASE . '_queue_lock';
 
-        if ($lock) {
-            // Check if the lock is stale (process might have crashed)
-            $lock_time    = (int)$lock;
-            $current_time = time();
+		// Check if lock exists and is recent
+		$result = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT IS_USED_LOCK(%s)",
+			$lock_name
+		));
 
-            // If lock is older than 5 minutes, it's probably stale
-            if (($current_time - $lock_time) > 300) {
-                $this->unlockQueue(); // Clear stale lock
-                return false;
-            }
-            return true;
-        }
-        return false;
-    }
+		return $result !== null;
+	}
 
     /**
      * Check if server is relatively idle (very low load)
@@ -300,25 +277,32 @@
      * Acquire a lock on the queue
      * @return bool
      */
-    protected function lockQueue():bool
-    {
-        // Try to acquire the lock
-        $result = set_transient(BASE . 'queue_lock', time());
+	protected function lockQueue(): bool
+	{
+		$lock_name = BASE . '_queue_lock';
 
-        if ($result) {
-            return true;
-        }
-        return false;
-    }
+		// Try to acquire lock with 0 timeout (non-blocking)
+		$result = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT GET_LOCK(%s, 0)",
+			$lock_name
+		));
+
+		return $result === '1';
+	}
 
     /**
      * Release the queue lock
      * @return void
      */
-    protected function unlockQueue():void
-    {
-        delete_transient(BASE . 'queue_lock');
-    }
+	protected function unlockQueue(): void
+	{
+		$lock_name = BASE . '_queue_lock';
+
+		$this->wpdb->query($this->wpdb->prepare(
+			"SELECT RELEASE_LOCK(%s)",
+			$lock_name
+		));
+	}
 
 
     /**
@@ -551,9 +535,7 @@
 				}
 			}
 
-			$this->updateLastModified($user_id);
-			$this->invalidateQueueCache();
-			$this->cache->delete(self::CACHE_USER_QUEUE_PREFIX . $user_id);
+			$this->invalidateUserQueue($user_id);
 			$this->runQueueOnShutdown();
 
 			return [
@@ -570,10 +552,6 @@
 		}
 	}
 
-	protected function updateLastModified(int $user_id) {
-		CacheManager::updateTimestamp("user_{$user_id}");
-	}
-
 	protected function deepMerge(array $existing, array $new): array
 	{
 		$merged = $existing;
@@ -866,7 +844,7 @@
         AND retries < %d
         ORDER BY
             FIELD(priority, 'high', 'normal', 'low'),
-            COALESCE(scheduled_at, created_at) ASC
+            COALESCE(scheduled_at, created_at)
         LIMIT %d
     ", $current_time, $this->max_attempts, $batch_size));
 
@@ -972,6 +950,23 @@
 	}
 
 	/**
+	 * Check if user's queue has been modified since given timestamp
+	 * Returns true if modified, false if not (can send 304)
+	 */
+	public function isUserQueueModified(int $user_id, int $since_timestamp): bool
+	{
+		return $this->cache::getTimestamp("user_{$user_id}") > $since_timestamp;
+	}
+	protected function invalidateUserQueue(int $user_id): void
+	{
+		// This automatically:
+		// 1. Updates HTTP timestamp for user_{$user_id}
+		// 2. Flushes user-specific caches
+		// 3. Triggers connected cache invalidation
+		CacheManager::invalidateAll("user_{$user_id}");
+	}
+
+	/**
 	 * Invalidate all queue-related caches
 	 */
 	protected function invalidateQueueCache(string $scope = 'all'): void
@@ -993,9 +988,9 @@
 			$this->cache->delete($key);
 		}
 
+		$this->cache->touch();
+
 		if ($scope === 'all') {
-			// Clear entire group for complete refresh
-			$this->cache->invalidate();
 			delete_transient('jvb_queue_status_counts');
 		}
 	}
@@ -1167,7 +1162,7 @@
 			if (!$updated) {
 				throw new Exception('Operation no longer available for processing');
 			}
-			$this->updateLastModified($operation->user_id);
+			$this->invalidateUserQueue($operation->user_id);
 
 			$data = json_decode($operation->request_data, true);
 			$progress_count = (int) $operation->progress_count;
@@ -1268,9 +1263,7 @@
 					);
 
 					// Now do post-completion tasks
-					$this->invalidateQueueCache('status');
-					$this->updateLastModified($operation->user_id);
-					$this->updateUserQueueTimestamp($operation->user_id);
+					$this->invalidateUserQueue($operation->user_id);
 
 					$this->trackOperationMetrics($operation->id);
 
@@ -1289,7 +1282,7 @@
 						['%s']
 					);
 
-					$this->updateLastModified($operation->user_id);
+					$this->invalidateUserQueue($operation->user_id);
 				}
 
 			} else {
@@ -1330,8 +1323,7 @@
 						['%s']
 					);
 
-					$this->invalidateQueueCache('status');
-					$this->updateLastModified($operation->user_id);
+					$this->invalidateUserQueue($operation->user_id);
 				} else {
 					// Failed but more to process - continue with next chunk
 					$this->wpdb->update(
@@ -1351,8 +1343,7 @@
 				}
 			}
 			// Clear operation cache after any update
-			$this->cache->delete(self::CACHE_OPERATION_PREFIX . $operation->id);
-			$this->updateLastModified($operation->user_id);
+			$this->invalidateUserQueue($operation->user_id);
 			return $filterResult;
 
 		} catch (Exception $e) {
@@ -1427,12 +1418,9 @@
 		$batch = $this->extractBatch($data, $keys, $current_progress, $chunk_size);
 
 		// Merge non-chunked data with chunked batch
-		$result = [];
-		foreach ($data as $k => $v) {
-			if (!in_array($k, $keys)) {
-				$result[$k] = $v; // Keep non-batch data
-			}
-		}
+		$result = array_filter($data, function ($k) use ($keys) {
+			return !in_array($k, $keys);
+		}, ARRAY_FILTER_USE_KEY);
 
 		// Add the batched data
 		foreach ($batch as $k => $v) {
@@ -1796,7 +1784,7 @@
             SELECT
                 status,
                 COUNT(*) as count,
-                SUM(CASE WHEN status = 'scheduled' AND scheduled_at <= '$current_time' THEN 1 ELSE 0 END) as scheduled_ready
+                SUM(IF(status = 'scheduled' AND scheduled_at <= '$current_time', 1, 0)) as scheduled_ready
             FROM $table
             GROUP BY status
         ", OBJECT_K);
@@ -2175,8 +2163,8 @@
                 DATE(completed_at) as date,
                 type,
                 COUNT(*) as total_operations,
-                SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful,
-                SUM(CASE WHEN status LIKE 'failed%' THEN 1 ELSE 0 END) as failed,
+                SUM(IF(status = 'completed', 1, 0)) as successful,
+                SUM(IF(status LIKE 'failed%', 1, 0)) as failed,
                 AVG(TIMESTAMPDIFF(SECOND, created_at, completed_at)) as avg_duration,
                 SUM(count) as items_processed
             FROM $table
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 2324496..5225f9b 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -212,6 +212,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<?php $this->renderLabel($name, $field); ?>
@@ -366,6 +367,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<?php $this->renderLabel($name, $field); ?>
@@ -424,6 +426,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?> row"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<?php $this->renderLabel($name, $field); ?>
@@ -474,6 +477,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<?php $this->renderLabel($name, $field); ?>
@@ -514,6 +518,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<fieldset>
@@ -559,6 +564,7 @@
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<fieldset>
@@ -600,6 +606,7 @@
 		<div class="field true-false <?= esc_attr($name) ?> row btw"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>>
 
 			<label class="toggle-switch row" <?= $describedBy ?>>
@@ -642,6 +649,7 @@
 		?>
 		<div class="field repeater <?=$name?>"
 			 data-field="<?= esc_attr($name); ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $describedBy ?>
 			<?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
 			<?=$conditional?>>
@@ -663,7 +671,7 @@
 				?>
 			</div>
 
-			<template class="<?=uniqid('repeaterTemplate')?>">
+			<template class="<?=uniqid('repeaterRow')?>">
 				<?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
 			</template>
 
@@ -756,7 +764,8 @@
 		?>
 		<<?= $fieldset?> class="field group <?= esc_attr($name) ?>"
 			<?= $conditional ?>
-				  data-field="<?= esc_attr($name) ?>"
+		  	data-field="<?= esc_attr($name) ?>"
+			data-field-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>
 			<?= $describedBy ?>>
 			<<?=$legend?>><?= esc_html($field['label']) ?></<?=$legend?>>
@@ -880,6 +889,8 @@
 		}
 		?>
 		<div class="field upload <?= esc_attr($name) ?>"
+			 data-field="<?=esc_attr($name)?>"
+			 data-field-type="upload"
 			<?= $dataAttrString ?>
 			<?= $conditional ?>>
 
@@ -1411,7 +1422,8 @@
 		<div class="field selector <?= esc_attr($type) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
-			 data-type="selector" data-subtype="<?= esc_attr($type)?>"
+			 data-field-type="selector"
+			 data-type="<?=esc_attr($field['type'])?>"
 			<?= $validationAttrs ?>
 			<?= $describedBy ?>>
 
@@ -1477,6 +1489,7 @@
 
 		<div class="field location <?= esc_attr($field_id) ?>"
 			 data-field="<?= esc_attr($field_id) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			 data-location-field-init="<?= $json_config ?>"<?=$describedBy?>>
 
 			<?php
@@ -1639,6 +1652,7 @@
 		?>
 		<div class="field tag-list <?= esc_attr($name) ?>"
 			 data-field="<?= esc_attr($name) ?>"
+			 data-field-type="<?=esc_attr($field['type'])?>"
 			 data-tag-format="<?= esc_attr($tagFormat) ?>"
 			<?= $describedBy ?>
 			<?= $conditional ?>
@@ -1681,7 +1695,7 @@
 			</div>
 
 			<!-- Template for new tags -->
-			<template class="tag-template">
+			<template class="<?=uniqid('tagListItem')?>">
 				<?php $this->renderTagItem($field['fields'], [], '', $name, $tagFormat); ?>
 			</template>
 
@@ -1715,7 +1729,8 @@
 				<input type="hidden"
 					   name="<?= esc_attr($full_name) ?>"
 					   value="<?= esc_attr($value) ?>"
-					   data-field="<?= esc_attr($field_name) ?>" />
+					   data-field="<?= esc_attr($field_name) ?>"
+					   data-field-type="<?=esc_attr($field_config['type'])?>" />
 			<?php endforeach; ?>
 
 			<button type="button" class="remove-tag" aria-label="Remove">
diff --git a/inc/meta/MetaManager.php b/inc/meta/MetaManager.php
index 57b8076..89029d2 100644
--- a/inc/meta/MetaManager.php
+++ b/inc/meta/MetaManager.php
@@ -28,27 +28,29 @@
 
 	protected ?string $baseKey = null;
 	protected \wpdb $wpdb;
-	protected array $postFields = [
-		'post_title',
-		'post_excerpt',
-		'post_content',
-		'post_date',
-		'post_status',
-		'post_modified',
-		'post_thumbnail'
-	];
 
-	protected array $userFields = [
-		'first_name',
-		'last_name',
-		'description',
-		'display_name',
-		'user_email',
-	];
-
-	protected array $termFields = [
-		'term_name',
-		'description'
+	protected array $wpDefaults = [
+		'post'	=> [
+			'post_title',
+			'post_excerpt',
+			'post_content',
+			'post_date',
+			'post_status',
+			'post_modified',
+			'post_thumbnail',
+			'menu_order'
+		],
+		'user'	=> [
+			'first_name',
+			'last_name',
+			'display_name',
+			'description',
+			'user_email',
+		],
+		'term'	=> [
+			'term_name',
+			'description'
+		]
 	];
 
 	public function __construct(int|string|null $ID = null, ?string $type = null, ?string $content = null)
@@ -115,32 +117,22 @@
 	public function getValue(string $name): mixed
 	{
 		//Get standard post fields first
-		switch ($name) {
-			case 'post_title':
-				return $this->data->post_title ?? '';
-			case 'post_excerpt':
-				return $this->data->post_excerpt ?? '';
-			case 'post_content':
-				return $this->data->post_content ?? '';
-			case 'featured_image':
-			case 'post_thumbnail':
-				return get_post_thumbnail_id($this->object_id);
-			case 'display_name':
-				if (is_null($this->data) || !$this->data->display_name) {
-					$user = get_userdata((int)get_post_meta($this->object_id, BASE . 'link', true));
-					return $user->display_name;
+		if (array_key_exists($this->object_type, $this->wpDefaults)) {
+			$defaults = $this->wpDefaults[$this->object_type];
+			if (in_array($name, $defaults)) {
+				if (in_array($name, ['featured_image', 'post_thumbnail'])) {
+					return get_post_thumbnail_id($this->object_id);
 				}
-				return $this->data->display_name ?? '';
-			case 'user_email':
-				if (is_null($this->data) || !$this->data->display_name) {
-					$user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true));
-					return $user->user_email;
-				}
-				return $this->data->user_email ?? '';
-			case 'term_name':
-				return htmlspecialchars_decode($this->data->name);
+				return match ($this->object_type) {
+					'term' => $this->getTermField($name),
+					'post' => $this->getPostField($name),
+					'user' => $this->getUserField($name),
+					default => ''
+				};
+			}
 		}
 
+
 		$meta_key = BASE . $name;
 		switch ($this->object_type) {
 			case 'post':
@@ -160,6 +152,37 @@
 		}
 	}
 
+	protected function getTermField(string $name): mixed
+	{
+		// WordPress handles entity decoding and filters
+		return match ($name) {
+			'term_name' => get_term_field('name', $this->object_id),
+			'description' => get_term_field('description', $this->object_id),
+			default => ''
+		};
+	}
+
+	protected function getPostField(string $name): mixed
+	{
+		return match ($name) {
+			'post_title' => get_the_title($this->object_id),
+			'post_excerpt' => get_the_excerpt($this->object_id),
+			'post_content' => get_post_field('post_content', $this->object_id),
+			default => $this->data->$name ?? ''
+		};
+	}
+
+	protected function getUserField(string $name): mixed
+	{
+		return match ($name) {
+			'display_name' => get_the_author_meta('display_name', $this->object_id),
+			'user_email' => get_the_author_meta('user_email', $this->object_id),
+			'first_name' => get_the_author_meta('first_name', $this->object_id),
+			'last_name' => get_the_author_meta('last_name', $this->object_id),
+			default => $this->data->$name ?? ''
+		};
+	}
+
 	/**
 	 * @param string $name
 	 *
@@ -255,91 +278,60 @@
 				return true;
 			}
 
-			switch ($name) {
-				case 'post_title':
+			if (array_key_exists($this->object_type, $this->wpDefaults)) {
+				$check = $this->wpDefaults[$this->object_type];
+				if (in_array($name, $check)) {
 					$ID = true;
-					if ($this->data->post_title !== $sanitized) {
-						$ID = wp_update_post([
-							'ID' => $this->object_id,
-							'post_title' => $sanitized
-						]);
+					if (in_array($name, ['featured_image', 'post_thumbnail'])) {
+						$old = get_post_thumbnail_id($this->object_id);
+						if ($old !== $sanitized) {
+							$ID = set_post_thumbnail($this->object_id, $sanitized);
+						}
+						return $ID !== false;
 					}
-					return ($ID !== 0);
-				case 'post_excerpt':
-					$ID = true;
-					if ($this->data->post_excerpt !== $sanitized) {
-						$ID = wp_update_post([
-							'ID' => $this->object_id,
-							'post_excerpt' => $sanitized
-						]);
-					}
-					return ($ID !== 0);
-				case 'post_content':
-					$ID = true;
-					if ($this->data->post_content !== $sanitized) {
-						$ID = wp_update_post([
-							'ID' => $this->object_id,
-							'post_content' => $sanitized
-						]);
-					}
-					return ($ID !== 0);
-				case 'featured_image':
-				case 'post_thumbnail':
-					$ID = true;
-					$old = get_post_thumbnail_id($this->object_id);
+					$old = $this->data->$name;
 					if ($old !== $sanitized) {
-						$ID = set_post_thumbnail($this->object_id, $sanitized);
-					}
-					return ($ID !== false);
-				case 'display_name':
-					$ID = true;
-					$object_id = $this->object_id;
-					$displayName = $this->data->display_name;
-					if (!$this->data->display_name) {
-						$user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true));
-						$object_id = $user->ID;
-						$displayName = $user->display_name;
-					}
+						switch ($this->object_type) {
+							case 'post':
+								$ID = wp_update_post([
+									'ID' => $this->object_id,
+									$name => $sanitized
+								]);
+								break;
+							case 'term':
+								$data = [$name => $sanitized];
+								if ($name === 'term_name') {
+									$data['slug'] = sanitize_title($sanitized);
+								}
+								$ID = wp_update_term(
+									$this->data->term_id,
+									$this->data->taxonomy,
+									$data
+								);
+								break;
+							case 'user':
+								$ID = wp_update_user([
+									'ID'	=> $this->object_id,
+									$name => $sanitized
+								]);
+								if ($name === 'display_name') {
+									$link = get_user_meta($this->object_id, BASE.'link', true);
+									if ($link !== '') {
+										wp_update_post([
+											'ID'	=> $link,
+											'post_title'	=> $sanitized
+										]);
+									}
+								}
 
-					if ($displayName !== $sanitized) {
-						$ID = wp_update_user([
-							'ID' => $object_id,
-							'display_name' => $sanitized
-						]);
-						$link = get_user_meta($object_id, BASE . 'link', true);
-						wp_update_post([
-							'ID' => $link,
-							'post_title' => $sanitized,
-						]);
+								break;
+						}
 					}
-					return (!is_wp_error($ID));
-				case 'user_email':
-					$ID = true;
-
-					$object_id = $this->object_id;
-					$email = $this->data->user_email;
-					if (!$this->data->display_name) {
-						$user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true));
-						$object_id = $user->ID;
-						$email = $user->user_email;
-					}
-					if ($email !== $sanitized) {
-						$ID = wp_update_user([
-							'ID' => $object_id,
-							'user_email' => $sanitized
-						]);
-					}
-					return (!is_wp_error($ID));
-				case 'term_name':
-					$ID = true;
-					$name = $this->data->name;
-					if ($name !== $sanitized) {
-						$ID = wp_update_term($this->data->term_id, $this->data->taxonomy, [
-							'name' => $sanitized,
-							'slug' => sanitize_title($sanitized)
-						]);
-					}
+					return $ID !== false;
+				}
 			}
+
+
 			if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) {
 				$set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false);
 			}
@@ -849,19 +841,7 @@
 			return [];
 		}
 
-		switch ($this->object_type) {
-			case 'user':
-				$check = $this->userFields;
-				break;
-			case 'term':
-				$check = $this->termFields;
-				break;
-			case 'post':
-				$check = $this->postFields;
-				break;
-			default:
-				$check = [];
-		}
+		$check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : [];
 
 		$setFields = array_intersect($check, $fields);
 		foreach ($setFields as $f) {
@@ -963,22 +943,20 @@
 		}
 
 		// Determine table based on object type
+		$check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : [];
 		switch ($this->object_type) {
 			case 'post':
 				$table = $this->wpdb->postmeta;
 				$id_column = 'post_id';
-				$check = $this->postFields;
 				break;
 			case 'term':
 				$table = $this->wpdb->termmeta;
 				$id_column = 'term_id';
-				$check = $this->termFields;
 				break;
 			case 'user':
 			case 'integrations':
 				$table = $this->wpdb->usermeta;
 				$id_column = 'user_id';
-				$check = $this->userFields;
 				break;
 			case 'options':
 				try {
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index e587f0d..bf0f1bf 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -444,34 +444,41 @@
             `id` VARCHAR(64) NOT NULL,
             `type` varchar(50) NOT NULL,
             `user_id` {$this->userIDType} NOT NULL,
+
             `request_data` JSON NOT NULL,
-            `count` int(11) NOT NULL DEFAULT 1,
-            `progress_count` int(11) DEFAULT 0,
+
+            `total_items` int(11) NOT NULL DEFAULT 1,
+            `processed_items` int(11) DEFAULT 0,
             `failed_items` JSON,
+
             `priority` enum('low', 'normal', 'high') DEFAULT 'normal',
-            `status` enum('pending', 'scheduled', 'processing','failed', 'failed_permanent', 'completed', 'completed_with_errors') DEFAULT 'pending',
-            `metadata` JSON DEFAULT NULL,
-            `merge` enum('merge', 'append', 'replace') DEFAULT 'merge',
-            `scheduled_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            `started_at` datetime DEFAULT NULL,
+//            `status` enum('pending', 'scheduled', 'processing','failed', 'failed_permanent', 'completed', 'completed_with_errors') DEFAULT 'pending',
+            `state` enum('pending', 'scheduled', 'processing', 'completed'),
+            `outcome` enum('success', 'partial', 'failed','failed_permanent'),
+
+			`retries` int(11) DEFAULT 0,
+            `last_error_hash` CHAR(32) DEFAULT NULL,
+            `error_message` text,
+
+			`scheduled_at` datetime DEFAULT CURRENT_TIMESTAMP,
+            `started_at` datetime DEFAULT CURRENT_TIMESTAMP,
             `completed_at` datetime DEFAULT NULL,
-            `retries` int(11) DEFAULT 0,
+
+            `metadata` JSON DEFAULT NULL,
             `result` JSON,
             `dependencies` JSON,
-            `error_message` text,
+//            `merge` enum('merge', 'append', 'replace') DEFAULT 'merge',
+
             `user_dismissed` tinyint(1) DEFAULT 0,
-                PRIMARY KEY (`id`),
-                KEY `idx_status_priority` (`status`, `priority`),
-                KEY `idx_user_type` (`user_id`, `type`),
-                KEY `idx_created_at` (`created_at`),
-                KEY `idx_completed_at` (`completed_at`),
-                KEY `idx_user_status` (`user_id`, `status`),
-                KEY `idx_retries_status` (`retries`, `status`),
-                KEY `idx_user_status_created` (`user_id`, `status`, `created_at`),
-                KEY `idx_scheduled_processing` (`scheduled_at`, `status`)
+            `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
+    		`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
+			PRIMARY KEY (`id`),
+		   	KEY `idx_run_queue` (state, priority, scheduled_at),
+			KEY `idx_user_ops` (user_id, state),
+			KEY `idx_user_type_pending` (user_id, type, state),
+			KEY `idx_completed_at` (completed_at)
             )",
+
 		'stats__operation_queue' => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
             `date` date NOT NULL,
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 38f9ddc..43b1f9f 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -1,4 +1,5 @@
 <?php
+
 namespace JVBase\rest\routes;
 
 use JVBase\JVB;
@@ -14,75 +15,75 @@
 use Exception;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit; // Exit if accessed directly
 }
 
 class ContentRoutes extends RestRouteManager
 {
-    protected array $fields = [];
-    protected array $taxonomies = [];
-    protected MetaManager $meta;
-    protected string $post_type = '';
-    protected string $user_id = '';
+	protected array $fields = [];
+	protected array $taxonomies = [];
+	protected MetaManager $meta;
+	protected string $post_type = '';
+	protected string $user_id = '';
 
 	//For Timeline-specific posts
 	protected array $timelineSharedFields = [];
 	protected array $timelineUniqueFields = [];
 
-    //TODO: Ensure we are handling the bulk operations for all processes
-    //TODO: Also invalidate feed caches on updates!!
+	//TODO: Ensure we are handling the bulk operations for all processes
+	//TODO: Also invalidate feed caches on updates!!
 
-    public function __construct()
-    {
-        $this->cache_name = 'user_content_'.get_current_user_id();
-        parent::__construct();
+	public function __construct()
+	{
+		$this->cache_name = 'user_content_' . get_current_user_id();
+		parent::__construct();
 		$this->cache->clear();
-        $this->action = 'dash-';
-        $this->operation_type = 'content_update';
-        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
-    }
+		$this->action = 'dash-';
+		$this->operation_type = 'content_update';
+		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+	}
 
-    /**
-     * Registers content routes
-     * @return void
-     */
-    public function registerRoutes():void
-    {
-        // Base content endpoint
-        register_rest_route($this->namespace, "/content", [
-            [
-                'methods' => 'GET',
-                'callback' => [$this, 'handleContentRequest'],
-                'permission_callback' => [$this, 'checkPermission'],
-            ],
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleContentUpdate'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
+	/**
+	 * Registers content routes
+	 * @return void
+	 */
+	public function registerRoutes(): void
+	{
+		// Base content endpoint
+		register_rest_route($this->namespace, "/content", [
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'handleContentRequest'],
+				'permission_callback' => [$this, 'checkPermission'],
+			],
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'handleContentUpdate'],
+				'permission_callback' => [$this, 'checkPermission']
+			]
+		]);
 
-        //TODO: consolidate create/batch in with create? I don't think we are ever creating a single item
-        register_rest_route($this->namespace, "/create", [
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleContentCreate'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
-        register_rest_route($this->namespace, "/create/batch", [
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleBatchCreation'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
-    }
+		//TODO: consolidate create/batch in with create? I don't think we are ever creating a single item
+		register_rest_route($this->namespace, "/create", [
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'handleContentCreate'],
+				'permission_callback' => [$this, 'checkPermission']
+			]
+		]);
+		register_rest_route($this->namespace, "/create/batch", [
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'handleBatchCreation'],
+				'permission_callback' => [$this, 'checkPermission']
+			]
+		]);
+	}
 
-	protected function initTimelineFields(string $content):void
+	protected function initTimelineFields(string $content): void
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')){
+		if (!Features::forContent($content)->has('is_timeline')) {
 			return;
 		}
 		$config = Features::getConfig($content);
@@ -95,10 +96,11 @@
 
 		$this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
 	}
-	public function getTimelineUniqueFields(string $content):array
+
+	public function getTimelineUniqueFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')){
+		if (!Features::forContent($content)->has('is_timeline')) {
 			return [];
 		}
 		$config = Features::getConfig($content);
@@ -112,156 +114,162 @@
 		}));
 	}
 
-	public function getTimelineSharedFields(string $content):array
+	public function getTimelineSharedFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')){
+		if (!Features::forContent($content)->has('is_timeline')) {
 			return [];
 		}
 		$config = Features::getConfig($content);
 		if (!$config || empty($config)) {
 			return [];
 		}
-		$allFields = $config['fields']??[];
+		$allFields = $config['fields'] ?? [];
 
 		return array_keys(array_filter($allFields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false) {
 				return true;
 			}
 			return false;
 		}));
 	}
 
-    /**
-     * Handle content update/creation
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentUpdate(WP_REST_Request$request):WP_REST_Response
-    {
-        $data = $request->get_params();
-		error_log('Received data: '.print_r($data, true));
-        $user_id = $data['user'];
+	/**
+	 * Handle content update/creation
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleContentUpdate(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_params();
+		error_log('Received data: ' . print_r($data, true));
+		$user_id = $data['user'];
 
+		if (!$this->userCheck($user_id)) {
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'You for real?'
+			]);
+		}
 
-        if (!isset($data['posts']) || !is_array($data['posts'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   =>'Invalid request format'
-            ]);
-        }
+		if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'No posts found'
+			]);
+		}
 
-        $count = count($data['posts']);
-        $operationId = $data['id'];
-        unset($data['user']);
-        unset($data['id']);
+		$count = count($data['posts']);
+		$operationId = $data['id'];
+		unset($data['user']);
+		unset($data['id']);
 
-        $queue = JVB()->queue();
-        $queue->queueOperation(
-            'content_update',
-            $user_id,
-            $data,
-            [
-                'count'   => $count,
-				'chunk_key'	=> 'posts',
+		$queue = JVB()->queue();
+		$queue->queueOperation(
+			'content_update',
+			$user_id,
+			$data,
+			[
+				'count' => $count,
+				'chunk_key' => 'posts',
 				'chunk_size' => 10,
-                'operation_id'      => $operationId
-            ]
-        );
-        return new WP_REST_Response([
-            'success'   => true,
-            'message'   => 'Queued for processing',
-            'operation' => $operationId
-        ]);
-    }
+				'operation_id' => $operationId
+			]
+		);
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Queued for processing',
+			'operation' => $operationId
+		]);
+	}
 
-    /**
-     * Handle content creation
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentCreate(WP_REST_Request$request):WP_REST_Response
-    {
-        $data = $request->get_json_params();
-        $user_id = $data['user'];
+	/**
+	 * Handle content creation
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleContentCreate(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_json_params();
+		$user_id = $data['user'];
 
-        if (!isset($data['posts']) || !is_array($data['posts'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   =>  'Invalid request format'
-            ]);
-        }
+		if (!isset($data['posts']) || !is_array($data['posts'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Invalid request format'
+			]);
+		}
 
-        $count = count($data['posts']);
-        $operationId = $data['id'];
-        unset($data['user']);
-        unset($data['id']);
-        JVB()->queue()->queueOperation(
-            'batch_creation',
-            $user_id,
-            $data,
-            [
-                'count'   => $count,
-                'operation_id'      => $operationId,
-            ]
-        );
+		$count = count($data['posts']);
+		$operationId = $data['id'];
+		unset($data['user']);
+		unset($data['id']);
+		JVB()->queue()->queueOperation(
+			'batch_creation',
+			$user_id,
+			$data,
+			[
+				'count' => $count,
+				'operation_id' => $operationId,
+			]
+		);
 
-        return new WP_REST_Response([
-            'success'   => true,
-            'message'   => 'Queued for processing',
-            'operation' => $operationId
-        ]);
-    }
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Queued for processing',
+			'operation' => $operationId
+		]);
+	}
 
 
-    /**
-     * Handle request
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentRequest(WP_REST_Request $request):WP_REST_Response
-    {
-        $params = $request->get_params();
-        $user_id = $params['user'];
-        if (!$this->userCheck($user_id)) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   => 'User does not match up. Are you a bot?',
-            ]);
-        }
+	/**
+	 * Handle request
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleContentRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$params = $request->get_params();
+		$user_id = $params['user'];
+		if (!$this->userCheck($user_id)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'User does not match up. Are you a bot?',
+			]);
+		}
 
-        $post_status = $params['status'];
-        if ($post_status === 'all') {
-            $post_status = ['publish', 'draft'];
-        } else {
-            $post_status = explode(',', $post_status);
-        }
-        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
+		$post_status = $params['status'];
+		if ($post_status === 'all') {
+			$post_status = ['publish', 'draft'];
+		} else {
+			$post_status = explode(',', $post_status);
+		}
+		$post_type = str_replace('-', '_', jvbCheckBase($params['content']));
 
 
-        // Build query args
-        $args = [
-            'post_type' => $post_type,
-            'posts_per_page' => $params['per_page']??30,
-            'paged' => $params['page'],
-            'orderby' => 'date',
-            'order' => 'DESC',
-            'author' => $user_id,
-            'post_status' => $post_status
-        ];
+		// Build query args
+		$args = [
+			'post_type' => $post_type,
+			'posts_per_page' => $params['per_page'] ?? 30,
+			'paged' => $params['page'],
+			'orderby' => 'date',
+			'order' => 'DESC',
+			'author' => $user_id,
+			'post_status' => $post_status
+		];
 		//Only top level posts for timeline types
 		if (Features::forContent($post_type)->has('is_timeline')) {
 			$args['post_parent'] = 0;
 		}
 
 		//Calendar filters
-		if (Features::forContent($post_type)->has('is_calendar'))  {
+		if (Features::forContent($post_type)->has('is_calendar')) {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
-		$taxonomies = array_filter($params, function($param) {
+		$taxonomies = array_filter($params, function ($param) {
 			return str_starts_with($param, 'tax_');
 		}, ARRAY_FILTER_USE_KEY);
 		if (!empty($taxonomies)) {
@@ -285,7 +293,7 @@
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
-        $key = $this->cache->generateKey($args);
+		$key = $this->cache->generateKey($args);
 		// Check HTTP cache headers with the specific content type
 		$content_type = $params['content'] ?? $params['type'];
 		$cache_check = $this->checkHeaders($request, $content_type, [
@@ -296,39 +304,39 @@
 		}
 
 
-        $cache = $this->cache->get($key);
-        if ($cache) {
-            $response = new WP_REST_Response($cache);
+		$cache = $this->cache->get($key);
+		if ($cache) {
+			$response = new WP_REST_Response($cache);
 			return $this->addCacheHeaders($response);
-        }
+		}
 
-		$this->post_type = jvbCheckBase($params['content']??$params['type']);
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
 		// Only expand search to taxonomies if we're actually going to query
 		if (array_key_exists('s', $args)) {
 			$args = $this->applySearchFilters($args, $params);
 		}
 
-        // Run query
-        $query = new WP_Query($args);
+		// Run query
+		$query = new WP_Query($args);
 
 
-        $this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
-        $this->taxonomies = $this->getTaxonomies($this->post_type);
-        $posts = array_map([$this, 'prepareItem'], $query->posts);
+		$this->fields = jvbGetFields(str_replace('-', '_', $this->post_type));
+		$this->taxonomies = $this->getTaxonomies($this->post_type);
+		$posts = array_map([$this, 'prepareItem'], $query->posts);
 
 
-        $data = [
-            'items' => $posts,
-            'total' => $query->found_posts,
-            'total_pages' => $query->max_num_pages
-        ];
+		$data = [
+			'items' => $posts,
+			'total' => $query->found_posts,
+			'total_pages' => $query->max_num_pages
+		];
 
 
-        $this->cache->set($key, $data);
+		$this->cache->set($key, $data);
 
-        $response = new WP_REST_Response($data);
+		$response = new WP_REST_Response($data);
 		return $this->addCacheHeaders($response);
-    }
+	}
 
 	protected function applySearchFilters(array $args, array $params): array
 	{
@@ -375,7 +383,7 @@
 		$term_queries = [];
 
 		foreach ($taxonomies as $taxonomy) {
-			$taxonomy_term_ids = array_filter($matching_term_ids, function($term_id) use ($taxonomy) {
+			$taxonomy_term_ids = array_filter($matching_term_ids, function ($term_id) use ($taxonomy) {
 				$term = get_term($term_id);
 				return !is_wp_error($term) && $term->taxonomy === $taxonomy;
 			});
@@ -411,49 +419,49 @@
 		return $args;
 	}
 
-    /**
-     * Gets allowed taxonomies for a particular content
-     * @param string $content
-     *
-     * @return array
-     */
-    protected function getTaxonomies(string $content):array
-    {
-        $taxonomy_for = jvbGlobalTaxonomyFor();
-        $out = [];
-        foreach ($taxonomy_for as $tax => $postTypes) {
-            if (in_array($content, $postTypes)) {
-                $out[BASE.$tax] = [
-                    'label' => JVB_CONTENT[$content]['plural'],
-                    'icon'  => $tax,
-                ];
-            }
-        }
-        return $out;
-    }
+	/**
+	 * Gets allowed taxonomies for a particular content
+	 * @param string $content
+	 *
+	 * @return array
+	 */
+	protected function getTaxonomies(string $content): array
+	{
+		$taxonomy_for = jvbGlobalTaxonomyFor();
+		$out = [];
+		foreach ($taxonomy_for as $tax => $postTypes) {
+			if (in_array($content, $postTypes)) {
+				$out[BASE . $tax] = [
+					'label' => JVB_CONTENT[$content]['plural'],
+					'icon' => $tax,
+				];
+			}
+		}
+		return $out;
+	}
 
-    /**
-     * Processes operation from queue
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array
-     */
-    protected function processBatches(object $operation, array $data):array
-    {
-        $this->user_id = $operation->user_id;
-        $posts = $data['posts'];
+	/**
+	 * Processes operation from queue
+	 * @param object $operation
+	 * @param array $data
+	 *
+	 * @return array
+	 */
+	protected function processBatches(object $operation, array $data): array
+	{
+		$this->user_id = $operation->user_id;
+		$posts = $data['posts'];
 
-        if (empty($posts)) {
-            return [
-                'success' => false,
-                'message' => 'No posts to update'
-            ];
-        }
+		if (empty($posts)) {
+			return [
+				'success' => false,
+				'message' => 'No posts to update'
+			];
+		}
 
-        $results = [];
+		$results = [];
 
-        foreach ($posts as $ID => $post_data) {
+		foreach ($posts as $ID => $post_data) {
 			if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
 				// Handle timeline posts - ensure we have a valid integer ID
 				$parent_id = (int)$ID;
@@ -473,35 +481,35 @@
 			}
 			if (str_starts_with($ID, 'new')) {
 
-				error_log('New post detected. Creating... with: '.print_r([
+				error_log('New post detected. Creating... with: ' . print_r([
 						'post_author' => $this->user_id,
-						'post_type'		=> jvbCheckBase($post_data['content']),
-						'post_title'	=> $post_data['post_title']??'',
-						'post_status'	=> $post_data['status']??'draft',
+						'post_type' => jvbCheckBase($post_data['content']),
+						'post_title' => $post_data['post_title'] ?? '',
+						'post_status' => $post_data['status'] ?? 'draft',
 					], true));
-				error_log('Recieved Data: '.print_r($post_data, true));
+				error_log('Recieved Data: ' . print_r($post_data, true));
 				$ID = wp_insert_post([
 					'post_author' => $this->user_id,
-					'post_type'		=> jvbCheckBase($post_data['content']),
-					'post_title'	=> $post_data['post_title']??'',
-					'post_status'	=> $post_data['status']??'draft',
+					'post_type' => jvbCheckBase($post_data['content']),
+					'post_title' => $post_data['post_title'] ?? '',
+					'post_status' => $post_data['status'] ?? 'draft',
 				]);
 				if (!$ID || is_wp_error($ID)) {
 					$results[$ID] = [
 						'success' => false,
-						'message'	=> 'Couldn\'t Create Post'
+						'message' => 'Couldn\'t Create Post'
 					];
 					continue;
 				}
 				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function($key) use ($fields) {
+				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
 					return array_key_exists($key, $fields);
 				}, ARRAY_FILTER_USE_KEY);
 
 				$meta = new MetaManager($ID, 'post');
 				$success = $meta->setAll($allowedFields);
 				$results[$ID] = [
-					'success'	=> $success
+					'success' => $success
 				];
 			} else {
 				if (!$this->verifyOwnership($ID)) {
@@ -511,7 +519,7 @@
 					];
 					continue;
 				}
-				error_log('Saving post data: '.print_r($post_data, true));
+				error_log('Saving post data: ' . print_r($post_data, true));
 
 				if (array_key_exists('post_status', $post_data)) {
 					switch ($post_data['post_status']) {
@@ -523,7 +531,7 @@
 							break;
 						case 'draft':
 							$result = wp_update_post([
-								'ID'	=> $ID,
+								'ID' => $ID,
 								'post_status' => 'draft'
 							]);
 							break;
@@ -535,26 +543,26 @@
 							return ['success' => (bool)$result];
 					}
 				}
-				error_log('Updating data: '.print_r($post_data, true));
+				error_log('Updating data: ' . print_r($post_data, true));
 				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function($key) use ($fields) {
+				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
 					return array_key_exists($key, $fields);
 				}, ARRAY_FILTER_USE_KEY);
 
-				error_log('Allowed Fields: '.print_r($allowedFields, true));
+				error_log('Allowed Fields: ' . print_r($allowedFields, true));
 				$meta = new MetaManager($ID, 'post');
 				$success = $meta->setAll($allowedFields);
 				$results[$ID] = [
-					'success'	=> $success
+					'success' => $success
 				];
 
 			}
 
-            CacheManager::for($post_data['content'])->clear();
+			CacheManager::for($post_data['content'])->clear();
 			if (jvbSiteUsesFeedBlock()) {
 				CacheManager::for('feed')->clear();
 			}
-        }
+		}
 
 		if (jvbSiteHasNotifications()) {
 			$this->notifications = JVB()->notification();
@@ -567,11 +575,11 @@
 		}
 
 
-        return [
-            'success' => true,
-            'result' => $results
-        ];
-    }
+		return [
+			'success' => true,
+			'result' => $results
+		];
+	}
 
 	/**
 	 * Extracts the postdata for timeline post child posts from the pseudo-repeater element
@@ -579,7 +587,7 @@
 	 * @param array $post_data
 	 * @return array|true[]
 	 */
-	protected function processTimelinePost(int $parent_id, array $post_data):array
+	protected function processTimelinePost(int $parent_id, array $post_data): array
 	{
 		if (!$this->verifyOwnership($parent_id)) {
 			return ['success' => false, 'message' => 'No permission'];
@@ -608,7 +616,7 @@
 		$clearParent = false;
 		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
 			// Remove post_title and post_thumbnail from shared taxonomies
-			$sharedTaxonomies = array_filter($sharedData, function($key) {
+			$sharedTaxonomies = array_filter($sharedData, function ($key) {
 				return $key !== 'post_title' && $key !== 'post_thumbnail';
 			}, ARRAY_FILTER_USE_KEY);
 
@@ -669,14 +677,14 @@
 				'post_parent' => $parent_id,
 				'orderby' => 'menu_order',
 				'post_status' => ['publish', 'draft'],
-				'fields'=> 'ids'
+				'fields' => 'ids'
 			]);
 
 			$prevDate = null;
 
-			foreach($post_data['timeline'] as $order => $timeline) {
+			foreach ($post_data['timeline'] as $order => $timeline) {
 				// Get unique fields for this specific timeline entry
-				$allowedFields = array_filter($timeline, function($key) use ($ignore) {
+				$allowedFields = array_filter($timeline, function ($key) use ($ignore) {
 					return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
 				}, ARRAY_FILTER_USE_KEY);
 
@@ -744,7 +752,7 @@
 
 				if (count($post_updates) > 1) {
 					$result = wp_update_post($post_updates);
-					error_log('Updated post '.$timeline['id'].' with: '.print_r($post_updates, true).' Result: '.$result);
+					error_log('Updated post ' . $timeline['id'] . ' with: ' . print_r($post_updates, true) . ' Result: ' . $result);
 					$clearParent = true;
 				}
 
@@ -755,9 +763,9 @@
 				// Set number taxonomy to menu_order (always update for reordering)
 				if (!$is_parent) {
 					$number_value = $order;
-					$term = get_term_by('name', (string)$number_value, BASE.'number');
+					$term = get_term_by('name', (string)$number_value, BASE . 'number');
 					if (!$term) {
-						$result = wp_insert_term((string)$number_value, BASE.'number');
+						$result = wp_insert_term((string)$number_value, BASE . 'number');
 						if ($result && !is_wp_error($result)) {
 							$term = $result['term_id'];
 						}
@@ -775,10 +783,10 @@
 						$date2 = new \DateTime($newDate);
 						$weeks = floor($date1->diff($date2)->days / 7);
 						if ($weeks > 0) {
-							$termToCheck = $weeks.' Weeks';
-							$term = get_term_by('name', $termToCheck, BASE.'timeline');
+							$termToCheck = $weeks . ' Weeks';
+							$term = get_term_by('name', $termToCheck, BASE . 'timeline');
 							if (!$term) {
-								$result = wp_insert_term($termToCheck, BASE.'timeline');
+								$result = wp_insert_term($termToCheck, BASE . 'timeline');
 								if ($result && !is_wp_error($result)) {
 									$term = $result['term_id'];
 								}
@@ -791,13 +799,13 @@
 				}
 				$prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
 
-				$updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
+				$updateValues = array_filter($allowedFields, function ($value, $key) use ($oldValues) {
 					return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
 				}, ARRAY_FILTER_USE_BOTH);
-				error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
+				error_log('Setting values for ' . $timeline['id'] . ': ' . print_r($updateValues, true));
 
 				$meta->setAll($updateValues);
-				$timeline['id'] = (int) $timeline['id'];
+				$timeline['id'] = (int)$timeline['id'];
 
 				$success[] = $timeline['id'];
 			}
@@ -822,130 +830,131 @@
 		]];
 	}
 
-    /**
-     * Handle batch content creation from uploads
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleBatchCreation(WP_REST_Request $request):WP_REST_Response
-    {
-        //Operation has two parts
-        //First, queue image processing
-        //Then queue post creation from the stored IDs, depending on mode
-            //if direct, each image becomes a new post
-            //if selection, each group becomes its own post,
-                // and ungrouped items each become their own post
-        if (!isset($_FILES['files'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   => 'No files uploaded...',
-            ]);
-        }
+	/**
+	 * Handle batch content creation from uploads
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleBatchCreation(WP_REST_Request $request): WP_REST_Response
+	{
+		//Operation has two parts
+		//First, queue image processing
+		//Then queue post creation from the stored IDs, depending on mode
+		//if direct, each image becomes a new post
+		//if selection, each group becomes its own post,
+		// and ungrouped items each become their own post
+		if (!isset($_FILES['files'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'No files uploaded...',
+			]);
+		}
 
-        $data = $request->get_params();
+		$data = $request->get_params();
 
 
-        $user_id = $data['user'];
-        if (!$this->userCheck($user_id)) {
-            return new WP_REST_Response([
-                'success'   => 'false',
-                'message'   => 'Invalid user match... are you a bot?'
-            ]);
-        }
-        $operation_id = $data['id'];
-        $response = new WP_REST_Response([
-            'success' => true,
-            'message' => 'Successfully sent to server. Added to queue.',
-            'operation_id' => $operation_id,
-            'status' => 'pending'
-        ]);
-        $this->queue = JVB()->queue();
-        JVB()->routes('uploads')->handleUploadRequest($request, false);
-        $this->queue->queueOperation(
-            'batch_creation',
-            $user_id,
-            [
-                'content' => $request->get_param('content'),
-                'mode'      => $request->get_param('mode') ?: 'direct',
-                'files_data'=> $request->get_param('files_data')
-            ],
-            [
-                'operation_id'  => $operation_id,
-                'priority'      => 'high',
-                'notification'  => true,
-                'depends_on'    => $operation_id.'_upload'
-            ]
-        );
+		$user_id = $data['user'];
+		if (!$this->userCheck($user_id)) {
+			return new WP_REST_Response([
+				'success' => 'false',
+				'message' => 'Invalid user match... are you a bot?'
+			]);
+		}
+		$operation_id = $data['id'];
+		$response = new WP_REST_Response([
+			'success' => true,
+			'message' => 'Successfully sent to server. Added to queue.',
+			'operation_id' => $operation_id,
+			'status' => 'pending'
+		]);
+		$this->queue = JVB()->queue();
+		JVB()->routes('uploads')->handleUploadRequest($request, false);
+		$this->queue->queueOperation(
+			'batch_creation',
+			$user_id,
+			[
+				'content' => $request->get_param('content'),
+				'mode' => $request->get_param('mode') ?: 'direct',
+				'files_data' => $request->get_param('files_data')
+			],
+			[
+				'operation_id' => $operation_id,
+				'priority' => 'high',
+				'notification' => true,
+				'depends_on' => $operation_id . '_upload'
+			]
+		);
 
-        return $response;
-    }
+		return $response;
+	}
 
-    /**
-     * Generates a post title, based on content type
-     * @param string $content the post type
-     *
-     * @return string
-     */
-    protected function generatePostTitle(string $content):string
-    {
-        $username = get_user_meta($this->user_id, 'first_name', true);
-        $link = get_user_meta($this->user_id, BASE.'link', true);
-        $city = jvbArtistCity($link);
-        return ucfirst($content).' by '.$city.' artist '.$username;
-    }
+	/**
+	 * Generates a post title, based on content type
+	 * @param string $content the post type
+	 *
+	 * @return string
+	 */
+	protected function generatePostTitle(string $content): string
+	{
+		$username = get_user_meta($this->user_id, 'first_name', true);
+		$link = get_user_meta($this->user_id, BASE . 'link', true);
+		$city = jvbArtistCity($link);
+		return ucfirst($content) . ' by ' . $city . ' artist ' . $username;
+	}
 
-    /**
-     * @param WP_Post $post the wordpress post object
-     *
-     * @return array
-     */
-    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
-    {
+	/**
+	 * @param WP_Post $post the post object
+	 *
+	 * @return array
+	 */
+	protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true): array
+	{
 		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
 			$this->initTimelineFields($post->post_type);
 			return $this->formatTimeline($post);
 		}
-        $this->meta = new MetaManager($post->ID, 'post');
-        $data = [
-            'id'        => $post->ID,
-			'title'		=> $post->post_title,
-            'status'    => $post->post_status,
-            'date'      => $post->post_date,
-            'modified'  => $post->post_modified,
-            'thumbnail' => get_the_post_thumbnail_url($post->ID),
-            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
-            'icon'      => $this->post_type,
-            'taxonomies'=> [],
-            'fields'    => ($fields) ? $this->meta->getAll() : [],
-			'images'	=> [],
-        ];
+		$this->meta = new MetaManager($post->ID, 'post');
+		$data = [
+			'id' => $post->ID,
+			'title' => $post->post_title,
+			'status' => $post->post_status,
+			'date' => $post->post_date,
+			'modified' => $post->post_modified,
+			'thumbnail' => get_the_post_thumbnail_url($post->ID),
+			'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
+			'icon' => $this->post_type,
+			'taxonomies' => [],
+			'fields' => ($fields) ? $this->meta->getAll() : [],
+			'images' => [],
+		];
 
-        // Add taxonomy terms
-        foreach ($this->taxonomies as $taxonomy => $options) {
-            $tax = str_replace(BASE, '', $taxonomy);
-            $terms = wp_get_object_terms(
-                $post->ID,
-                $taxonomy,
-                ['fields' => 'id=>name']
-            );
-            $data['taxonomies'][$tax] = [
-                'terms' => (is_wp_error($terms))? [] : $terms,
-                'name'  => $options['label'],
-                'icon'  => $tax
-            ];
-        }
+		// Add taxonomy terms
+		foreach ($this->taxonomies as $taxonomy => $options) {
+			$tax = str_replace(BASE, '', $taxonomy);
+			$terms = wp_get_object_terms(
+				$post->ID,
+				$taxonomy,
+				['fields' => 'id=>name']
+			);
+			$data['taxonomies'][$tax] = [
+				'terms' => (is_wp_error($terms)) ? [] : $terms,
+				'name' => $options['label'],
+				'icon' => $tax
+			];
+		}
 
 		$images = $this->extractImages();
 
 
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
+		if (!empty($images)) {
+			$data['images'] = $images;
+		}
 
-        return $data;
-    }
-	protected function extractImages(array $fields = []):array
+		return $data;
+	}
+
+	protected function extractImages(array $fields = []): array
 	{
 		//Extract images
 		$images = [];
@@ -959,11 +968,11 @@
 
 		if (!empty($get)) {
 			$allImages = $this->meta->getAll($get);
-			foreach($allImages as $k => $imgs){
+			foreach ($allImages as $k => $imgs) {
 				$temp = explode(',', $imgs);
-				foreach($temp as $img) {
+				foreach ($temp as $img) {
 					if (is_numeric($img) && !array_key_exists($img, $images)) {
-						$images[$img] = jvbImageData((int) $img);
+						$images[$img] = jvbImageData((int)$img);
 					}
 				}
 			}
@@ -971,7 +980,7 @@
 		return $images;
 	}
 
-	public function formatTimeline(WP_Post $post):array
+	public function formatTimeline(WP_Post $post): array
 	{
 		$item = $this->prepareItem($post, true, false);
 		//Step 1: Get the fields that apply to all posts
@@ -979,7 +988,7 @@
 		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
 		//Step 2: Get the fields for each individual posts
-		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']);
 		array_unshift($children, $post->ID);
 
 		$subFields = [];
@@ -987,184 +996,185 @@
 		foreach ($children as $child) {
 			$meta = new MetaManager($child, 'post');
 			$f = $meta->getAll($this->timelineUniqueFields);
-			$f =  ['id' => $child] + $f;
+			$f = ['id' => $child] + $f;
 			$subFields[] = $f;
 
-			$images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
+			$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
 		}
 		$item['fields']['timeline'] = $subFields;
 		$item['images'] = $item['images'] + $images;
 
+
 		return $item;
 	}
 
-    /**
-     * Builds the taxonomy query
-     * @param array $taxonomies
-     *
-     * @return array|string[]
-     */
-    protected function buildTaxQuery(array $taxonomies):array
-    {
-        $tax_query = [];
-		error_log('Taxonomies in query: '.print_r($taxonomies, true));
+	/**
+	 * Builds the taxonomy query
+	 * @param array $taxonomies
+	 *
+	 * @return array|string[]
+	 */
+	protected function buildTaxQuery(array $taxonomies): array
+	{
+		$tax_query = [];
+		error_log('Taxonomies in query: ' . print_r($taxonomies, true));
 
-        foreach ($taxonomies as $taxonomy => $terms) {
-            if (!empty($terms)) {
-                $tax_query[] = [
-                    'taxonomy' => jvbCheckBase($taxonomy),
-                    'field' => 'term_id',
-                    'terms' => array_map('absint', (array)$terms)
-                ];
-            }
-        }
+		foreach ($taxonomies as $taxonomy => $terms) {
+			if (!empty($terms)) {
+				$tax_query[] = [
+					'taxonomy' => jvbCheckBase($taxonomy),
+					'field' => 'term_id',
+					'terms' => array_map('absint', (array)$terms)
+				];
+			}
+		}
 
 
-        return count($tax_query) > 1
-            ? array_merge(['relation' => 'AND'], $tax_query)
-            : $tax_query;
-    }
+		return count($tax_query) > 1
+			? array_merge(['relation' => 'AND'], $tax_query)
+			: $tax_query;
+	}
 
-    /**
-     * Builds the date query
-     * @param array $date_params
-     *
-     * @return array
-     */
-    protected function buildDateQuery(array $date_params):array
-    {
-        $query = [];
+	/**
+	 * Builds the date query
+	 * @param array $date_params
+	 *
+	 * @return array
+	 */
+	protected function buildDateQuery(array $date_params): array
+	{
+		$query = [];
 
-        if (!empty($date_params['after'])) {
-            $query['after'] = sanitize_text_field($date_params['after']);
-        }
+		if (!empty($date_params['after'])) {
+			$query['after'] = sanitize_text_field($date_params['after']);
+		}
 
-        if (!empty($date_params['before'])) {
-            $query['before'] = sanitize_text_field($date_params['before']);
-        }
+		if (!empty($date_params['before'])) {
+			$query['before'] = sanitize_text_field($date_params['before']);
+		}
 
-        if (isset($date_params['inclusive'])) {
-            $query['inclusive'] = (bool)$date_params['inclusive'];
-        }
+		if (isset($date_params['inclusive'])) {
+			$query['inclusive'] = (bool)$date_params['inclusive'];
+		}
 
-        return empty($query) ? [] : [$query];
-    }
+		return empty($query) ? [] : [$query];
+	}
 
-    /**
-     * @param int $post_id
-     *
-     * @return bool
-     */
-    protected function verifyOwnership(int $post_id):bool
-    {
-        $post = get_post($post_id);
-        return $post && $post->post_author == $this->user_id;
-    }
+	/**
+	 * @param int $post_id
+	 *
+	 * @return bool
+	 */
+	protected function verifyOwnership(int $post_id): bool
+	{
+		$post = get_post($post_id);
+		return $post && $post->post_author == $this->user_id;
+	}
 
-    /**
-     * Processes operation from Operation Queue
-     * @param WP_Error|array $result
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array|WP_Error
-     */
-    public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error
-    {
-        if ($operation->type === 'batch_creation') {
-            $JVB = JVB();
-            $queue = $JVB->queue();
+	/**
+	 * Processes operation from Operation Queue
+	 * @param WP_Error|array $result
+	 * @param object $operation
+	 * @param array $data
+	 *
+	 * @return array|WP_Error
+	 */
+	public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
+	{
+		if ($operation->type === 'batch_creation') {
+			$JVB = JVB();
+			$queue = $JVB->queue();
 
-            $images = $queue->getOperationValue($operation->id.'_upload', 'result')??false;
+			$images = $queue->getOperationValue($operation->id . '_upload', 'result') ?? false;
 
-            $this->user_id = $operation->user_id;
-            $this->post_type = BASE.$data['content'];
-            try {
-                $results = [];
-                if ($images) {
-                    if ($data['mode'] == 'selection') {
-                        $total = count($images);
-                        foreach ($images as $group => $files) {
-                            $settings = json_decode($data['files_data'][$group]);
+			$this->user_id = $operation->user_id;
+			$this->post_type = BASE . $data['content'];
+			try {
+				$results = [];
+				if ($images) {
+					if ($data['mode'] == 'selection') {
+						$total = count($images);
+						foreach ($images as $group => $files) {
+							$settings = json_decode($data['files_data'][$group]);
 
-                            switch ($settings->type) {
-                                case 'group':
-                                    $featuredIndex = $settings->metadata->featuredFile??0;
-                                    $title = $settings->metadata->title??$this->generatePostTitle($data['content']);
-                                    $new = wp_insert_post([
-                                        'post_type'     => BASE.$data['content'],
-                                        'post_title'    => $title,
-                                        'post_status'   => 'draft',
-                                        'post_author'   => $operation->user_id
-                                    ]);
-                                    if ($new && !is_wp_error($new)) {
-                                        set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
-                                        unset($files[$featuredIndex]);
-                                        if (!empty($files)) {
-                                            $meta = new MetaManager($new, 'post');
-                                            $IDs = array_column($files, 'attachment_id');
-                                            $meta->updateValue('gallery', implode(',', $IDs));
-                                        }
-                                        $results[] = $new;
+							switch ($settings->type) {
+								case 'group':
+									$featuredIndex = $settings->metadata->featuredFile ?? 0;
+									$title = $settings->metadata->title ?? $this->generatePostTitle($data['content']);
+									$new = wp_insert_post([
+										'post_type' => BASE . $data['content'],
+										'post_title' => $title,
+										'post_status' => 'draft',
+										'post_author' => $operation->user_id
+									]);
+									if ($new && !is_wp_error($new)) {
+										set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
+										unset($files[$featuredIndex]);
+										if (!empty($files)) {
+											$meta = new MetaManager($new, 'post');
+											$IDs = array_column($files, 'attachment_id');
+											$meta->updateValue('gallery', implode(',', $IDs));
+										}
+										$results[] = $new;
 //                                        $queue->updateOperationProgress($operation->id, $group + 1, $total);
-                                    }
-                                    break;
-                                default:
-                                    foreach ($files as $img) {
-                                        $new = wp_insert_post([
-                                            'post_type'     => BASE. $data['content'],
-                                            'post_title'    => $this->generatePostTitle($data['content']),
-                                            'post_status'   => 'draft',
-                                            'post_author'   => $operation->user_id
-                                        ]);
+									}
+									break;
+								default:
+									foreach ($files as $img) {
+										$new = wp_insert_post([
+											'post_type' => BASE . $data['content'],
+											'post_title' => $this->generatePostTitle($data['content']),
+											'post_status' => 'draft',
+											'post_author' => $operation->user_id
+										]);
 
-                                        if ($new && !is_wp_error($new)) {
-                                            set_post_thumbnail($new, $img['attachment_id']);
-                                            $results[] = $new;
+										if ($new && !is_wp_error($new)) {
+											set_post_thumbnail($new, $img['attachment_id']);
+											$results[] = $new;
 //                                            $queue->updateOperationProgress($operation->id, $group + 1, $total);
-                                        }
-                                    }
-                                    break;
-                            }
-                        }
-                    } else {
-                        $total = count($images);
-                        foreach ($images as $key => $img) {
-                            $new = wp_insert_post([
-                                'post_type'     => BASE.$data['content'],
-                                'post_title'    => $this->generatePostTitle($data['content']),
-                                'post_status'   => 'draft',
-                                'post_author'   => $operation->user_id
-                            ]);
-                            if ($new && !is_wp_error($new)) {
-                                set_post_thumbnail($new, $img['attachment_id']);
-                            }
-                            $results[] = $new;
+										}
+									}
+									break;
+							}
+						}
+					} else {
+						$total = count($images);
+						foreach ($images as $key => $img) {
+							$new = wp_insert_post([
+								'post_type' => BASE . $data['content'],
+								'post_title' => $this->generatePostTitle($data['content']),
+								'post_status' => 'draft',
+								'post_author' => $operation->user_id
+							]);
+							if ($new && !is_wp_error($new)) {
+								set_post_thumbnail($new, $img['attachment_id']);
+							}
+							$results[] = $new;
 //                            $queue->updateOperationProgress($operation->id, $key + 1, $total);
-                        }
-                    }
+						}
+					}
 
-                    //Clear cache
+					//Clear cache
 					CacheManager::for($data['content'])->clear();
-                    CacheManager::for('feed')->clear();
-                }
+					CacheManager::for('feed')->clear();
+				}
 
 				return [
-					'success'	=> true,
-					'result'	=> $results
+					'success' => true,
+					'result' => $results
 				];
-            } catch (Exception $e) {
-                $JVB->error()->log(
-                    '[ContentRoutes]:processOperation',
-                    $e->getMessage()
-                );
-            }
+			} catch (Exception $e) {
+				$JVB->error()->log(
+					'[ContentRoutes]:processOperation',
+					$e->getMessage()
+				);
+			}
 
-            return $results;
-        } elseif ($operation->type == 'content_update') {
-            $result = $this->processBatches($operation, $data);
-        }
+			return $results;
+		} elseif ($operation->type == 'content_update') {
+			$result = $this->processBatches($operation, $data);
+		}
 
-        return $result;
-    }
+		return $result;
+	}
 }
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index ecef889..521de4f 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -1113,8 +1113,8 @@
 		ob_start();
 		?>
 		<div class="item-select">
-			<input type="checkbox" class="select-item">
-			<label class="select-item-label">
+			<input type="checkbox" class="select-item" name="select-item" id="item">
+			<label class="select-item-label" for="item">
 				<span class="screen-reader-text">Select this <?= $this->singular ?></span>
 			</label>
 		</div>
@@ -1262,6 +1262,10 @@
 						<?php
 						if (in_array('edit', $this->caps)) {
 							echo $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '';
+							if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) {
+								$config['autocomplete'] = true;
+							}
+
 							echo $this->meta->render('form', $name, $config);
 							echo $makeThisDetailed ? '</details>' : '';
 						} else {
@@ -1340,6 +1344,11 @@
 					?>
 					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
 						<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+						<?php
+						if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) {
+							$config['autocomplete'] = true;
+						}
+						?>
 						<?php $this->meta->render('form', $name, $config); ?>
 						<?= $makeThisDetailed ? '</details>' : '' ?>
 					</td>
@@ -1472,10 +1481,10 @@
 	protected function renderStatusRadios(): string {
 		ob_start();
 		?>
-		<div class="radio-options status-options row">
+		<div class="radio-options status-options row" data-field="post_status" data-field-type="radio">
 			<?php foreach ($this->statuses as $status):
 				if ($status === 'all') continue;
-				if (!in_array($status, $this->allowedStatuses)) continue;
+				if (!array_key_exists($status, $this->allowedStatuses)) continue;
 				$config = $this->allowedStatuses[$status];
 				?>
 
@@ -1488,6 +1497,7 @@
 					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
 				</label>
 			<?php endforeach; ?>
+			<span class="validation-message" hidden role="alert"></span>
 		</div>
 		<?php
 		return ob_get_clean();
@@ -1550,7 +1560,7 @@
 			<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
 			<input type="hidden" name="content" value="<?=$this->dataType?>" />
 			<div class="fields">
-				<div class="field-group radio-options row">
+				<div class="field-group radio-options row" data-field="post_status" data-field-type="radio">
 					<span>Status:</span>
 					<?php
 					$this->getApplicableStatuses('edit');
diff --git a/src/feed/view.js b/src/feed/view.js
index 8066082..3590a17 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -6,6 +6,7 @@
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
 		this.cache = new window.jvbCache('feed');
+		this.templates = window.jvbTemplates;
 
 		this.config = {
 			source: '',
@@ -20,6 +21,7 @@
 	}
 	init() {
 		this.initElements();
+		this.defineTemplates();
 		this.initListeners();
 		this.initFilters();
 
@@ -326,17 +328,9 @@
 		const term = this.selector.store.get(termId);
 		if (!term) return;
 		if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
-		let icon = this.getTaxonomyIcon(term.taxonomy);
-		let template = window.getTemplate('feedTerm');
-		if (!template) return;
-		let [iconEl,span] = [template.querySelector('.icon'), template.querySelector('span')];
-		if (!iconEl || !span) return;
-		template.dataset.id = term.id;
-		template.dataset.taxonomy = term.taxonomy;
-		iconEl.className = `icon icon-${icon}`;
-		span.textContent = term.name;
 
-		this.ui.selected.append(template);
+		term.icon = this.getTaxonomyIcon(term.taxonomy);
+		this.ui.selected.append(this.templates.create('feedTerm', term));
 	}
 
 	processCachedFilters() {
@@ -478,26 +472,18 @@
 			this.showEmptyState();
 			this.a11y.announceItems(0, this.isFirstPage());
 		} else {
-			const fragment = document.createDocumentFragment();
-			const processBatch = (startIndex) => {
-				const endIndex = Math.min(startIndex + 10, items.length);
-				for (let i = startIndex; i < endIndex; i++) {
-					const item = items[i];
-					const element = this.createItemElement(item);
-					fragment.append(element);
-				}
-				if (endIndex < items.length) {
-					requestAnimationFrame(() => processBatch(endIndex));
-				} else {
+			window.chunkIt(
+				items,
+				(item) => this.createItemElement(item),
+				(fragment) => {
 					this.removePlaceholders();
 					this.ui.grid.append(fragment);
-
 					if (this.config.gallery) this.gallery.buildGalleryItems('.item img');
 					this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
 					this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false);
-				}
-			};
-			processBatch(0);
+				},
+				5
+			).then(()=>{});
 		}
 
 		this.updateFilterControls();
@@ -505,48 +491,11 @@
 
 	showEmptyState() {
 		window.removeChildren(this.ui.grid);
-		let template = window.getTemplate('emptyState');
-		if (!template) return;
-		this.ui.grid.append(template);
-
+		this.ui.grid.append(this.templates.create('emptyState'));
 	}
 
 	createItemElement(item) {
-		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
-		const isTimeline = Object.hasOwn(template.dataset, 'timeline');
-
-		for (let [fieldName, value] of Object.entries(item.fields)) {
-			if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
-
-			let el = template.querySelector(`[data-field="${fieldName}"]`);
-			if (!el) continue;
-			if (value === '') {
-				el.remove();
-				continue;
-			}
-
-			if (this.isImageField(item, value)) {
-				this.formatImageField(el, value, item);
-			} else if (this.isTaxonomyField(item, fieldName)) {
-				this.formatTaxonomyField(el, item, fieldName, value);
-			} else if (this.isTimeField(el)) {
-				this.formatTimeField(el, value);
-			} else {
-				this.formatField(el, value);
-			}
-		}
-
-		let link = template.querySelector('a');
-		if (link && item.url !== '') {
-			[link.href, link.title] =
-				[item.url, `View ${item.fields['post_title']??'Item'}`];
-		}
-
-		if (isTimeline) {
-			this.addTimelineElements(item, template);
-		}
-
-		return template;
+		return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item);
 	}
 	splitIDs(value) {
 		return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
@@ -679,6 +628,64 @@
 		}
 	}
 
+	defineTemplates() {
+		const T = this.templates;
+		const f = this;
+
+		T.define('feedTerm', {
+			refs: {
+				icon: '.icon',
+				span: 'span'
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.id = data.id;
+				el.dataset.taxonomy = data.taxonomy;
+				if (refs.icon) refs.icon.className = `icon icon=${data.icon}`;
+				if (refs.span) refs.span.textContent = data.name;
+			}
+		});
+		T.define('emptyState');
+
+		this.contentTypes.forEach(content => {
+			T.define(`feedItem${window.uppercaseFirst(content)}`, {
+				refs: {
+					link: 'a',
+				},
+				manyRefs: {
+					fields: '[data-field]',
+				},
+				setup({el, refs, manyRefs, data}) {
+					const isTimeline = Object.hasOwn(el.dataset, 'timeline');
+					if (manyRefs.fields) {
+						for (let field of manyRefs.fields) {
+							if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue;
+
+							const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false;
+							if (!value) {
+								field.remove();
+								continue;
+							}
+							if (f.isImageField(data, value)) {
+								f.formatImageField(field, value, data);
+							} else if (f.isTaxonomyField(data, field.dataset.field)) {
+								f.formatTaxonomyField(field, data, field.dataset.field, value);
+							} else if (f.isTimeField(field)) {
+								f.formatTimeField(field, value);
+							} else {
+								f.formatField(field, value);
+							}
+						}
+						if (refs.link && data.url !== '') {
+							refs.link.href = data.url;
+							refs.link.title = `View ${data.fields['post_title']??'Item'}`;
+						}
+						if (isTimeline ) f.addTimelineElements(data, el);
+					}
+				}
+			})
+		});
+	}
+
 	// addPlaceholders() {
 	// 	let total = this.contentTypes.length;
 	// 	const fragment = document.createDocumentFragment();
diff --git a/src/forms/view.js b/src/forms/view.js
index 20b1f71..5b58641 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -9,30 +9,31 @@
  */
 class FormBlock {
 	constructor() {
-		this.controller = new window.jvbForm();
+		this.controller = window.jvbForm;
 
 		document.querySelectorAll('.jvb-form-block form').forEach(form => {
 			this.controller.registerForm(form, {
 				autosave: true,
-				autoUpload: false
+				autoUpload: false,
+				showSummary: true,
 			});
 		});
 
 		this.controller.subscribe((event, data) => {
 			if (event === 'form-submit') {
-				this.handleFormSubmission(data);
+				this.handleFormSubmission(data).then(()=>{});
 			}
 		});
 	}
 
-	async handleFormSubmission(data) {
-		const { formId, config: formConfig, fullData: formData } = data;
-		const form = formConfig.element;
+	async handleFormSubmission(eventData) {
+		const { config, data } = eventData;
+		const form = config.element;
 
 		const submitData = new FormData();
 
 		// Add regular form fields
-		for (const [key, value] of Object.entries(formData)) {
+		for (const [key, value] of Object.entries(data)) {
 			if (key === '_wpnonce' || key === '_wp_http_referer') continue;
 
 			if (Array.isArray(value)) {
@@ -56,7 +57,7 @@
 			}
 		}
 
-		this.controller.showFormStatus(formId, 'uploading');
+		this.controller.showFormStatus(config.id, 'uploading');
 
 		try {
 			const response = await fetch(`${jvbSettings.api}forms`, {
@@ -68,13 +69,13 @@
 			const result = await response.json();
 
 			if (!response.ok) {
-				this.controller.showFormStatus(formId, 'error');
+				this.controller.showFormStatus(config.id, 'error');
 				this.controller.handleFormError(form, result);
 				return;
 			}
 
-			this.controller.showFormStatus(formId, 'submitted');
-			this.controller.showSummary(formId, '.jvb-form-block');
+			this.controller.showFormStatus(config.id, 'submitted');
+			this.controller.showSummary(config.id, '.jvb-form-block');
 
 			// Clean up uploaded files
 			if (window.jvbUploads) {
@@ -87,13 +88,13 @@
 
 		} catch (error) {
 			console.error('Form submission error:', error);
-			this.controller.showFormStatus(formId, 'error');
+			this.controller.showFormStatus(config.id, 'error');
 			this.controller.handleFormError(form, {
 				message: 'Network error. Please check your connection and try again.',
 				code: 'network_error'
 			});
 		} finally {
-			await this.controller.store.delete(formId);
+			await this.controller.store.delete(config.id);
 		}
 	}
 }

--
Gitblit v1.10.0