From 181938ea32cf2c11d10d56018df22586adff7860 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 11 Jan 2026 17:07:14 +0000
Subject: [PATCH] =CRUD.js cleanup, DataStore.js filtering optimization

---
 assets/js/min/crud.min.js             |    2 
 assets/js/concise/PopulateForm.js     |    2 
 assets/js/concise/TaxonomySelector.js |   67 
 assets/js/min/uploader.min.js         |    2 
 inc/helpers/renderFields.php          |   39 
 assets/js/concise/Queue.js            |  155 +
 inc/ui/CRUDSkeleton.php               |   38 
 assets/css/dash.min.css               |    2 
 assets/js/concise/DataStore.js        |  154 +
 assets/js/min/dataStore.min.js        |    2 
 inc/rest/routes/ContentRoutes.php     |   92 +
 assets/js/min/selector.min.js         |    2 
 inc/managers/CRUDManager.php          |    2 
 assets/js/concise/HandleSelection.js  |  332 ++-
 inc/meta/MetaForm.php                 |  121 
 inc/rest/routes/UploadRoutes.php      |  170 +-
 assets/js/min/populate.min.js         |    2 
 assets/js/min/queue.min.js            |    2 
 assets/js/min/handleSelection.min.js  |    2 
 assets/js/concise/CRUDOld.js          |  778 ++++++++++
 inc/registry/CheckCustomTables.php    |   19 
 assets/js/concise/CRUD.js             | 1896 ++++++++++++++++++-------
 assets/js/concise/UploadManager.js    |  333 ++--
 23 files changed, 3,059 insertions(+), 1,155 deletions(-)

diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 7435ee0..7bd590d 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}@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%}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(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.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%}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 641a928..9e2e9bd 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -1,29 +1,182 @@
-/**
- * Main CRUD Manager - Coordinates everything
- */
 class CRUDManager {
-	constructor(config) {
+	constructor(){
+		this.container = document.querySelector('.crud[data-content]:not([data-ignore])');
+		if (!this.container) return;
+		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.config = config;
-		this.content = config.content || false;
-		this.settings = window.jvbUserSettings;
 		this.a11y = window.jvbA11y;
-		if (!this.content) {
-			return;
-		}
+		this.error = window.jvbError;
+		this.cache = new window.jvbCache(this.content);
+
+		this.activeItem = null;
 		this.isTimeline = false;
-		this.currentItemID = null;
+		this.items = {
+			list: new Map(),
+			grid: new Map(),
+			table: new Map()
+		}; //DOM references
 
+		this.init();
+	}
+
+	init() {
 		this.initElements();
-		this.updateBulkOptions();
+		this.initListeners();
+		let cached = this.initSettings();
+		this.initStore(cached);
+		this.checkHideFilters();
+		this.initIntegrations();
+		this.initUploader();
+		this.initModals();
+	}
 
-		// Initialize components
+	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 taxFilters = document.querySelectorAll('[data-filter="taxonomies"]');
+		if (taxFilters.length > 0) {
+			this.ui.filters.taxonomies = {};
+			taxFilters.forEach(tax => {
+				const taxonomy = tax.dataset.taxonomy;
+				this.ui.filters.taxonomies[taxonomy] = tax;
+				this.allowedFilters.push(`tax_${taxonomy}`);
+			});
+		}
+		this.isTimeline = !!document.querySelector('[data-timeline]');
+	}
+		initUploader() {
+			if (!this.ui.uploader) return;
+
+			window.jvbUploads.scanFields(this.ui.uploader);
+			window.jvbUploads.subscribe((event, data) => {
+				if (event === 'sent-to-queue') {
+					if (data === this.ui.uploader.dataset.uploader) {
+						window.debouncer.schedule('crud-complete', ()=> {
+							this.store.clearCache();
+						});
+					}
+				}
+			});
+		}
+		initModals() {
+			this.modals = {};
+			for (let [name, modal] of Object.entries(this.ui.modals)) {
+				if (!modal.modal) continue;
+				this.modals[name] = new window.jvbModal(modal.modal);
+
+				this.modals[name].subscribe((event, data) => {
+					switch (event) {
+						case 'modal-close':
+							this.activeItem = null;
+							const formId = this.ui.modals[name].form.dataset.formId;
+							if (formId) {
+								this.formController.cleanupForm(formId);
+							}
+
+							this.ui.modals[name].form.reset();
+
+							if (name === 'date') {
+								this.handleCustomDateSelection()
+							}
+							break;
+						case 'modal-open':
+
+							break;
+					}
+				})
+			}
+
+		}
+
+	initStore(cached) {
+		let filters = {
+			... this.defaults,
+			...cached
+		};
 		const store = window.jvbStore.register(
 			this.content,
 			{
 				storeName: this.content,
 				keyPath: 'id',
-				endpoint: 'content',
+				endpoint: this.endpoint??'content',	//for taxonomy stores
 				headers: {
 					'action_nonce': window.auth.getNonce('dash'),
 				},
@@ -34,52 +187,43 @@
 					{ name: 'modified', keyPath: 'modified'},
 					{ name: 'title', keyPath: 'title'}
 				],
-				filters: {
-					content: this.content,
-					user: window.auth.getUser(),
-					page: 1,
-					status: 'all',
-					orderby: 'modified', //or title
-					order: 'desc'
-				},
-				TTL: 30 * 60 * 1000,	//30 minutes cache
+				filters: filters,
+				ignore: ['content', 'user'],
+				TTL: 60 * 60 * 1000, 		//1 hour cache
 				showLoading: true,
-			});
+			}
+		);
 		this.store = store[this.content];
 
-		this.status = 'all';
-		this.filterTimeout = null;
-
-		this.viewController = new window.jvbViews(this.ui.container, this.store);
-		this.tableForm = null;
-		this.tableChanges = new Map();
-
-
-		this.formController = (this.isTimeline) ? new window.jvbForm({collectFormData: () => this.collectTimelineData.bind(this)}) : new window.jvbForm();
-		this.viewController.subscribe((event, form) => {
-			if (event === 'table-view' && !this.tableForm) {
-				if (!this.tableForm) {
-					this.tableForm = this.formController.registerForm(form, {
-						autosave: false,
-						formStatus: false,
-						isTable: true,
-					});
-				}
-
-			} else if (event === 'not-table-view') {
-				if (this.tableForm) {
-
-				}
-			} else if (event === 'order-changed') {
-				let data = this.store.get(form);
-				if (!data) {
-					return;
-				}
-				let changes = {};
-				changes[form] = data;
-				this.savePosts(changes, `Updating progression order`);
+		this.store.subscribe((event, data) => {
+			switch (event) {
+				case 'data-loaded':
+					this.render();
+					this.selectionHandler.collectItems();
+					break;
+			}
+		})
+	}
+	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((event, data) => {
+			this.selected = new Set([...data.selectedItems].map(id => parseInt(id)));
+			this.ui.bulk.control.hidden = this.selected.size === 0;
+			this.ui.bulk.count.hidden = this.selected.size === 0;
+			this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
+		});
+
+		this.formController = new window.jvbForm();
 
 		this.formController.subscribe((event, data) => {
 			switch(event) {
@@ -91,406 +235,742 @@
 		});
 
 		this.queue.subscribe((event, data) => {
-			if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
-			if (event === 'operation-completed') {
-				this.handleQueueSuccess(event, data);
-			} else if (event === 'operation-failed-permanent') {
-				this.handleQueueFailure(event, data);
+			if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
+				&& event === 'operation-status'
+				&& data.status === 'completed') {
+				this.store.clearCache();
+			}
+		});
+	}
+
+	initSettings() {
+		this.defaults = {
+			content: this.content,
+			user: window.auth.getUser(),
+			page: 1,
+			status: 'all',
+			orderby: 'date',
+			order: 'desc',
+			search: '',
+		}
+
+		let updateFilters = {};
+		//current view (defaults to grid)
+		let defaultView = this.container.dataset.view??'grid'
+		this.view = this.cache.get('view')??defaultView;
+		if (this.view !== defaultView) {
+			this.ui.views[this.view].checked = true;
+		}
+		//current status (defaults to all)
+		this.status = this.cache.get('status')??this.defaults.status;
+		if (this.status !== this.defaults.status) {
+			this.ui.filters.status[this.status].checked = true;
+			updateFilters.status = this.status;
+		}
+		//orderby & order
+		this.orderby = this.cache.get('orderby')??this.defaults.orderby;
+		if (this.orderby !== this.defaults.orderby) {
+			this.ui.filters.orderby[this.orderby].checked = true;
+			updateFilters.orderBy = this.orderby;
+		}
+		this.order = this.cache.get('order')??this.defaults.order;
+		if (this.order !== this.defaults.order) {
+			this.ui.filters.order[this.order].checked = true;
+			updateFilters.order = this.order;
+		}
+
+		if (this.ui.filters.taxonomies) {
+			Object.entries(this.ui.filters.taxonomies).forEach(([taxonomy, element]) => {
+				const filterKey = `tax_${taxonomy}`;
+				const cached = this.cache.get(filterKey);
+				if (cached) {
+					element.value = cached;
+					updateFilters[filterKey] = cached;
+				}
+			});
+		}
+
+		let tabDirection = this.cache.get('tabNav')??'horizontal';
+		if (this.ui.table.nav && tabDirection === 'vertical') {
+			this.ui.table.nav.checked = true;
+		}
+
+
+
+		//Setup details open functionality
+		let details = {
+			showFilters: {
+				element: this.ui.filters.container,
+				default: 'closed',
+			},
+			showUploader: {
+				element: this.ui.uploader,
+				default: 'open'
+			}
+		};
+		for (let [name, conf] of Object.entries(details)) {
+			if (conf.element) {
+				let cached = this.cache.get(name)??conf.default;
+				conf.element.open = cached === 'open';
+				conf.element.addEventListener('toggle', ()=> {
+					this.cache.set(name, conf.element.open ? 'open' : 'closed');
+				});
+			}
+		}
+
+		return updateFilters;
+	}
+	/****************************************************************
+	 EVENT LISTENERS
+	****************************************************************/
+	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);
+		if (this.ui.filters.search) {
+			this.ui.filters.search.addEventListener('input', this.inputHandler);
+		}
+	}
+	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);
+			return;
+		}
+
+		let filter = window.targetCheck(e, '[data-filter]');
+		if (filter) {
+			this.handleFilterChange(filter);
+			return;
+		}
+
+		let view = window.targetCheck(e, 'input[data-view]');
+		if (view) {
+			this.handleViewChange(view);
+			return;
+		}
+
+		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) {
+				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');
+			}
+		}
+	}
+		handleBulkAction(bulkAction) {
+			if (bulkAction.value.startsWith('tax-')) {
+				const selectedOption = bulkAction.options[bulkAction.selectedIndex];
+				const taxonomy = selectedOption.dataset.taxonomy;
+				const single = selectedOption.dataset.single;
+				const plural = selectedOption.dataset.plural;
+
+				window.jvbSelector.openEmpty(
+					taxonomy,
+					single,
+					plural,
+					(result) => this.handleBulkTaxonomy(result)
+				);
+				bulkAction.value = '';
+
+				return;
+			}
+			switch(bulkAction.value) {
+				case 'edit':
+					this.openBulkEditModal();
+					break;
+				case 'publish':
+				case 'trash':
+				case 'delete':
+					this.setBulkStatus(bulkAction.value);
+					break;
+				case 'draft':
+				case 'restore':
+					this.setBulkStatus('draft');
+					break;
+			}
+		}
+	handleBulkTaxonomy(result) {
+		if (!result.termIds.length || !this.selected.size) return;
+
+		const changes = {};
+		const taxonomyField = `tax_${result.taxonomy}`;
+
+		this.selected.forEach(itemID => {
+			const item = this.store.get(parseInt(itemID));
+			if (!item) return;
+
+			// Merge existing terms with new ones
+			const existingTerms = item.taxonomies?.[result.taxonomy] || [];
+			const existingIds = existingTerms.map(t => t.id);
+			const newIds = [...new Set([...existingIds, ...result.termIds])];
+
+			changes[itemID] = {
+				[taxonomyField]: newIds.join(','),
+				content: this.content
+			};
+		});
+
+		if (Object.keys(changes).length > 0) {
+			this.savePosts(
+				changes,
+				`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
+			);
+		}
+
+		this.selectionHandler.clearSelection();
+	}
+	handleFilterChange(target) {
+		let filter = target.dataset.filter;
+
+		if (filter === 'date' && target.value === 'custom') {
+			target.value = '';
+			this.modals.date.handleOpen();
+			return;
+		}
+
+		if (filter === 'date' && target.value !== '') {
+			this.setFilter('date-filter', target.value);
+			// Clear custom range
+			this.deleteFilter('dateFrom');
+			this.deleteFilter('dateTo');
+			this.checkHideFilters();
+			return;
+		}
+
+		if (filter === 'taxonomies') {
+			filter = `tax_${target.dataset.taxonomy}`;
+		}
+
+		this.setFilter(filter, target.value);
+	}
+	checkHideFilters() {
+		const filters = this.store.filters;
+		const hasActiveFilter = Object.entries(filters).some(([key, value]) => {
+			// Skip internal props
+			if (['content', 'user', 'page'].includes(key)) return false;
+			// Check if differs from default
+			return this.defaults[key] !== value && value !== '' && value !== null;
+		});
+
+		this.ui.buttons.clearFilters.hidden = !hasActiveFilter;
+	}
+	clearAllFilters() {
+		let currentFilters = this.store.filters;
+		this.store.clearFilters();
+		for (let [filter, value] of Object.entries(currentFilters)) {
+			this.cache.remove(filter);
+			this.deleteFilter(filter, value);
+		}
+		this.a11y.announce('All filters cleared');
+	}
+
+	handleCustomDateSelection() {
+
+		// Check if month select was used
+		if (this.ui.modals.date.month && this.ui.modals.date.month.value) {
+			const [year, month] = this.ui.modals.date.month.value.split('-');
+			const firstDay = `${year}-${month}-01`;
+			const lastDay = new Date(year, parseInt(month), 0).getDate();
+			const lastDayFormatted = `${year}-${month}-${String(lastDay).padStart(2, '0')}`;
+
+			this.setFilter('dateFrom', firstDay);
+			this.setFilter('dateTo', lastDayFormatted);
+
+			// Clear the regular date-filter
+			this.deleteFilter('date-filter');
+
+			// Reset month select for next time
+			this.ui.modals.date.month.value = '';
+		}
+		// Otherwise check custom range
+		else if (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);
+
+			// Clear the regular date-filter
+			this.deleteFilter('date-filter');
+
+			// Reset inputs for next time
+			this.ui.modals.date.start.value = '';
+			this.ui.modals.date.end.value = '';
+		}
+
+		this.checkHideFilters();
+	}
+		handleViewChange(view) {
+			this.view = view.dataset.view;
+			this.cache.set('view', this.view);
+			this.render();
+		}
+	handleClick(e) {
+		let clearSearch = window.targetCheck(e, '.clear-search');
+		if (clearSearch) {
+			this.deleteFilter('search', '');
+		}
+		let actionButton = window.targetCheck(e, '[data-action]');
+		if (actionButton) {
+			e.preventDefault();
+			let itemID = actionButton.dataset.id;
+
+			switch (actionButton.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.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...`);
+					break;
+				case 'bulk-edit':
+					if (this.selected.size > 0) {
+						this.openBulkEditModal();
+					}
+					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();
+					}
+					break;
+				case 'refresh':
+					this.store.clearCache();
+					this.store.fetch();
+					break;
+				case 'clear-filters':
+					this.clearAllFilters();
+					break;
+			}
+		}
+
+		const applyDate = window.targetCheck(e, '.apply-date-filter');
+		if (applyDate) {
+			this.handleCustomDateSelection();
+			this.modals.date.handleClose();
+			return;
+		}
+
+		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();
+		let query = e.target.value.trim();
+		let key = `${this.content}-search`;
+
+		console.log('Maybe search', query);
+		if (query.length === 0) {
+			this.deleteFilter('search', '');
+			return;
+		}
+
+		// Require minimum 2 characters
+		// if (query.length < 2) {
+		// 	return;
+		// }
+
+		window.debouncer.schedule(
+			key,
+			() => {
+				console.log('Searching for', query);
+				this.a11y.announce(`Searching for "${query}"...`);
+				this.store.setFilters({ search: query, page: 1 });
+			},
+			300
+		);
+	}
+
+	handleKeys(e) {
+		if (!this.tabNav) return;
+
+		if (e.key === 'Tab') {
+			e.preventDefault();
+
+			const currentCell = e.target.closest('[data-field]');
+			const currentRow = e.target.closest('tr');
+
+			if (!currentCell || !currentRow) return;
+
+			const fieldName = currentCell.dataset.field;
+			const isShift = e.shiftKey;
+
+			// Find next editable row
+			let targetRow = this.findNextEditableRow(currentRow, isShift);
+
+			// If no target row found, wrap around
+			if (!targetRow) {
+				targetRow = this.wrapToRow(currentRow, isShift);
+			}
+
+			if (targetRow) {
+				this.focusFieldInRow(targetRow, fieldName, isShift);
+			}
+		}
+	}
+	findNextEditableRow(currentRow, goBackward = false) {
+		let row = goBackward ? currentRow.previousElementSibling : currentRow.nextElementSibling;
+
+		// For timeline tables, skip non-editable rows
+		while (row && !this.isEditableRow(row)) {
+			row = goBackward ? row.previousElementSibling : row.nextElementSibling;
+		}
+
+		return row;
+	}
+
+	wrapToRow(currentRow, goBackward = false) {
+		if (this.isTimeline) {
+			// For timeline, stay within the same tbody
+			const tbody = currentRow.closest('tbody');
+			if (!tbody) return null;
+
+			const rows = Array.from(tbody.querySelectorAll('tr'))
+				.filter(row => this.isEditableRow(row));
+
+			return goBackward ? rows[rows.length - 1] : rows[0];
+		} else {
+			// For regular tables, use all rows in tbody
+			if (!this.ui.table.body) return null;
+
+			const rows = Array.from(this.ui.table.body.querySelectorAll('tr'))
+				.filter(row => this.isEditableRow(row));
+
+			return goBackward ? rows[rows.length - 1] : rows[0];
+		}
+	}
+	isEditableRow(row) {
+		// Skip thead/tfoot
+		if (row.closest('thead') || row.closest('tfoot')) {
+			return false;
+		}
+
+		// For timeline, check for specific classes
+		if (this.isTimeline) {
+			return row.classList.contains('shared') || row.classList.contains('timeline-point');
+		}
+
+		// For regular tables, check for data-id
+		return !!row.dataset.id;
+	}
+
+	focusFieldInRow(row, fieldName, fromAbove = false) {
+		const targetCell = row.querySelector(`[data-field="${fieldName}"]`);
+		if (!targetCell) return;
+
+		const input = this.findFocusableInput(targetCell);
+		if (input) {
+			input.focus();
+
+			// Select text if it's a text input
+			if (input.select && input.type === 'text') {
+				input.select();
+			}
+
+			// Announce for accessibility
+			const direction = fromAbove ? 'next' : 'previous';
+			this.a11y?.announce(`Moved to ${fieldName} in ${direction} row`);
+		}
+	}
+
+	findFocusableInput(cell) {
+		const selectors = [
+			'input:not([type="hidden"]):not([disabled])',
+			'textarea:not([disabled])',
+			'select:not([disabled])',
+			'button:not([disabled])'
+		];
+
+		for (const selector of selectors) {
+			const element = cell.querySelector(selector);
+			if (element) return element;
+		}
+
+		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;
 
-		// Track initialization
-		this.initialized = false;
+		// Save to store and server
+		this.store.save(item);
+		this.savePosts(
+			{ [itemID]: { timeline: reorderedTimeline, content: this.content } },
+			'Reordering timeline...',
+			true
+		);
 
-		this.init();
+		this.a11y?.announce(`Moved to position ${newIndex + 1}`);
 	}
+
+	/*******************************************************************
+	 MODALS
+	*******************************************************************/
+	openEditModal(itemID) {
+		let item = this.store.get(parseInt(itemID));
+		if (!item) return;
+		this.activeItem = itemID;
+		this.ui.modals.edit.modal.dataset.itemId = itemID;
+		this.ui.modals.edit.modal.dataset.content = this.content;
+		this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`;
+		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.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??''
+			}
+
+			label.title = item.fields['post_title']??'';
+			this.ui.modals.bulkEdit.selected.append(template);
+		});
+		if (this.ui.modals.bulkEdit.h2) {
+			this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
+		}
+		this.formController.registerForm(this.ui.modals.bulkEdit.form);
+	}
+
+	/*****************************************************************
+	 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.currentItemID] = changes;
-			this.savePosts(theChanges, title);
+			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;
 		}
 
-		let itemsToRemove = [];
-		console.log(data);
-		switch (true) {
-			case data.config.element === this.ui.forms.edit:
-				theChanges[this.currentItemID] = changes;
-				title = `Saving ${title} Changes`;
-				// Check if status change requires removal
-				if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
-					itemsToRemove.push(this.currentItemID);
+		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);
+			}
+		} 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);
 				}
-				break;
-			case data.config.element === this.ui.forms.bulkEdit:
-				let selected = data.config.element.querySelectorAll('.selected input:checked');
-				selected.forEach(sel => {
-					theChanges[sel.value] = changes;
-					// Check if status change requires removal
-					if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
-						itemsToRemove.push(sel.value);
-					}
-				});
-
-				title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
-				break;
-			case data.config.element === this.ui.forms.create:
-				if (event === 'form-submit') {
-					theChanges[data.config.data['form-id']] = changes;
-					title = `Saving ${title} Changes`;
-				}
-				break;
-		}
-
-		// Handle visual removal with stagger effect
-		if (itemsToRemove.length > 0) {
-			let delay = 0;
-			itemsToRemove.forEach(itemId => {
-				setTimeout(() => {
-					const element = document.querySelector(`.item[data-id="${itemId}"]`);
-					if (element) {
-						window.fade(element, false);
-					}
-				}, delay);
-				delay += 50; // Stagger by 50ms
+				num++;
 			});
-
-			// Clear selection after bulk edit with staggered removal
-			if (data.config.element === this.ui.forms.bulkEdit) {
-				setTimeout(() => {
-					this.viewController.clearSelection();
-				}, delay + 100);
+			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);
+		this.savePosts(theChanges, title, event === 'form-submit');
 	}
+	savePosts(changes, title, delay = false) {
+		if (Object.keys(changes).length === 0) return;
 
-	shouldRemoveItem(newStatus) {
-		return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
-			(newStatus !== this.status);
-	}
-
-	savePosts(changes, title) {
-		if (Object.keys(changes).length === 0) {
-			return;
-		}
-
-		//ensure content is in each post
-		for (let postId in changes) {
-			if (!changes[postId]['content']) {
-				changes[postId]['content'] = this.content;
+		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: 'content',
+			endpoint: this.endpoint,
 			headers: {
 				'action_nonce': window.auth.getNonce('dash'),
 			},
 			data: {
-				posts: changes,
+				posts: validChanges,
 			},
-			delay: true,
+			delay: delay,
 			popup: `Saving changes`,
 			title: title
 		};
-
 		this.queue.addToQueue(operation);
-
-	}
-	async handleQueueSuccess(event, data) {
-		this.store.clearCache();
-		this.store.fetch();
-	}
-	handleQueueFailure(event, data) {
-		console.error('Operation failed permanently:', data);
-		// Optionally show error notification to user
-		this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
 	}
 
-	initElements() {
-		this.elements = {
-			modals: {
-				create: 'dialog.create',
-				edit: 'dialog.edit',
-				bulkEdit: 'dialog.bulkEdit'
-			},
-			container: '.crud[data-content]',
-			grid: '.item-grid',
-			bulkSelectActions: '.bulk-action-select',
-			forms: {
-				create: 'dialog.create form',
-				edit: 'dialog.edit form',
-				bulkEdit: 'dialog.bulkEdit form'
-			},
-			uploader: 'details.uploader'
-		};
-		this.ui = window.uiFromSelectors(this.elements);
-		if (this.ui.uploader) {
-			window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
-
-			window.jvbUploads.subscribe((event, data) => {
-				if (event === 'sent-to-queue') {
-					console.log(data);
-					if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
-						window.debouncer.schedule('crud-complete', ()=> {
-							this.store.clearCache();
-						});
-					}
-				}
-			});
-		}
-		this.isTimeline = !!document.querySelector('[data-timeline]');
-	}
-	init() {
-		if (this.ui.uploader){
-			this.settings.addSetting(this.ui.uploader, 'open');
-			this.ui.uploader.addEventListener('toggle', (e) =>{
-				this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
-			});
-		}
-
-		// Set up filter controls
-		this.filterHandler = this.handleFilterChange.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-
-
-
-		this.modals = {};
-		for (let [name, modal] of Object.entries(this.ui.modals)) {
-			this.modals[name] = new window.jvbModal(modal);
-
-			this.modals[name].subscribe((event, data) => {
-				switch (event) {
-					case 'modal-close':
-						this.currentItemID = null;
-						this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
-						//double check we have finished saving
-						break;
-					case 'modal-open':
-						//probably not needed in this class
-						break;
-				}
-			});
-		}
-
-		// Set up global event delegation
-		this.setupEventDelegation();
-
-		this.setupFilters();
-
-		this.initialized = true;
-	}
-
-	setupEventDelegation() {
-		document.addEventListener('change', this.changeHandler);
-		// Single event listener for all CRUD actions
-		document.addEventListener('click', (e) => {
-			// Check for action buttons
-			const actionBtn = e.target.closest('[data-action]');
-			if (actionBtn) {
-				e.preventDefault();
-				const action = actionBtn.dataset.action;
-				const id = actionBtn.dataset.id;
-
-				switch(action) {
-					case 'edit':
-						this.populateEditForm(id);
-						this.modals.edit.handleOpen();
-						break;
-
-					case 'delete':
-						if (confirm('Delete this item?')) {
-							let changes = {};
-							changes[actionBtn.dataset.id] = {
-								'post_status': 'delete',
-								'content': this.content
-							};
-							window.fade(actionBtn.closest('.item'), false);
-							this.savePosts(changes, `Sending ${this.singular} to trash...`);
-							this.store.delete(id);
-						}
-						break;
-					case 'trash':
-						let changes = {};
-						changes[actionBtn.dataset.id] = {
-							'post_status': 'trash',
-							'content': this.content
-						};
-						window.fade(actionBtn.closest('.item'), false);
-						this.savePosts(changes, `Sending ${this.singular} to trash...`);
-						break;
-
-					case 'create':
-						this.modals.create.dataset.itemId = 'new';
-						this.modals.create.dataset.content = this.content;
-						this.modals.create.handleOpen();
-						break;
-
-					case 'bulk-edit':
-						const selected = Array.from(this.viewController.selectedItems);
-						if (selected.length > 0) {
-
-							this.modals.bulkEdit.handleOpen();
-						}
-						break;
-
-					case 'bulk-delete':
-						const toDelete = Array.from(this.viewController.selectedItems);
-						if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
-							toDelete.forEach(id => this.store.delete(id));
-							this.viewController.clearSelection();
-						}
-						break;
-
-					case 'sync':
-						// this.store.syncQueue();
-						break;
-
-					case 'refresh':
-						this.store.fetch();
-						break;
-				}
-			}
-
-			let createButton = e.target.closest('.create-item');
-			if (createButton) {
-				this.formController.registerForm(this.ui.forms.create);
-				this.modals.create.handleOpen();
-			}
-
-			let clearSelection = e.target.closest('.cancel-bulk');
-			if (clearSelection) {
-				this.viewController.selectAll(false);
-			}
-		});
-
-		// Keyboard shortcuts
-		document.addEventListener('keydown', (e) => {
-			// Ctrl/Cmd + A to select all
-			if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
-				if (this.ui.container && this.ui.container.contains(document.activeElement)) {
-					e.preventDefault();
-					this.viewController.selectAll();
-				}
-			}
-
-			// ESC to clear selection
-			if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) {
-				this.viewController.clearSelection();
-			}
-		});
-	}
-	handleChange(e) {
-		if (e.target.closest('[data-id]')) {
-			if (this.isTimeline) {
-				this.handleTimelineTableChange(e);
-			} else {
-				this.handleTableChange(e);
-			}
-			return;
-		}
-		if (e.target.classList.contains('bulk-action-select')) {
-			if (e.target.value.startsWith('tax-')) {
-				const taxonomy = e.target.value.replace('tax-', '');
-				this.openTaxonomyModal(taxonomy);
-				e.target.value = '';
-				return;
-			}
-
-			switch (e.target.value) {
-				case 'edit':
-					this.populateBulkEdit();
-					this.modals.bulkEdit.handleOpen();
-					break;
-				case 'publish':
-					this.setBulkStatus('publish');
-					break;
-				case 'draft':
-					this.setBulkStatus('draft');
-					break;
-				case 'trash':
-					this.setBulkStatus('trash');
-					break;
-				case 'restore':
-					this.setBulkStatus('draft');
-					break;
-				case 'delete':
-					this.setBulkStatus('delete');
-					break;
-			}
-		}
-		if (window.targetCheck(e, 'select[data-filter]')) {
-			this.handleFilterChange(e);
-		}
-	}
 	handleTableChange(e) {
-		const row = e.target.closest('tr[data-id]');
-		if (!row) return;
+		const container = this.isTimeline
+			? e.target.closest('tbody[data-id]')
+			: e.target.closest('tr[data-id]');
+		if (!container) return;
 
 		const input = e.target;
-		const postID = parseInt(row.dataset.id);
-		const fieldName = input.closest(['data-field'])?.dataset.field;
-		if (!fieldName) return;
+		const field = input.closest('[data-field]')?.dataset.field;
+		if (!field) return;
 
-		const item = this.store.get(postID);
-		if (!item) return;
-
-		item.fields[fieldName] = this.getInputValue(input);
-
-		this.store.save(item);
-
-		let post = {};
-		post[postID] = item.fields;
-		this.savePosts(post, `Saving changes to ${this.content}`);
-	}
-	handleTimelineTableChange(e) {
-		const tbody = e.target.closest('tbody[data-id]');
-		if (!tbody) return;
-
-		const input = e.target;
-		const fieldName = input.closest('[data-field]')?.dataset.field;
-
-		if (!fieldName) return;
-
-		const parentID = parseInt(tbody.dataset.id);
-		const timelinePoint = input.closest('tr.timeline-point');
-
-		const item = this.store.get(parentID);
+		const itemID = parseInt(container.dataset.id);
+		const item = this.store.get(itemID);
 		if (!item) return;
 
 		const value = this.getInputValue(input);
 
-		// Check if this is a specific point, or a shared value
-		if (timelinePoint) {
-			const imgID = timelinePoint.dataset.imageId;
-			if (!item.fields.timeline) {
-				item.fields.timeline = {};
+		// 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;
 			}
-			if (!item.fields.timeline[imgID]) {
-				item.fields.timeline[imgID] = {};
-			}
-			item.fields.timeline[imgID][fieldName] = value;
 		} else {
-			item.fields[fieldName] = value;
+			item.fields[field] = value;
 		}
 
-		//Update store directly
 		this.store.save(item);
-
-		let changes = {};
-		changes[parentID] = item.fields;
-		this.savePosts(changes, 'Updating progress post');
+		this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
 	}
+
 	getInputValue(input) {
 		if (input.type === 'checkbox') {
 			return input.checked ? (input.value || '1') : '';
@@ -500,54 +980,16 @@
 		}
 		return input.value;
 	}
-
-	openTaxonomyModal(taxonomy) {
-		// Check if jvbSelector exists
-		if (!window.jvbSelector) {
-			console.error('TaxonomySelector not initialized');
-			return;
-		}
-
-		// Open the selector in filter mode
-		window.jvbSelector.openForFilter(
-			taxonomy,
-			(selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
-		);
-	}
-	handleBulkTaxonomy(selectedIds, taxonomy) {
-		// Callback when terms are selected
-		if (selectedIds.length > 0) {
-			selectedIds = selectedIds.join(',');
-			let changes = {};
-			let selected = Array.from(this.viewController.selectedItems);
-
-
-			selected.forEach(sel => {
-				changes[sel] = {
-					content: this.content
-				};
-				changes[sel][taxonomy] = selectedIds;
-			});
-
-
-			let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
-			this.viewController.clearSelection();
-			this.savePosts(changes, title);
-		}
-	}
-
 	setBulkStatus(status) {
-		if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
-			return;
-		}
+		if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
 
 		let changes = {};
-		for (let selected of this.viewController.selectedItems) {
-			changes[selected] = {
+		this.selected.forEach(itemID => {
+			changes[itemID] = {
 				post_status: status,
 				content: this.content
 			};
-		}
+		});
 		let title;
 		switch (status) {
 			case 'delete':
@@ -556,150 +998,474 @@
 			default:
 				title = window.uppercaseFirst(status)+'ing';
 		}
-
-		if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
-			let delay = 0;
-			for (let selected of this.viewController.selectedItems) {
-				setTimeout(() => {
-					const element = document.querySelector(`.item[data-id="${selected}"]`);
-					if (element) {
-						window.fade(element, false);
-					}
-				}, delay);
-				delay += 50; // Increment delay for staggered effect
-			}
+		if (this.shouldRemoveItemUI(status)) {
+			this.removeItems(Object.keys(changes));
 		}
-		// Clear selection even if items aren't being removed
-		this.viewController.clearSelection();
-
-
+		this.selectionHandler.clearSelection();
 		if (Object.keys(changes).length !== 0) {
-			this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
+			this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
+		}
+	}
+	/***************************************************************
+	 VIEW
+	***************************************************************/
+	render() {
+		const items = this.store.getFiltered();
+		if (items.length === 0) {
+			this.renderEmpty();
+			return;
+		}
+
+		switch (this.view) {
+			case 'grid':
+				this.renderGrid(items);
+				break;
+			case 'table':
+				this.renderTable(items);
+				break;
+			case 'list':
+				this.renderList(items);
+				break;
+		}
+		this.updateUI();
+	}
+	updateUI() {
+		if (this.ui.bulk.action) {
+			let options = false;
+			let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
+			if (this.status === 'trash' && hasEdit) {
+				window.removeChildren(this.ui.bulk.action);
+				options = window.getTemplate('trashOptions');
+			} else if (this.status !== 'trash' && !hasEdit) {
+				window.removeChildren(this.ui.bulk.action);
+				options = window.getTemplate('notTrashOptions');
+			}
+			if (options) {
+				options.querySelectorAll('option').forEach((option, index)=> {
+					if (index === 0) option.checked = true;
+					this.ui.bulk.action.append(option);
+				});
+			}
+			this.ui.bulk.action.value = '';
+		}
+		if (this.selected.size > 0) {
+			this.selectionHandler.updateSelectionUI();
 		}
 	}
 
-	handleFilterChange(e) {
-		let target = e.target;
-		let filter = target.dataset.filter;
-		if (filter === 'taxonomies') {
-			let taxonomy = target.dataset.taxonomy;
-			this.store.setFilter(`tax_${taxonomy}`, target.value);
-		} else {
-			this[target.dataset.filter] = target.value;
-			this.store.setFilter(target.dataset.filter, target.value);
-			if (target.dataset.filter === 'status') {
-				this.updateBulkOptions(target.value);
-			}
+	renderEmpty() {
+		this.toggleTable(false);
+		window.removeChildren(this.ui.grid);
+		const empty = window.getTemplate('emptyState');
+		if (empty) {
+			this.ui.grid.append(empty);
+			this.a11y.announceItems(0,false,false);
 		}
 	}
-	updateBulkOptions(status = 'all') {
-		if (status === 'trash') {
-			if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
-				window.removeChildren(this.ui.bulkSelectActions);
-				let options = window.getTemplate('trashOptions');
-				options.querySelectorAll('option').forEach((option, index) => {
-					if (index === 0) {
-						option.checked = true;
+
+	toggleTable(on = true) {
+		if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
+
+		if (on && !this.ui.table.form) {
+			let table = window.getTemplate('contentTable');
+			this.container.append(table);
+			this.ui.table = window.uiFromSelectors(this.selectors.table);
+			this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
+		}
+
+		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 (this.ui.table.body) {
+				window.removeChildren(this.ui.table.body);
+			}
+		}
+		this.keyHandler = this.handleKeys.bind(this);
+		if (on) {
+			document.addEventListener('keydown', this.keyHandler);
+		} else {
+			document.removeEventListener('keydown', this.keyHandler);
+		}
+		if (this.isTimeline && !on) this.toggleTimelineListeners(on);
+	}
+
+	renderGrid(items) {
+		window.removeChildren(this.ui.grid);
+		this.toggleTable(false);
+
+		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);
+	}
+
+	renderList(items) {
+		window.removeChildren(this.ui.grid);
+		this.toggleTable(false);
+
+		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);
+	}
+
+	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);
+		});
+
+		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');
+	}
+
+	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;
+	}
+
+	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'] || '');
 					}
-					this.ui.bulkSelectActions.append(option);
+
+					row.insertBefore(point, pointTemplate);
 				});
+				pointTemplate.remove();
 			}
 		} else {
-			if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
-				window.removeChildren(this.ui.bulkSelectActions);
-
-				let options = window.getTemplate('notTrashOptions');
-				options.querySelectorAll('option').forEach((option, index) => {
-					this.ui.bulkSelectActions.append(option);
-				});
+			// Standard table row
+			if (this.ui.table.form?.dataset.edit !== undefined) {
+				new window.jvbPopulate(row, item);
+			} else {
+				this.populateTableReadOnly(row, item);
 			}
+			this.cleanupTableRow(row);
 		}
-		if (this.ui.bulkSelectActions) {
-			this.ui.bulkSelectActions.value = '';
+
+		this.items.table.set(item.id, row);
+
+		return row;
+	}
+
+	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;
+			}
 		}
 	}
 
-	populateBulkEdit() {
-		const container = this.modals.bulkEdit.modal.querySelector('form .selected');
-		if (!container) return;
-
-		window.removeChildren(container);
-		for (let selected of this.viewController.selectedItems) {
-			let item = this.store.get(selected);
-
-			const img = window.getTemplate('bulkItem');
-			if (!img) return;
-
-			const checkbox = img.querySelector('input[type=checkbox]');
-			const image = img.querySelector('img');
-
-			if (checkbox) {
-				checkbox.id = `bulk_${item.id}`;
-				checkbox.value = item.id;
-				checkbox.checked = true;
-			}
-
-			if (image && item.thumbnail) {
-				image.src = item.thumbnail;
-				image.alt = item.alt || '';
-			}
-
-			container.append(img);
-		}
-		let modal = this.modals.bulkEdit.modal;
-		[
-			modal.querySelector('h2 span').textContent
-		] = [
-			this.viewController.selectedItems.size
-		];
-
-		this.formController.registerForm(this.ui.forms.bulkEdit);
-	}
-
-	populateEditForm(itemID) {
-		this.currentItemID = itemID;
-
-		let item = this.store.get(parseInt(itemID));
-		if (item) {
-			this.ui.modals.edit.dataset.itemId = itemID;
-			this.ui.modals.edit.dataset.content = this.content;
-
-			let form = this.ui.modals.edit.querySelector('form');
-			this.ui.modals.edit.querySelector('h2').textContent = `Editing ${item.fields.post_title}`;
-			form.dataset.formId = `edit-${itemID}`;
-
-			new window.jvbPopulate(form, item);
-
-			this.formController.registerForm(this.ui.forms.edit);
-		}
-	}
-
-	setupFilters() {
-		// Search
-		const searchInput = document.querySelector('.all-filters input[type="search"]');
-		if (searchInput) {
-			let searchTimeout;
-			searchInput.addEventListener('input', () => {
-				if (searchInput.value.length > 3) {
-					clearTimeout(searchTimeout);
-					searchTimeout = setTimeout(() => {
-						this.store.setFilter('search', searchInput.value);
-					}, 300);
-				} else if (searchInput.value.length === 0) {
-					this.store.removeFilter('search');
+	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;
+		});
+	}
+	/***************************************************************
+	 UTILITY
+	***************************************************************/
+	shouldRemoveItemUI(newStatus) {
+		return (this.status === 'all' && !['publish', 'draft'].includes(newStatus))
+			|| 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;
+		});
 	}
 
+	setFilters(filters) {
+		for (let [key, value] of Object.entries(filters)) {
+			if (!this.allowedFilters.includes(key)) {
+				delete filters[key];
+				continue;
+			}
+			this.cache.set(key, value);
+
+			let el = this.findFilterEl(key);
+			this.setElValue(el, value);
+		}
+		this.store.setFilters(filters);
+	}
+	setFilter(name, value) {
+		if (!this.allowedFilters.includes(name)) return;
+		this.cache.set(name, value);
+		let el = this.findFilterEl(name, value);
+		this.setElValue(el, value);
+		//TODO: If we set the element to checked, does that automatically call the change listener, which then also sets the store filter and cache?
+		this.store.setFilter(name, value);
+	}
+
+	deleteFilter(name, value) {
+		if (!this.allowedFilters.includes(name)) return;
+		if (Object.hasOwn(this.defaults, name)) {
+			this.setFilter(name, this.defaults[name]);
+			return;
+		}
+		let el = this.findFilterEl(name, value);
+		this.setElValue(el, false);
+		this.cache.remove(name);
+		this.setFilter(name, '');
+	}
+	setElValue(element, value) {
+		if (!element) return;
+		if (!value) {
+			if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = '';
+			if (['text', 'search'].includes(element.type)) element.value = '';
+			if (element.type === 'radio') element.checked = false;
+			return;
+		}
+
+		if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = value;
+		if (['text', 'search'].includes(element.type)) element.value = value;
+		if (element.type === 'radio') element.checked = true;
+	}
+	findFilterEl(name, value) {
+		//Handle exceptions first (custom date elements)
+		if (['date-filter', 'dateFrom', 'dateTo'].includes(name)) {
+			switch (name) {
+				case 'date-filter':
+					name = 'month';
+					break;
+				case 'dateFrom':
+					name = 'start';
+					break;
+				case 'dateTo':
+					name = 'end';
+					break;
+			}
+			return this.ui.modals.date[name];
+		}
+		// Handle taxonomy filters
+		if (name.includes('tax_')) {
+			const taxonomy = name.replace('tax_', '');
+			const element = this.ui.filters.taxonomies?.[taxonomy];
+			if (element) {
+				return element;
+			}
+			console.warn('Taxonomy filter element not found:', taxonomy);
+			return null;
+		}
+
+		if (!Object.hasOwn(this.ui.filters, name)) {
+			console.warn('Filter el not found: ', name);
+			return false;
+		}
+
+		let el = this.ui.filters[name];
+		if (typeof el === 'object') {
+			if (!Object.hasOwn(this.ui.filters[name], value)) {
+				console.log('Sub filter el not found: ', value);
+				console.log(name);
+				return false;
+			}
+			el = this.ui.filters[name][value];
+		}
+		return el;
+	}
+	/***************************************************************
+	 CLEANUP
+	***************************************************************/
 	destroy() {
-		document.querySelectorAll('[data-filter]').forEach(filter => {
-			filter.removeEventListener('change', this.filterHandler);
-		});
+		if (this.timelineSortables) {
+			this.timelineSortables.forEach(sortable => sortable.destroy());
+			this.timelineSortables = [];
+		}
+		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();
+		}
 	}
 }
 
-// Initialize when ready
 document.addEventListener('DOMContentLoaded', async function()  {
 	window.auth.subscribe((event) => {
 		if (event === 'auth-loaded') {
diff --git a/assets/js/concise/CRUDOld.js b/assets/js/concise/CRUDOld.js
new file mode 100644
index 0000000..a9fc815
--- /dev/null
+++ b/assets/js/concise/CRUDOld.js
@@ -0,0 +1,778 @@
+/**
+ * Main CRUD Manager - Coordinates everything
+ */
+class CRUDManagerOld {
+	constructor(config) {
+		this.queue = window.jvbQueue;
+		this.config = config;
+		this.content = config.content || false;
+		this.settings = window.jvbUserSettings;
+		this.a11y = window.jvbA11y;
+		if (!this.content) {
+			return;
+		}
+		this.isTimeline = false;
+		this.currentItemID = null;
+
+		this.initElements();
+		this.updateBulkOptions();
+
+		// Initialize components
+		const store = window.jvbStore.register(
+			this.content,
+			{
+				storeName: this.content,
+				keyPath: 'id',
+				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: {
+					content: this.content,
+					user: window.auth.getUser(),
+					page: 1,
+					status: 'all',
+					orderby: 'modified', //or title
+					order: 'desc'
+				},
+				TTL: 30 * 60 * 1000,	//30 minutes cache
+				showLoading: true,
+			});
+		this.store = store[this.content];
+
+		this.status = 'all';
+		this.filterTimeout = null;
+
+		this.viewController = new window.jvbViews(this.ui.container, this.store);
+		this.tableForm = null;
+		this.tableChanges = new Map();
+
+
+		this.formController = new window.jvbForm();
+		this.viewController.subscribe((event, form) => {
+			if (event === 'table-view' && !this.tableForm) {
+				if (!this.tableForm) {
+					this.tableForm = this.formController.registerForm(form, {
+						autosave: false,
+						formStatus: false,
+						isTable: true,
+					});
+				}
+
+			} else if (event === 'not-table-view') {
+				if (this.tableForm) {
+
+				}
+			} else if (event === 'order-changed') {
+				let data = this.store.get(form);
+				if (!data) {
+					return;
+				}
+				let changes = {};
+				changes[form] = data;
+				this.savePosts(changes, `Updating progression order`);
+			}
+		});
+
+		this.formController.subscribe((event, data) => {
+			switch(event) {
+				case 'form-submit':
+				case 'form-autosave':
+					this.handleFormChange(event,data);
+					break;
+			}
+		});
+
+		this.queue.subscribe((event, data) => {
+			if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
+			if (event === 'operation-completed') {
+				this.handleQueueSuccess(event, data);
+			} else if (event === 'operation-failed-permanent') {
+				this.handleQueueFailure(event, data);
+			}
+		});
+
+
+		// Track initialization
+		this.initialized = false;
+
+		this.init();
+	}
+	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.currentItemID] = changes;
+			this.savePosts(theChanges, title);
+			return;
+		}
+
+		let itemsToRemove = [];
+		console.log(data);
+		switch (true) {
+			case data.config.element === this.ui.forms.edit:
+				theChanges[this.currentItemID] = changes;
+				title = `Saving ${title} Changes`;
+				// Check if status change requires removal
+				if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
+					itemsToRemove.push(this.currentItemID);
+				}
+				break;
+			case data.config.element === this.ui.forms.bulkEdit:
+				let selected = data.config.element.querySelectorAll('.selected input:checked');
+				selected.forEach(sel => {
+					theChanges[sel.value] = changes;
+					// Check if status change requires removal
+					if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
+						itemsToRemove.push(sel.value);
+					}
+				});
+
+				title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
+				break;
+			case data.config.element === this.ui.forms.create:
+				if (event === 'form-submit') {
+					theChanges[data.config.data['form-id']] = changes;
+					title = `Saving ${title} Changes`;
+				}
+				break;
+		}
+
+		// Handle visual removal with stagger effect
+		if (itemsToRemove.length > 0) {
+			let delay = 0;
+			itemsToRemove.forEach(itemId => {
+				setTimeout(() => {
+					const element = document.querySelector(`.item[data-id="${itemId}"]`);
+					if (element) {
+						window.fade(element, false);
+					}
+				}, delay);
+				delay += 50; // Stagger by 50ms
+			});
+
+			// Clear selection after bulk edit with staggered removal
+			if (data.config.element === this.ui.forms.bulkEdit) {
+				setTimeout(() => {
+					this.viewController.clearSelection();
+				}, delay + 100);
+			}
+		}
+
+		if (Object.keys(theChanges).length === 0) {
+			return;
+		}
+
+		this.savePosts(theChanges, title);
+	}
+
+	shouldRemoveItem(newStatus) {
+		return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
+			(newStatus !== this.status);
+	}
+
+	savePosts(changes, title) {
+		if (Object.keys(changes).length === 0) {
+			return;
+		}
+
+		//ensure content is in each post
+		for (let postId in changes) {
+			if (!changes[postId]['content']) {
+				changes[postId]['content'] = this.content;
+			}
+		}
+		let operation = {
+			endpoint: 'content',
+			headers: {
+				'action_nonce': window.auth.getNonce('dash'),
+			},
+			data: {
+				posts: changes,
+			},
+			delay: true,
+			popup: `Saving changes`,
+			title: title
+		};
+
+		this.queue.addToQueue(operation);
+
+	}
+	async handleQueueSuccess(event, data) {
+		this.store.clearCache();
+		this.store.fetch();
+	}
+	handleQueueFailure(event, data) {
+		console.error('Operation failed permanently:', data);
+		// Optionally show error notification to user
+		this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
+	}
+
+	initElements() {
+		this.elements = {
+			modals: {
+				create: 'dialog.create',
+				edit: 'dialog.edit',
+				bulkEdit: 'dialog.bulkEdit'
+			},
+			container: '.crud[data-content]',
+			grid: '.item-grid',
+			bulkSelectActions: '.bulk-action-select',
+			forms: {
+				create: 'dialog.create form',
+				edit: 'dialog.edit form',
+				bulkEdit: 'dialog.bulkEdit form'
+			},
+			uploader: 'details.uploader'
+		};
+		this.ui = window.uiFromSelectors(this.elements);
+		if (this.ui.uploader) {
+			window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
+
+			window.jvbUploads.subscribe((event, data) => {
+				if (event === 'sent-to-queue') {
+					console.log(data);
+					if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
+						window.debouncer.schedule('crud-complete', ()=> {
+							this.store.clearCache();
+						});
+					}
+				}
+			});
+		}
+		this.isTimeline = !!document.querySelector('[data-timeline]');
+	}
+	init() {
+		if (this.ui.uploader){
+			this.settings.addSetting(this.ui.uploader, 'open');
+			this.ui.uploader.addEventListener('toggle', (e) =>{
+				this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
+			});
+		}
+
+		// Set up filter controls
+		this.filterHandler = this.handleFilterChange.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+
+
+
+		this.modals = {};
+		for (let [name, modal] of Object.entries(this.ui.modals)) {
+			this.modals[name] = new window.jvbModal(modal);
+
+			this.modals[name].subscribe((event, data) => {
+				switch (event) {
+					case 'modal-close':
+						this.currentItemID = null;
+						this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
+						//double check we have finished saving
+						break;
+					case 'modal-open':
+						//probably not needed in this class
+						break;
+				}
+			});
+		}
+
+		// Set up global event delegation
+		this.setupEventDelegation();
+
+		this.setupFilters();
+
+		this.initialized = true;
+	}
+
+	setupEventDelegation() {
+		document.addEventListener('change', this.changeHandler);
+		// Single event listener for all CRUD actions
+		document.addEventListener('click', (e) => {
+			// Check for action buttons
+			const actionBtn = e.target.closest('[data-action]');
+			if (actionBtn) {
+				e.preventDefault();
+				const action = actionBtn.dataset.action;
+				const id = actionBtn.dataset.id;
+
+				switch(action) {
+					case 'edit':
+						this.populateEditForm(id);
+						this.modals.edit.handleOpen();
+						break;
+
+					case 'delete':
+						if (confirm('Delete this item?')) {
+							let changes = {};
+							changes[actionBtn.dataset.id] = {
+								'post_status': 'delete',
+								'content': this.content
+							};
+							window.fade(actionBtn.closest('.item'), false);
+							this.savePosts(changes, `Sending ${this.singular} to trash...`);
+							this.store.delete(id);
+						}
+						break;
+					case 'trash':
+						let changes = {};
+						changes[actionBtn.dataset.id] = {
+							'post_status': 'trash',
+							'content': this.content
+						};
+						window.fade(actionBtn.closest('.item'), false);
+						this.savePosts(changes, `Sending ${this.singular} to trash...`);
+						break;
+
+					case 'create':
+						this.modals.create.dataset.itemId = 'new';
+						this.modals.create.dataset.content = this.content;
+						this.modals.create.handleOpen();
+						break;
+
+					case 'bulk-edit':
+						const selected = Array.from(this.viewController.selectedItems);
+						if (selected.length > 0) {
+
+							this.modals.bulkEdit.handleOpen();
+						}
+						break;
+
+					case 'bulk-delete':
+						const toDelete = Array.from(this.viewController.selectedItems);
+						if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
+							toDelete.forEach(id => this.store.delete(id));
+							this.viewController.clearSelection();
+						}
+						break;
+
+					case 'sync':
+						// this.store.syncQueue();
+						break;
+
+					case 'refresh':
+						this.store.fetch();
+						break;
+				}
+			}
+
+			let createButton = e.target.closest('.create-item');
+			if (createButton) {
+				this.formController.registerForm(this.ui.forms.create);
+				this.modals.create.handleOpen();
+			}
+
+			let clearSelection = e.target.closest('.cancel-bulk');
+			if (clearSelection) {
+				this.viewController.selectAll(false);
+			}
+		});
+
+		// Keyboard shortcuts
+		document.addEventListener('keydown', (e) => {
+			// Ctrl/Cmd + A to select all
+			if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
+				if (this.ui.container && this.ui.container.contains(document.activeElement)) {
+					e.preventDefault();
+					this.viewController.selectAll();
+				}
+			}
+
+			// ESC to clear selection
+			if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) {
+				this.viewController.clearSelection();
+			}
+		});
+	}
+	handleChange(e) {
+		if (e.target.closest('[data-id]')) {
+			if (this.isTimeline) {
+				this.handleTimelineTableChange(e);
+			} else {
+				this.handleTableChange(e);
+			}
+			return;
+		}
+		if (e.target.classList.contains('bulk-action-select')) {
+			if (e.target.value.startsWith('tax-')) {
+				const taxonomy = e.target.value.replace('tax-', '');
+				this.openTaxonomyModal(taxonomy);
+				e.target.value = '';
+				return;
+			}
+
+			switch (e.target.value) {
+				case 'edit':
+					this.populateBulkEdit();
+					this.modals.bulkEdit.handleOpen();
+					break;
+				case 'publish':
+					this.setBulkStatus('publish');
+					break;
+				case 'draft':
+					this.setBulkStatus('draft');
+					break;
+				case 'trash':
+					this.setBulkStatus('trash');
+					break;
+				case 'restore':
+					this.setBulkStatus('draft');
+					break;
+				case 'delete':
+					this.setBulkStatus('delete');
+					break;
+			}
+		}
+		if (window.targetCheck(e, 'select[data-filter]')) {
+			this.handleFilterChange(e);
+		}
+	}
+	handleTableChange(e) {
+		const row = e.target.closest('tr[data-id]');
+		if (!row) return;
+
+		const input = e.target;
+		const postID = parseInt(row.dataset.id);
+		const fieldName = input.closest(['data-field'])?.dataset.field;
+		if (!fieldName) return;
+
+		const item = this.store.get(postID);
+		if (!item) return;
+
+		item.fields[fieldName] = this.getInputValue(input);
+
+		this.store.save(item);
+
+		let post = {};
+		post[postID] = item.fields;
+		this.savePosts(post, `Saving changes to ${this.content}`);
+	}
+	handleTimelineTableChange(e) {
+		const tbody = e.target.closest('tbody[data-id]');
+		if (!tbody) return;
+
+		const input = e.target;
+		const fieldName = input.closest('[data-field]')?.dataset.field;
+
+		if (!fieldName) return;
+
+		const parentID = parseInt(tbody.dataset.id);
+		const timelinePoint = input.closest('tr.timeline-point');
+
+		const item = this.store.get(parentID);
+		if (!item) return;
+
+		const value = this.getInputValue(input);
+
+		// Check if this is a specific point, or a shared value
+		if (timelinePoint) {
+			const imgID = timelinePoint.dataset.imageId;
+			if (!item.fields.timeline) {
+				item.fields.timeline = {};
+			}
+			if (!item.fields.timeline[imgID]) {
+				item.fields.timeline[imgID] = {};
+			}
+			item.fields.timeline[imgID][fieldName] = value;
+		} else {
+			item.fields[fieldName] = value;
+		}
+
+		//Update store directly
+		this.store.save(item);
+
+		let changes = {};
+		changes[parentID] = item.fields;
+		this.savePosts(changes, 'Updating progress post');
+	}
+	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;
+	}
+
+	collectTimelineData(form) {
+		const formData = new FormData(form);
+		const data = {};
+		const timeline = [];
+
+		// Get the base content type
+		data.content = form.closest('dialog')?.dataset.content || this.content;
+		data.user = window.auth.getUser();
+
+		// Collect timeline entries
+		const timelineGroups = form.querySelectorAll('.upload-group');
+
+		timelineGroups.forEach((group, index) => {
+			const groupId = group.dataset.groupId;
+			const entry = {
+				id: group.dataset.itemId || 'new',
+				order: index
+			};
+
+			// Get group-specific fields
+			const titleInput = group.querySelector(`[name="${groupId}[post_title]"], [name="${groupId}_post_title"]`);
+			const excerptInput = group.querySelector(`[name="${groupId}[post_excerpt]"], [name="${groupId}_post_excerpt"]`);
+
+			if (titleInput) entry.post_title = titleInput.value;
+			if (excerptInput) entry.post_excerpt = excerptInput.value;
+
+			// Get images in this group
+			const images = Array.from(group.querySelectorAll('.item-grid.group .item[data-upload-id]'));
+			entry.images = images.map(img => img.dataset.uploadId);
+
+			// Check for featured image
+			const featuredInput = group.querySelector('[name*="featured"]:checked');
+			if (featuredInput) {
+				entry.featured = featuredInput.value;
+			}
+
+			timeline.push(entry);
+		});
+
+		// Get ungrouped uploads (if any)
+		const mainGrid = form.querySelector('.item-grid.preview');
+		if (mainGrid) {
+			const ungroupedImages = Array.from(mainGrid.querySelectorAll('.item[data-upload-id]'));
+			ungroupedImages.forEach(img => {
+				timeline.push({
+					id: 'new',
+					images: [img.dataset.uploadId]
+				});
+			});
+		}
+
+		// Collect other form fields (shared fields)
+		for (let [key, value] of formData.entries()) {
+			// Skip group-specific fields
+			if (!key.includes('[') && !key.includes('_group_') && key !== 'timeline') {
+				data[key] = value;
+			}
+		}
+
+		data.timeline = timeline;
+
+		return data;
+	}
+
+	openTaxonomyModal(taxonomy) {
+		// Check if jvbSelector exists
+		if (!window.jvbSelector) {
+			console.error('TaxonomySelector not initialized');
+			return;
+		}
+
+		// Open the selector in filter mode
+		window.jvbSelector.openForFilter(
+			taxonomy,
+			(selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
+		);
+	}
+	handleBulkTaxonomy(selectedIds, taxonomy) {
+		// Callback when terms are selected
+		if (selectedIds.length > 0) {
+			selectedIds = selectedIds.join(',');
+			let changes = {};
+			let selected = Array.from(this.viewController.selectedItems);
+
+
+			selected.forEach(sel => {
+				changes[sel] = {
+					content: this.content
+				};
+				changes[sel][taxonomy] = selectedIds;
+			});
+
+
+			let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
+			this.viewController.clearSelection();
+			this.savePosts(changes, title);
+		}
+	}
+
+	setBulkStatus(status) {
+		if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
+			return;
+		}
+
+		let changes = {};
+		for (let selected of this.viewController.selectedItems) {
+			changes[selected] = {
+				post_status: status,
+				content: this.content
+			};
+		}
+		let title;
+		switch (status) {
+			case 'delete':
+				title = 'Deleting';
+				break;
+			default:
+				title = window.uppercaseFirst(status)+'ing';
+		}
+
+		if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
+			let delay = 0;
+			for (let selected of this.viewController.selectedItems) {
+				setTimeout(() => {
+					const element = document.querySelector(`.item[data-id="${selected}"]`);
+					if (element) {
+						window.fade(element, false);
+					}
+				}, delay);
+				delay += 50; // Increment delay for staggered effect
+			}
+		}
+		// Clear selection even if items aren't being removed
+		this.viewController.clearSelection();
+
+
+		if (Object.keys(changes).length !== 0) {
+			this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
+		}
+	}
+
+	handleFilterChange(e) {
+		let target = e.target;
+		let filter = target.dataset.filter;
+		if (filter === 'taxonomies') {
+			let taxonomy = target.dataset.taxonomy;
+			this.store.setFilter(`tax_${taxonomy}`, target.value);
+		} else {
+			this[target.dataset.filter] = target.value;
+			this.store.setFilter(target.dataset.filter, target.value);
+			if (target.dataset.filter === 'status') {
+				this.updateBulkOptions(target.value);
+			}
+		}
+	}
+	updateBulkOptions(status = 'all') {
+		if (status === 'trash') {
+			if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
+				window.removeChildren(this.ui.bulkSelectActions);
+				let options = window.getTemplate('trashOptions');
+				options.querySelectorAll('option').forEach((option, index) => {
+					if (index === 0) {
+						option.checked = true;
+					}
+					this.ui.bulkSelectActions.append(option);
+				});
+			}
+		} else {
+			if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
+				window.removeChildren(this.ui.bulkSelectActions);
+
+				let options = window.getTemplate('notTrashOptions');
+				options.querySelectorAll('option').forEach((option, index) => {
+					this.ui.bulkSelectActions.append(option);
+				});
+			}
+		}
+		if (this.ui.bulkSelectActions) {
+			this.ui.bulkSelectActions.value = '';
+		}
+	}
+
+	populateBulkEdit() {
+		const container = this.modals.bulkEdit.modal.querySelector('form .selected');
+		if (!container) return;
+
+		window.removeChildren(container);
+		for (let selected of this.viewController.selectedItems) {
+			let item = this.store.get(selected);
+
+			const img = window.getTemplate('bulkItem');
+			if (!img) return;
+
+			const checkbox = img.querySelector('input[type=checkbox]');
+			const image = img.querySelector('img');
+
+			if (checkbox) {
+				checkbox.id = `bulk_${item.id}`;
+				checkbox.value = item.id;
+				checkbox.checked = true;
+			}
+
+			if (image && item.thumbnail) {
+				image.src = item.thumbnail;
+				image.alt = item.alt || '';
+			}
+
+			container.append(img);
+		}
+		let modal = this.modals.bulkEdit.modal;
+		[
+			modal.querySelector('h2 span').textContent
+		] = [
+			this.viewController.selectedItems.size
+		];
+
+		this.formController.registerForm(this.ui.forms.bulkEdit);
+	}
+
+	populateEditForm(itemID) {
+		this.currentItemID = itemID;
+
+		let item = this.store.get(parseInt(itemID));
+		if (item) {
+			this.ui.modals.edit.dataset.itemId = itemID;
+			this.ui.modals.edit.dataset.content = this.content;
+
+			let form = this.ui.modals.edit.querySelector('form');
+			this.ui.modals.edit.querySelector('h2').textContent = `Editing ${item.fields.post_title}`;
+			form.dataset.formId = `edit-${itemID}`;
+
+			new window.jvbPopulate(form, item);
+
+			this.formController.registerForm(this.ui.forms.edit);
+		}
+	}
+
+	setupFilters() {
+		// Search
+		const searchInput = document.querySelector('.all-filters input[type="search"]');
+		if (searchInput) {
+			let searchTimeout;
+			searchInput.addEventListener('input', () => {
+				if (searchInput.value.length > 3) {
+					clearTimeout(searchTimeout);
+					searchTimeout = setTimeout(() => {
+						this.store.setFilter('search', searchInput.value);
+					}, 300);
+				} else if (searchInput.value.length === 0) {
+					this.store.removeFilter('search');
+				}
+			});
+		}
+	}
+
+	destroy() {
+		document.querySelectorAll('[data-filter]').forEach(filter => {
+			filter.removeEventListener('change', this.filterHandler);
+		});
+	}
+}
+
+// Initialize when ready
+document.addEventListener('DOMContentLoaded', async function()  {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			let container = document.querySelector('[data-content]');
+			if (container && !Object.hasOwn(container.dataset, 'ignore')) {
+				window.crudManager = new CRUDManager({
+					content: container.dataset.content,
+				});
+			}
+		}
+	});
+});
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 439225d..00211e2 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -82,6 +82,7 @@
 					endpoint: null,
 					apiBase: jvbSettings.api,
 					filters: {},
+					ignore: [],			//any filters to ignore when filtering store locally
 					required: null,
 
 					// Cache
@@ -105,6 +106,11 @@
 				_initialized: false
 			};
 
+			store.ignoreFilters = new Set([
+				... ['search', 'page', 'per_page', 'orderby', 'order'],
+				... store.config.ignore
+			]);
+
 			store.config.headers = {
 				'X-WP-Nonce': window.auth.getNonce(),
 				...store.config.headers
@@ -791,20 +797,20 @@
 		// Reject functions
 		if (type === 'function') {
 			if (validate) return { valid: false, error: `Function at ${path}` };
-			console.debug(`[DataStore] Stripped function at ${path}`);
+
 			return { valid: true, data: undefined };
 		}
 
 		// DOM elements
 		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
 			if (validate) return { valid: false, error: `DOM element at ${path}` };
-			console.debug(`[DataStore] Stripped DOM element at ${path}`);
+
 			return { valid: true, data: undefined };
 		}
 
 		// FormData - convert and continue
 		if (obj instanceof FormData) {
-			console.debug(`[DataStore] Converting FormData at ${path}`);
+
 			return { valid: true, data: this.formDataToObject(obj) };
 		}
 
@@ -846,7 +852,7 @@
 		}
 
 		if (validate) return { valid: false, error: `Unknown type at ${path}` };
-		console.debug(`[DataStore] Stripped unknown type at ${path}`);
+
 		return { valid: true, data: undefined };
 	}
 
@@ -939,51 +945,97 @@
 		const cacheEntry = store.cache.get(cacheKey);
 
 		// First check if we have cached results for exact filters
-		if (cacheEntry && cacheEntry.items) {
-			return cacheEntry.items.reduce((acc, id) => {
-				const item = store.data.get(id);
-				if (item) acc.push(item);
-				return acc;
-			}, []);
+		if (cacheEntry?.items) {
+			return this.applyOrdering(
+				cacheEntry.items.reduce((acc, id) => {
+					const item = store.data.get(id);
+					if (item) acc.push(item);
+					return acc;
+				}, []),
+				store
+			);
 		}
 
-		// If we have a search filter and complete base data, filter locally
-		if (store.filters.search && store.filters.search.trim()) {
-			const searchQuery = store.filters.search.toLowerCase().trim();
+		const allItems = Array.from(store.data.values());
+		const searchQuery = store.filters.search?.toLowerCase().trim() || '';
 
-			// Get all items and filter them locally
-			const allItems = Array.from(store.data.values());
+		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;
 
-			// Filter by current filters (excluding search and page)
-			let filtered = allItems.filter(item => {
-				// Apply all filters except search and page
-				for (const [key, value] of Object.entries(store.filters)) {
-					if (key === 'search' || key === 'page') continue;
-
-					if (value !== null && value !== undefined && value !== '') {
-						if (item[key] !== value) return false;
-					}
+				// 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;
 				}
-				return true;
+
+				if (String(item[key]) !== String(value)) return false;
+			}
+
+			// Apply search if present
+			return !(searchQuery && !this.searchObject(item, searchQuery));
+
+
+		});
+
+		return this.applyOrdering(filtered, store);
+	}
+
+	applyOrdering(items, store) {
+		if (!Array.isArray(items)) items = Array.from(items);
+		if (items.length === 0) return items;
+
+		if (store.filters.orderby || store.filters.order) {
+			const orderby = store.filters.orderby || 'date';
+			const order = (store.filters.order || 'desc').toLowerCase();
+
+			items.sort((a, b) => {
+				let aVal, bVal;
+
+				switch (orderby) {
+					case 'alphabetical':
+					case 'title':
+						aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
+						bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
+						break;
+					case 'modified':
+						aVal = new Date(a.modified || 0);
+						bVal = new Date(b.modified || 0);
+						break;
+					case 'date':
+					default:
+						aVal = new Date(a.date || 0);
+						bVal = new Date(b.date || 0);
+				}
+
+				if (aVal < bVal) return order === 'asc' ? -1 : 1;
+				if (aVal > bVal) return order === 'asc' ? 1 : -1;
+				return 0;
 			});
-
-			// Apply search filter to common searchable fields
-			filtered = filtered.filter(item => {
-				// Search in common fields: name, title, path, description
-				const searchableFields = ['name', 'title', 'path', 'description', 'slug'];
-
-				return searchableFields.some(field => {
-					const value = item[field];
-					if (!value) return false;
-					return value.toLowerCase().includes(searchQuery);
-				});
-			});
-
-			return filtered;
 		}
+		return items;
+	}
 
-		// Fallback to all data
-		return this.getAll(name);
+	searchObject(obj, search) {
+		if (!obj || typeof obj !== 'object') return false;
+
+		for (const value of Object.values(obj)) {
+			if (value === null || value === undefined) continue;
+
+			if (typeof value === 'object') {
+				if (this.searchObject(value, search)) return true;
+				continue;
+			}
+
+			if (typeof value === 'string' && value.toLowerCase().includes(search)) {
+				return true;
+			}
+		}
+		return false;
 	}
 
 	async clear(name) {
@@ -1017,14 +1069,18 @@
 			}
 		});
 
-		const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
-
 		this.notify(name, 'filters-changed', {
 			oldFilters,
 			filters: store.filters,
 			updates
 		});
 
+		this.notify(name, 'data-loaded', {
+			cached: true,
+			items: this.getFiltered(name)
+		});
+
+		const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
 		if (store.config.endpoint && shouldFetch) {
 			await this.fetch(name);
 		} else if (store.config.endpoint) {
@@ -1046,8 +1102,18 @@
 		if (!store.config.endpoint || !store.lastResponse) {
 			return true;
 		}
+		
+		if (store.lastResponse.has_more === false) {
+			// Check if new filters are a subset of what we have
+			const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
+				if (store.ignoreFilters.has(key)) return true;
+				if (key === 'page') return true; // Handle pagination locally
+				return true; // We have all data, can filter locally
+			});
 
-		// PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages
+			if (isSubsetFilter) return false;
+		}
+
 		if ('page' in updates) {
 			const newPage = updates.page;
 			const oldPage = oldFilters.page || 1;
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 3034a1c..32c07c8 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -1,25 +1,77 @@
 class HandleSelection {
-	constructor(options) {
-		this.container = options.container; // An actual element, not class name
-		this.selectors = {
-			item: options.item || '.item',
-			count: options.count || '.selection-count',
-			bulkControls: options.bulkControls || '.selection-actions',
-			checkbox: options.checkbox || '[name*="select-item"]',
-			selectAll: options.selectAll || '[data-select-all]',
-			wrapper: options.wrapper || ':has(.item-grid)'
+	constructor(container, options = {}) {
+		this.container = container;
+		const defaults = {
+			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: {},
 		};
+		this.selectors = window.deepMerge(defaults, options);
 
-		this.ui = window.uiFromSelectors(this.selectors, this.container)
+		this.a11y = window.jvbA11y;
 
 		this.selectedItems = new Set();
 		this.lastSelected = null; // For shift+click range selection
 		this.lastSelectedWrapper = null; //Tracks which wrapper we're in
 		this.lastClicked = null;
 		this.subscribers = new Set();
+		this.items = new Map();
 
+		this.initElements();
 		this.initListeners();
+		//store items in memory if available
+		this.collectItems();
 	}
+	removeDataReferences() {
+		let selectors = JSON.parse(JSON.stringify(this.selectors));
+		delete selectors.item.idAttribute;
+		delete selectors.wrapper.id;
+		delete selectors.selectAll.target;
+		return selectors;
+	}
+	initElements() {
+		this.index = 0;
+		let selectors = this.removeDataReferences();
+		this.ui = window.uiFromSelectors(selectors, this.container);
+
+		this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach(wrapper => {
+			this.addWrapper(wrapper);
+		});
+	}
+		addWrapper(el) {
+			let id = this.selectors.wrapper.id;
+			if (!Object.hasOwn(el.dataset, id)) {
+				el.setAttribute(`data-${id}`, this.index);
+				this.index++;
+			}
+			let selectors = this.removeDataReferences().selectAll;
+
+			//store the DOM of the grid and selectAll
+			this.ui.wrappers[el.dataset[id]] = {
+				element: el,
+				items: el.querySelector(this.selectors.items),
+				selectAll: window.uiFromSelectors(selectors, el)
+			};
+		}
+		removeWrapper(el) {
+			delete this.ui.wrappers[el.dataset[this.selectors.wrapper.id]];
+		}
 
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
@@ -28,29 +80,29 @@
 
 		this.container.addEventListener('change', this.changeHandler);
 		this.container.addEventListener('click', this.clickHandler);
-		this.container.addEventListener('keydown', this.keyHandler);
+		document.addEventListener('keydown', this.keyHandler);
 	}
 
 	handleChange(e) {
-		// Select all
-		if (e.target.matches(this.selectors.selectAll)) {
+		if (!this.container.contains(e.target)) return;
+
+		// Select all checkbox
+		if (e.target.matches(this.selectors.selectAll.checkbox)) {
 			this.handleSelectAll(e.target);
 			return;
 		}
 
-		// Individual checkbox
-		if (e.target.matches(this.selectors.checkbox)) {
-			const item = e.target.closest(this.selectors.item);
+		// Individual checkbox - only process if not already handled by shift-click
+		if (e.target.matches(this.selectors.item.checkbox)) {
+			const item = e.target.closest(this.selectors.item.item);
 			if (!item) return;
 
-			// Find the immediate wrapper - check group first, then preview
-			const wrapper = this.getItemWrapper(item);
-			const id = this.getItemId(item);
+			const wrapper = this.getItemWrapper(e.target);
+			if (!wrapper) return;
 
-			// Clear selection if clicking in different wrapper without shift
-			if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
-				this.clearSelection();
-			}
+			this.lastClicked = wrapper.element;
+			const id = this.getItemId(item);
+			if (!id) return;
 
 			if (e.target.checked) {
 				this.select(id, false);
@@ -59,42 +111,38 @@
 			}
 
 			this.lastSelected = id;
-			this.lastSelectedWrapper = wrapper;
+			this.lastSelectedWrapper = wrapper.element;
+			this.updateSelectionUI();
 		}
 	}
 
 	handleClick(e) {
-		const item = e.target.closest(this.selectors.item);
-		const wrapper = item ? this.getItemWrapper(item) : null;
-
-		if (wrapper) {
-			this.lastClicked = wrapper;
-		}
-
-		// Handle non-checkbox clicks on items
-		if (item && !e.target.matches(this.selectors.checkbox)) {
-			if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
-				this.clearSelection();
-				this.lastSelectedWrapper = wrapper;
-			}
-		}
-
-		// Shift+click for range selection
+		// Only care about shift-clicks on checkboxes/labels
 		if (!e.shiftKey) return;
 
-		const checkbox = e.target.closest(this.selectors.checkbox);
-		if (!checkbox || !this.lastSelected || !this.lastSelectedWrapper) return;
+		const item = e.target.closest(this.selectors.item.item);
+		if (!item) return;
 
-		if (!item || !wrapper) return;
+		// Check if clicking checkbox or its label
+		const clickedCheckbox = e.target.matches(this.selectors.item.checkbox);
+		const clickedLabel = e.target.closest('label[for]');
 
-		// Range selection only works within the same wrapper
-		if (wrapper !== this.lastSelectedWrapper) return;
+		if (!clickedCheckbox && !clickedLabel) return;
 
-		const items = Array.from(wrapper.querySelectorAll(this.selectors.item));
+		const wrapper = this.getItemWrapper(item);
+		if (!wrapper) return;
+		// Can't do range selection without a previous selection in same wrapper
+		if (!this.lastSelected || !this.lastSelectedWrapper || wrapper.element !== this.lastSelectedWrapper) {
+			return; // Let change handler deal with it normally
+		}
+
+		e.preventDefault(); // Stop the checkbox from toggling
+
+		// Do range selection
 		const currentId = this.getItemId(item);
-
-		const lastIndex = items.findIndex(el => this.getItemId(el) === this.lastSelected);
-		const currentIndex = items.findIndex(el => this.getItemId(el) === currentId);
+		const items = this.getWrapperChildren(wrapper);
+		const lastIndex = items.findIndex(itemId => itemId === this.lastSelected);
+		const currentIndex = items.findIndex(itemId => itemId === currentId);
 
 		if (lastIndex === -1 || currentIndex === -1) return;
 
@@ -102,106 +150,71 @@
 		const rangeItems = items.slice(start, end + 1);
 
 		rangeItems.forEach(rangeItem => {
-			this.select(this.getItemId(rangeItem));
+			this.select(rangeItem,true,false);
 		});
 
+		this.lastSelected = currentId;
+		this.updateSelectionUI();
+
 		this.notify('range-selected', {
 			selectedItems: new Set(this.selectedItems),
 			wrapper: wrapper
 		});
 	}
 
+	getWrapperChildren(wrapper) {
+		return Array.from(wrapper.items.children)
+			.map(item => this.getItemId(item));
+	}
+
 	getItemWrapper(item) {
 		if (!item) return null;
+		let wrapper = item.closest(this.selectors.wrapper.wrapper);
+		if (!wrapper)return null;
+		return this.getWrapper(wrapper);
+	}
 
-		// Split the compound selector and check each one
-		const wrapperSelectors = this.selectors.wrapper.split(',').map(s => s.trim());
-
-		for (const selector of wrapperSelectors) {
-			const wrapper = item.closest(selector);
-			if (wrapper) return wrapper;
-		}
-
-		return null;
+	getWrapper(wrapper) {
+		return this.ui.wrappers[wrapper.dataset[this.selectors.wrapper.id]]??null;
 	}
 
 	handleKeys(e) {
-		// Ctrl/Cmd + A: Select all
-		if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
-			e.preventDefault();
-
-			// If no lastClicked wrapper, clear everything
-			if (!this.lastClicked) {
-				this.clearSelection();
-				if (window.jvbA11y) {
-					window.jvbA11y.announce('Selection cleared');
-				}
-				return;
-			}
-
-			// First escape: clear items in the current wrapper
-			const wrapperItems = this.lastClicked.querySelectorAll(this.selectors.item);
-			const wrapperIds = Array.from(wrapperItems).map(item => this.getItemId(item));
-			const hadWrapperSelection = wrapperIds.some(id => this.selectedItems.has(id));
-
-			if (hadWrapperSelection) {
-				// Clear just the wrapper's items
-				wrapperIds.forEach(id => this.deselect(id));
-
-				// If there are still items selected elsewhere, announce partial clear
-				if (this.selectedItems.size > 0) {
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Selection cleared in current group');
-					}
-				} else {
-					if (window.jvbA11y) {
-						window.jvbA11y.announce('Selection cleared');
-					}
-				}
-			} else {
-				// Second escape or no selection in wrapper: clear everything
-				this.clearSelection();
-				if (window.jvbA11y) {
-					window.jvbA11y.announce('All selections cleared');
-				}
-			}
-		}
-
 		// Escape: Deselect all
 		if (e.key === 'Escape' && this.selectedItems.size > 0) {
-			this.clearSelection();
-			if (window.jvbA11y) {
-				window.jvbA11y.announce('Selection cleared');
+			e.preventDefault();
+			if (Object.keys(this.ui.wrappers).length > 1 && this.lastClicked) {
+				this.clearWrapperSelection(this.lastClicked);
+			} else {
+				this.clearSelection();
 			}
 		}
 	}
 
 	handleSelectAll(trigger) {
-		const wrapper = this.getItemWrapper(trigger) || trigger.closest(this.selectors.wrapper);
+		const wrapper = this.getItemWrapper(trigger);
 		if (!wrapper) return;
 
 		// Clear any existing selection from other wrappers first
-		if (this.lastSelectedWrapper && wrapper !== this.lastSelectedWrapper) {
-			this.clearSelection();
-		}
+		//Add this back if it makes more sense to clear between select-alls
+		// if (this.lastSelectedWrapper && wrapper.element !== this.lastSelectedWrapper) {
+		// 	this.clearSelection();
+		// }
 
-		const items = wrapper.querySelectorAll(this.selectors.item);
-		const ids = Array.from(items).map(item => this.getItemId(item));
+		const ids = this.getWrapperChildren(wrapper);
 
 		if (trigger.checked) {
 			ids.forEach(id => this.select(id, true, false));
-			this.lastSelectedWrapper = wrapper;
+			this.lastSelectedWrapper = wrapper.element;
 		} else {
 			ids.forEach(id => this.deselect(id, true, false));
-			if (this.selectedItems.size === 0) {
-				this.lastSelectedWrapper = null;
-			}
+			this.lastSelectedWrapper = null;
 		}
 
-		let label = trigger.nextElementSibling || trigger.previousElementSibling;
-		if (label && label.tagName === 'LABEL') {
-			label.textContent = (trigger.checked && items.length > 0) ? 'Clear Selection' : 'Select All';
+		// Update label text
+		if (wrapper.selectAll.span) {
+			wrapper.selectAll.span.textContent = (trigger.checked && ids.length > 0) ? 'Clear Selection' : 'Select All';
 		}
+
 		this.updateSelectionUI();
 
 		this.notify('select-all', {
@@ -213,7 +226,11 @@
 	}
 
 	getItemId(item) {
-		return item.dataset.uploadId;
+		if (!item instanceof Element) {
+			item = item.element??false;
+			if (!item) return;
+		}
+		return item.dataset[`${this.selectors.item.idAttribute}`];
 	}
 
 	/*******************************************************************
@@ -247,22 +264,34 @@
 	}
 
 	clearSelection() {
-		this.selectedItems.forEach(id => this.setCheckboxState(id, false));
+		this.selectedItems.forEach(id => this.deselect(id,true,false));
 		this.selectedItems.clear();
 		this.lastSelected = null;
 		this.lastSelectedWrapper = null;
 
 		// Uncheck all select-all triggers
-		this.container.querySelectorAll(this.selectors.selectAll).forEach(trigger => {
-			trigger.checked = false;
-			const label = trigger.nextElementSibling || trigger.previousElementSibling;
-			if (label?.tagName === 'LABEL') {
-				label.textContent = 'Select All';
+		for (let wrapper of Object.values(this.ui.wrappers)) {
+			if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+			if (wrapper.selectAll.span) {
+				wrapper.selectAll.span.textContent = 'Select All';
 			}
-		});
+		}
+		this.a11y.announce('Selection cleared');
 		this.updateSelectionUI();
 		this.notify('selection-cleared', { selectedItems: new Set() });
 	}
+	clearWrapperSelection(wrapper) {
+		wrapper = this.getWrapper(wrapper);
+		if(!wrapper) return;
+		this.getWrapperChildren(wrapper).forEach(id => this.deselect(id, true, false));
+		if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+		if (wrapper.selectAll.span) {
+			wrapper.selectAll.span.textContent = 'Select All';
+		}
+		this.a11y.announce('Selection cleared in group');
+		this.updateSelectionUI();
+		this.notify('wrapper-selection-cleared', {selectedItems: this.selectedItems});
+	}
 
 	isSelected(id) {
 		return this.selectedItems.has(id);
@@ -276,32 +305,67 @@
 	 * DOM HELPERS
 	 *******************************************************************/
 	setCheckboxState(id, checked) {
-		const item = this.container.querySelector(`[data-upload-id="${id}"]`);
-		const checkbox = item?.querySelector(this.selectors.checkbox);
-		if (checkbox && checkbox.checked !== checked) {
-			checkbox.checked = checked;
+		const item = this.getItem(id);
+		if (!item || !item.checkbox) return;
+
+		if (item.checkbox.checked !== checked) {
+			item.checkbox.checked = checked;
 		}
 	}
 	updateSelectionUI() {
-		if (!this.lastClicked || !this.ui.count) return;
+		if (!this.lastClicked) return;
+
+		const wrapper = this.getWrapper(this.lastClicked);
+		if (!wrapper || !wrapper.selectAll) return;
 
 		const count = this.selectedItems.size;
 
-		// Update bulk controls visibility
-		let controls = this.lastClicked.querySelector(this.selectors.bulkControls);
+		// Fix property paths:
+		let controls = wrapper.selectAll.bulkControls;
 		if (controls) {
 			controls.hidden = count === 0;
 		}
 
-		// Update count display
-		let countEl = this.lastClicked.querySelector(this.selectors.count);
+		let countEl = wrapper.selectAll.count;
 		if (countEl) {
 			const itemText = count === 1 ? 'item' : 'items';
-			countEl.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+			countEl.textContent = count === 0 ? '' : `${count} ${itemText} selected`;
 			countEl.hidden = count === 0;
 		}
 	}
 	/*******************************************************************
+	 ITEM DOM CACHING
+	*******************************************************************/
+	collectItems() {
+		this.container.querySelectorAll(this.selectors.item.item).forEach(item => {
+			this.setItem(item, true);
+		});
+	}
+
+	getItem(id) {
+		if (this.items.has(id)) return this.items.get(id);
+		return this.setItem(id);
+	}
+	setItem(id, isElement = false) {
+		let element = (!isElement)
+			? this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${id}"]`)
+			: id;
+		if (!element) return null;
+
+		id = this.getItemId(element);
+		if (this.items.has(id)) return this.items.get(id);
+
+		this.items.set(id, {
+			element: element,
+			checkbox: element.querySelector(this.selectors.item.checkbox)
+		});
+		return this.items.get(id);
+	}
+
+	camelToKebab(str) {
+		return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+	}
+	/*******************************************************************
 	 * EVENT SYSTEM
 	 *******************************************************************/
 	subscribe(callback) {
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index 11ed8ae..cf48f19 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -285,8 +285,6 @@
 	 * Populate timeline gallery - FIXED iteration
 	 */
 	populateTimelineGallery(fieldWrapper, fieldName, fieldValue) {
-		console.log('Populating Timeline Gallery', fieldValue);
-
 		if (!fieldValue || !Array.isArray(fieldValue)) {
 			console.warn('Timeline field value must be an array');
 			return;
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 478be92..db25262 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -152,7 +152,7 @@
 		window.addEventListener('beforeunload', this.unloadHandler);
 	}
 		handleOnline() {
-			this.updatePanel();
+			this.updatePanel('synced');
 			if (this.getQueueByStatus(this.pendingStatuses).length > 0) {
 				this.processQueue();
 			}
@@ -174,6 +174,7 @@
 			const refresh = window.targetCheck(e, this.selectors.refresh.button);
 			if (refresh) {
 				this.store.clearCache();
+				this.store.clearFilters();
 				this.store.fetch();
 				return;
 			}
@@ -304,6 +305,7 @@
 			title: 'Operation',
 			status: 'queued',
 			timestamp: Date.now(),
+			created_at: new Date().toISOString(),
 			retries: 0,
 			user: this.user,
 			... operation
@@ -441,8 +443,7 @@
 		this.setProcessing(false);
 		this.stopActivityTracking();
 
-		// this.toggleQueue(this.maybeStartPolling());
-
+		this.toggleQueue(this.maybeStartPolling());
 	}
 
 	async processOperation(operation) {
@@ -545,13 +546,37 @@
 		});
 	}
 
+	sortOperations(ops) {
+		const statusPriority = {
+			'processing': 0,
+			'uploading': 1,
+			'pending': 2,
+			'queued': 3,
+			'localProcessing': 4,
+			'failed': 5,
+			'completed': 6,
+			'failed_permanent': 7
+		};
+
+		return ops.sort((a, b) => {
+			// First by status priority
+			const priorityDiff = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
+			if (priorityDiff !== 0) return priorityDiff;
+
+			// Then by updated_at (most recent first)
+			const aTime = a.updated_at ?? a.timestamp ?? 0;
+			const bTime = b.updated_at ?? b.timestamp ?? 0;
+			return new Date(bTime) - new Date(aTime);
+		});
+	}
+
 	getAllQueue() {
 		let ops = [... new Set([
 			...Array.from(this.store.data.values()),
 			... Array.from(this.queue.values())
 		])];
 		//Sort operations by operation updated_at
-		return this.sortByDate(ops);
+		return this.sortOperations(ops);
 	}
 
 	getQueueByStatus(status) {
@@ -563,7 +588,7 @@
 			...Array.from(this.store.filterByIndex({status: status})),
 			...Array.from(this.queue.values()).filter(op => status.includes(op.status))
 		])];
-		return this.sortByDate(ops);
+		return this.sortOperations(ops);
 	}
 
 
@@ -589,32 +614,53 @@
 	 POLLING
 	****************************************************************************/
 	maybeStartPolling() {
-		const incomplete = this.getQueueByStatus(this.pendingStatuses);
+		const incomplete = this.getQueueByStatus([...this.pendingStatuses, ...this.workingStatuses]);
 		if (incomplete.length > 0) {
 			this.startPolling();
 			return true;
 		}
+		this.updatePanel('synced');
 		return false;
 	}
 	startPolling() {
 		if (this.isPolling) return;
 		this.isPolling = true;
 		this.updatePanel('pending');
+		this.runPollCycle();
+	}
 
-		this.pollTimer = setInterval(async () => {
-			try {
-				this.store.clearCache();
-				await this.store.fetch();
-				if (!this.maybeStartPolling()) {
-					this.stopPolling();
-					this.updatePanel('synced');
-				}
-			} catch (error) {
-				console.error('Polling error:', error);
+	async runPollCycle() {
+		if (!this.isPolling) return;
+
+		try {
+			this.store.clearCache();
+			await this.store.fetch();
+			if (!this.maybeStartPolling()) {
+				this.stopPolling();
+				this.updatePanel('synced');
+				return;
 			}
-		},
-			5000);
-		this.startCountdown();
+		} catch (error) {
+			console.error('Polling error:', error);
+		}
+
+		// Schedule next poll with countdown
+		this.startCountdown(5, () => this.runPollCycle());
+	}
+
+	startCountdown(count, onComplete) {
+		this.ui.refresh.countdown.classList.add('counting');
+		this.ui.refresh.countdown.textContent = count;
+
+		this.countdownTimer = setInterval(() => {
+			count--;
+			if (count > 0) {
+				this.ui.refresh.countdown.textContent = count;
+			} else {
+				this.stopCountdown();
+				if (onComplete) onComplete();
+			}
+		}, 1000);
 	}
 
 	stopPolling() {
@@ -626,24 +672,14 @@
 		}
 		this.stopCountdown();
 	}
-	startCountdown(count = 5) {
-		if (!this.isPolling) return;
-		this.ui.refresh.countdown.textContent = count;
-		this.countdownTimer = setInterval(async() => {
-			count--;
-			if (count >= 0) {
-				this.ui.refresh.countdown.textContent = count;
-			}else {
-				this.ui.refresh.countdown.textContent = '';
-				this.stopCountdown();
-			}
-		},1000);
-	}
+
 	stopCountdown() {
 		if (this.countdownTimer) {
 			clearInterval(this.countdownTimer);
 			this.countdownTimer = null;
 		}
+		this.ui.refresh.countdown.classList.remove('counting');
+		this.ui.refresh.countdown.textContent = '';
 	}
 	/****************************************************************************
 	 UI
@@ -661,6 +697,12 @@
 			this.ui.actions.retry.disabled = operations.filter(op => op.status === 'failed').length === 0;
 			this.ui.actions.clear.disabled = operations.filter(op => op.status === 'completed').length ===0;
 
+			const activeCount = operations.filter(op =>
+				[...this.pendingStatuses, ...this.workingStatuses].includes(op.status)
+			).length;
+			this.ui.toggle.count.hidden = activeCount === 0;
+			this.ui.toggle.count.textContent = activeCount;
+
 			for (let status of this.statuses) {
 				if (status === 'failed_permanent') continue;
 				let total = operations.filter(op => op.status === status).length;
@@ -681,26 +723,38 @@
 
 		const status = this.store.filters?.status ?? 'all';
 		const operations = (status === 'all') ? this.getAllQueue() : this.getQueueByStatus(status);
+		const sortedOps = this.sortOperations(operations);
 
-		window.removeChildren(this.ui.items.container);
-
-		if (operations.length === 0) {
+		if (sortedOps.length === 0) {
+			window.removeChildren(this.ui.items.container);
 			const empty = window.getTemplate('emptyQueue');
 			this.ui.items.container.append(empty);
 			this.a11y.announce('No items in queue');
 			return;
+		} else {
+			this.ui.items.container.querySelector('.empty-group')?.remove();
 		}
 
-		operations.forEach(op => {
-			let item = this.items.get(op.id);
+		// Track which items should exist
+		const expectedIds = new Set(sortedOps.map(op => op.id));
 
+		// Remove items that shouldn't exist
+		this.items.forEach((item, id) => {
+			if (!expectedIds.has(id)) {
+				item.element?.remove();
+				this.items.delete(id);
+			}
+		});
+
+		// Update/add items in order
+		sortedOps.forEach((op, index) => {
+			let item = this.items.get(op.id);
 			if (!item) {
-				// Create new element and reference
 				item = this.createOperationElement(op);
 			}
-
 			if (item?.element) {
 				this.updateOperationUI(op.id);
+				// Reorder by re-appending (moves to end in correct order)
 				this.ui.items.container.append(item.element);
 			}
 		});
@@ -742,20 +796,15 @@
 				item.ui.startedAt.textContent = window.formatTimeAgo(op.created_at);
 			}
 			let text = op.status === 'completed' ? 'Completed: ' : 'Last updated: ';
-			let shouldShow =Object.hasOwn(op, 'updated_at') || Object.hasOwn(op, 'completed_at');
-			item.ui.completed.wrap.hidden = !shouldShow;
-			if (shouldShow && item.ui.completed.label && item.ui.completed.time) {
-				let time;
-				if (Object.hasOwn(op, 'completed_at')) {
-					time = op.completed_at;
-				} else {
-					time = op.updated_at;
-				}
-
-				item.ui.completed.label.textContent = text;
-				item.ui.completed.time.setAttribute('datetime', time);
-				item.ui.completed.time.textContent = window.formatTimeAgo(time);
+			const shouldShowCompleted = op.status === 'completed' && (op.completed_at || op.updated_at);
+			item.ui.completed.wrap.hidden = !shouldShowCompleted;
+			if (shouldShowCompleted) {
+				const completedTime = op.completed_at ?? op.updated_at;
+				item.ui.completed.label.textContent = 'Completed: ';
+				item.ui.completed.time.setAttribute('datetime', completedTime);
+				item.ui.completed.time.textContent = window.formatTimeAgo(completedTime);
 			}
+
 			window.showProgress(item.ui.progress, progress, 100, this.statusLabel(op.status));
 			if (item.ui.actions.cancel) item.ui.actions.cancel.hidden = this.completedStatuses.includes(op.status);
 			if (item.ui.actions['retry']) {
@@ -786,7 +835,7 @@
 
 	updatePanel(status = 'syncing') {
 		if (!this.panelStatuses.includes(status)) return;
-		this.ui.panel.classList.remove(this.panelStatuses);
+		this.ui.panel.classList.remove(...this.panelStatuses);
 		this.ui.panel.classList.add(status);
 	}
 	/****************************************************************************
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 1d84d93..5244f02 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -411,16 +411,74 @@
 		this.a11y.announce(message);
 	}
 
+	openEmpty(taxonomy, singular, plural, onComplete) {
+		// Store the callback for when modal closes
+		this.emptyCallback = onComplete;
+
+		// Create a temporary "field" for bulk operations
+		const bulkFieldId = `empty-${taxonomy}-${Date.now()}`;
+
+		if (!this.fields.has(bulkFieldId)) {
+			this.fields.set(bulkFieldId, {
+				id: bulkFieldId,
+				taxonomy: taxonomy,
+				singular: singular,
+				plural: plural,
+				canSearch: true,
+				canCreate:  false,
+				hasAutocomplete: false,
+				isFilter: false,
+				isEmpty: true,
+				limit: 0,
+				ui: {},
+				element: null,
+				value: null,
+				toggle: null,
+				checked: true
+			});
+			this.selectedTerms.set(bulkFieldId, new Set());
+		}
+
+		this.setField(bulkFieldId);
+		this.ui.modal.title.textContent = `Add to ${plural}`;
+
+		if (this.ui.search?.container) {
+			this.ui.search.container.hidden = false;
+		}
+
+		window.removeChildren(this.ui.selected);
+		window.removeChildren(this.ui.terms.list);
+
+		this.modal.handleOpen();
+	}
+
 	closeModal() {
 		const field = this.fields.get(this.activeField);
 		if (!field) return;
 		this.observer.unobserve(this.ui.terms.sentinel);
 		window.removeChildren(this.ui.terms.list);
 
-		this.notify('selected-terms', {
-			terms: this.selectedTerms.get(this.activeField),
-			taxonomy: field.taxonomy
-		});
+		if (field.isEmpty  && this.emptyCallback) {
+			const selectedTermIds = Array.from(this.selectedTerms.get(this.activeField) || []);
+			const selectedTerms = selectedTermIds.map(id => this.store.get(id)).filter(Boolean);
+
+			this.emptyCallback({
+				taxonomy: field.taxonomy,
+				termIds: selectedTermIds,
+				terms: selectedTerms
+			});
+
+			// Cleanup temporary bulk field
+			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: field.taxonomy
+			});
+		}
 
 		this.activeField = null;
 
@@ -675,6 +733,7 @@
 			.some(term=>term.taxonomy === taxonomy);
 
 		fields.forEach(field => {
+			if (!field.toggle) return;
 			field.toggle.disabled = !hasItems && !field.canCreate;
 			field.toggle.title = !hasItems
 				? `No ${field.singular} available`
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index e743fa8..ebc4209 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -34,7 +34,7 @@
 						{ name: 'field', keyPath: 'field' },
 						{ name: 'status', keyPath: 'status' },
 						{ name: 'group', keyPath: 'group' },
-						{ name: 'src', keyPath: 'src' }
+						{ name: 'src', keyPath: 'src' },
 					],
 				},
 				{
@@ -57,33 +57,23 @@
 		this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads'));
 		this.stores.groups.subscribe(this.handleStores.bind(this, 'groups'));
 		this.queue.subscribe((event, operation) => {
-			console.log(event, operation);
-			if (!Object.hasOwn(operation, 'endpoint') || !['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
-				return;
+			if ((event === 'operation-status' || event === 'cancel-operation')
+				&& ['image_upload', 'video_upload', 'document_upload'].includes(operation.type)) {
+				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);
+				this.setBulkUpload(uploads, 'status', operation.status).then(()=>{});
+				if (operation.status === 'completed') {
+					uploads.forEach(upload => {
+						this.removeUpload(upload).then(()=>{});
+					});
+				}
 			}
 
-
-			const fieldId = operation.data instanceof FormData
-				? operation.data.get('fieldId')
-				: operation.data?.fieldId;
-			if (!fieldId) {
-				return;
-			}
-			switch (event) {
-				case 'cancel-operation':
-					this.handleOperationCancelled(fieldId).then(()=>{});
-					break;
-				case 'operation-status':
-					this.handleFieldStatus(fieldId, operation).then(()=>{});
-					break;
-				case 'operation-completed':
-					this.handleOperationComplete(operation, fieldId).then(()=>{});
-					break;
-				case 'operation-failed':
-				case 'operation-failed-permanent':
-					this.handleOperationFailed(operation, fieldId).then(()=>{});
-					break;
-			}
 		});
 	}
 
@@ -95,7 +85,7 @@
 		if (event === 'data-ready') {
 			this.stores.ready.push(storeName);
 			if (this.storesReady()) {
-				this.checkRecovery();
+				this.checkRecovery().then(()=>{});
 			}
 		}
 	}
@@ -128,9 +118,9 @@
 					details: '.file-upload-container .progress .details',
 					icon: '.file-upload-container .progress .icon'
 				},
-				selectAll: '[name="select-all-uploads"]',
+				selectAll: '[data-select-all]',
 				actions: '.selection-actions',
-				count: '.selection-count',
+				count: '.selected .info',
 				hidden: 'input[type="hidden"]'
 			},
 			// groups = selectors that affect groups as a whole
@@ -151,8 +141,8 @@
 				total: '.group-content .group-count'
 			},
 			items: {
-				item: '[data-upload-id]',
-				checkbox: '[name*="select-item"]',
+				item: '.item.upload',
+				checkbox: '[name="select-item"]',
 				featured: '[name="featured"]',
 				image: 'img',
 				details: 'details',
@@ -296,23 +286,36 @@
 			this.queueUploadMeta(e).then(()=>{});
 		}
 	}
-		handleGroupMetaChange(input) {
-			const element = input.closest(this.selectors.group.fields);
-			if (!element) return;
+	handleGroupMetaChange(input) {
+		// Get the groupId directly from the input's data attribute
+		const groupId = input.dataset.groupId;
+		if (!groupId) return;
 
-			const groupId = element.dataset.groupId;
-			const group = this.stores.groups.get(groupId);  // Changed from this.groups
+		// Capture values immediately (before debouncer)
+		const inputName = input.name;
+		const inputValue = input.value;
+
+		// Extract the field name from the input name
+		// Names are like "groupId[post_title]" or "groupId_post_title"
+		const name = inputName
+			.replace(`${groupId}[`, '')
+			.replace(`${groupId}_`, '')
+			.replace(']', '');
+
+		// Schedule the save with captured values
+		window.debouncer.schedule(`group-meta-${groupId}-${name}`, async () => {
+			const group = this.stores.groups.get(groupId);
 			if (!group) return;
 
-			window.debouncer.schedule(`group-meta-${groupId}`, async (input, groupId) => {
-				let name = input.name
-					.replace(`${groupId}_`, '')
-					.replace(`${groupId}[`, '')
-					.replace(']', '');
-				group.fields[name] = input.value;
-				await this.setGroup(groupId, group);
-			}, 300);
-		}
+			// Initialize fields object if it doesn't exist
+			if (!group.fields) {
+				group.fields = {};
+			}
+
+			group.fields[name] = inputValue;
+			await this.setGroup(groupId, group);
+		}, 300);
+	}
 	handleDragEnter(e) {
 		if (!e.dataTransfer.types.includes('Files')) return;
 		const dropZone = e.target.closest(this.selectors.fields.dropZone);
@@ -349,7 +352,9 @@
 
 		const fieldId = this.getFieldIdFromElement(dropZone);
 		if (fieldId) {
-			this.processFiles(fieldId, files).then(()=>{});
+			this.processFiles(fieldId, files).then(()=>{
+				this.updateHandlerItems(fieldId);
+			});
 			this.a11y.announce(`${files.length} file(s) dropped for upload`);
 		}
 	}
@@ -440,6 +445,7 @@
 			},
 			append: '_upload'
 		}
+
 		try {
 			return await this.queue.addToQueue(operation);
 		} catch (error) {
@@ -460,12 +466,17 @@
 		let files = [];
 
 		for (const group of groups) {
+			const groupElement = this.groups.get(group.id)?.element;
+			const fields = this.collectGroupFieldsFromDOM(groupElement, group.id);
+
 			const post = {
 				images: [],
-				fields: group.fields??{}
+				fields: fields
 			};
 
-			const groupUploads = uploads.filter(u => u.group === group.id);
+			// Use helper to get uploads in stored order
+			const groupUploads = this.getGroupUploadsInOrder(group);
+
 			for (const upload of groupUploads) {
 				const file = this.formatFile(upload);
 				if (file) {
@@ -474,10 +485,13 @@
 						upload_id: upload.id,
 						index: uploadMap.length
 					};
-					let uploadEl = this.uploads.get(upload.id);
-					if (uploadEl.ui?.featured?.checked) {
+
+					const uploadEl = this.uploads.get(upload.id);
+					const featuredInput = uploadEl?.element?.querySelector(`input[name="${group.id}_featured"]`);
+					if (featuredInput?.checked) {
 						post.fields.featured = upload.id;
 					}
+
 					post.images.push(imageData);
 					uploadMap.push(upload.id);
 				}
@@ -485,8 +499,8 @@
 			posts.push(post);
 		}
 
+		// Handle remaining uploads not in any group
 		const remaining = uploads.filter(u => !u.group);
-
 		for (const upload of remaining) {
 			const post = {
 				images: [],
@@ -496,7 +510,6 @@
 			const file = this.formatFile(upload);
 			if (file) {
 				files.push(file);
-
 				const imageData = {
 					upload_id: upload.id,
 					index: uploadMap.length
@@ -506,9 +519,42 @@
 			}
 			posts.push(post);
 		}
+
 		return {posts, uploadMap, files};
 	}
 
+	getGroupUploadsInOrder(group) {
+		if (!group.uploads || group.uploads.length === 0) return [];
+
+		return group.uploads
+			.map(uploadId => this.stores.uploads.get(uploadId))
+			.filter(Boolean); // Remove any that don't exist
+	}
+
+	collectGroupFieldsFromDOM(groupElement, groupId) {
+		if (!groupElement) return {};
+
+		const fields = {};
+		const inputs = groupElement.querySelectorAll('input, textarea, select');
+
+		inputs.forEach(input => {
+			// Extract field name from input name like "groupId[post_title]"
+			const name = input.name
+				.replace(`${groupId}[`, '')
+				.replace(`${groupId}_`, '')
+				.replace(']', '');
+
+			// Skip system fields like featured, select-all
+			if (['featured', 'select-all'].some(skip => name.includes(skip))) return;
+
+			if (input.value) {
+				fields[name] = input.value;
+			}
+		});
+
+		return fields;
+	}
+
 	collectUploads(fieldId) {
 		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
 		if (uploads.length === 0) return;
@@ -545,37 +591,6 @@
 		return await this.sendToQueue('uploads/meta', queueData, 'Uploading Meta', '', true);
 	}
 
-	async handleOperationComplete(operation, fieldId) {
-		const response = operation.response;
-
-		// Handle direct upload results (from uploads endpoint)
-		if (response?.data) {
-			const results = Array.isArray(response.data) ? response.data : Object.values(response.data);
-			for (const result of results) {
-				if (result.upload_id && result.attachment_id) {
-					const upload = this.stores.uploads.get(result.upload_id);
-					if (upload) {
-						upload.attachmentId = result.attachment_id;
-						upload.status = 'completed';
-						await this.stores.uploads.save(upload);
-					}
-				}
-			}
-		}
-
-		// Clear completed uploads and groups
-		const uploads = this.stores.uploads.filterByIndex({field: fieldId});
-		const groups = this.stores.groups.filterByIndex({field: fieldId});
-
-		await Promise.all([
-			...uploads
-				.filter(upload => upload.status === 'completed')
-				.map(upload => this.clearUpload(upload.id)),
-			...groups.map(group => this.stores.groups.delete(group.id))
-		]);
-
-		this.notify('uploads-complete', { fieldId, response });
-	}
 	/*********************************************************************
 	 FIELD LOGIC
 	*********************************************************************/
@@ -963,12 +978,16 @@
 		document.body.append(notification);
 		notification = document.querySelector('dialog.restore-uploads');
 		this.restoreModal = new window.jvbModal(notification);
-		this.restoreSelection = new window.jvbHandleSelection({
-			container: notification,
-			wrapper: '.restore-uploads .wrap',
-			bulkControls: '.selection-actions',
-			selectAll: '#select-all-restore',
-			count: '.selection-count'
+		this.restoreSelection = new window.jvbHandleSelection(notification,
+			{
+				wrapper: {
+					wrapper: '.wrap'
+				},
+				selectAll: {
+					bulkControls: '.selection-actions',
+					checkbox: '#select-all-restore',
+					count: '.selection-count'
+				}
 		});
 		this.restoreModal.handleOpen();
 	}
@@ -1012,10 +1031,9 @@
 
 		let usedIds = [];
 		for (let gr of groups) {
-
 			let group = this.stores.groups.get(gr);
-			 await this.createGroup(fieldId,gr);
-			 let element = this.groups.get(gr);
+			await this.createGroup(fieldId, gr);
+			let element = this.groups.get(gr);
 
 			let theseUploads = uploads.filter(upload => upload.group === gr);
 			if (group && this.groups.has(gr)) {
@@ -1195,6 +1213,9 @@
 
 	async setBulkUpload(uploads, key, value) {
 		const promises = Array.from(uploads).map(async (upload) => {
+			if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+			if (!upload) return;
+
 			if (key === 'status') {
 				await this.setUploadStatus(upload, value);
 			}
@@ -1205,6 +1226,8 @@
 	}
 
 	async setUploadStatus(upload, status) {
+		if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+		if (!upload) return;
 		if (upload.progress) {
 			window.showProgress(upload.progress, this.getStatusProgress(status), 100, this.getStatusText(status), this.queue.icons[status]??'');
 		}
@@ -1218,6 +1241,8 @@
 			group.uploads = group.uploads.filter(id => id !== uploadId);
 			if (group.uploads.length === 0) {
 				await this.removeGroup(group.id, false);
+			} else {
+				await this.stores.groups.save(group);
 			}
 		}
 
@@ -1319,10 +1344,12 @@
 			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]`;
 			}
@@ -1339,6 +1366,8 @@
 			element: element,
 			ui: window.uiFromSelectors(this.selectors.group, element)
 		});
+
+		this.getSelectionHandler(fieldId)?.addWrapper(element);
 		return element;
 	}
 
@@ -1390,12 +1419,19 @@
 				group.uploads = group.uploads.filter(id => id !== uploadId);
 				if (group.uploads.length === 0) {
 					await this.removeGroup(group.id, false);
+				} else {
+					await this.stores.groups.save(group);
 				}
 			}
 		}
 
 		//clear any selection
 		if (element.ui.checkbox) element.ui.checkbox.checked = false;
+		// Remove from field-level selection
+		const fieldHandler = this.selectionHandlers.get(upload.field);
+		if (fieldHandler && fieldHandler.isSelected(uploadId)) {
+			fieldHandler.deselect(uploadId);
+		}
 		if (this.selected.get(upload.field)?.has(uploadId)) {
 			this.selected.get(upload.field).delete(uploadId);
 		}
@@ -1409,15 +1445,18 @@
 			if (group) {
 				group.uploads.push(uploadId);
 				upload.group = groupId;
-				this.stores.groups.save(group);
+				await this.stores.groups.save(group);
 			}
 		}
 
 		let target = (groupId) ? this.groups.get(groupId)?.ui.grid : field.ui.grid;
 		if (target) {
-			target.append(element.element)
+			target.append(element.element);
+			if (groupId) {
+				await this.handleReorder(upload.field, groupId);
+			}
 		}
-		this.stores.uploads.save(upload);
+		await this.stores.uploads.save(upload);
 	}
 
 	handleDeleteGroup(button) {
@@ -1455,14 +1494,22 @@
 				keepUploads ? this.addToGroup(uploadId, null) : this.removeUpload(uploadId)
 			)
 		);
+		const field = this.fields.get(group.field);
+		if (field) {
+			const sortableKey = this.getGroupKey(group.field, groupId);
+			const selectionHandler = this.selectionHandlers.get(sortableKey);
+			if (selectionHandler?.destroy) {
+				selectionHandler.destroy();
+			}
+			this.selectionHandlers.get(group.field)?.removeWrapper(element.element);
 
-		// Destroy the Sortable for this group
-		const sortableKey = this.getGroupKey(group.field, groupId);
-		const sortable = this.sortables.get(sortableKey);
-		if (sortable?.destroy) {
-			sortable.destroy();
+			// Existing sortable cleanup
+			const sortable = this.sortables.get(sortableKey);
+			if (sortable?.destroy) {
+				sortable.destroy();
+			}
+			this.sortables.delete(sortableKey);
 		}
-		this.sortables.delete(sortableKey);
 
 		if (element?.element) {
 			element.element.remove();
@@ -1487,30 +1534,11 @@
 	/*******************************************************************************
 	 OPERATION METHODS
 	*******************************************************************************/
-	async handleOperationCancelled(fieldId) {
-		const uploads = this.stores.uploads.filterByIndex({field: fieldId});
-		const groups = this.stores.groups.filterByIndex({field: fieldId});
-
-		await Promise.all([
-			...uploads.map(upload => this.removeUpload(upload.id)),
-			...groups.map(group => this.removeGroup(group.id, false))
-		]);
-		this.a11y.announce('Upload Cancelled');
-	}
-
-	async handleOperationFailed(operation, fieldId) {
-		// Mark uploads as failed, maybe show retry UI
-		await this.setBulkUpload(
-			this.stores.uploads.filterByIndex({field: fieldId}),
-			'status',
-			'failed'
-		);
-	}
-
-	async handleFieldStatus(fieldId, operation) {
-		let status = operation.status;
-		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
-		await this.setBulkUpload(uploads, 'status', status);
+	async handleOperationCancelled(uploads) {
+		if (uploads.length === 0) return;
+		uploads.forEach(upload => {
+			this.removeUpload(upload);
+		});
 	}
 	/*******************************************************************************
 	 SELECTION HANDLERS
@@ -1525,20 +1553,27 @@
 		if (!this.selectionHandlers.has(key)) {
 			let field = this.fields.get(fieldId);
 			if (!field) return;
-			let handler = new window.jvbHandleSelection({
-				container:  field.element,
-				item: this.selectors.items.item,
-				count: this.selectors.fields.count,
-				bulkControls: this.selectors.fields.actions,
-				checkbox: this.selectors.items.checkbox,
-				selectAll: this.selectors.fields.selectAll,
-				wrapper: `${this.selectors.fields.preview}, ${this.selectors.group.item}`,
+			if (field.config.destination !== 'post_group') return;
+			let handler = new window.jvbHandleSelection(field.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'
+				},
 			});
 
 			handler.subscribe((event, data) => {
 				this.selected.set(fieldId, data.selectedItems);
-				console.log(Array.from(this.selected));
-				this.syncSortableSelection(fieldId, data.selectedItems);
+				this.syncSortableSelection(fieldId);
 			});
 
 			this.selectionHandlers.set(key, handler);
@@ -1546,6 +1581,11 @@
 
 		return this.selectionHandlers.get(key);
 	}
+	updateHandlerItems(fieldId) {
+		let handler = this.getSelectionHandler(fieldId);
+		if (!handler) return;
+		handler.collectItems();
+	}
 	/*******************************************************************************
 	 SORTABLE
 	*******************************************************************************/
@@ -1652,19 +1692,20 @@
 
 	async sortableDrop(evt, fieldId) {
 		const dropTarget = evt.to;
-
 		const items = evt.items?.length > 0 ? Array.from(evt.items) : [evt.item];
 		const uploadIds = items.map(item => item.dataset.uploadId).filter(Boolean);
 
 		if (uploadIds.length === 0) return;
 
-		// Determine target group from the grid's data attribute
 		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();
 	}
 
@@ -1689,28 +1730,34 @@
 	}
 
 	handleReorder(fieldId, groupId = null) {
-		let target = (groupId) ? this.groups.get(groupId)?.ui.grid : this.fields.get(fieldId)?.ui.grid;
+		let target = (groupId)
+			? this.groups.get(groupId)?.ui.grid
+			: this.fields.get(fieldId)?.ui.grid;
+
 		if (!target) {
-			console.log ('Couldn\'t Reorder items...');
+			console.log('Couldn\'t Reorder items...');
 			return;
 		}
-		//Get current order from DOM
-		let items = Array.from(target.querySelectorAll(this.selectors.items.item+':not(.ghost)'))
+
+		// Get current order from DOM
+		let items = Array.from(target.children)
+			.filter(el => el.matches(this.selectors.items.item) && !el.classList.contains('ghost'))
 			.map(upload => upload.dataset.uploadId)
 			.filter(id => id);
 
-
 		if (!groupId) {
 			let hiddenInput = this.fields.get(fieldId)?.ui.hidden;
 			if (hiddenInput) {
 				hiddenInput.value = items.join(',');
 			}
 		} else {
-			let group = this.groups.get(groupId);
+			let group = this.stores.groups.get(groupId);
 			if (group) {
 				group.uploads = items;
+				this.stores.groups.save(group).then(()=>{});
 			}
 		}
+
 		this.a11y.announce('Items reordered');
 	}
 	/*******************************************************************************
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 7f71379..dfe392e 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",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:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&["content","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(console.log(t),!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},delay:!0,popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearCache()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={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[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemId="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][i]=n}else a.fields[i]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemId=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");this.ui.modals.edit.querySelector("h2").textContent=`Editing ${t.fields.post_title}`,s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('.all-filters input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}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.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){this.activeItem=null;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`;console.log("Maybe search",t),0!==t.length?window.debouncer.schedule(i,(()=>{console.log("Searching for",t),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=e,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 console.log("Sub filter el not found: ",t),console.log(e),!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
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 441b6ec..73152e3 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:{},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.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}`}:(console.debug(`[DataStore] Stripped 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}`}:(console.debug(`[DataStore] Stripped DOM element at ${s}`),{valid:!0,data:void 0});if(e instanceof FormData)return console.debug(`[DataStore] Converting FormData at ${s}`),{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}`}:(console.debug(`[DataStore] Stripped 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&&r.items)return r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);if(t.filters.search&&t.filters.search.trim()){const e=t.filters.search.toLowerCase().trim();let s=Array.from(t.data.values()).filter((e=>{for(const[s,r]of Object.entries(t.filters))if("search"!==s&&"page"!==s&&null!=r&&""!==r&&e[s]!==r)return!1;return!0}));return s=s.filter((t=>["name","title","path","description","slug"].some((s=>{const r=t[s];return!!r&&r.toLowerCase().includes(e)})))),s}return this.getAll(e)}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}));const a=await this.shouldFetchWithFilters(e,t,i);this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),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("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),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
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index 26a0fa2..cee4f65 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.selectors={item:e.item||".item",count:e.count||".selection-count",bulkControls:e.bulkControls||".selection-actions",checkbox:e.checkbox||'[name*="select-item"]',selectAll:e.selectAll||"[data-select-all]",wrapper:e.wrapper||":has(.item-grid)"},this.ui=window.uiFromSelectors(this.selectors,this.container),this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.initListeners()}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),this.container.addEventListener("keydown",this.keyHandler)}handleChange(e){if(e.target.matches(this.selectors.selectAll))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.checkbox)){const t=e.target.closest(this.selectors.item);if(!t)return;const s=this.getItemWrapper(t),i=this.getItemId(t);this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&this.clearSelection(),e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s}}handleClick(e){const t=e.target.closest(this.selectors.item),s=t?this.getItemWrapper(t):null;if(s&&(this.lastClicked=s),t&&!e.target.matches(this.selectors.checkbox)&&this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&(this.clearSelection(),this.lastSelectedWrapper=s),!e.shiftKey)return;if(!e.target.closest(this.selectors.checkbox)||!this.lastSelected||!this.lastSelectedWrapper)return;if(!t||!s)return;if(s!==this.lastSelectedWrapper)return;const i=Array.from(s.querySelectorAll(this.selectors.item)),l=this.getItemId(t),c=i.findIndex((e=>this.getItemId(e)===this.lastSelected)),r=i.findIndex((e=>this.getItemId(e)===l));if(-1===c||-1===r)return;const[n,h]=[Math.min(c,r),Math.max(c,r)];i.slice(n,h+1).forEach((e=>{this.select(this.getItemId(e))})),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:s})}getItemWrapper(e){if(!e)return null;const t=this.selectors.wrapper.split(",").map((e=>e.trim()));for(const s of t){const t=e.closest(s);if(t)return t}return null}handleKeys(e){if((e.ctrlKey||e.metaKey)&&"a"===e.key){if(e.preventDefault(),!this.lastClicked)return this.clearSelection(),void(window.jvbA11y&&window.jvbA11y.announce("Selection cleared"));const t=this.lastClicked.querySelectorAll(this.selectors.item),s=Array.from(t).map((e=>this.getItemId(e)));s.some((e=>this.selectedItems.has(e)))?(s.forEach((e=>this.deselect(e))),this.selectedItems.size>0?window.jvbA11y&&window.jvbA11y.announce("Selection cleared in current group"):window.jvbA11y&&window.jvbA11y.announce("Selection cleared")):(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("All selections cleared"))}"Escape"===e.key&&this.selectedItems.size>0&&(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared"))}handleSelectAll(e){const t=this.getItemWrapper(e)||e.closest(this.selectors.wrapper);if(!t)return;this.lastSelectedWrapper&&t!==this.lastSelectedWrapper&&this.clearSelection();const s=t.querySelectorAll(this.selectors.item),i=Array.from(s).map((e=>this.getItemId(e)));e.checked?(i.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t):(i.forEach((e=>this.deselect(e,!0,!1))),0===this.selectedItems.size&&(this.lastSelectedWrapper=null));let l=e.nextElementSibling||e.previousElementSibling;l&&"LABEL"===l.tagName&&(l.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:i,selectedItems:new Set(this.selectedItems)})}getItemId(e){return e.dataset.uploadId}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.setCheckboxState(e,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null,this.container.querySelectorAll(this.selectors.selectAll).forEach((e=>{e.checked=!1;const t=e.nextElementSibling||e.previousElementSibling;"LABEL"===t?.tagName&&(t.textContent="Select All")})),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.container.querySelector(`[data-upload-id="${e}"]`),i=s?.querySelector(this.selectors.checkbox);i&&i.checked!==t&&(i.checked=t)}updateSelectionUI(){if(!this.lastClicked||!this.ui.count)return;const e=this.selectedItems.size;let t=this.lastClicked.querySelector(this.selectors.bulkControls);t&&(t.hidden=0===e);let s=this.lastClicked.querySelector(this.selectors.count);if(s){const t=1===e?"item":"items";s.textContent=0===e?"":`{ ${e} ${t} selected }`,s.hidden=0===e}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.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){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
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index 6dd9498..f05859a 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 l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=i.join(","),window.jvbTaxonomy&&requestAnimationFrame((()=>{window.jvbTaxonomy.updateFieldFromInput(l)})))}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 l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=i.join(","));const r=e.querySelector(".item-grid"),o=e.querySelector(".file-upload-container");r&&window.removeChildren(r),e.querySelector(".progress")?.remove(),r&&(i.forEach((e=>{const t=window.getTemplate("uploadItem");t?(this.populateUploadItem(t,e),r.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 l=e.querySelector("img");if(e.querySelector("video")?.remove(),this.item.images[t]){const a=this.item.images[t];l&&(l.src=a.medium||a.small||a.large||"",l.alt=a["image-alt-text"]||a.alt||"");const i=e.querySelector('[name="image-title"]'),r=e.querySelector('[name="image-alt-text"]'),o=e.querySelector('[name="image-caption"]');i&&(i.value=a["image-title"]||a.title||""),r&&(r.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(console.log("Populating Timeline Gallery",a),!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"),l=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,l=e.id;t.dataset.id=a,t.dataset.postId=l;let r=t.querySelector('input[name="select-item"]'),o=t.querySelector('label[for="select-item"]');r&&o&&(r.name=`select-item-${a}`,r.id=r.name,o.htmlFor=r.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"),r=a.name.replace("upload_data::","").replace(/^\[.*?\]/,"");let o=e[r];void 0===o&&s&&(o=s[r]),null!=o&&this.populateField(t,r,o);const n=`[${l}]${r}`,c=n;a.name=n,a.id=c,i&&(i.htmlFor=c)})),i.append(t)}a.length>0&&l&&(l.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 l=e.querySelector(".editor"),r=a||"<p><br></p>";if(l){let e=l.__quill;if(!e&&window.Quill)for(let t of window.Quill.instances||[])if(t.container===l){e=t;break}e?(e.root.innerHTML=r,l.__quill=e):l.innerHTML=r}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||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=i);const r=e.querySelector(`input[type="radio"][name="${t}"][value="${i}"]`);r&&(r.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 l=e.querySelector(`[name="${t}_${i}"], [name="${i}"]`);l&&(l.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"),l=e.querySelector("template");i&&l?(window.removeChildren(i),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(l.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${r}`,o.dataset.index=r;const n=o.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const i=e.name,l=`${t}:${r}:${i}`,o=`${t}-${r}-${i}-${e.value}`;e.name=l,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
+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
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index b7501ce..725c753 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(),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(),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(),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()}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)))}getAllQueue(){let e=[...new Set([...Array.from(this.store.data.values()),...Array.from(this.queue.values())])];return this.sortByDate(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.sortByDate(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).length>0&&(this.startPolling(),!0)}startPolling(){this.isPolling||(this.isPolling=!0,this.updatePanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updatePanel("synced"))}catch(e){console.error("Polling error:",e)}}),5e3),this.startCountdown())}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.stopCountdown())}startCountdown(e=5){this.isPolling&&(this.ui.refresh.countdown.textContent=e,this.countdownTimer=setInterval((async()=>{--e>=0?this.ui.refresh.countdown.textContent=e:(this.ui.refresh.countdown.textContent="",this.stopCountdown())}),1e3))}stopCountdown(){this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null)}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;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);if(window.removeChildren(this.ui.items.container),0===t.length){const e=window.getTemplate("emptyQueue");return this.ui.items.container.append(e),void this.a11y.announce("No items in queue")}t.forEach((e=>{let t=this.items.get(e.id);t||(t=this.createOperationElement(e)),t?.element&&(this.updateOperationUI(e.id),this.ui.items.container.append(t.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));let a="completed"===s.status?"Completed: ":"Last updated: ",r=Object.hasOwn(s,"updated_at")||Object.hasOwn(s,"completed_at");if(t.ui.completed.wrap.hidden=!r,r&&t.ui.completed.label&&t.ui.completed.time){let e;e=Object.hasOwn(s,"completed_at")?s.completed_at:s.updated_at,t.ui.completed.label.textContent=a,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.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
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index 1714dda..a36e129 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)}closeModal(){const e=this.fields.get(this.activeField);if(!e)return;this.observer.unobserve(this.ui.terms.sentinel),window.removeChildren(this.ui.terms.list),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.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.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
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 7650155..565cbd3 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(console.log(e,t),!Object.hasOwn(t,"endpoint")||!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}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:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",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:"[data-upload-id]",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.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),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.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 s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.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}}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)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}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({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#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=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){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.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.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(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)}),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))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),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,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),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))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),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){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}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;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}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)))),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.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}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.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
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 7384fc7..3fa3207 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -558,39 +558,38 @@
 	$dataID = ($ID) ? ['image-id' => $ID] : false;
 	$addID = ($ID) ? '-'.$ID : '';
 
-
 	$defaultFields = [
 		'image-title'.$addID => [
-			'type'	=> 'text',
-			'label'	=> 'Image Title',
-			'value'	=> $title,
-			'data'	=> $dataID
+			'type'  => 'text',
+			'label' => 'Image Title',
+			'value' => $title,
+			'data'  => $dataID
 		],
 		'image-alt-text'.$addID => [
-			'type'	=> 'text',
-			'label'	=> 'Alt Text',
-			'value'	=> $alt,
-			'hint'	=> 'Alt text helps the visually impaired, as well as some benefits for SEO.',
-			'data'	=> $dataID
+			'type'  => 'text',
+			'label' => 'Alt Text',
+			'value' => $alt,
+			'hint'  => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+			'data'  => $dataID
 		],
 		'image-caption'.$addID => [
-			'type'	=> 'textarea',
-			'value'	=> $caption,
-			'label'	=> 'Image Caption',
-			'data'	=> $dataID
+			'type'  => 'textarea',
+			'value' => $caption,
+			'label' => 'Image Caption',
+			'data'  => $dataID
 		]
 	];
 	$fields = array_merge($defaultFields, $fields);
 
 	$config = [
-		'type'	=> 'group',
-		'wrap'	=> 'details',
-		'label'	=> 'Image Info',
-		'hint'	=> 'These will be automatically generated if left blank.',
-		'fields'	=> $fields
+		'type'   => 'group',
+		'wrap'   => 'details',
+		'label'  => 'Image Info',
+		'hint'   => 'These will be automatically generated if left blank.',
+		'fields' => $fields
 	];
 
-	return $form->render('image_data',null, $config,false, true);
+	return $form->render('image_data', null, $config, false, true);
 }
 
 
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index d425070..b868510 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -41,6 +41,8 @@
 		$this->content = $content;
 		$this->cache = CacheManager::for($content);
 
+		$this->cache->clear();
+
 		// Create and configure skeleton
 		$this->skeleton = new CRUDSkeleton();
 		$this->configure();
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index afaf791..2324496 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -1100,71 +1100,72 @@
 		$dataID = ($id) ? ['id' => $id] : '';
 		?>
 		<div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>>
-				<div class="preview">
-					<?php jvbRenderProgressBar('',true) ?>
-					<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
-					<label for="select-item<?=$addID?>" aria-label="Select image">
-						<?= ($attachment) ?: '<img>
-						<video></video>
-						<span></span>' ?>
-					</label>
-					<div class="item-actions row btw">
-						<div class="radio-button">
-							<input type="radio" class="featured btn" name="featured" id="featured" hidden>
-							<label for="featured">
-								<?=jvbIcon('star')?>
-								<?=jvbIcon('star', ['style' => 'fill'])?>
-								<span class="screen-reader-text">Set as featured image</span>
-							</label>
-						</div>
-
-						<button type="button" data-action="delete-upload" title="Remove from Group">
-							<?=jvbIcon('trash')?>
-						</button>
+			<div class="preview">
+				<?php jvbRenderProgressBar('',true) ?>
+				<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
+				<label for="select-item<?=$addID?>" aria-label="Select image">
+					<?= ($attachment) ?: '<img>
+                <video></video>
+                <span></span>' ?>
+				</label>
+				<div class="item-actions row btw">
+					<div class="radio-button">
+						<input type="radio" class="featured btn" name="featured" id="featured" hidden>
+						<label for="featured">
+							<?=jvbIcon('star')?>
+							<?=jvbIcon('star', ['style' => 'fill'])?>
+							<span class="screen-reader-text">Set as featured image</span>
+						</label>
 					</div>
+
+					<button type="button" data-action="delete-upload" title="Remove from Group">
+						<?=jvbIcon('trash')?>
+					</button>
 				</div>
-				<details>
-					<summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
+			</div>
+			<details>
+				<summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
 
-		<?php
+				<?php
+				$fields = array_key_exists('fields', $config) ? $config['fields'] : [];
 
-		$fields = array_key_exists('fields', $config) ? $config['fields'] : [];
-		$fields = array_merge([
-			'upload_data'	=> [
-				'type'	=> 'group',
-				'wrap'	=> 'details',
-				'label'	=> 'Image Info',
-				'hint'	=> 'These will be automatically generated if left blank.',
-				'fields'	=> [
-					'image-title'.$addID => [
-						'type'	=> 'text',
-						'label'	=> 'Image Title',
-						'value'	=> $title,
-						'data'	=> $dataID
-					],
-					'image-alt-text'.$addID => [
-						'type'	=> 'text',
-						'label'	=> 'Alt Text',
-						'value'	=> $alt,
-						'hint'	=> 'Alt text helps the visually impaired, as well as some benefits for SEO.',
-						'data'	=> $dataID
-					],
-					'image-caption'.$addID => [
-						'type'	=> 'textarea',
-						'value'	=> $caption,
-						'label'	=> 'Image Caption',
-						'data'	=> $dataID
-					]
-				]
-			]
-		], $fields);
+				// Only add image_data if not already provided
+				if (!array_key_exists('image_data', $fields)) {
+					$fields['image_data'] = [
+						'type'   => 'group',
+						'wrap'   => 'details',
+						'label'  => 'Image Info',
+						'hint'   => 'These will be automatically generated if left blank.',
+						'fields' => [
+							'image-title'.$addID => [
+								'type'  => 'text',
+								'label' => 'Image Title',
+								'value' => $title,
+								'data'  => $dataID
+							],
+							'image-alt-text'.$addID => [
+								'type'  => 'text',
+								'label' => 'Alt Text',
+								'value' => $alt,
+								'hint'  => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+								'data'  => $dataID
+							],
+							'image-caption'.$addID => [
+								'type'  => 'textarea',
+								'value' => $caption,
+								'label' => 'Image Caption',
+								'data'  => $dataID
+							]
+						]
+					];
+				}
 
-		$meta = new MetaManager($id);
-		foreach ($fields as $field => $config) {
-			$meta->render('form', $field, $config);
-		}
-		?>
-				</details>
+				$meta = new MetaManager($id);
+				foreach ($fields as $field => $config) {
+					$meta->render('form', $field, $config);
+				}
+				?>
+			</details>
 		</div>
 		<?php
 	}
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 118111c..e587f0d 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -44,25 +44,6 @@
 		$this->userIDType = $this->getColumnType($this->userTable, 'ID');
 		$this->termIDType = $this->getColumnType($this->wpdb->terms, 'term_id');
 		$this->postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
-
-		error_log("JVB DEBUG: userTable = " . $this->userTable);
-		error_log("JVB DEBUG: Users table exists = " . ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->userTable}'") ? 'yes' : 'no'));
-		error_log("JVB DEBUG: Users engine = " . $this->wpdb->get_var("SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}'"));
-		error_log("JVB DEBUG: MySQL version = " . $this->wpdb->db_version());
-		$usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->userTable}'");
-		$termsStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->terms}'");
-		error_log("JVB DEBUG: wp_users collation = " . $usersStatus->Collation);
-		error_log("JVB DEBUG: wp_terms collation = " . $termsStatus->Collation);
-		error_log("JVB DEBUG: Our charset_collate = " . $this->wpdb->get_charset_collate());
-
-		error_log("JVB FK Types: users.ID={$this->userIDType}, terms.term_id={$this->termIDType}, posts.ID={$this->postIDType}");
-
-		$usersDb = $this->wpdb->get_var(
-			"SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}' LIMIT 1"
-		);
-		error_log("JVB DEBUG: wp_users is in database = " . $usersDb);
-		error_log("JVB DEBUG: Current DB_NAME = " . DB_NAME);
-		error_log("JVB DEBUG: Tables being created in = " . $this->wpdb->dbname);
 	}
 
 	protected function getMultisiteUsersTable():string
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 3d455d5..38f9ddc 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -274,14 +274,14 @@
 		if (array_key_exists('taxonomies', $params)) {
 			$args = $this->applyTaxonomyFilters($args, $params);
 		}
-		if (array_key_exists('date', $params) && !empty($params['date'])) {
+		if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
 			$args = $this->applyDateFilters($args, $params);
 		}
 		if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
 			$args = $this->applyOrderFilters($args, $params);
 		}
 
-		if (!empty($params['search'])) {
+		if (array_key_exists('search', $params)) {
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
@@ -302,10 +302,15 @@
 			return $this->addCacheHeaders($response);
         }
 
+		$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);
 
-        $this->post_type = $params['content']??$params['type'];
 
         $this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
         $this->taxonomies = $this->getTaxonomies($this->post_type);
@@ -325,6 +330,87 @@
 		return $this->addCacheHeaders($response);
     }
 
+	protected function applySearchFilters(array $args, array $params): array
+	{
+		$search_term = sanitize_text_field($params['search']);
+
+		// Search term is already in $args['s'] from earlier
+
+		// Get all taxonomies registered to this post type
+		$taxonomies = get_object_taxonomies($this->post_type, 'names');
+
+		if (empty($taxonomies)) {
+			return $args;
+		}
+
+		// Cache the taxonomy term lookup per search term + post type
+		$term_cache_key = 'search_terms_' . md5($search_term . $this->post_type);
+		$matching_term_ids = $this->cache->get($term_cache_key);
+
+		if ($matching_term_ids === false) {
+			$matching_term_ids = [];
+
+			foreach ($taxonomies as $taxonomy) {
+				$terms = get_terms([
+					'taxonomy' => $taxonomy,
+					'search' => $search_term,
+					'hide_empty' => false,
+					'fields' => 'ids'
+				]);
+
+				if (!is_wp_error($terms) && !empty($terms)) {
+					$matching_term_ids = array_merge($matching_term_ids, $terms);
+				}
+			}
+
+			// Cache term IDs for 1 hour
+			$this->cache->set($term_cache_key, $matching_term_ids, 3600);
+		}
+
+		if (empty($matching_term_ids)) {
+			return $args;
+		}
+
+		// Build tax_query for matching terms
+		$term_queries = [];
+
+		foreach ($taxonomies as $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;
+			});
+
+			if (!empty($taxonomy_term_ids)) {
+				$term_queries[] = [
+					'taxonomy' => $taxonomy,
+					'field' => 'term_id',
+					'terms' => array_values($taxonomy_term_ids),
+					'operator' => 'IN'
+				];
+			}
+		}
+
+		if (!empty($term_queries)) {
+			if (isset($args['tax_query'])) {
+				$args['tax_query'] = [
+					'relation' => 'OR',
+					$args['tax_query'],
+					[
+						'relation' => 'OR',
+						...$term_queries
+					]
+				];
+			} else {
+				$args['tax_query'] = [
+					'relation' => 'OR',
+					...$term_queries
+				];
+			}
+		}
+
+		return $args;
+	}
+
     /**
      * Gets allowed taxonomies for a particular content
      * @param string $content
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 614e74c..941207b 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -270,26 +270,27 @@
 	{
 		$uploader = new UploadManager();
 		$secured_files = [];
-		$upload_map = [];
 		$errors = [];
 
 		$context = $args;
 		unset($context['upload_ids']);
 
-		$config = $this->getFieldConfig($args);
 		$file_array = $files['files'] ?? $files;
-		$tmp_names = isset($file_array['tmp_name'][0]) && is_array($file_array['tmp_name'][0])
-			? $file_array['tmp_name'][0]
-			: $file_array['tmp_name'];
+		if (!is_array($file_array['tmp_name'])) {
+			$file_array = [
+				'name' => [$file_array['name']],
+				'type' => [$file_array['type']],
+				'tmp_name' => [$file_array['tmp_name']],
+				'error' => [$file_array['error']],
+				'size' => [$file_array['size']]
+			];
+		}
+		$tmp_names = $file_array['tmp_name'] ?? [];
+
+
 
 		foreach ($tmp_names as $index => $tmp_name) {
-			$file_data = isset($file_array['tmp_name'][0]) && is_array($file_array['tmp_name'][0]) ? [
-				'name' => $file_array['name'][0][$index],
-				'type' => $file_array['type'][0][$index],
-				'tmp_name' => $tmp_name,
-				'error' => $file_array['error'][0][$index],
-				'size' => $file_array['size'][0][$index]
-			] : [
+			$file_data = [
 				'name' => $file_array['name'][$index],
 				'type' => $file_array['type'][$index],
 				'tmp_name' => $tmp_name,
@@ -309,8 +310,8 @@
 					throw new Exception('Failed to secure file');
 				}
 
-				$frontend_id = $args['upload_ids'][$index] ?? 'upload_' . $index;
-				$upload_map[$index] = $frontend_id;
+				// Embed upload_id directly in the secured file data
+				$secured['upload_id'] = $args['upload_ids'][$index] ?? 'upload_' . $index;
 
 				$secured_files[] = $secured;
 			} catch (Exception $e) {
@@ -320,7 +321,6 @@
 
 		return [
 			'files' => $secured_files,
-			'upload_map' => $upload_map,
 			'errors' => $errors
 		];
 	}
@@ -343,7 +343,7 @@
 	/**
 	 * Queue files for processing
 	 */
-	protected function queueProcessing(array $secured_data, array $args):string
+	protected function queueProcessing(array $secured_data, array $args): string
 	{
 		$operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
 
@@ -353,20 +353,18 @@
 		} elseif ($operation_type === 'document') {
 			$chunkSize = 10;
 		}
+
 		JVB()->queue()->queueOperation(
 			$operation_type,
 			$args['user'],
 			array_merge(
-				[
-					'secured_files' 	=> $secured_data['files'],
-					'upload_map' 		=> $secured_data['upload_map'],
-				],
+				['secured_files' => $secured_data['files']],
 				$args
 			),
 			[
-				'operation_id'	=> $args['upload'],
-				'chunk_key'		=> 'secured_files',
-				'chunk_size'	=> $chunkSize
+				'operation_id'  => $args['upload'],
+				'chunk_key'     => 'secured_files',
+				'chunk_size'    => $chunkSize
 			]
 		);
 
@@ -376,9 +374,9 @@
 				$args['user'],
 				$args,
 				[
-					'priority'		=> 'high',
-					'operation_id'	=> $args['id'],
-					'depends_on'	=> $args['upload']
+					'priority'      => 'high',
+					'operation_id'  => $args['id'],
+					'depends_on'    => $args['upload']
 				]
 			);
 		}
@@ -387,16 +385,17 @@
 			'temporary_cleanup',
 			$args['user'],
 			[
-				'files'		=> $secured_data['files'],
-				'context'	=> $args,
+				'files'     => $secured_data['files'],
+				'context'   => $args,
 			],
 			[
-				'priority'		=> 'low',
-				'chunk_size'	=> 5,
-				'chunk_key'		=> 'files',
-				'depends_on'	=> $args['upload']
+				'priority'      => 'low',
+				'chunk_size'    => 5,
+				'chunk_key'     => 'files',
+				'depends_on'    => $args['upload']
 			]
 		);
+
 		return $args['id'];
 	}
 
@@ -579,9 +578,8 @@
 
 			$args = $data;
 			unset($args['secured_files']);
-			unset($args['upload_map']);
 
-			foreach ($data['secured_files'] as $index => $secured_file) {
+			foreach ($data['secured_files'] as $secured_file) {
 				$result = $uploader->processUpload(
 					$secured_file['temp_path'],
 					array_merge(
@@ -593,7 +591,7 @@
 							'term_id' => (int)($data['term_id'] ?? 0),
 							'original_name' => $secured_file['original_name'],
 							'mime_type' => $secured_file['mime_type'],
-							'content' => $data['content'] ?? '', // For directory pattern
+							'content' => $data['content'] ?? '',
 						]
 					)
 				);
@@ -601,20 +599,19 @@
 				if (!is_wp_error($result)) {
 					$standardized = $this->standardizeResult($result);
 
-					// Map to frontend upload ID
-					if (isset($data['upload_map'][$index])) {
-						$standardized['upload_id'] = $data['upload_map'][$index];
+					// Use embedded upload_id from the secured file itself
+					$standardized['upload_id'] = $secured_file['upload_id'] ?? null;
+
+					if ($standardized['upload_id']) {
+						$processed_results[$standardized['upload_id']] = $standardized;
+					} else {
+						$processed_results[] = $standardized;
 					}
 
 					// Apply frontend metadata if provided
-					if (!empty($data['frontend_metadata'][$index])) {
-						$this->applyMeta(
-							$standardized['attachment_id'],
-							$data['frontend_metadata'][$index]
-						);
+					if (!empty($secured_file['metadata'])) {
+						$this->applyMeta($standardized['attachment_id'], $secured_file['metadata']);
 					}
-
-					$processed_results[$standardized['upload_id']] = $standardized;
 				}
 			}
 
@@ -639,8 +636,8 @@
 				]
 			);
 			return [
-				'success'	=> false,
-				'result'	=> $e->getMessage()
+				'success'   => false,
+				'result'    => $e->getMessage()
 			];
 		}
 	}
@@ -1089,16 +1086,12 @@
 		try {
 			$files = $request->get_file_params();
 			$args = $this->buildUploadArgs($request);
-			$data = $request->get_params();
-
-			global $_FILES;
 
 			if (!$args['content'] || !$args['user'] || !$args['posts']) {
-
 				$this->logError('Missing required data');
 				return new WP_REST_Response([
-					'success'	=> false,
-					'message'	=> 'Missing required data'
+					'success'   => false,
+					'message'   => 'Missing required data'
 				]);
 			}
 
@@ -1126,10 +1119,7 @@
 				$operation_type,
 				$args['user'],
 				array_merge(
-					[
-						'secured_files' => $secured_files['files'],
-						'upload_map' => $secured_files['upload_map'],
-					],
+					['secured_files' => $secured_files['files']],
 					$args
 				),
 				[
@@ -1185,40 +1175,55 @@
 			$queue = JVB()->queue();
 			$all_uploaded_images = [];
 
-			// Get the upload operation ID from data
+			// Get the upload operation ID from dependencies
 			$dependencies = json_decode($operation->dependencies, true);
-			$uploads = [];
-			foreach($dependencies as $dependency) {
-				$op = $queue->getOperation($dependency);
 
-				$res = json_decode($op->result, true);
-				if ($op->status === 'completed') {
-					$uploads = array_merge($uploads, $res);
+			if (empty($dependencies)) {
+				return [
+					'success'   => false,
+					'result'    => 'No dependencies found'
+				];
+			}
+
+			// Collect uploads from all dependency operations
+			$uploads = [];
+			foreach ($dependencies as $dependency) {
+				// Use getOperationValue like ContentRoutes does
+				$res = $queue->getOperationValue($dependency, 'result');
+
+				if (empty($res)) {
+					continue;
+				}
+
+				// Results are stored at root level, keyed by upload_id
+				// Filter to only include actual upload results (arrays with attachment_id)
+				foreach ($res as $key => $value) {
+					if (is_array($value) && isset($value['attachment_id'])) {
+						$uploads[$key] = $value;
+					}
 				}
 			}
 
-
 			if (empty($uploads)) {
 				return [
-					'success'	=> false,
-					'result'	=> 'No uploads to process'
+					'success'   => false,
+					'result'    => 'No uploads to process'
 				];
 			}
 
 			// Build map of upload_id => attachment_id
-			foreach ($uploads as $img) {
-				if (isset($img['upload_id'], $img['attachment_id'])) {
-					$all_uploaded_images[$img['upload_id']] = [
-						'upload_id' => $img['upload_id'],
-						'attachment_id' => (int)$img['attachment_id']
-					];
-				}
+			foreach ($uploads as $upload_id => $img) {
+				$all_uploaded_images[$upload_id] = [
+					'upload_id' => $upload_id,
+					'attachment_id' => (int)$img['attachment_id']
+				];
 			}
 
 			$content = jvbCheckBase($data['content']);
 			if (Features::forContent($data['content'])->has('is_timeline')) {
 				return $this->processTimelineUploads($data, $uploads, $all_uploaded_images, $operation);
 			}
+
 			$user = (int)$data['user'];
 			$created_posts = [];
 			$used_upload_ids = [];
@@ -1233,7 +1238,7 @@
 					? sanitize_textarea_field($post['fields']['post_excerpt'])
 					: '';
 
-				$args =[
+				$args = [
 					'post_type' => $content,
 					'post_author' => $user,
 					'post_status' => 'draft',
@@ -1246,20 +1251,18 @@
 				if ($new_post_id && !is_wp_error($new_post_id)) {
 					$created_posts[] = $new_post_id;
 
-					// Get featured image upload_id
-
-
-					$featured_upload_id = array_key_exists('featured', $post['fields']) ? (int)$post['fields']['featured'] : null;
+					// Get featured image upload_id - string, not int!
+					$featured_upload_id = $post['fields']['featured'] ?? null;
 					$featured_attachment_id = null;
 					$gallery_attachment_ids = [];
 
 					// Process all images for this post
-					foreach ($post['images'] as $key => $img) {
+					foreach ($post['images'] as $img) {
 						$upload_id = $img['upload_id'];
 						$used_upload_ids[] = $upload_id;
 
 						if (isset($all_uploaded_images[$upload_id])) {
-							$attachment_id = (int)$all_uploaded_images[$upload_id]['attachment_id'];
+							$attachment_id = $all_uploaded_images[$upload_id]['attachment_id'];
 
 							if ($upload_id === $featured_upload_id) {
 								$featured_attachment_id = $attachment_id;
@@ -1271,10 +1274,9 @@
 
 					// Set featured image
 					if ($featured_attachment_id) {
-						set_post_thumbnail($new_post_id, (int)$featured_attachment_id);
+						set_post_thumbnail($new_post_id, $featured_attachment_id);
 					} elseif (!empty($gallery_attachment_ids)) {
-						// If no featured set, use first image
-						set_post_thumbnail($new_post_id, (int)$gallery_attachment_ids[0]);
+						set_post_thumbnail($new_post_id, $gallery_attachment_ids[0]);
 						array_shift($gallery_attachment_ids);
 					}
 
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 1268132..ecef889 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -604,17 +604,19 @@
 	protected function renderContent(): void {
 		$dataIgnore = $this->useCRUDjs ? '' : ' data-ignore';
 		?>
-		<section class="items-list <?= esc_attr($this->dataType) ?> crud" data-content="<?= esc_attr($this->dataType) ?>" data-view="<?= $this->defaultView?>"<?=$dataIgnore?>>
-			<?php
-			$this->renderControlsAndFilters();
+		<section class="items-list <?= esc_attr($this->dataType) ?> crud" data-content="<?= esc_attr($this->dataType) ?>" data-singular="<?=$this->singular?>" data-plural="<?=$this->plural?>" data-view="<?= $this->defaultView?>"<?=$dataIgnore?>>
+			<div class="wrap">
+				<?php
+				$this->renderControlsAndFilters();
 
-			if ($this->showBulkControls) {
-				$this->renderBulkActions();
-			}
-			?>
+				if ($this->showBulkControls) {
+					$this->renderBulkActions();
+				}
+				?>
 
-			<div class="<?= esc_attr($this->dataType) ?> item-grid" role="grid"></div>
-			<div class="scroll-sentinel" aria-hidden="true"></div>
+				<div class="<?= esc_attr($this->dataType) ?> item-grid" role="grid"></div>
+				<div class="scroll-sentinel" aria-hidden="true"></div>
+			</div>
 		</section>
 		<?php
 	}
@@ -628,7 +630,7 @@
 		}
 		?>
 		<details class="all-filters col start" data-ignore>
-			<summary>Filters</summary>
+			<summary>Filters <button hidden data-action="clear-filters" data-ignore><?=jvbIcon('x')?><span>Clear Filters</span></span></button></summary>
 			<?php
 
 			$this->renderSearch();
@@ -640,6 +642,7 @@
 				$this->renderColumnSelector();
 			}
 			?>
+			<button data-action="refresh" data-ignore><?=jvbIcon('arrows-clockwise')?><span>Hard Refresh</span></span></button>
 		</details>
 		<?php
 	}
@@ -747,8 +750,11 @@
 					$i = 0;
 					foreach ($option as $opt => $label) {
 						$icon = $opt === 'date' ? 'calendar' : $opt;
+						$value = $opt;
+						$value = ($value === 'sort-ascending') ? 'asc' : $value;
+						$value = ($value === 'sort-descending') ? 'desc' : $value;
 						?>
-						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
+						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$value?>"<?=$i===0 ? ' checked':''?>>
 
 						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbDashIcon($icon)?></label>
 						<?php
@@ -822,11 +828,11 @@
 					<label for="date-start" class="col">
 						From
 					</label>
-					<input type="date" id="date-start" class="date-start">
+					<input type="date" id="date-start" class="date-start" name="date-start">
 					<label for="date-end" class="col">
 					   To
 					</label>
-					<input type="date" id="date-end" class="date-end">
+					<input type="date" id="date-end" class="date-end" name="date-end">
 				</div>
 				<div class="month-picker">
 					<label>
@@ -870,7 +876,7 @@
 		$out = '';
 		if (!empty($terms)) {
 			$out .= sprintf(
-				'<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
+				'<div class="row nowrap"><label class="m-0" for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
                 <select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s">
                 <option value="">by %s</option>',
 				$taxonomy,
@@ -986,7 +992,7 @@
 				}
 				foreach ($this->taxonomies as $taxonomy => $config) {
 					?>
-					<option value="tax-<?=$taxonomy?>">Add to <?= JVB_TAXONOMY[$taxonomy]['singular']??$config['label'] ?></option>
+					<option value="tax-<?=$taxonomy?>" data-type="selector" data-single="<?=JVB_TAXONOMY[$taxonomy]['singular']?>" data-plural="<?=JVB_TAXONOMY[$taxonomy]['plural']?>" data-taxonomy="<?=$taxonomy?>">Add to <?= JVB_TAXONOMY[$taxonomy]['singular']??$config['label'] ?></option>
 					<?php
 				}
 				?>
@@ -1169,11 +1175,11 @@
 				<?= $this->renderItemSelect()?>
 				<?=$this->renderImage() ?>
 				<div class="col start w-full">
-					<?= $this->renderItemActions()?>
 					<h3 data-field="post_title"></h3>
 					<p data-attr="date"></p>
 					<p data-field="price"></p>
 					<div data-field="post_excerpt"></div>
+					<?= $this->renderItemActions()?>
 				</div>
 			</div>
 		</template>

--
Gitblit v1.10.0