From b0194e10a87e16797a568d8a30d53ebecd27d8a4 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 18 Oct 2025 15:04:51 +0000
Subject: [PATCH] =DataStore.js and UploaderManager.js overhaul

---
 assets/js/dash/UtilityFunctions.js          |   60 
 assets/css/dash.min.css                     |    2 
 assets/js/min/dragHandler.min.js            |    1 
 jvb.php                                     |   51 
 inc/helpers/ui.php                          |   33 
 assets/js/min/square.min.js                 |    2 
 build/video/index.asset.php                 |    2 
 inc/rest/routes/TermRoutes.php              |    3 
 inc/managers/OperationQueue.php             |   35 
 assets/js/min/form.min.js                   |    2 
 inc/managers/DashboardManager.php           |  222 
 inc/managers/CRUDManager.php                |    8 
 assets/js/min/referral.min.js               |    1 
 assets/js/min/queue.min.js                  |    2 
 assets/js/min/tabs.min.js                   |    2 
 assets/js/min/favourites.min.js             |    2 
 assets/js/min/utility.min.js                |    2 
 templates/dashboard/sections/shop.php       |    2 
 inc/managers/_setup.php                     |    7 
 assets/js/concise/DataStoreOld.js           |  931 ++
 JVBase.php                                  |   35 
 inc/managers/LoginManager.php               |    2 
 inc/integrations/Square.php                 |   79 
 assets/js/concise/Queue.js                  |   63 
 build/video/block.json                      |   26 
 assets/js/concise/Popup.js                  |   79 
 inc/registry/MakeCalendarType.php           |    2 
 assets/js/concise/Referral.js               |  446 +
 assets/js/min/selector.min.js               |    2 
 assets/js/concise/HandleSelection.js        |  398 
 assets/js/concise/FormController.js         |   81 
 assets/js/concise/UploadManagerOld.js       | 4114 +++++++++
 inc/managers/DirectoryManager.php           |    6 
 assets/js/min/handleSelection.min.js        |    1 
 assets/js/concise/UploadManager.js          | 4459 +++++-----
 src/video/block.json                        |   26 
 assets/js/dash/SquareCheckout.js            |   44 
 assets/js/concise/UserSettings.js           |  127 
 assets/js/min/crud.min.js                   |    2 
 src/video/editor.scss                       |   80 
 assets/js/concise/PopulateForm.js           |    2 
 assets/js/dash/CRUD.js                      |   29 
 inc/helpers/renderFields.php                |  152 
 assets/css/forms.min.css                    |    2 
 assets/js/concise/FrontendFavourites.js     |  126 
 assets/js/concise/View.js                   |   35 
 src/video/style.scss                        |  145 
 assets/js/min/popup.min.js                  |    1 
 inc/rest/routes/ReferralRoutes.php          |  324 
 inc/meta/MetaTypeManager.php                |    5 
 src/video/edit.js                           |  101 
 templates/dashboard/sections/favourites.php |    2 
 inc/managers/ReferralManager.php            |  821 +
 inc/managers/MagicLinkManager.php           |  642 +
 build/video/index.css                       |    2 
 inc/rest/_setup.php                         |    3 
 build/video/index-rtl.css                   |    2 
 inc/meta/MetaForm.php                       |  396 
 inc/rest/routes/UploadRoutes.php            | 2254 ++--
 assets/js/min/populate.min.js               |    2 
 build/video/style-index.css                 |    2 
 build/video/style-index-rtl.css             |    2 
 inc/managers/UploadManager.php              | 1256 +-
 inc/utility/Features.php                    |   23 
 assets/css/nav.min.css                      |    2 
 assets/js/concise/UploadManagerOlder.js     | 4010 +++++++++
 assets/js/concise/TaxonomySelector.js       |   94 
 assets/js/min/uploader.min.js               |    2 
 assets/js/min/view.min.js                   |    2 
 globals.php                                 |    7 
 inc/blocks/VideoCoverBlock.php              |    5 
 assets/js/concise/DataStore.js              | 1265 +-
 webpack.jvb.js                              |    4 
 templates/dashboard/sections/news.php       |    2 
 inc/meta/MetaSanitizer.php                  |    2 
 assets/js/concise/DragHandler.js            |  392 
 assets/js/min/dataStore.min.js              |    2 
 build/video/index.js                        |    2 
 assets/js/dash/Tabs.js                      |    4 
 inc/rest/routes/MagicLinkRoutes.php         |  235 
 inc/managers/UploadManager2.php             | 1206 ++
 inc/integrations/Helcim.php                 |    2 
 82 files changed, 19,714 insertions(+), 5,295 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 9f4519c..7191fff 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -25,6 +25,7 @@
 use JVBase\rest\routes\FormRoutes;
 use JVBase\rest\routes\NewsRoutes;
 use JVBase\rest\routes\ReferralRoutes;
+use JVBase\rest\routes\MagicLinkRoutes;
 use JVBase\rest\routes\ResponseRoutes;
 use JVBase\rest\routes\OptionsRoutes;
 use JVBase\rest\routes\VoteRoutes;
@@ -83,6 +84,11 @@
 			'userTerms'		=> new UserTermsManager(),
         ];
 
+		add_action('wp_footer', [$this, 'additionalActions']);
+
+		if (Features::forSite()->has('magicLink')) {
+			$this->routes['magicLink'] = new MagicLinkRoutes();
+		}
 		if (Features::forSite()->has('referrals')) {
 			$this->managers['referral'] = new ReferralManager();
 			$this->routes['referral'] = new ReferralRoutes();
@@ -262,4 +268,33 @@
 		return $this->managers['referral'];
 	}
 
+	public function additionalActions():void
+	{
+		$extras = apply_filters('jvbAdditionalActions', []);
+		$extras = array_filter($extras, function ($extra) {
+			return is_array($extra) && array_key_exists('button', $extra) && array_key_exists('content', $extra);
+		});
+		if (empty ($extras)) {
+			return;
+		}
+		$buttons = array_map(function ($extra) {
+			return $extra['button'];
+		}, $extras);
+		$contents = array_map(function($extra) {
+			return $extra['content'];
+		}, $extras);
+
+		if (!empty ($buttons)) {
+			?>
+			<section class="additional-actions">
+				<div class="buttons col">
+					<?= implode($buttons); ?>
+				</div>
+				<div class="actions">
+					<?= implode($contents); ?>
+				</div>
+			</section>
+			<?php
+		}
+	}
 }
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index d5e44db..55cbc7a 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard h1:first-of-type{margin-top:0!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid:has(.select-item:checked) .item{padding:2rem;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{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--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(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-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(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.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(--transition-base),width var(--transition-base),padding var(--transition-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(--transition-base),width var(--transition-base),padding var(--transition-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}.create-item{left:auto!important;right:1rem;bottom:var(--offHeight)!important}.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(--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.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}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(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.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(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--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}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.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}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard h1:first-of-type{margin-top:0!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.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{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--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(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-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(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.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(--transition-base),width var(--transition-base),padding var(--transition-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(--transition-base),width var(--transition-base),padding var(--transition-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}.create-item{left:auto!important;right:1rem;bottom:var(--offHeight)!important}.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(--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.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}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(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.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(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--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}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.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}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index 8642cb1..4acd93b 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:var(--action-rgb-subtle);position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:var(--action-rgb-subtle-hover);border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.image:has(.upload-item) .file-upload-container{display:none}.field.image{position:relative}.field.image:not(.uploading) .progress{display:none}.field.image .actions{position:absolute;top:0;right:0}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:calc(100% - (var(--padding) * 2.5));height:calc(100% - (var(--padding) * 2))}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] [for=select-item]{display:none}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}
\ No newline at end of file
+details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.item,.upload.item{border-radius:var(--innerRadius);overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:calc(100% - (var(--padding) * 2.5));height:calc(100% - (var(--padding) * 2))}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/
\ No newline at end of file
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index 08f398d..81f1362 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav{--py:.25rem;--px:1rem;max-width:100%}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;background-color:var(--base);color:var(--contrast);border-radius:0}.toggle svg{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) svg,.has-submenu:hover>button:not(.notifications,.quick-help) svg{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:9999;position:fixed;width:var(--height);bottom:0;right:0}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-end;--align:center;position:fixed;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);z-index:var(--zz-top);top:0;right:-300vw;padding:0;width:100%;height:100vh;overflow:hidden auto;transition:right var(--timing) var(--function)}@media (min-width:768px){nav.always>ul{--justify:flex-start}}nav.always.open>ul{width:100%;right:0;gap:0}nav.always>ul li.active,nav.always>ul li:focus-within,nav.always>ul li:hover{background-color:var(--overlay-heavy)}nav.always li{width:100%;height:fit-content}nav.always a{--py:1rem;width:100%}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .list,nav.always>button .x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .x,nav.always>button .list{transform:scale(1);height:32px;width:32px}@media (min-width:768px){nav.always a{padding:2rem 0}nav.always>ul{padding:var(--height) 0}}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:999}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--large)}}nav.on-this-page{--justify:space-between;max-width:none;z-index:99;margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:99999999;width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle svg{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.is-style-condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.2rem;--px:1rem}.is-style-condensed>ul{--wrap:wrap}.is-style-condensed ul{--justify:center;--gap:0}.is-style-condensed li{width:fit-content}.is-style-condensed li+li::before{content:'·';display:block;padding:0 .5em}.is-style-condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:9999;box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}body>header{justify-content:space-between}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start}.current-hours{position:sticky;top:var(--height);z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}
\ No newline at end of file
+nav{--py:.25rem;--px:1rem;max-width:100%}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;background-color:var(--base);color:var(--contrast);border-radius:0}.toggle svg{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) svg,.has-submenu:hover>button:not(.notifications,.quick-help) svg{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:9999;position:fixed;width:var(--height);bottom:0;right:0}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-end;--align:center;position:fixed;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);z-index:var(--zz-top);top:0;right:-300vw;padding:0;width:100%;height:100vh;overflow:hidden auto;transition:right var(--timing) var(--function)}@media (min-width:768px){nav.always>ul{--justify:flex-start}}nav.always.open>ul{width:100%;right:0;gap:0}nav.always>ul li.active,nav.always>ul li:focus-within,nav.always>ul li:hover{background-color:var(--overlay-heavy)}nav.always li{width:100%;height:fit-content}nav.always a{--py:1rem;width:100%}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .list,nav.always>button .x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .x,nav.always>button .list{transform:scale(1);height:32px;width:32px}@media (min-width:768px){nav.always a{padding:2rem 0}nav.always>ul{padding:var(--height) 0}}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:999}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--large)}}nav.on-this-page{--justify:space-between;max-width:none;z-index:99;margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:99999999;width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle svg{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.is-style-condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.2rem;--px:1rem}.is-style-condensed>ul{--wrap:wrap}.is-style-condensed ul{--justify:center;--gap:0}.is-style-condensed li{width:fit-content}.is-style-condensed li+li::before{content:'·';display:block;padding:0 .5em}.is-style-condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:9999;box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}body>header{justify-content:space-between}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}
\ No newline at end of file
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 4f4ba59..946bf0e 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -1,94 +1,121 @@
 /**
- * Handles GET Requests, storing responses by a key of filters, set with setFilter method
- * Stores:
- * 		- Items: the individual item data, mapped by postID/termID
- * 		- Cache: the cacheKey generated by filters, and the results in the value
- * 		- httpHeaders: If Modified Since tracking
- * 		- domCache: rendered DOM elements, to reduce on-page rendering
+ * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
+ *
+ * Configuration-based approach for different storage needs:
+ * - Configurable endpoint, keyPath, and indexes
+ * - Built-in ETag and If-Modified-Since support
+ * - Automatic DOM reference stripping
+ * - TTL-based cache invalidation
  */
 class DataStore {
 	constructor(config = {}) {
+		// Core configuration with sensible defaults
 		this.config = {
+			// Storage configuration
 			name: 'default',
-			endpoint: false,
+			version: 1,
+			storeName: 'items',
+			keyPath: 'id',
+			indexes: [], // Array of {name, keyPath, unique}
+
+			// API configuration
+			endpoint: null,
 			apiBase: jvbSettings.api,
-			TTL: 3600000, // 1 hour default
-			showLoading: true,
 			headers: {},
 			filters: {},
+
+			// Cache configuration
+			TTL: 3600000, // 1 hour default
+			useHttpCaching: true, // ETag and If-Modified-Since
+			cacheKeyStrategy: 'filters', // How to generate cache keys
+
+			// UI configuration
+			showLoading: true,
+
+			// Features
+			stripDOMReferences: true,
+			storeBlobs: false,
+
 			...config
 		};
-		if (!this.config.endpoint) {
-			console.warn('No endpoint set. Only saving locally');
-		}
+
+		// Initialize base properties
+		this.db = null;
+		this.data = new Map();
+		this.cache = new Map();
+		this.httpHeaders = new Map();
+		this.subscribers = new Set();
+		this.currentRequest = null;
+		this.filters = this.config.filters??{};
+
+		// Set up headers
+		this.headers = {
+			'X-WP-Nonce': jvbSettings?.nonce,
+			...this.config.headers
+		};
 
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...this.config.headers
-		};
-
-		// Data stores
-		this.items = new Map();
-		this.cache = new Map(); //TODO: call this resultsCache
-		this.httpHeaders = new Map();
-		this.domCache = new Map();
-		this.forms = new Map();
-
-		// State management
-		this.filters = config.filters ?? {};
-		this.subscribers = new Set();
-		this.db = null;
-		this.currentRequest = null;
-
-		// Server Timestamps - needed?
-		this.cachedContent = JSON.parse(cacheJVB.cache) || {};
-		this.lastTimestampUpdate = Date.now();
-
+		// Auto-initialize
 		this.initDB();
-		document.addEventListener('beforeUnload', () =>this.destroy());
+
+		// Cleanup on page unload
+		window.addEventListener('beforeunload', () => this.destroy());
 	}
 
+	/**
+	 * Initialize IndexedDB with configurable schema
+	 */
 	async initDB() {
-		if (!('indexedDB' in window)) return;
+		if (!('indexedDB' in window)) {
+			console.warn('IndexedDB not supported');
+			return;
+		}
 
-		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
+		const dbName = `jvb_${this.config.name}_db`;
+		const request = indexedDB.open(dbName, this.config.version);
 
 		request.onupgradeneeded = (e) => {
 			const db = e.target.result;
-			// Items store
-			if (!db.objectStoreNames.contains('items')) {
-				db.createObjectStore('items', { keyPath: 'id' });
-			}
 
-			// DOM cache for rendered elements
-			if (!db.objectStoreNames.contains('dom')) {
-				db.createObjectStore('dom', { keyPath: 'id' });
-			}
-
-			if (!db.objectStoreNames.contains('forms')) {
-				let forms = db.createObjectStore('forms', {
-					keyPath: 'formId',
+			// Create main store with configurable keyPath
+			if (!db.objectStoreNames.contains(this.config.storeName)) {
+				const store = db.createObjectStore(this.config.storeName, {
+					keyPath: this.config.keyPath
 				});
-				forms.createIndex('status', 'status', {unique:false});
-				forms.createIndex('operationId', 'operationId', {unique:false});
-				forms.createIndex('timestamp', 'timestamp', {unique:false});
+
+				// Add configured indexes
+				this.config.indexes.forEach(index => {
+					store.createIndex(
+						index.name,
+						index.keyPath || index.name,
+						{ unique: index.unique || false }
+					);
+				});
 			}
 
-			// Cache store for GET requests with endpoint index
-			if (!db.objectStoreNames.contains('cache')) {
+			// Cache store for HTTP responses
+			if (this.config.endpoint && !db.objectStoreNames.contains('cache')) {
 				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
 				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
 				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
 				cacheStore.createIndex('filters', 'filters', { unique: false });
 			}
 
-			// HTTP headers store
-			if (!db.objectStoreNames.contains('headers')) {
+			// HTTP headers store for ETag/If-Modified-Since
+			if (this.config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
 				db.createObjectStore('headers', { keyPath: 'key' });
 			}
+
+			if (this.config.storeBlobs && !db.objectStoreNames.contains('blobs')) {
+				db.createObjectStore('blobs', { keyPath: 'uploadId' });
+			}
+
+			// Call optional schema extension
+			if (this.config.onUpgrade) {
+				this.config.onUpgrade(db, e.oldVersion, e.newVersion);
+			}
 		};
 
 		request.onsuccess = (e) => {
@@ -97,135 +124,293 @@
 		};
 
 		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
+			console.error(`IndexedDB error for ${dbName}:`, e);
+			if (this.config.onError) {
+				this.config.onError(e);
+			}
 		};
 	}
 
+	/**
+	 * Load all data from IndexedDB
+	 */
 	async loadFromDB() {
 		if (!this.db) return;
 
+		const loadPromises = [
+			this.loadData()
+		];
+
+		if (this.config.endpoint) {
+			loadPromises.push(this.loadCache());
+		}
+
+		if (this.config.useHttpCaching) {
+			loadPromises.push(this.loadHeaders());
+		}
+
 		try {
-			await Promise.all([
-				this.loadItems(),
-				this.loadCache(),
-				this.loadHeaders(),
-				this.loadDOMCache(),
-				this.loadForms()
-			]);
+			await Promise.all(loadPromises);
+			this.notify('data-loaded', {
+				count: this.data.size,
+				store: this.config.storeName
+			});
 		} catch (error) {
 			console.error('Error loading from DB:', error);
 		}
 	}
 
-	async loadItems() {
+	/**
+	 * Load main data from IndexedDB
+	 */
+	async loadData() {
 		if (!this.db) return;
 
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['items'], 'readonly');
-			const store = tx.objectStore('items');
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
 			const request = store.getAll();
 
 			request.onsuccess = (e) => {
 				e.target.result.forEach(item => {
-					this.items.set(item.id, item);
-				});
-				this.notify('items-loaded', { items: Array.from(this.items.values()) });
-				resolve();
-			};
-		});
-	}
+					// Strip DOM references if needed
+					const cleaned = this.config.stripDOMReferences
+						? this.stripDOMReferences(item)
+						: item;
 
-	async loadCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['cache'], 'readonly');
-			const store = tx.objectStore('cache');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					if (this.isCacheValid(item)) {
-						this.cache.set(item.key, item);
-					}
+					const key = this.getItemKey(cleaned);
+					this.data.set(key, cleaned);
 				});
 				resolve();
 			};
+
+			request.onerror = (e) => reject(e);
 		});
 	}
 
-	async loadHeaders() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['headers'], 'readonly');
-			const store = tx.objectStore('headers');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(header => {
-					this.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadDOMCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['dom'], 'readonly');
-			const store = tx.objectStore('dom');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(domEntry => {
-					// Convert stored HTML back to DOM elements
-					const reconstructed = {};
-					Object.entries(domEntry.views).forEach(([viewName, html]) => {
-						const temp = document.createElement('div');
-						temp.innerHTML = html;
-						reconstructed[viewName] = temp.firstElementChild;
-					});
-					this.domCache.set(domEntry.id, reconstructed);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadForms() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['forms'], 'readonly');
-			const store = tx.objectStore('forms');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(form => {
-					this.forms.set(form.key, form);
-				});
-				resolve();
-			};
-		});
-	}
-
-	setLoading(on) {
-		this.body.classList.toggle('loading', on);
-		if (on) {
-			this.loading.showModal();
-		} else {
-			this.loading.close();
-		}
-
-	}
-
 	/**
-	 * Main fetch method with caching and conditional requests
+	 * Strip DOM references from an object (recursive)
 	 */
-	async fetch(endpoint = null, options = {}) {
+	stripDOMReferences(obj) {
+		if (!obj || typeof obj !== 'object') return obj;
+
+		// Handle arrays
+		if (Array.isArray(obj)) {
+			return obj.map(item => this.stripDOMReferences(item));
+		}
+
+		// Handle objects
+		const cleaned = {};
+		for (const [key, value] of Object.entries(obj)) {
+			// Skip DOM-related properties
+			if (this.isDOMReference(key, value)) {
+				continue;
+			}
+
+			// Handle Set/Map collections
+			if (value instanceof Set) {
+				cleaned[key] = Array.from(value);
+			} else if (value instanceof Map) {
+				cleaned[key] = Object.fromEntries(value);
+			} else if (typeof value === 'object' && value !== null) {
+				cleaned[key] = this.stripDOMReferences(value);
+			} else {
+				cleaned[key] = value;
+			}
+		}
+
+		return cleaned;
+	}
+
+	/**
+	 * Check if a property is a DOM reference
+	 */
+	isDOMReference(key, value) {
+		// Check value types
+		if (value instanceof HTMLElement ||
+			value instanceof NodeList ||
+			value instanceof HTMLCollection ||
+			(value && value.nodeType !== undefined)) {
+			return true;
+		}
+
+		// Check key names
+		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
+		if (domKeys.some(k => key.toLowerCase().includes(k))) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get the key for an item based on configured keyPath
+	 */
+	getItemKey(item) {
+		if (typeof this.config.keyPath === 'function') {
+			return this.config.keyPath(item);
+		}
+
+		// Support nested keypaths like 'meta.id'
+		const keys = this.config.keyPath.split('.');
+		let value = item;
+
+		for (const key of keys) {
+			value = value?.[key];
+		}
+
+		return value;
+	}
+
+	/**
+	 * Save a single item
+	 */
+	async save(item) {
+		const key = this.getItemKey(item);
+
+		// Strip DOM references if configured
+		const cleaned = this.config.stripDOMReferences
+			? this.stripDOMReferences(item)
+			: item;
+
+		// Store in memory
+		this.data.set(key, cleaned);
+
+		// Persist to IndexedDB
+		await this.saveToDB(cleaned);
+
+		// Notify subscribers
+		this.notify('item-saved', { item: cleaned, key });
+
+		return cleaned;
+	}
+
+	/**
+	 * Save item to IndexedDB
+	 */
+	async saveToDB(item) {
+		if (!this.db) return;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readwrite');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.put(item);
+
+			request.onsuccess = () => resolve();
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Batch save multiple items
+	 */
+	async saveMany(items) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction([this.config.storeName], 'readwrite');
+		const store = tx.objectStore(this.config.storeName);
+
+		const promises = items.map(item => {
+			const cleaned = this.config.stripDOMReferences
+				? this.stripDOMReferences(item)
+				: item;
+
+			const key = this.getItemKey(cleaned);
+			this.data.set(key, cleaned);
+
+			return store.put(cleaned);
+		});
+
+		await Promise.all(promises);
+		this.notify('items-saved', { count: items.length });
+	}
+
+	/**
+	 * Get a single item
+	 */
+	get(key) {
+		return this.data.get(key);
+	}
+
+	/**
+	 * Get all items
+	 */
+	getAll() {
+		return Array.from(this.data.values());
+	}
+
+	/**
+	 * Delete an item
+	 */
+	async delete(key, storeName = null) {
+		this.data.delete(key);
+
+		if (!storeName) {
+			storeName = this.config.storeName;
+		}
+		if (this.db) {
+			const tx = this.db.transaction([storeName], 'readwrite');
+			const store = tx.objectStore(storeName);
+			await store.delete(key);
+		}
+
+		this.notify('item-deleted', { key });
+	}
+
+	async saveBlob(key, blob) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['blobs'], 'readwrite');
+		const store = tx.objectStore('blobs');
+		await store.put({ key, data: blob, type: blob.type, name: blob.name });
+	}
+
+	async getBlob(key) {
+		if (!this.db) return null;
+
+		return new Promise(resolve => {
+			const tx = this.db.transaction(['blobs'], 'readonly');
+			const request = tx.objectStore('blobs').get(key);
+			request.onsuccess = () => resolve(request.result);
+			request.onerror = () => resolve(null);
+		});
+	}
+
+	/**
+	 * Clear all data
+	 */
+	async clear() {
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.domCache) {
+			this.domCache.clear();
+		}
+
+		if (this.db) {
+			const stores = [this.config.storeName];
+			if (this.config.endpoint) stores.push('cache');
+			if (this.config.useHttpCaching) stores.push('headers');
+
+			const tx = this.db.transaction(stores, 'readwrite');
+			stores.forEach(storeName => {
+				if (this.db.objectStoreNames.contains(storeName)) {
+					tx.objectStore(storeName).clear();
+				}
+			});
+		}
+
+		this.notify('data-cleared');
+	}
+
+	/**
+	 * Fetch data from server with HTTP caching
+	 */
+	async fetch(options = {}) {
+		if (!this.config.endpoint) {
+			throw new Error('No endpoint configured for fetch');
+		}
 		const {
 			filters = this.filters,
 			headers = {},
@@ -235,136 +420,93 @@
 			this.setLoading(true);
 		}
 
+		const cacheKey = this.generateCacheKey(filters);
 
-		// Use provided endpoint or config endpoint
-		const apiEndpoint = endpoint || this.config.endpoint;
-		if (!apiEndpoint) {
-			throw new Error('No endpoint specified');
+		//Check Cached data
+		const cachedData = this.cache.get(cacheKey);
+		if (cachedData && this.isCacheValid(cachedData)) {
+			return cachedData.data;
 		}
 
-		// Generate cache key from endpoint and filters
-		const cacheKey = this.generateCacheKey(apiEndpoint, filters);
-		const cleanedFilters = this.cleanFilters(filters);
-
-		// Build request URL
-		const params = new URLSearchParams(cleanedFilters);
-		const url = `${this.config.apiBase}${apiEndpoint}${params.toString() ? '?' + params : ''}`;
-
-		// Prepare headers with conditional requests
+		// Build request headers with HTTP caching
 		const requestHeaders = {
 			...this.headers,
 			...headers
 		};
 
-		// Add conditional headers from stored data
-		const headerKey = this.generateHeaderKey(url);
-		const storedHeaders = this.httpHeaders.get(headerKey);
-		const cachedData = this.cache.get(cacheKey);
-
-		if (storedHeaders && cachedData) {
-			if (storedHeaders.etag) {
-				requestHeaders['If-None-Match'] = storedHeaders.etag;
-			}
-			if (storedHeaders.lastModified) {
-				requestHeaders['If-Modified-Since'] = storedHeaders.lastModified;
+		if (this.config.useHttpCaching) {
+			const httpCache = this.httpHeaders.get(cacheKey);
+			if (httpCache) {
+				if (httpCache.etag) {
+					requestHeaders['If-None-Match'] = httpCache.etag;
+				}
+				if (httpCache.lastModified) {
+					requestHeaders['If-Modified-Since'] = httpCache.lastModified;
+				}
 			}
 		}
 
+		// Build URL with filters
+
+		const cleanedFilters = this.cleanFilters(filters);
+		const params = new URLSearchParams(cleanedFilters);
+		const url = `${this.config.apiBase}${this.config.endpoint}${params.toString() ? '?' + params : ''}`;
+
 		try {
 			const response = await fetch(url, {
 				method: 'GET',
 				headers: requestHeaders
 			});
 
-			console.log('DataStore response status: ',response.status);
-			// Handle 304 Not Modified - return cached data
-			if (response.status === 304) {
-				console.debug(`304 Not Modified for ${url}`);
-				if (cachedData) {
-					// Update timestamp but keep data
-					cachedData.timestamp = Date.now();
-					this.cache.set(cacheKey, cachedData);
-					await this.saveCacheToDB(cacheKey, cachedData);
-
-					// Store current request info
-					this.currentRequest = {
-						filters: cleanedFilters,
-						data: cachedData.data,
-						cached: true
-					};
-
-					//TODO: should this be items-loaded?
-					this.notify('data-cached', {
-						data: cachedData.data,
-						filters: cleanedFilters,
-						cached: true
-					});
-					return cachedData.data;
-				}
+			// Handle 304 Not Modified
+			if (response.status === 304 && cachedData) {
+				// Update timestamp but keep existing data
+				cachedData.timestamp = Date.now();
+				this.saveCache(cacheKey, cachedData);
+				return cachedData.data;
 			}
 
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
 
-			// Store response headers for future conditional requests
-			this.storeResponseHeaders(headerKey, response);
-
 			const data = await response.json();
 
-			console.log('Fetched data: ', data);
+			// Store HTTP caching headers
+			if (this.config.useHttpCaching) {
+				this.storeResponseHeaders(cacheKey, response);
+			}
 
 			// Cache the response
 			const cacheEntry = {
 				key: cacheKey,
-				endpoint: apiEndpoint,
 				data: data,
 				timestamp: Date.now(),
-				filters: cleanedFilters
+				endpoint: this.config.endpoint,
+				filters: filters
 			};
 
 			this.cache.set(cacheKey, cacheEntry);
-			await this.saveCacheToDB(cacheKey, cacheEntry);
+			this.saveCache(cacheKey, cacheEntry);
 
-			// Update items if data contains them
-			if (data.items && this.config.endpoint === apiEndpoint) {
-				this.updateItems(data.items);
+			// Process and store items
+			if (Array.isArray(data)) {
+				await this.saveMany(data);
+			} else if (data.items) {
+				await this.saveMany(data.items);
 			}
 
-			// Store current request info
-			this.currentRequest = {
-				filters: cleanedFilters,
-				data: data,
-				cached: false
-			};
-
-			this.notify('data-fetched', {
-				endpoint: apiEndpoint,
-				data: data,
-				filters: cleanedFilters
-			});
 			return data;
 
 		} catch (error) {
 			console.error('Fetch error:', error);
 
-			// Try to return stale cache on error
+			// Return cached data if available, even if expired
 			if (cachedData) {
-				console.warn('Returning stale cache due to fetch error');
-				this.currentRequest = {
-					filters: cleanedFilters,
-					data: cachedData.data,
-					cached: true,
-					stale: true
-				};
-				this.notify('stale-cache-used', {
-					data: cachedData.data,
-					filters: cleanedFilters
-				});
+				console.warn('Using stale cache due to fetch error');
 				return cachedData.data;
 			}
 
-			this.notify('fetch-error', { error, filters: cleanedFilters });
 			throw error;
 		} finally {
 			if (this.config.showLoading) {
@@ -373,209 +515,6 @@
 		}
 	}
 
-	/**
-	 * Update items in local store
-	 */
-	updateItems(items) {
-		this.items.clear();
-		items.forEach(item => {
-			this.items.set(item.id, item);
-		});
-		this.saveItemsToDB();
-		this.notify('items-updated', { items });
-	}
-
-	/**
-	 * Get current request data and state
-	 */
-	getCurrentRequest() {
-		return this.currentRequest;
-	}
-
-	/**
-	 * Get a specific item by ID
-	 */
-	getItem(id) {
-		let check = parseInt(id);
-		id = isNaN(check) ? id : check;
-		const item = this.items.get(id);
-		return item ? this.unserializeData(item) : null;
-	}
-
-	setItem(id, data, mergeExisting = true) {
-		if (mergeExisting && this.items.has(id)) {
-			let existing = this.getItem(id); // Get unserialized version
-			data = window.deepMerge(existing, data);
-		}
-
-		const serialized = this.serializeData(data);
-		this.items.set(id, serialized); // Store serialized version
-		this.saveItemsToDB();
-		this.notify('item-stored', data); // Notify with original data
-		return data;
-	}
-
-	hasUnrecoverableFiles(data) {
-		if (!data || typeof data !== 'object') return false;
-
-		if (data._wasFile || data._wasBlob) return true;
-
-		if (Array.isArray(data)) {
-			return data.some(item => this.hasUnrecoverableFiles(item));
-		}
-
-		if (data instanceof FormData) {
-			for (const [key, value] of data.entries()) {
-				if (value instanceof File || value instanceof Blob) return true;
-			}
-			return false;
-		}
-
-		return Object.values(data).some(value => this.hasUnrecoverableFiles(value));
-	}
-
-	serializeFormData(formData) {
-		const obj = {};
-
-		for (const [key, value] of formData.entries()) {
-			// Handle file metadata (can't store actual file)
-			if (value instanceof File) {
-				continue;
-			}
-			// Check if key already exists (for multiple values)
-			if (key in obj) {
-				// Convert to array if not already
-				if (!Array.isArray(obj[key])) {
-					obj[key] = [obj[key]];
-				}
-				obj[key].push(value);
-			} else {
-				obj[key] = value;
-			}
-		}
-		return obj;
-	}
-
-	serializeData(data) {
-		if (!data) return null;
-
-		if (data instanceof HTMLElement) {
-			return null;
-		}
-		if (typeof data !== 'object') return data;
-
-		if (data === null) return null;
-
-		if (data instanceof FormData) {
-			return {
-				_type: 'FormData',
-				... this.serializeFormData(data)
-			};
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.serializeData(item));
-		}
-
-		// Handle Date objects
-		if (data instanceof Date) {
-			return {
-				_type: 'Date',
-				value: data.toISOString()
-			};
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) {
-			output[key] = this.serializeData(value);
-		}
-		return output;
-	}
-
-	unserializeData(data) {
-		if (!data || typeof data !== 'object') return data;
-		if (data === null) return null;
-
-		// Check for special types
-		if (data._type) {
-			switch (data._type) {
-				case 'FormData':
-					return this.unserializeFormData(data);
-				case 'File':
-					// Can't reconstruct File, return metadata with warning flag
-					return {
-						_wasFile: true,
-						_fileMetadata: data,
-						name: data.name,
-						type: data.type,
-						size: data.size
-					};
-				case 'Blob':
-					// Can't reconstruct Blob
-					return {
-						_wasBlob: true,
-						_blobMetadata: data,
-						type: data.type,
-						size: data.size
-					};
-				case 'Date':
-					return new Date(data.value);
-			}
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.unserializeData(item));
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) { // Fixed: 'of' not 'in'
-			output[key] = this.unserializeData(value);
-		}
-		return output;
-	}
-	unserializeFormData(data) {
-		const formData = new FormData();
-
-		for (const [key, value] of Object.entries(data)) {
-			if (Array.isArray(value)) {
-				value.forEach(item => {
-					if (item?._isFile) {
-						console.warn(`Cannot restore file "${item.name}" from stored data`);
-						// Optionally append metadata as JSON string for reference
-						formData.append(key + '_was_file', JSON.stringify(item));
-					} else {
-						formData.append(key, item);
-					}
-				});
-			} else if (value?._isFile) {
-				console.warn(`Cannot restore file "${value.name}" from stored data`);
-				// Optionally append metadata as JSON string for reference
-				formData.append(key + '_was_file', JSON.stringify(value));
-			} else if (value !== null && value !== undefined) {
-				formData.append(key, value);
-			}
-		}
-
-		return formData;
-	}
-
-
-	clearItem(key) {
-		this.items.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['items'], 'readwrite');
-			const store = tx.objectStore('items');
-			store.delete(key);
-		}
-	}
-
-	/**
-	 * Filter helpers
-	 */
 	cleanFilters(filters) {
 		const cleaned = {};
 		Object.entries(filters).forEach(([key, value]) => {
@@ -600,7 +539,29 @@
 		return cleaned;
 	}
 
+	/**
+	 * Generate cache key from filters
+	 */
+	generateCacheKey(filters) {
+		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
+			return this.config.generateCacheKey(filters);
+		}
+
+		// Default strategy: sort keys and create string
+		const sorted = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+
+		return JSON.stringify(sorted);
+	}
+
 	setFilter(key, value) {
+		if (!this.filters) {
+			this.filters = {};
+		}
 		const oldValue = this.filters[key];
 
 		if (value === '' || value === null || value === undefined) {
@@ -660,25 +621,28 @@
 	}
 
 	/**
-	 * Cache management
+	 * Set multiple filters at once
 	 */
-	generateCacheKey(endpoint, filters) {
-		const sorted = Object.keys(filters).sort().reduce((obj, key) => {
-			obj[key] = filters[key];
-			return obj;
-		}, {});
-		return `${endpoint}_${JSON.stringify(sorted)}`;
+	setFilters(filters) {
+		this.filters = { ...this.filters, ...filters };
+		if (this.config.autoFetch !== false) {
+			return this.fetch(this.filters);
+		}
 	}
 
-	generateHeaderKey(url) {
-		return `headers_${url}`;
-	}
-
-	isCacheValid(cacheEntry, maxAge = this.config.TTL) {
+	/**
+	 * Check if cache entry is still valid
+	 */
+	isCacheValid(cacheEntry) {
 		if (!cacheEntry || !cacheEntry.timestamp) return false;
-		return (Date.now() - cacheEntry.timestamp) < maxAge;
+
+		const age = Date.now() - cacheEntry.timestamp;
+		return age < this.config.TTL;
 	}
 
+	/**
+	 * Store HTTP response headers for caching
+	 */
 	storeResponseHeaders(key, response) {
 		const headers = {
 			key,
@@ -688,10 +652,167 @@
 		};
 
 		this.httpHeaders.set(key, headers);
-		this.saveHeadersToDB(key, headers);
+
+		if (this.db) {
+			const tx = this.db.transaction(['headers'], 'readwrite');
+			const store = tx.objectStore('headers');
+			store.put(headers);
+		}
+	}
+
+	/**
+	 * Save cache entry to IndexedDB
+	 */
+	async saveCache(key, data) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['cache'], 'readwrite');
+		const store = tx.objectStore('cache');
+		await store.put(data);
+	}
+
+	/**
+	 * Load cache from IndexedDB
+	 */
+	async loadCache() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['cache'], 'readonly');
+			const store = tx.objectStore('cache');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					if (this.isCacheValid(item)) {
+						this.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+		});
+	}
+
+	/**
+	 * Load HTTP headers from IndexedDB
+	 */
+	async loadHeaders() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['headers'], 'readonly');
+			const store = tx.objectStore('headers');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(header => {
+					this.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+		});
 	}
 
 
+	/**
+	 * Subscribe to store events
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	/**
+	 * Notify subscribers of events
+	 */
+	notify(event, data = {}) {
+		this.subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/**
+	 * Query items using an index
+	 */
+	async query(indexName, value) {
+		if (!this.db) return [];
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+
+			if (!store.indexNames.contains(indexName)) {
+				reject(new Error(`Index ${indexName} does not exist`));
+				return;
+			}
+
+			const index = store.index(indexName);
+			const request = value !== undefined
+				? index.getAll(value)
+				: index.getAll();
+
+			request.onsuccess = (e) => {
+				const results = e.target.result.map(item => {
+					return this.config.stripDOMReferences
+						? this.stripDOMReferences(item)
+						: item;
+				});
+				resolve(results);
+			};
+
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Count items in store
+	 */
+	async count() {
+		if (!this.db) return this.data.size;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.count();
+
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading.showModal();
+		} else {
+			this.loading.close();
+		}
+
+	}
+
+	/**
+	 * Cleanup and destroy
+	 */
+	destroy() {
+		if (this.currentRequest) {
+			this.currentRequest.abort();
+		}
+
+		this.subscribers.clear();
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.db) {
+			this.db.close();
+			this.db = null;
+		}
+	}
 
 	clearCache() {
 		this.cache.clear();
@@ -704,231 +825,7 @@
 
 		this.notify('cache-cleared');
 	}
-
-	invalidateCache(pattern) {
-		const keysToDelete = [];
-
-		this.cache.forEach((value, key) => {
-			if (typeof pattern === 'string' && key.includes(pattern)) {
-				keysToDelete.push(key);
-			} else if (pattern instanceof RegExp && pattern.test(key)) {
-				keysToDelete.push(key);
-			}
-		});
-
-		keysToDelete.forEach(key => {
-			this.cache.delete(key);
-			if (this.db) {
-				const tx = this.db.transaction(['cache'], 'readwrite');
-				const store = tx.objectStore('cache');
-				store.delete(key);
-			}
-		});
-
-		this.notify('cache-invalidated', { count: keysToDelete.length });
-	}
-
-	/**
-	 * DOM Cache Management
-	 */
-
-	/**
-	 * Store rendered DOM element for a specific item and view
-	 */
-	storeDOMElement(itemId, viewName, element) {
-		if (!this.domCache.has(itemId)) {
-			this.domCache.set(itemId, {});
-		}
-
-		const itemCache = this.domCache.get(itemId);
-		itemCache[viewName] = element.cloneNode(true);
-		this.domCache.set(itemId, itemCache);
-
-		// Save to IndexedDB
-		this.saveDOMCacheToDB(itemId, itemCache);
-	}
-
-
-	/**
-	 * Retrieve cached DOM element for a specific item and view
-	 */
-	getDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		if (itemCache && itemCache[viewName]) {
-			return itemCache[viewName].cloneNode(true);
-		}
-		return null;
-	}
-
-	/**
-	 * Check if DOM element exists in cache
-	 */
-	hasDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		return itemCache && itemCache[viewName];
-	}
-
-	/**
-	 * Clear DOM cache for a specific item
-	 */
-	clearDOMCache(itemId) {
-		this.domCache.delete(itemId);
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.delete(itemId);
-		}
-	}
-
-	/**
-	 * Clear all DOM cache
-	 */
-	clearAllDOMCache() {
-		this.domCache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Helper method to render or retrieve cached DOM elements
-	 */
-	renderOrRetrieve(item, viewName, renderFunction) {
-		// Check cache first
-		const cached = this.getDOMElement(item.id, viewName);
-		if (cached) {
-			return cached;
-		}
-
-		// Render new element
-		const element = renderFunction(item);
-
-		// Cache the rendered element
-		this.storeDOMElement(item.id, viewName, element);
-
-		return element;
-	}
-	/**
-	 * Database operations
-	 */
-	async saveItemsToDB() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['items'], 'readwrite');
-		const store = tx.objectStore('items');
-
-		store.clear();
-		this.items.forEach(item => {
-			if (!item._deleted) {
-				store.put(item);
-			}
-		});
-	}
-
-	async saveCacheToDB(key, data) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['cache'], 'readwrite');
-		const store = tx.objectStore('cache');
-		store.put(data);
-	}
-
-	async saveHeadersToDB(key, headers) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['headers'], 'readwrite');
-		const store = tx.objectStore('headers');
-		store.put(headers);
-	}
-
-	async saveDOMCacheToDB(itemId, domCache) {
-		if (!this.db) return;
-
-		// Convert DOM elements to HTML strings for storage
-		const serialized = {
-			id: itemId,
-			views: {}
-		};
-
-		Object.entries(domCache).forEach(([viewName, element]) => {
-			if (element && element.outerHTML) {
-				serialized.views[viewName] = element.outerHTML;
-			}
-		});
-
-		const tx = this.db.transaction(['dom'], 'readwrite');
-		const store = tx.objectStore('dom');
-		store.put(serialized);
-	}
-
-	async saveFormsToDB(key, form) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['forms'], 'readwrite');
-		const store = tx.objectStore('forms');
-		store.put(form);
-	}
-
-	storeForm(key, form) {
-		this.forms.set(key, form);
-		this.saveFormsToDB(key, form);
-	}
-
-	getForm(key) {
-		return this.forms.has(key) ? this.forms.get(key) : null;
-	}
-
-	getAllForms() {
-		return this.forms;
-	}
-
-	clearForm(key) {
-		this.forms.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('forms');
-			store.delete(key);
-		}
-	}
-
-	clearAllForms() {
-		this.forms.clear();
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	/**
-	 * Cleanup
-	 */
-	destroy() {
-		if (this.db) {
-			this.db.close();
-		}
-		this.subscribers.clear();
-		this.items.clear();
-		this.cache.clear();
-		this.domCache.clear();
-		this.httpHeaders.clear();
-	}
 }
 
+// Export for use
 window.jvbStore = DataStore;
diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
new file mode 100644
index 0000000..3686ac2
--- /dev/null
+++ b/assets/js/concise/DataStoreOld.js
@@ -0,0 +1,931 @@
+/**
+ * Handles GET Requests, storing responses by a key of filters, set with setFilter method
+ * Stores:
+ * 		- Items: the individual item data, mapped by postID/termID
+ * 		- Cache: the cacheKey generated by filters, and the results in the value
+ * 		- httpHeaders: If Modified Since tracking
+ * 		- domCache: rendered DOM elements, to reduce on-page rendering
+ */
+class DataStore {
+	constructor(config = {}) {
+		this.config = {
+			name: 'default',
+			endpoint: false,
+			apiBase: jvbSettings.api,
+			TTL: 3600000, // 1 hour default
+			showLoading: true,
+			headers: {},
+			filters: {},
+			...config
+		};
+		if (!this.config.endpoint) {
+			console.warn('No endpoint set. Only saving locally');
+		}
+
+		this.body = document.body;
+		this.loading = document.querySelector('dialog.loading');
+
+		this.headers = {
+			'X-WP-Nonce': jvbSettings.nonce,
+			...this.config.headers
+		};
+
+		// Data stores
+		this.items = new Map();
+		this.cache = new Map(); //TODO: call this resultsCache
+		this.httpHeaders = new Map();
+		this.domCache = new Map();
+		this.forms = new Map();
+
+		// State management
+		this.filters = config.filters ?? {};
+		this.subscribers = new Set();
+		this.db = null;
+		this.currentRequest = null;
+
+		// Server Timestamps - needed?
+		this.cachedContent = JSON.parse(cacheJVB.cache) || {};
+		this.lastTimestampUpdate = Date.now();
+
+		this.initDB();
+		document.addEventListener('beforeUnload', () =>this.destroy());
+	}
+
+	async initDB() {
+		if (!('indexedDB' in window)) return;
+
+		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
+
+		request.onupgradeneeded = (e) => {
+			const db = e.target.result;
+			// Items store
+			if (!db.objectStoreNames.contains('items')) {
+				db.createObjectStore('items', { keyPath: 'id' });
+			}
+
+			// DOM cache for rendered elements
+			if (!db.objectStoreNames.contains('dom')) {
+				db.createObjectStore('dom', { keyPath: 'id' });
+			}
+
+			if (!db.objectStoreNames.contains('forms')) {
+				let forms = db.createObjectStore('forms', {
+					keyPath: 'formId',
+				});
+				forms.createIndex('status', 'status', {unique:false});
+				forms.createIndex('operationId', 'operationId', {unique:false});
+				forms.createIndex('timestamp', 'timestamp', {unique:false});
+			}
+
+			// Cache store for GET requests with endpoint index
+			if (!db.objectStoreNames.contains('cache')) {
+				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
+				cacheStore.createIndex('filters', 'filters', { unique: false });
+			}
+
+			// HTTP headers store
+			if (!db.objectStoreNames.contains('headers')) {
+				db.createObjectStore('headers', { keyPath: 'key' });
+			}
+		};
+
+		request.onsuccess = (e) => {
+			this.db = e.target.result;
+			this.loadFromDB();
+		};
+
+		request.onerror = (e) => {
+			console.error('IndexedDB error:', e);
+		};
+	}
+
+	async loadFromDB() {
+		if (!this.db) return;
+
+		try {
+			await Promise.all([
+				this.loadItems(),
+				this.loadCache(),
+				this.loadHeaders(),
+				this.loadDOMCache(),
+				this.loadForms()
+			]);
+		} catch (error) {
+			console.error('Error loading from DB:', error);
+		}
+	}
+
+	async loadItems() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['items'], 'readonly');
+			const store = tx.objectStore('items');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					this.items.set(item.id, item);
+				});
+				this.notify('items-loaded', { items: Array.from(this.items.values()) });
+				resolve();
+			};
+		});
+	}
+
+	async loadCache() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['cache'], 'readonly');
+			const store = tx.objectStore('cache');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					if (this.isCacheValid(item)) {
+						this.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+		});
+	}
+
+	async loadHeaders() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['headers'], 'readonly');
+			const store = tx.objectStore('headers');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(header => {
+					this.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+		});
+	}
+
+	async loadDOMCache() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['dom'], 'readonly');
+			const store = tx.objectStore('dom');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(domEntry => {
+					// Convert stored HTML back to DOM elements
+					const reconstructed = {};
+					Object.entries(domEntry.views).forEach(([viewName, html]) => {
+						const temp = document.createElement('div');
+						temp.innerHTML = html;
+						reconstructed[viewName] = temp.firstElementChild;
+					});
+					this.domCache.set(domEntry.id, reconstructed);
+				});
+				resolve();
+			};
+		});
+	}
+
+	async loadForms() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['forms'], 'readonly');
+			const store = tx.objectStore('forms');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(form => {
+					this.forms.set(form.key, form);
+				});
+				resolve();
+			};
+		});
+	}
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading.showModal();
+		} else {
+			this.loading.close();
+		}
+
+	}
+
+	/**
+	 * Main fetch method with caching and conditional requests
+	 */
+	async fetch(endpoint = null, options = {}) {
+		const {
+			filters = this.filters,
+			headers = {},
+		} = options;
+
+		if (this.config.showLoading) {
+			this.setLoading(true);
+		}
+
+
+		// Use provided endpoint or config endpoint
+		const apiEndpoint = endpoint || this.config.endpoint;
+		if (!apiEndpoint) {
+			throw new Error('No endpoint specified');
+		}
+
+		// Generate cache key from endpoint and filters
+		const cacheKey = this.generateCacheKey(apiEndpoint, filters);
+		const cleanedFilters = this.cleanFilters(filters);
+
+		// Build request URL
+		const params = new URLSearchParams(cleanedFilters);
+		const url = `${this.config.apiBase}${apiEndpoint}${params.toString() ? '?' + params : ''}`;
+
+		// Prepare headers with conditional requests
+		const requestHeaders = {
+			...this.headers,
+			...headers
+		};
+
+		// Add conditional headers from stored data
+		const headerKey = this.generateHeaderKey(url);
+		const storedHeaders = this.httpHeaders.get(headerKey);
+		const cachedData = this.cache.get(cacheKey);
+
+		if (storedHeaders && cachedData) {
+			if (storedHeaders.etag) {
+				requestHeaders['If-None-Match'] = storedHeaders.etag;
+			}
+			if (storedHeaders.lastModified) {
+				requestHeaders['If-Modified-Since'] = storedHeaders.lastModified;
+			}
+		}
+
+		try {
+			const response = await fetch(url, {
+				method: 'GET',
+				headers: requestHeaders
+			});
+
+			// Handle 304 Not Modified - return cached data
+			if (response.status === 304) {
+				console.debug(`304 Not Modified for ${url}`);
+				if (cachedData) {
+					// Update timestamp but keep data
+					cachedData.timestamp = Date.now();
+					this.cache.set(cacheKey, cachedData);
+					await this.saveCacheToDB(cacheKey, cachedData);
+
+					// Store current request info
+					this.currentRequest = {
+						filters: cleanedFilters,
+						data: cachedData.data,
+						cached: true
+					};
+
+					//TODO: should this be items-loaded?
+					this.notify('data-cached', {
+						data: cachedData.data,
+						filters: cleanedFilters,
+						cached: true
+					});
+					return cachedData.data;
+				}
+			}
+
+			if (!response.ok) {
+				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+			}
+
+			// Store response headers for future conditional requests
+			this.storeResponseHeaders(headerKey, response);
+
+			const data = await response.json();
+
+			// Cache the response
+			const cacheEntry = {
+				key: cacheKey,
+				endpoint: apiEndpoint,
+				data: data,
+				timestamp: Date.now(),
+				filters: cleanedFilters
+			};
+
+			this.cache.set(cacheKey, cacheEntry);
+			await this.saveCacheToDB(cacheKey, cacheEntry);
+
+			// Update items if data contains them
+			if (data.items && this.config.endpoint === apiEndpoint) {
+				this.updateItems(data.items);
+			}
+
+			// Store current request info
+			this.currentRequest = {
+				filters: cleanedFilters,
+				data: data,
+				cached: false
+			};
+
+			this.notify('data-fetched', {
+				endpoint: apiEndpoint,
+				data: data,
+				filters: cleanedFilters
+			});
+			return data;
+
+		} catch (error) {
+			console.error('Fetch error:', error);
+
+			// Try to return stale cache on error
+			if (cachedData) {
+				console.warn('Returning stale cache due to fetch error');
+				this.currentRequest = {
+					filters: cleanedFilters,
+					data: cachedData.data,
+					cached: true,
+					stale: true
+				};
+				this.notify('stale-cache-used', {
+					data: cachedData.data,
+					filters: cleanedFilters
+				});
+				return cachedData.data;
+			}
+
+			this.notify('fetch-error', { error, filters: cleanedFilters });
+			throw error;
+		} finally {
+			if (this.config.showLoading) {
+				this.setLoading(false);
+			}
+		}
+	}
+
+	/**
+	 * Update items in local store
+	 */
+	updateItems(items) {
+		this.items.clear();
+		items.forEach(item => {
+			this.items.set(item.id, item);
+		});
+		this.saveItemsToDB();
+		this.notify('items-updated', { items });
+	}
+
+	/**
+	 * Get current request data and state
+	 */
+	getCurrentRequest() {
+		return this.currentRequest;
+	}
+
+	/**
+	 * Get a specific item by ID
+	 */
+	getItem(id) {
+		let check = parseInt(id);
+		id = isNaN(check) ? id : check;
+		const item = this.items.get(id);
+		return item ? this.unserializeData(item) : null;
+	}
+
+	setItem(id, data, mergeExisting = true) {
+		if (mergeExisting && this.items.has(id)) {
+			let existing = this.getItem(id); // Get unserialized version
+			data = window.deepMerge(existing, data);
+		}
+
+		const serialized = this.serializeData(data);
+		this.items.set(id, serialized); // Store serialized version
+		this.saveItemsToDB();
+		this.notify('item-stored', data); // Notify with original data
+		return data;
+	}
+
+	hasUnrecoverableFiles(data) {
+		if (!data || typeof data !== 'object') return false;
+
+		if (data._wasFile || data._wasBlob) return true;
+
+		if (Array.isArray(data)) {
+			return data.some(item => this.hasUnrecoverableFiles(item));
+		}
+
+		if (data instanceof FormData) {
+			for (const [key, value] of data.entries()) {
+				if (value instanceof File || value instanceof Blob) return true;
+			}
+			return false;
+		}
+
+		return Object.values(data).some(value => this.hasUnrecoverableFiles(value));
+	}
+
+	serializeFormData(formData) {
+		const obj = {};
+
+		for (const [key, value] of formData.entries()) {
+			// Handle file metadata (can't store actual file)
+			if (value instanceof File) {
+				continue;
+			}
+			// Check if key already exists (for multiple values)
+			if (key in obj) {
+				// Convert to array if not already
+				if (!Array.isArray(obj[key])) {
+					obj[key] = [obj[key]];
+				}
+				obj[key].push(value);
+			} else {
+				obj[key] = value;
+			}
+		}
+		return obj;
+	}
+
+	serializeData(data) {
+		if (!data) return null;
+
+		if (data instanceof HTMLElement) {
+			return null;
+		}
+		if (typeof data !== 'object') return data;
+
+		if (data === null) return null;
+
+		if (data instanceof FormData) {
+			return {
+				_type: 'FormData',
+				... this.serializeFormData(data)
+			};
+		}
+
+		// Handle Arrays
+		if (Array.isArray(data)) {
+			return data.map(item => this.serializeData(item));
+		}
+
+		// Handle Date objects
+		if (data instanceof Date) {
+			return {
+				_type: 'Date',
+				value: data.toISOString()
+			};
+		}
+
+		// Handle plain objects
+		const output = {};
+		for (const [key, value] of Object.entries(data)) {
+			output[key] = this.serializeData(value);
+		}
+		return output;
+	}
+
+	unserializeData(data) {
+		if (!data || typeof data !== 'object') return data;
+		if (data === null) return null;
+
+		// Check for special types
+		if (data._type) {
+			switch (data._type) {
+				case 'FormData':
+					return this.unserializeFormData(data);
+				case 'File':
+					// Can't reconstruct File, return metadata with warning flag
+					return {
+						_wasFile: true,
+						_fileMetadata: data,
+						name: data.name,
+						type: data.type,
+						size: data.size
+					};
+				case 'Blob':
+					// Can't reconstruct Blob
+					return {
+						_wasBlob: true,
+						_blobMetadata: data,
+						type: data.type,
+						size: data.size
+					};
+				case 'Date':
+					return new Date(data.value);
+			}
+		}
+
+		// Handle Arrays
+		if (Array.isArray(data)) {
+			return data.map(item => this.unserializeData(item));
+		}
+
+		// Handle plain objects
+		const output = {};
+		for (const [key, value] of Object.entries(data)) { // Fixed: 'of' not 'in'
+			output[key] = this.unserializeData(value);
+		}
+		return output;
+	}
+	unserializeFormData(data) {
+		const formData = new FormData();
+
+		for (const [key, value] of Object.entries(data)) {
+			if (Array.isArray(value)) {
+				value.forEach(item => {
+					if (item?._isFile) {
+						console.warn(`Cannot restore file "${item.name}" from stored data`);
+						// Optionally append metadata as JSON string for reference
+						formData.append(key + '_was_file', JSON.stringify(item));
+					} else {
+						formData.append(key, item);
+					}
+				});
+			} else if (value?._isFile) {
+				console.warn(`Cannot restore file "${value.name}" from stored data`);
+				// Optionally append metadata as JSON string for reference
+				formData.append(key + '_was_file', JSON.stringify(value));
+			} else if (value !== null && value !== undefined) {
+				formData.append(key, value);
+			}
+		}
+
+		return formData;
+	}
+
+
+	clearItem(key) {
+		this.items.delete(key);
+		if (this.db) {
+			const tx = this.db.transaction(['items'], 'readwrite');
+			const store = tx.objectStore('items');
+			store.delete(key);
+		}
+	}
+
+	/**
+	 * Filter helpers
+	 */
+	cleanFilters(filters) {
+		const cleaned = {};
+		Object.entries(filters).forEach(([key, value]) => {
+			if (value !== null && value !== undefined && value !== '') {
+				// Handle special cases based on existing patterns
+				if (key === 'taxonomies' && typeof value === 'object') {
+					Object.entries(value).forEach(([taxName, terms]) => {
+						if (Array.isArray(terms) && terms.length > 0) {
+							cleaned[`tax_${taxName}`] = terms.join(',');
+						} else if (terms) {
+							cleaned[`tax_${taxName}`] = terms;
+						}
+					});
+				} else if (key === 'date' && typeof value === 'object') {
+					if (value.after) cleaned.after = value.after;
+					if (value.before) cleaned.before = value.before;
+				} else {
+					cleaned[key] = value;
+				}
+			}
+		});
+		return cleaned;
+	}
+
+	setFilter(key, value) {
+		const oldValue = this.filters[key];
+
+		if (value === '' || value === null || value === undefined) {
+			delete this.filters[key];
+		} else {
+			this.filters[key] = value;
+		}
+
+		this.notify('filters-changed', {
+			filters: this.filters,
+			changed: { key, oldValue, newValue: value }
+		});
+
+		// Auto-fetch if endpoint is configured
+		if (this.config.endpoint) {
+			this.fetch();
+		}
+	}
+
+	/**
+	 * Remove a filter
+	 */
+	removeFilter(key) {
+		const oldValue = this.filters[key];
+
+		if (oldValue !== undefined) {
+			delete this.filters[key];
+			this.notify('filters-changed', {
+				filters: this.filters,
+				removed: { key, oldValue }
+			});
+
+			// Auto-fetch if endpoint is configured
+			if (this.config.endpoint) {
+				this.fetch();
+			}
+		}
+	}
+
+	/**
+	 * Clear all filters
+	 */
+	clearFilters() {
+		const oldFilters = { ...this.filters };
+		//Restore baseline filters
+		this.filters = this.config.filters;
+
+		this.notify('filters-cleared', {
+			oldFilters,
+			filters: this.filters
+		});
+
+		// Auto-fetch if endpoint is configured
+		if (this.config.endpoint) {
+			this.fetch();
+		}
+	}
+
+	/**
+	 * Cache management
+	 */
+	generateCacheKey(endpoint, filters) {
+		const sorted = Object.keys(filters).sort().reduce((obj, key) => {
+			obj[key] = filters[key];
+			return obj;
+		}, {});
+		return `${endpoint}_${JSON.stringify(sorted)}`;
+	}
+
+	generateHeaderKey(url) {
+		return `headers_${url}`;
+	}
+
+	isCacheValid(cacheEntry, maxAge = this.config.TTL) {
+		if (!cacheEntry || !cacheEntry.timestamp) return false;
+		return (Date.now() - cacheEntry.timestamp) < maxAge;
+	}
+
+	storeResponseHeaders(key, response) {
+		const headers = {
+			key,
+			etag: response.headers.get('ETag'),
+			lastModified: response.headers.get('Last-Modified'),
+			timestamp: Date.now()
+		};
+
+		this.httpHeaders.set(key, headers);
+		this.saveHeadersToDB(key, headers);
+	}
+
+
+
+	clearCache() {
+		this.cache.clear();
+
+		if (this.db) {
+			const tx = this.db.transaction(['cache'], 'readwrite');
+			const store = tx.objectStore('cache');
+			store.clear();
+		}
+
+		this.notify('cache-cleared');
+	}
+
+	invalidateCache(pattern) {
+		const keysToDelete = [];
+
+		this.cache.forEach((value, key) => {
+			if (typeof pattern === 'string' && key.includes(pattern)) {
+				keysToDelete.push(key);
+			} else if (pattern instanceof RegExp && pattern.test(key)) {
+				keysToDelete.push(key);
+			}
+		});
+
+		keysToDelete.forEach(key => {
+			this.cache.delete(key);
+			if (this.db) {
+				const tx = this.db.transaction(['cache'], 'readwrite');
+				const store = tx.objectStore('cache');
+				store.delete(key);
+			}
+		});
+
+		this.notify('cache-invalidated', { count: keysToDelete.length });
+	}
+
+	/**
+	 * DOM Cache Management
+	 */
+
+	/**
+	 * Store rendered DOM element for a specific item and view
+	 */
+	storeDOMElement(itemId, viewName, element) {
+		if (!this.domCache.has(itemId)) {
+			this.domCache.set(itemId, {});
+		}
+
+		const itemCache = this.domCache.get(itemId);
+		itemCache[viewName] = element.cloneNode(true);
+		this.domCache.set(itemId, itemCache);
+
+		// Save to IndexedDB
+		this.saveDOMCacheToDB(itemId, itemCache);
+	}
+
+
+	/**
+	 * Retrieve cached DOM element for a specific item and view
+	 */
+	getDOMElement(itemId, viewName) {
+		const itemCache = this.domCache.get(itemId);
+		if (itemCache && itemCache[viewName]) {
+			return itemCache[viewName].cloneNode(true);
+		}
+		return null;
+	}
+
+	/**
+	 * Check if DOM element exists in cache
+	 */
+	hasDOMElement(itemId, viewName) {
+		const itemCache = this.domCache.get(itemId);
+		return itemCache && itemCache[viewName];
+	}
+
+	/**
+	 * Clear DOM cache for a specific item
+	 */
+	clearDOMCache(itemId) {
+		this.domCache.delete(itemId);
+
+		if (this.db) {
+			const tx = this.db.transaction(['dom'], 'readwrite');
+			const store = tx.objectStore('dom');
+			store.delete(itemId);
+		}
+	}
+
+	/**
+	 * Clear all DOM cache
+	 */
+	clearAllDOMCache() {
+		this.domCache.clear();
+
+		if (this.db) {
+			const tx = this.db.transaction(['dom'], 'readwrite');
+			const store = tx.objectStore('dom');
+			store.clear();
+		}
+	}
+
+	/**
+	 * Helper method to render or retrieve cached DOM elements
+	 */
+	renderOrRetrieve(item, viewName, renderFunction) {
+		// Check cache first
+		const cached = this.getDOMElement(item.id, viewName);
+		if (cached) {
+			return cached;
+		}
+
+		// Render new element
+		const element = renderFunction(item);
+
+		// Cache the rendered element
+		this.storeDOMElement(item.id, viewName, element);
+
+		return element;
+	}
+	/**
+	 * Database operations
+	 */
+	async saveItemsToDB() {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['items'], 'readwrite');
+		const store = tx.objectStore('items');
+
+		store.clear();
+		this.items.forEach(item => {
+			if (!item._deleted) {
+				store.put(item);
+			}
+		});
+	}
+
+	async saveCacheToDB(key, data) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['cache'], 'readwrite');
+		const store = tx.objectStore('cache');
+		store.put(data);
+	}
+
+	async saveHeadersToDB(key, headers) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['headers'], 'readwrite');
+		const store = tx.objectStore('headers');
+		store.put(headers);
+	}
+
+	async saveDOMCacheToDB(itemId, domCache) {
+		if (!this.db) return;
+
+		// Convert DOM elements to HTML strings for storage
+		const serialized = {
+			id: itemId,
+			views: {}
+		};
+
+		Object.entries(domCache).forEach(([viewName, element]) => {
+			if (element && element.outerHTML) {
+				serialized.views[viewName] = element.outerHTML;
+			}
+		});
+
+		const tx = this.db.transaction(['dom'], 'readwrite');
+		const store = tx.objectStore('dom');
+		store.put(serialized);
+	}
+
+	async saveFormsToDB(key, form) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['forms'], 'readwrite');
+		const store = tx.objectStore('forms');
+		store.put(form);
+	}
+
+	storeForm(key, form) {
+		this.forms.set(key, form);
+		this.saveFormsToDB(key, form);
+	}
+
+	getForm(key) {
+		return this.forms.has(key) ? this.forms.get(key) : null;
+	}
+
+	getAllForms() {
+		return this.forms;
+	}
+
+	clearForm(key) {
+		this.forms.delete(key);
+		if (this.db) {
+			const tx = this.db.transaction(['forms'], 'readwrite');
+			const store = tx.objectStore('forms');
+			store.delete(key);
+		}
+	}
+
+	clearAllForms() {
+		this.forms.clear();
+		if (this.db) {
+			const tx = this.db.transaction(['forms'], 'readwrite');
+			const store = tx.objectStore('dom');
+			store.clear();
+		}
+	}
+
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/**
+	 * Cleanup
+	 */
+	destroy() {
+		if (this.db) {
+			this.db.close();
+		}
+		this.subscribers.clear();
+		this.items.clear();
+		this.cache.clear();
+		this.domCache.clear();
+		this.httpHeaders.clear();
+	}
+}
+
+window.jvbStore = DataStore;
diff --git a/assets/js/concise/DragHandler.js b/assets/js/concise/DragHandler.js
new file mode 100644
index 0000000..e305775
--- /dev/null
+++ b/assets/js/concise/DragHandler.js
@@ -0,0 +1,392 @@
+/**
+ * DragHandler.js
+ * Generic drag and drop controller for mouse and touch via Pointer Events API
+ */
+class DragHandler {
+	constructor(config) {
+		this.draggableSelector = config.draggableSelector;
+		this.dropTargetSelector = config.dropTargetSelector;
+		this.getItemId = config.getItemId;
+		this.getSelectedItems = config.getSelectedItems;
+		this.validateDrop = config.validateDrop;
+		this.onDrop = config.onDrop;
+
+		this.handleSelector = config.handleSelector || null;
+		this.ignoreSelector = config.ignoreSelector || 'input, button, label, select, textarea, a';
+		this.onDragStart = config.onDragStart || null;
+		this.onDragEnd = config.onDragEnd || null;
+
+		this.previewElement = config.previewElement || 'img, video, .icon';
+
+		this.previewOptions = {
+			offset: { x: -30, y: -40 },
+			showCount: true,
+			...config.previewOptions
+		};
+
+		this.dragThreshold = {
+			time: 90, // ms to hold before drag starts
+			distance: 5 // px of movement allowed during hold
+		};
+
+		this.state = this.getInitialState();
+
+		this.onPointerDown = this.handlePointerDown.bind(this);
+		this.onPointerMove = this.handlePointerMove.bind(this);
+		this.onPointerUp = this.handlePointerUp.bind(this);
+		this.onPointerCancel = this.handlePointerCancel.bind(this);
+
+		this.init();
+	}
+
+	getInitialState() {
+		return {
+			active: false,
+			itemIds: [],
+			startPos: null,
+			currentPos: null,
+			targetElement: null,
+			previewElement: null,
+			startTime: null,
+			holdTimer: null,
+			pointerId: null,
+			pointerTarget: null
+		};
+	}
+
+	init() {
+		document.addEventListener('pointerdown', this.onPointerDown);
+	}
+
+	handlePointerDown(e) {
+		if (this.shouldIgnoreElement(e.target)) return;
+
+		const draggableElement = e.target.closest(this.draggableSelector);
+		if (!draggableElement) return;
+
+		if (this.handleSelector) {
+			const handle = e.target.closest(this.handleSelector);
+			if (!handle || !draggableElement.contains(handle)) return;
+		}
+
+		const itemId = this.getItemId(draggableElement);
+		if (!itemId) return;
+
+		const selectedItems = this.getSelectedItems(draggableElement);
+		const itemIds = selectedItems.includes(itemId) ? selectedItems : [itemId];
+
+		e.preventDefault();
+
+		this.state = {
+			active: false,
+			itemIds: itemIds,
+			startPos: { x: e.clientX, y: e.clientY },
+			currentPos: { x: e.clientX, y: e.clientY },
+			targetElement: null,
+			previewElement: null,
+			startTime: Date.now(),
+			holdTimer: null,
+			draggableElement: draggableElement,
+			pointerId: e.pointerId,
+			pointerTarget: draggableElement
+		};
+
+
+		document.addEventListener('pointermove', this.onPointerMove);
+		document.addEventListener('pointerup', this.onPointerUp);
+		document.addEventListener('pointercancel', this.onPointerCancel);
+
+		this.state.holdTimer = setTimeout(() => {
+			if (this.state && !this.state.active) {
+				this.startDrag();
+			}
+		}, this.dragThreshold.time);
+	}
+
+	startDrag() {
+		// Don't start if already cancelled
+		if (!this.state || this.state.active) return;
+
+		this.state.holdTimer = null;
+		this.state.active = true;
+
+		if (this.state.pointerTarget && this.state.pointerId !== null) {
+			try {
+				this.state.pointerTarget.setPointerCapture(this.state.pointerId);
+			} catch (e) {
+				console.warn('Could not capture pointer:', e);
+			}
+		}
+
+		this.state.previewElement = this.createPreview(
+			this.state.draggableElement,
+			this.state.itemIds
+		);
+
+
+		this.applyDraggingState(true);
+		this.updatePreview();
+
+		if (this.onDragStart) {
+			this.onDragStart(this.state.itemIds, this.state.draggableElement);
+		}
+
+		if (window.jvbA11y) {
+			const message = this.state.itemIds.length > 1
+				? `Started dragging ${this.state.itemIds.length} items`
+				: 'Started dragging item';
+			window.jvbA11y.announce(message);
+		}
+	}
+
+	handlePointerMove(e) {
+		if (!this.state) return;
+
+		this.state.currentPos = { x: e.clientX, y: e.clientY };
+
+		// Check if we've moved beyond threshold before timer expires
+		if (!this.state.active && this.state.holdTimer) {
+			const dx = e.clientX - this.state.startPos.x;
+			const dy = e.clientY - this.state.startPos.y;
+			const distance = Math.sqrt(dx * dx + dy * dy);
+
+			// If moved significantly while waiting, start drag immediately
+			if (distance > this.dragThreshold.distance * 2) {
+				clearTimeout(this.state.holdTimer);
+				this.startDrag();
+			}
+		}
+
+		if (!this.state.active) return;
+		e.preventDefault();
+
+		this.updatePreview();
+
+		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
+		const dropTarget = this.findDropTarget(elementUnderPointer);
+
+		if (dropTarget !== this.state.targetElement) {
+			this.clearTargetHighlight();
+			this.state.targetElement = dropTarget;
+
+			if (dropTarget && this.validateDrop(this.state.itemIds, dropTarget)) {
+				this.highlightTarget(dropTarget);
+			}
+		}
+	}
+
+	handlePointerUp(e) {
+		if (!this.state) return;
+
+		// If timer still running, this was a quick click - cancel drag and allow click
+		if (this.state.holdTimer && !this.state.active) {
+			this.cancelDragStart();
+			return;
+		}
+
+		// Only process drop if drag was actually active
+		if (!this.state.active) {
+			this.cleanup();
+			return;
+		}
+
+		document.removeEventListener('pointermove', this.onPointerMove);
+		document.removeEventListener('pointerup', this.onPointerUp);
+		document.removeEventListener('pointercancel', this.onPointerCancel);
+
+		const { itemIds, targetElement } = this.state;
+		let success = false;
+
+		if (targetElement && this.validateDrop(itemIds, targetElement)) {
+			this.onDrop(itemIds, targetElement);
+			success = true;
+		}
+
+		this.cleanup();
+
+		if (this.onDragEnd) {
+			this.onDragEnd(itemIds, success);
+		}
+
+		if (window.jvbA11y) {
+			const message = success
+				? (itemIds.length > 1 ? `Moved ${itemIds.length} items` : 'Item moved')
+				: 'Drag cancelled';
+			window.jvbA11y.announce(message);
+		}
+	}
+
+	handlePointerCancel(e) {
+		if (!this.state) return;
+
+		document.removeEventListener('pointermove', this.onPointerMove);
+		document.removeEventListener('pointerup', this.onPointerUp);
+		document.removeEventListener('pointercancel', this.onPointerCancel);
+
+		// Clear timer if still running
+		if (this.state.holdTimer) {
+			clearTimeout(this.state.holdTimer);
+			this.state.holdTimer = null;
+		}
+
+		this.cleanup();
+
+		if (window.jvbA11y) {
+			window.jvbA11y.announce('Drag cancelled');
+		}
+	}
+
+	cancelDragStart() {
+		if (this.state && this.state.holdTimer) {
+			clearTimeout(this.state.holdTimer);
+			this.state.holdTimer = null;
+		}
+
+		document.removeEventListener('pointermove', this.onPointerMove);
+		document.removeEventListener('pointerup', this.onPointerUp);
+		document.removeEventListener('pointercancel', this.onPointerCancel);
+
+		this.cleanup();
+	}
+
+	createPreview(draggableElement, itemIds) {
+		let preview = window.getTemplate('dragPreview');
+		if (!preview) {
+			preview = this.createPreviewElement();
+		}
+
+		let itemsContainer = preview.querySelector('.drag-items');
+
+		let wrapperTemplate = itemsContainer.querySelector('.drag-item');
+		itemIds.forEach((id, index) => {
+			let itemElement = this.findElementByItemId(id);
+			if (itemElement) {
+				let previewPart = itemElement.querySelector(this.previewElement)?.cloneNode(true);
+				if (previewPart) {
+					let wrapper = wrapperTemplate.cloneNode(true);
+					wrapper.appendChild(previewPart);
+					itemsContainer.append(wrapper);
+				}
+			}
+		});
+		wrapperTemplate.remove();
+
+		let countBadge = preview.querySelector('.drag-count');
+		if (countBadge) {
+			countBadge.textContent = '{' + itemIds.length + '}';
+			countBadge.hidden = itemIds.length <= 1;
+		}
+
+		document.body.appendChild(preview);
+		return preview;
+	}
+
+	createPreviewElement() {
+		const preview = document.createElement('div');
+		preview.className = 'drag-preview';
+		let items = preview.cloneNode(true);
+		let count = preview.cloneNode(true);
+		let item = preview.cloneNode(true);
+		item.className = 'drag-item';
+		items.className = 'drag-items';
+		items.append(item);
+		count.className = 'drag-count';
+
+		preview.append(items);
+		preview.append(count);
+		return preview;
+	}
+
+	updatePreview() {
+		if (!this.state.previewElement || !this.state.currentPos) return;
+
+		this.state.previewElement.style.left = `${this.state.currentPos.x + this.previewOptions.offset.x}px`;
+		this.state.previewElement.style.top = `${this.state.currentPos.y + this.previewOptions.offset.y}px`;
+	}
+
+	findDropTarget(element) {
+		if (!element) return null;
+		return element.closest(this.dropTargetSelector);
+	}
+
+	highlightTarget(target) {
+		if (!target) return;
+
+		target.classList.add('dragover');
+
+		if (this.state.isMultiDrag) {
+			target.classList.add('multi-drop');
+			target.setAttribute('data-item-count', this.state.itemIds.length);
+		}
+
+		if (navigator.vibrate) {
+			const pattern = this.state.isMultiDrag ? [25, 10, 25] : [25];
+			navigator.vibrate(pattern);
+		}
+	}
+
+	clearTargetHighlight() {
+		document.querySelectorAll('.dragover').forEach(el => {
+			el.classList.remove('dragover', 'multi-drop');
+			el.removeAttribute('data-item-count');
+		});
+	}
+
+	applyDraggingState(isDragging) {
+		this.state.itemIds.forEach(id => {
+			const element = this.findElementByItemId(id);
+			if (element) {
+				element.classList.toggle('dragging', isDragging);
+			}
+		});
+	}
+
+	findElementByItemId(itemId) {
+		// Try the actual selector pattern first
+		const elements = document.querySelectorAll(this.draggableSelector);
+		for (const el of elements) {
+			if (this.getItemId(el) === itemId) {
+				return el;
+			}
+		}
+		return null;
+	}
+
+	shouldIgnoreElement(element) {
+		return (element.matches(this.ignoreSelector));
+	}
+
+	cleanup() {
+		if (this.state && this.state.holdTimer) {
+			clearTimeout(this.state.holdTimer);
+		}
+
+		if (this.state && this.state.pointerTarget && this.state.pointerId !== null) {
+			try {
+				this.state.pointerTarget.releasePointerCapture(this.state.pointerId);
+			} catch (e) {
+				// Pointer may have been auto-released
+			}
+		}
+
+		this.clearTargetHighlight();
+		this.applyDraggingState(false);
+
+		if (this.state && this.state.previewElement) {
+			this.state.previewElement.remove();
+		}
+
+		this.state = this.getInitialState();
+	}
+
+	destroy() {
+		this.cleanup();
+
+		document.removeEventListener('pointerdown', this.onPointerDown);
+		document.removeEventListener('pointermove', this.onPointerMove);
+		document.removeEventListener('pointerup', this.onPointerUp);
+		document.removeEventListener('pointercancel', this.onPointerCancel);
+	}
+}
+
+window.jvbDragHandler = DragHandler;
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index eea0597..6663484 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -3,11 +3,20 @@
  * Works with DataStore for CRUD operations and standalone for front-end forms
  */
 class FormController {
-	constructor(store = null) {
-		this.store = store; // Optional - for CRUD operations
-		if (!store) {
-			this.store = new window.jvbStore({name:'forms', TTL: 604800});
-		}
+	constructor() {
+		this.store = new window.jvbStore({
+			name:'forms',
+			storeName: 'forms',
+			keyPath: 'formId',
+			indexes: [
+				{ name: 'status', keyPath: 'status' },
+				{ name: 'operationId', keyPath: 'operationId' },
+				{ name: 'timestamp', keyPath: 'timestamp' },
+				{ name: 'formType', keyPath: 'type' }
+			],
+			TTL: 604800000, //7 days
+		});
+
 		this.debouncer = window.debouncer;
 
 		this.ignore = [];
@@ -52,21 +61,38 @@
 		// Check for pending operations on page load
 		await this.checkPendingOperations();
 
+		this.store.subscribe(this.handleStoreEvent.bind(this));
+
 		// Set up global form handlers for standalone forms
 		this.initListeners();
 	}
 
+	handleStoreEvent(event, data) {
+		switch(event) {
+			case 'item-saved':
+				if (data.item.status === 'autosave') {
+					this.showFormStatus(data.item.formId, 'autosave');
+				}
+				break;
+			case 'data-loaded':
+
+				break;
+		}
+	}
+
 	/**
 	 * Check for pending operations from previous session
 	 */
 	async checkPendingOperations() {
-		if (!this.store) return;
-		try {
-			let pending = this.store.getAllForms();
+		const pendingForms = await this.store.query('status', 'pending');
 
-		} catch (error) {
-			console.error('Failed to load pending forms:', error);
-		}
+		if (pendingForms.length === 0) return;
+
+		// Group by form type or page
+		const grouped = this.groupPendingForms(pendingForms);
+
+		// Show consolidated notification
+		this.showPendingNotification(grouped);
 	}
 
 	/**
@@ -121,7 +147,7 @@
 	 * Discard pending form data
 	 */
 	async discardPendingForm(formId) {
-		this.store.clearForm(formId);
+		this.store.delete(formId);
 
 		if (window.jvbA11y) {
 			window.jvbA11y.announce('Previous changes discarded');
@@ -485,16 +511,18 @@
 	/**
 	 * Initialize image upload fields
 	 */
-	initImageUploadFields() {
-		window.jvbUploads.scanFields();
+	initImageUploadFields(form) {
+		window.jvbUploads.scanFields(form);
 	}
 
 	/* ========== Event Handlers ========== */
 
 	handleSubmit(event) {
+		//TODO: submit data, if successful, delete from store
 		if (this.subscribers.size > 0 ){
 			const form = event.target;
 			if (!form.dataset.formId) return;
+			this.store.delete(form.dataset.formId);
 
 			event.preventDefault();
 
@@ -629,7 +657,6 @@
 	 * Get appropriate delay based on field type and context
 	 */
 	getDelayForField(field) {
-		console.log('Get Delay for Field', field);
 		// Text fields get longer delay for typing
 		if (field.type === 'text' || field.type === 'textarea') {
 			return this.autoSaveDefaults.typingDelay;
@@ -665,7 +692,14 @@
 
 	async autosave(formConfig) {
 		const formData = this.collectFormData(formConfig.element);
-		this.cacheFormData(formConfig, formData);
+
+		await this.store.save({
+			formId: formConfig.id,
+			data: formData,
+			status: 'draft',
+			timestamp: Date.now()
+		});
+		this.showFormStatus(formConfig.id, 'saved');
 
 		// Get only changed fields
 		const changes = this.getChangedFields(formConfig.data, formData);
@@ -691,20 +725,6 @@
 		});
 	}
 
-	cacheFormData(formConfig, formData) {
-		try {
-			this.store.storeForm(formConfig.id, {
-				formId: formConfig.id,
-				formData: formData,
-				timestamp: Date.now(),
-				status: 'pending',
-				operationId: null
-			});
-		} catch (error) {
-			console.error('Failed to cache form data:', error);
-		}
-	}
-
 	/**
 	 * Check if form has unsaved changes
 	 */
@@ -971,7 +991,6 @@
 	cleanupForm(formId) {
 		const formConfig = this.forms.get(formId);
 		if (!formConfig) return;
-		console.log('Cleaning up form', formConfig);
 
 		// Check for unsaved changes
 		if (this.hasUnsavedChanges(formId)) {
diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index 2ef641b..769b4aa 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -3,9 +3,13 @@
 		// Initialize DataStore for queue persistence
 		this.store = new window.jvbStore({
 			name: 'favourites',
+			storeName: 'favourites',
 			endpoint: 'favourites',
-			useIndexedDB: true,
-			TTL: Infinity,
+			indexes: [
+				{name: 'content', keyPath: 'content'},
+				{name: 'listId', keyPath: 'listId'},
+			],
+			TTL: 86400000,
 			showLoading: false,
 			filters: {
 				user: jvbSettings.currentUser,
@@ -17,6 +21,14 @@
 			}
 		});
 
+		this.listStore = new window.jvbStore({
+			name: 'favourites_lists',
+			storeName: 'lists',
+			keyPath: 'listId',
+			endpoint: 'favourites/lists',
+			TTL: 86400000,
+		})
+
 		this.store.subscribe((event, data) => {
 			switch (event) {
 				case 'data-fetched':
@@ -61,12 +73,110 @@
 		});
 	}
 
-	isFavourited(content, id){
-		if(!jvbSettings.currentUser){
-			return false;
-		}
-		let item = this.store.getItem(id);
-		return (item) ? item.action === 'add' : false;
+	// async toggleFavourite(itemType, itemId) {
+	// 	const favId = `${this.userId}_${itemType}_${itemId}`;
+	// 	const existing = this.store.get(favId);
+	//
+	// 	if (existing) {
+	// 		// Remove favorite
+	// 		await this.store.delete(favId);
+	//
+	// 		// Queue for server deletion
+	// 		if (window.jvbQueue) {
+	// 			window.jvbQueue.addOperation({
+	// 				type: 'remove_favourite',
+	// 				endpoint: 'favourites',
+	// 				data: { favId }
+	// 			});
+	// 		}
+	//
+	// 		return false; // Not favorited anymore
+	//
+	// 	} else {
+	// 		// Add favorite
+	// 		const favourite = {
+	// 			id: favId,
+	// 			userId: this.userId,
+	// 			itemType,
+	// 			itemId,
+	// 			listId: 'default',
+	// 			timestamp: Date.now()
+	// 		};
+	//
+	// 		await this.store.save(favourite);
+	//
+	// 		// Queue for server save
+	// 		if (window.jvbQueue) {
+	// 			window.jvbQueue.addOperation({
+	// 				type: 'add_favourite',
+	// 				endpoint: 'favourites',
+	// 				data: favourite
+	// 			});
+	// 		}
+	//
+	// 		return true; // Now favorited
+	// 	}
+	// }
+
+
+	// async createList(name, visibility = 'private') {
+	// 	const list = {
+	// 		listId: `list_${Date.now()}`,
+	// 		userId: this.userId,
+	// 		name,
+	// 		visibility,
+	// 		created: Date.now(),
+	// 		itemCount: 0
+	// 	};
+	//
+	// 	await this.listsStore.save(list);
+	//
+	// 	// Queue for server
+	// 	if (window.jvbQueue) {
+	// 		window.jvbQueue.addOperation({
+	// 			type: 'create_list',
+	// 			endpoint: 'favourite-lists',
+	// 			data: list
+	// 		});
+	// 	}
+	//
+	// 	return list;
+	// }
+	//
+	// async addToList(listId, itemType, itemId) {
+	// 	const favId = `${this.userId}_${itemType}_${itemId}_${listId}`;
+	//
+	// 	const favourite = {
+	// 		id: favId,
+	// 		userId: this.userId,
+	// 		itemType,
+	// 		itemId,
+	// 		listId,
+	// 		timestamp: Date.now()
+	// 	};
+	//
+	// 	await this.store.save(favourite);
+	//
+	// 	// Update list count
+	// 	const list = this.listsStore.get(listId);
+	// 	if (list) {
+	// 		list.itemCount++;
+	// 		await this.listsStore.save(list);
+	// 	}
+	//
+	// 	// Queue for server
+	// 	if (window.jvbQueue) {
+	// 		window.jvbQueue.addOperation({
+	// 			type: 'add_to_list',
+	// 			endpoint: 'favourites',
+	// 			data: favourite
+	// 		});
+	// 	}
+	// }
+
+	isFavourited(itemType, itemId) {
+		const favId = `${this.userId}_${itemType}_${itemId}`;
+		return this.store.get(favId) !== undefined;
 	}
 }
 document.addEventListener('DOMContentLoaded', function() {
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
new file mode 100644
index 0000000..52e233d
--- /dev/null
+++ b/assets/js/concise/HandleSelection.js
@@ -0,0 +1,398 @@
+/**
+ * HandleSelection - Reusable selection management for items in grids
+ *
+ * Handles selection logic including:
+ * - Individual item selection/deselection
+ * - Select all / Clear all
+ * - Range selection (shift+click)
+ * - Selection count updates
+ * - Bulk action visibility
+ *
+ * @class HandleSelection
+ */
+class HandleSelection {
+	/**
+	 * @param {Object} config - Configuration object
+	 * @param {HTMLElement} config.container - Container element holding items
+	 * @param {Object} config.ui - UI element references
+	 * @param {HTMLElement} config.ui.selectAll - Select all checkbox
+	 * @param {HTMLElement} config.ui.bulkControls - Bulk actions container
+	 * @param {HTMLElement} config.ui.count - Selection count display
+	 * @param {string} config.itemSelector - Selector for individual items
+	 * @param {string} config.checkboxSelector - Selector for item checkboxes
+	 * @param {Function} config.onSelectionChange - Optional callback when selection changes
+	 */
+	constructor(config) {
+		this.container = config.container;
+		this.ui = config.ui || {};
+		this.itemSelector = config.itemSelector || '.item';
+		this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]';
+
+		this.selectedItems = new Set();
+		this.lastSelected = null;
+		this.subscribers = new Set();
+
+		this.init();
+	}
+
+	init() {
+		// Bind event handlers
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.keyHandler = this.handleKeys.bind(this);
+
+		// Attach listeners
+		this.container.addEventListener('click', this.clickHandler);
+		this.container.addEventListener('change', this.changeHandler);
+		this.container.addEventListener('keydown', this.keyHandler);
+	}
+
+	handleKeys(e) {
+		// Ctrl/Cmd + A: Select all
+		if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
+			e.preventDefault();
+
+			if (this.ui.selectAll) {
+				this.ui.selectAll.checked = true;
+				this.selectAll(true);
+				if (window.jvbA11y) {
+					window.jvbA11y.announce('All items selected');
+				}
+			}
+		}
+
+		// Escape: Deselect all
+		if (e.key === 'Escape' && this.selectedItems.size > 0) {
+			this.selectAll(false);
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Selection cleared');
+			}
+		}
+
+		// Delete/Backspace: Remove selected items
+		if ((e.key === 'Delete' || e.key === 'Backspace') &&
+			!e.target.matches('input, textarea')
+			&& this.selectedItems > 0) {
+			e.preventDefault();
+			if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) {
+				this.deselect(this.selectedItems);
+			}
+		}
+	}
+
+	handleClick(e) {
+		const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`);
+		if (!checkbox) return;
+
+		// Get the actual checkbox element
+		const input = checkbox.tagName === 'LABEL'
+			? document.getElementById(checkbox.getAttribute('for'))
+			: checkbox;
+
+		if (!input) return;
+
+		// Handle shift+click for range selection
+		if (e.shiftKey && this.lastSelected) {
+			e.preventDefault();
+			this.handleRangeSelection(input);
+		} else {
+			// Store last clicked for range selection
+			const item = input.closest(this.itemSelector);
+			if (item) {
+				this.lastSelected = item;
+			}
+		}
+	}
+
+	handleChange(e) {
+		if (this.ui.selectAll && e.target === this.ui.selectAll) {
+			this.selectAll(e.target.checked);
+		} else {
+			const checkbox = e.target.closest(this.checkboxSelector);
+			if (!checkbox) return;
+
+			this.toggleSelection(this.getItemId(checkbox));
+		}
+	}
+
+	/**
+	 * Toggle selection state of an item
+	 */
+	toggleSelection(id) {
+		if (!id) return;
+
+		let selected = true;
+		if (this.selectedItems.has(id)) {
+			selected = false;
+			this.selectedItems.delete(id);
+		} else {
+			this.selectedItems.add(id);
+		}
+
+		if (selected) {
+			this.notify('item-selected', {
+				selectedItem: id,
+				selectedItems: this.selectedItems,
+				container: this.container
+			});
+		} else {
+			this.notify('item-deselected', {
+				selectedItem: id,
+				selectedItems: this.selectedItems,
+				container: this.container
+			});
+		}
+
+
+		this.updateSelectionUI();
+	}
+
+	/**
+	 * Select or deselect all items
+	 */
+	selectAll(checked) {
+		const items = this.container.querySelectorAll(this.itemSelector);
+
+		if (!checked) {
+			this.selectedItems.clear();
+			if (this.ui.selectAll) {
+				this.ui.selectAll.checked = false;
+			}
+		}
+
+		items.forEach(item => {
+			const id = this.getItemId(item);
+			const checkbox = item.querySelector(this.checkboxSelector);
+
+			if (checkbox) {
+				checkbox.checked = checked;
+			}
+
+			if (checked && id) {
+				this.selectedItems.add(id);
+			}
+		});
+
+		this.notify('select-all', {
+			container: this.container,
+			selected: checked,
+			items: items
+		})
+
+		this.updateSelectionUI();
+	}
+
+	/**
+	 * Clear all selections
+	 */
+	clearSelection() {
+		this.selectAll(false);
+	}
+
+	/**
+	 * Handle shift+click range selection
+	 */
+	handleRangeSelection(currentCheckbox) {
+		if (!this.lastSelected) {
+			this.lastSelected = currentCheckbox.closest(this.itemSelector);
+			return;
+		}
+
+		const currentItem = currentCheckbox.closest(this.itemSelector);
+		if (!currentItem) return;
+
+		// Get all items
+		const allItems = Array.from(this.container.querySelectorAll(this.itemSelector));
+
+		// Find indices
+		const lastIndex = allItems.indexOf(this.lastSelected);
+		const currentIndex = allItems.indexOf(currentItem);
+
+		if (lastIndex === -1 || currentIndex === -1) return;
+
+		// Determine range (handle both directions)
+		const startIndex = Math.min(lastIndex, currentIndex);
+		const endIndex = Math.max(lastIndex, currentIndex);
+
+		let isChecked = !currentCheckbox.checked;
+		// Select all items in range
+		for (let i = startIndex; i <= endIndex; i++) {
+			const item = allItems[i];
+			const checkbox = item.querySelector(this.checkboxSelector);
+			const id = this.getItemId(item);
+
+			if (checkbox && id) {
+				checkbox.checked = isChecked;
+				this.selectedItems.add(id);
+			}
+		}
+
+		// Update last selected to current
+		this.lastSelected = currentItem;
+
+		this.updateSelectionUI();
+
+		this.notify('range-selected', {
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+
+		// Announce for accessibility
+		const selectedCount = endIndex - startIndex + 1;
+		if (window.jvbA11y) {
+			window.jvbA11y.announce(`Selected ${selectedCount} items in range`);
+		}
+	}
+
+	/**
+	 * Update selection UI elements
+	 */
+	updateSelectionUI() {
+		const count = this.selectedItems.size;
+		const totalItems = this.container.querySelectorAll(this.itemSelector).length;
+
+		// Update bulk controls visibility
+		if (this.ui.bulkControls) {
+			this.ui.bulkControls.hidden = count === 0;
+		}
+
+		// Update count display
+		if (this.ui.count) {
+			const itemText = count === 1 ? 'item' : 'items';
+			this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+			this.ui.count.hidden = count === 0;
+		}
+
+		// Update select all checkbox state
+		if (this.ui.selectAll) {
+			this.ui.selectAll.checked = totalItems > 0 && count === totalItems;
+			this.ui.selectAll.indeterminate = count > 0 && count < totalItems;
+
+			// Update label text if available
+			const label = this.ui.selectAll.nextElementSibling ||
+				this.ui.selectAll.previousElementSibling;
+			if (label && label.tagName === 'LABEL') {
+				label.textContent = (totalItems > 0 && count === totalItems)
+					? 'Clear Selection'
+					: 'Select All';
+			}
+		}
+	}
+
+	/**
+	 * Get item ID from element
+	 */
+	getItemId(element) {
+		const item = element.closest(this.itemSelector);
+		if (!item) return null;
+
+		// Try common ID attributes in order
+		return item.dataset.id ||
+			item.dataset.itemId ||
+			item.dataset.uploadId ||
+			item.id;
+	}
+
+	/**
+	 * Get all selected item IDs as array
+	 */
+	getSelected() {
+		return Array.from(this.selectedItems);
+	}
+
+	/**
+	 * Check if an item is selected
+	 */
+	isSelected(id) {
+		return this.selectedItems.has(id);
+	}
+
+	/**
+	 * Programmatically select specific items
+	 */
+	select(ids) {
+		const idArray = Array.isArray(ids) ? ids : [ids];
+
+		idArray.forEach(id => {
+			this.selectedItems.add(id);
+
+			// Update checkbox if element exists
+			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+			if (item) {
+				const checkbox = item.querySelector(this.checkboxSelector);
+				if (checkbox) checkbox.checked = true;
+			}
+		});
+
+		this.updateSelectionUI();
+
+		this.notify('item-selected', {
+			selectedItem: id,
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+	}
+
+	/**
+	 * Programmatically deselect specific items
+	 */
+	deselect(ids) {
+		const idArray = Array.isArray(ids) ? ids : [ids];
+
+		idArray.forEach(id => {
+			this.selectedItems.delete(id);
+
+			// Update checkbox if element exists
+			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+			if (item) {
+				const checkbox = item.querySelector(this.checkboxSelector);
+				if (checkbox) checkbox.checked = false;
+			}
+		});
+
+		this.updateSelectionUI();
+
+		this.notify('item-deselected', {
+			selectedItem: id,
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+	}
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/**
+	 * Clean up event listeners
+	 */
+	destroy() {
+		// Remove event listeners
+		if (this.container) {
+			this.container.removeEventListener('click', this.clickHandler);
+			this.container.removeEventListener('change', this.changeHandler);
+			this.container.removeEventListener('keydown', this.keyHandler);
+		}
+
+		// Clear selections
+		this.clearSelection();
+
+		// Clear subscribers
+		this.subscribers.clear();
+
+		// Clear references
+		this.container = null;
+		this.ui = null;
+		this.lastSelected = null;
+	}
+}
+
+// Export for use in other modules
+window.jvbHandleSelection = HandleSelection;
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index ec60ffc..838e5b6 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -435,7 +435,7 @@
 		// Update image display
 		const grid = fieldWrapper.querySelector('.item-grid');
 		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
-
+		fieldWrapper.querySelector('.progress')?.remove();
 		if (grid) {
 			window.removeChildren(grid);
 			imageIds.forEach(imageId => {
diff --git a/assets/js/concise/Popup.js b/assets/js/concise/Popup.js
new file mode 100644
index 0000000..7ef07c9
--- /dev/null
+++ b/assets/js/concise/Popup.js
@@ -0,0 +1,79 @@
+class Popup {
+	constructor(config = {}) {
+		this.config = {
+			toggle: document.querySelector('[data-toggles]'),
+			popup: document.querySelector('.jvb-pop'),
+			name: 'Popup',
+			onOpen: () =>{},
+			onClose: () =>{},
+			onEscape: () =>{},
+			... config
+		};
+
+		this.a11y = window.jvbA11y;
+		this.toggleID = this.config.toggle.id === '' ? '.'+this.config.toggle.className.split(' ').join('.') : this.config.toggle.id;
+
+		this.initListeners();
+	}
+
+	initListeners()
+	{
+		this.clickHandler = this.handleClick.bind(this);
+		this.keyHandler = this.handleEscape.bind(this);
+
+		document.addEventListener('click', this.handleToggle.bind(this));
+	}
+
+	handleToggle(e) {
+		if (e.target === this.config.toggle || e.target.closest(this.toggleID)){
+			this.togglePopup();
+		}
+	}
+	handleClick(e) {
+		if (!this.config.popup.contains(e.target)
+			&& e.target !== this.config.toggle) {
+			this.closePopup();
+		} else if (window.targetCheck(e, '.close')) {
+			this.closePopup();
+		}
+	}
+
+	togglePopup() {
+		if (!this.config.popup.classList.contains('expanded')) {
+			this.openPopup();
+		} else {
+			this.closePopup();
+		}
+	}
+	openPopup(message = `Opened ${this.config.name}`) {
+		this.config.popup.classList.add('expanded');
+		this.config.toggle.classList.add('expanded');
+		this.config.toggle.title = `Hide ${this.config.name}`;
+		this.config.toggle.ariaExpanded = true;
+		this.config.toggle.querySelector('span').textContent = `Close ${this.config.name}`;
+		this.a11y.announce(message);
+		this.config.onOpen();
+		document.addEventListener('keydown', this.keyHandler);
+		document.addEventListener('click', this.clickHandler);
+	}
+	closePopup(message = `Closed ${this.config.name}`) {
+		this.config.popup.classList.remove('expanded');
+		this.config.toggle.classList.remove('expanded');
+		this.config.toggle.title = `Show ${this.config.name}`;
+		this.config.toggle.ariaExpanded = false;
+
+		this.config.toggle.querySelector('span').textContent = '';
+		this.a11y.announce(message);
+		this.config.onClose();
+		document.removeEventListener('keydown', this.keyHandler);
+		document.removeEventListener('click', this.clickHandler);
+	}
+	handleEscape(e) {
+		if (e.key === 'Escape') {
+			this.closePopup(`Closed ${this.config.name} with escape key`);
+			this.config.onEscape();
+		}
+	}
+}
+
+window.jvbPopup = Popup;
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 6b58475..b817cbb 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -29,10 +29,15 @@
 		// Initialize DataStore for queue persistence
 		this.store = new window.jvbStore({
 			name: 'queue',
+			storeName: 'operations',
+			keyPath: 'id',
 			endpoint: this.config.endpoint,
-			useIndexedDB: true,
 			TTL: Infinity, //Queue data doesn't expire,
-			showLoading: false
+			indexes: [
+				{name: 'status', keyPath: 'status'},
+				{name: 'type', keyPath: 'type'},
+			],
+			showLoading: false,
 		});
 
 		this.queue = new Map();
@@ -63,6 +68,15 @@
 		// Initialize
 		this.initUI();
 		this.initListeners();
+		console.log(this.ui);
+		if (this.ui.panel) {
+			this.popup = new window.jvbPopup({
+				popup: this.ui.panel,
+				toggle: this.ui.toggle,
+				name: 'Queue Panel',
+			});
+		}
+
 		this.initQueue();
 
 		if (this.user) {
@@ -172,7 +186,7 @@
 
 	setQueue(item) {
 		this.queue.set(item.id, item);
-		this.store.setItem(item.id, item);
+		this.store.save(item.id, item);
 	}
 
 	updateOperationStatus(itemID, status) {
@@ -281,12 +295,21 @@
 				operation.data.append('id', operation.id);
 				operation.data.append('user', this.user);
 				requestBody = operation.data;
+				// console.log('Sending formData: ');
+				// for (const pair of requestBody.entries()) {
+				// 	console.log(pair[0], pair[1]);
+				// }
 			} else {
 				requestBody = JSON.stringify({
 					...operation.data,
 					id: operation.id,
 					user: this.user
 				});
+				// console.log('Sending data: ', {
+				// 	...operation.data,
+				// 	id: operation.id,
+				// 	user: this.user
+				// });
 				operation.headers['Content-Type'] = 'application/json';
 			}
 
@@ -555,7 +578,6 @@
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
 		this.changeHandler = this.handleChange.bind(this);
-		this.keyHandler = this.handleEscape.bind(this);
 
 		document.addEventListener('click', this.clickHandler);
 		this.ui.panel?.addEventListener('change', this.changeHandler);
@@ -580,16 +602,7 @@
 		window.addEventListener('beforeunload', this.handleBeforeUnload);
 	}
 	handleClick(e) {
-		if(!e.target.closest(this.selectors.panel) && !e.target.closest(this.selectors.toggle)) {
-			if (this.panelIsOpen()) {
-				this.togglePanel(false);
-			}
-			return;
-		}
-
-		if (e.target.closest(this.selectors.toggle)) {
-			this.togglePanel(!this.panelIsOpen());
-		} else if (e.target.closest(this.selectors.refreshButton)) {
+		if (e.target.closest(this.selectors.refreshButton)) {
 			this.pollServer(true);
 		} else if (e.target.closest(this.selectors.clearButton)) {
 			const completedOps = this.getOperationsByStatus('completed');
@@ -619,28 +632,6 @@
 	handleChange(e) {
 	}
 
-	handleEscape(e) {
-		if (e.key === 'Escape') {
-			this.togglePanel(false);
-		}
-	}
-	panelIsOpen() {
-		return this.ui.panel?.classList.contains('expanded');
-	}
-	togglePanel(open) {
-		if (!this.ui.panel) return;
-
-		if (open) {
-			document.addEventListener('keydown', this.keyHandler);
-		} else {
-			document.removeEventListener('keydown', this.keyHandler);
-		}
-		this.ui.toggle.title = (open) ? 'Hide Queue' : 'Show Queue';
-		this.a11y.announce((open) ? 'Opened Queue Panel': 'Closed Queue Panel');
-		this.ui.panel.ariaExpanded = open;
-		this.ui.panel.classList.toggle('expanded', open);
-	}
-
 	/*********************************************
 	UI
 	 *********************************************/
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
new file mode 100644
index 0000000..6f2442d
--- /dev/null
+++ b/assets/js/concise/Referral.js
@@ -0,0 +1,446 @@
+/**
+ * Referral Widget Manager
+ * Handles both logged-in share widget and public code validation widget
+ *
+ */
+
+class Referral {
+	constructor() {
+		this.container = document.querySelector('.jvb-referral');
+		if (!this.container) {
+			return;
+		}
+
+		this.a11y = window.jvbA11y;
+
+		this.toggle = document.querySelector('button[data-action="toggle-referral"]');
+
+		this.initElements();
+		this.initListeners();
+		this.checkForReferral();
+	}
+
+	initElements()
+	{
+		this.selectors = {
+			copy: 'button.copy',
+			login: '.login',
+			submit: '[type=submit]',
+		};
+
+		this.forms = this.container.querySelectorAll('form');
+		console.log(this.forms);
+		this.popup = new window.jvbPopup({
+			toggle: this.toggle,
+			popup: this.container,
+			name: 'Referral Box',
+			onOpen: () => {
+				this.forms.forEach(form => {
+					form.addEventListener('submit', this.submitHandler);
+				});
+				this.container.addEventListener('click', this.clickHandler);
+				this.container.addEventListener('input', this.inputHandler);
+			},
+			onClose: () => {
+				this.forms.forEach(form => {
+					form.removeEventListener('submit', this.submitHandler);
+				});
+				this.container.removeEventListener('click', this.clickHandler);
+				this.container.removeEventListener('input', this.inputHandler);
+			}
+		});
+
+		this.tabs = null;
+		if (this.container.querySelector('nav.tabs')) {
+			this.tabs = new window.jvbTabs(this.container, {updateURL: false});
+		}
+
+		this.ui = window.uiFromSelectors(this.selectors, this.container);
+	}
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.inputHandler = this.handleInput.bind(this);
+		this.submitHandler = this.handleFormSubmit.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+
+	}
+
+	handleClick(e) {
+		if (e.target.classList.contains('.copy')) {
+			let target = e.target.dataset.target;
+			let value = this.container.querySelector(`#${target}`);
+			value = (value) ? value.textContent : false;
+			if (value) {
+				this.handleCopy(e.target, value);
+			}
+		}
+	}
+
+	handleChange(e) {
+		if (e.target.id === 'referral-code') {
+			window.debouncer.schedule(
+				'check-referral',
+				()=> this.makeRequest('referrals/check-code', {code: e.target.value})),
+				150
+		}
+	}
+
+	handleInput(e) {
+		if (e.target.id === 'referral-code') {
+			e.target.value = e.target.value.toUpperCase();
+		}
+	}
+
+	// ==========================================
+	// SHARE WIDGET (Logged-In Users)
+	// ==========================================
+
+	initShareWidget() {
+		this.initCopyButton();
+		this.loadStats();
+	}
+
+	/**
+	 * Check for ?ref parameter in URL and pre-fill code
+	 */
+	async checkForReferral() {
+		const isLoggedIn = this.getUrlParameter('seeReferral');
+		const refCode = this.getUrlParameter('ref');
+		if (!isLoggedIn && !refCode) {
+			return;
+		}
+		if (!refCode) {
+			this.popup.openPopup();
+			return;
+		}
+
+		const codeInput = this.container.querySelector('#referral-code-input');
+		if (!codeInput) return;
+
+		// Convert to uppercase
+		const code = refCode.toUpperCase();
+
+		// Pre-fill the code input
+		codeInput.value = code;
+		codeInput.readOnly = true; // Make it read-only since it came from link
+
+		this.popup.togglePopup();
+
+		// Validate the code immediately to show referrer info
+		try {
+			const referrer = await this.validateCodeOnly(code);
+
+			if (referrer.success) {
+				// Show referrer info banner
+				this.showReferrerBanner(referrer.referrer_name, code);
+
+				// Focus on name input (first empty field)
+				const nameInput = this.container.querySelector('#referral-name');
+				if (nameInput) {
+					nameInput.focus();
+				}
+			} else {
+				// Invalid code - make input editable and show error
+				codeInput.readOnly = false;
+				this.showMessage('This referral link is invalid. Please enter a valid code.', 'error');
+			}
+		} catch (error) {
+			console.error('Error validating code:', error);
+			codeInput.readOnly = false;
+		}
+
+		// Clean up URL (remove ?ref parameter)
+		this.removeUrlParameter('ref');
+	}
+
+	/**
+	 * Get URL parameter value
+	 */
+	getUrlParameter(name) {
+		const urlParams = new URLSearchParams(window.location.search);
+		return urlParams.get(name);
+	}
+
+	/**
+	 * Remove URL parameter (clean URL)
+	 */
+	removeUrlParameter(name) {
+		const url = new URL(window.location);
+		url.searchParams.delete(name);
+		window.history.replaceState({}, document.title, url.toString());
+	}
+
+	/**
+	 * Validate code without registering (just check if valid)
+	 */
+	async validateCodeOnly(code) {
+		const response = await fetch(`${jvbSettings.api}/referrals/check-code`, {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({ code: code })
+		});
+
+		return await response.json();
+	}
+
+	/**
+	 * Show banner with referrer info
+	 */
+	showReferrerBanner(referrerName, code) {
+		const header = this.container.querySelector('.referral-header');
+		if (!header) return;
+
+		// Create banner
+		const banner = document.createElement('div');
+		banner.className = 'referrer-banner';
+		banner.innerHTML = `
+            <div class="banner-icon">🎉</div>
+            <div class="banner-content">
+                <strong>${window.escapeHtml(referrerName)}</strong> referred you!
+                <div class="banner-code">Code: <code>${window.escapeHtml(code)}</code></div>
+            </div>
+        `;
+
+		// Insert after header
+		header.parentNode.insertBefore(banner, header.nextSibling);
+
+		// Update header text
+		const headerTitle = header.querySelector('h3');
+		if (headerTitle) {
+			headerTitle.textContent = 'Complete Your Registration';
+		}
+
+		const headerDesc = header.querySelector('p');
+		if (headerDesc) {
+			headerDesc.textContent = 'Enter your details below to claim your welcome reward!';
+		}
+	}
+
+	/**
+	 * Copy referral link to clipboard
+	 */
+	handleCopy(button, text = '') {
+		if (text === '' || typeof text !== 'string') {
+			return;
+		}
+		let originalText = button.textContent;
+		if (navigator.clipboard || navigator.clipboard.writeText) {
+			navigator.clipboard.writeText(text).then(() => {
+				button.textContent = 'Copied!';
+				button.style.background = '#00a32a';
+
+				setTimeout(() => {
+					button.textContent = originalText;
+					button.style.background = '';
+				}, 2000);
+			})
+		}
+	}
+
+	async loadStats() {
+		const statsContainer = this.container.querySelector('.referral-stats');
+		if (!statsContainer) return;
+
+		try {
+			const response = await fetch(`${jvbSettings.api}/referrals/stats`, {
+				headers: { 'X-WP-Nonce': jvbSettings.nonce }
+			});
+
+			const data = await response.json();
+			if (data.success && data.stats) {
+				this.updateStats(data.stats);
+			}
+		} catch (error) {
+			console.error('Error loading stats:', error);
+		}
+	}
+
+	/**
+	 * Update stats display
+	 */
+	updateStats(stats) {
+		const elements = {
+			total: this.container.querySelector('[data-stat="total"]'),
+			treated: this.container.querySelector('[data-stat="treated"]'),
+			pending: this.container.querySelector('[data-stat="pending"]'),
+			rewards: this.container.querySelector('[data-stat="rewards"]')
+		};
+
+		if (elements.total) elements.total.textContent = stats.total_referrals || 0;
+		if (elements.treated) elements.treated.textContent = stats.treated_count || 0;
+		if (elements.pending) elements.pending.textContent = stats.pending_count || 0;
+		if (elements.rewards) {
+			elements.rewards.textContent = '$' + parseFloat(stats.available_rewards || 0).toFixed(2);
+		}
+	}
+
+	/**
+	 * Handle form submission
+	 */
+	async handleFormSubmit(event) {
+		console.log('Form Submission!');
+		window.debouncer.cancel('check-referral');
+		event.preventDefault();
+		console.log('Still working?');
+
+		const form = event.target;
+
+		// Get form data
+		const formData = new FormData(form);
+
+		let data = {};
+
+		// Disable form
+		this.setFormLoading(true, form);
+
+		try {
+
+			let result = {success: false, message: ''};
+			console.log(form);
+			console.log(form.id);
+			if (form.id === 'referral-code-form') {
+				if (!formData.get('name')) {
+					result.message += 'We need your name to know who you are.';
+				}
+				if (!formData.get('email')) {
+					result.message += 'We need your email to confirm you have access to it.';
+				}
+				if (!formData.get('referral_code')) {
+					result.message += 'We need the referral code to know who sent you.';
+				}
+				if (formData.get('name') && formData.get('email') && formData.get('referral_code')) {
+					data.name = formData.get('name');
+					data.email = formData.get('email');
+					data.code = formData.get('referral_code');
+					result = await this.makeRequest('referrals/register', data);
+				}
+			} else if (form.id === 'login-form' && formData.get('login-email')) {
+				data.type = 'login';
+				data.email = formData.get('login-email');
+				data.context = {};
+				data.context['redirect_to'] = window.location.href+'?seeReferral=1';
+				console.log('Making Request with: ', data);
+				result = await this.makeRequest('magic-link', data);
+			}
+
+
+			if (result.success) {
+				this.handleSuccess(result);
+			} else {
+				this.showMessage(result.message || 'Something went wrong. Please try again.', 'error');
+				this.setFormLoading(false, form);
+			}
+		} catch (error) {
+			console.error('Error registering:', error);
+			this.showMessage('Something went wrong. Please try again.', 'error');
+			this.setFormLoading(false, form);
+		} finally {
+			this.setFormLoading(false, form);
+		}
+	}
+
+	async makeRequest(endpoint, data) {
+		if (![
+			'magic-link',
+			'referrals/register',
+			'referrals/check-code'
+		].includes(endpoint)) {
+			return {success:false, message: 'Something went wrong (Invalid endpoint).'}
+		}
+		console.log('Endpoint: ', endpoint);
+		console.log('Data: ', data);
+		const response = await fetch(`${jvbSettings.api}${endpoint}`, {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				'X-WP-Nonce': jvbSettings.nonce,
+			},
+			body: JSON.stringify(data)
+		});
+
+		return await response.json();
+	}
+
+	/**
+	 * Show success state
+	 */
+	handleSuccess(result) {
+		//Hide forms
+		this.container.querySelectorAll('form').forEach(form => {
+			window.fade(form, false);
+		});
+
+		const successState = this.container.querySelector('.success-message');
+		if (!successState) return;
+
+		// Show success message
+		successState.hidden = false;
+
+		// Scroll to message
+		successState.scrollIntoView({
+			behavior: 'smooth',
+			block: 'center'
+		});
+
+		// Fire custom event
+		this.dispatchEvent('emailSent', {
+			email: result.email
+		});
+	}
+
+	/**
+	 * Set form loading state
+	 */
+	setFormLoading(loading, form) {
+		const inputs = form.querySelectorAll('input');
+
+		inputs.forEach(input => input.disabled = loading);
+		let status = form.querySelector('.status');
+		let message = status.querySelector('.message');
+		status.hidden = loading;
+		status.classList.toggle('loading', loading);
+		if (loading) {
+			message.textContent = 'Checking with server...';
+		} else {
+			message.textContent = '';
+		}
+	}
+
+	/**
+	 * Show message
+	 */
+	showMessage(text, type = 'success') {
+		const messageDiv = this.container.querySelector('#referral-message');
+		if (!messageDiv) return;
+
+		messageDiv.textContent = text;
+		messageDiv.className = 'message ' + type;
+		messageDiv.style.display = 'block';
+
+		if (type === 'error') {
+			setTimeout(() => {
+				messageDiv.style.display = 'none';
+			}, 5000);
+		}
+	}
+
+	/**
+	 * Dispatch custom event
+	 */
+	dispatchEvent(eventName, detail) {
+		const event = new CustomEvent('referralWidget:' + eventName, {
+			detail: detail,
+			bubbles: true
+		});
+		this.container.dispatchEvent(event);
+	}
+}
+
+
+document.addEventListener('DOMContentLoaded', () => {
+	window.jvbReferral = new Referral();
+});
+
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index b13e9ae..b571e2c 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -8,9 +8,25 @@
 		this.error = window.jvbError;
 		this.index = -1;
 
-		// DataStore instances per taxonomy
-		this.stores = new Map();
-		this.storeSubscriptions = new Map();
+		this.store = new window.jvbStore({
+			name: `taxonomies`,
+			storeName: `terms`,
+			keyPath: 'id',
+			indexes: [
+				{name: 'taxonomy', keyPath: 'taxonomy'},
+				{name: 'parent', keyPath: 'parent'},
+				{name: 'slug', keyPath: 'slug', unique: true},
+				{name: 'count', keyPath: 'count'},
+			],
+			endpoint: 'terms',
+			TTL: 7200000, //2 hours
+			filters: {
+				taxonomy: '',
+				page: 1,
+				search: '',
+				parent: 0
+			}
+		});
 
 		// Central field management
 		this.fields = new Map();
@@ -39,35 +55,8 @@
 		this.initModal();
 		this.scanExistingFields();
 		this.initGlobalListeners();
-	}
 
-	/**
-	 * Get or create a DataStore for a taxonomy
-	 */
-	getOrCreateStore(taxonomy) {
-		if (!this.stores.has(taxonomy)) {
-			const store = new window.jvbStore({
-				name: `tax_${taxonomy}`,
-				endpoint: 'terms',
-				TTL: 3600000, // 1 hour cache
-				filters: {
-					taxonomy: taxonomy,
-					page: 1,
-					search: '',
-					parent: 0
-				}
-			});
-
-			// Subscribe to store events
-			const unsubscribe = store.subscribe((event, data) => {
-				this.handleStoreEvent(taxonomy, event, data);
-			});
-
-			this.stores.set(taxonomy, store);
-			this.storeSubscriptions.set(taxonomy, unsubscribe);
-		}
-
-		return this.stores.get(taxonomy);
+		this.store.subscribe(this.handleStoreEvent.bind(this));
 	}
 
 	/**
@@ -227,7 +216,7 @@
 		this.fields.set(fieldId, config);
 
 		// Ensure store exists for this taxonomy
-		this.getOrCreateStore(config.taxonomy);
+		this.store.setFilter('taxonomy', config.taxonomy);
 
 		// Initialize display for any pre-selected values
 		if (config.selectedTerms.size > 0) {
@@ -252,7 +241,6 @@
 		const field = this.fields.get(fieldId);
 		if (!field || field.selectedTerms.size === 0) return;
 
-		const store = this.getOrCreateStore(field.taxonomy);
 		const selectedIds = Array.from(field.selectedTerms);
 
 		// Check store for cached terms first
@@ -260,7 +248,7 @@
 		const needsFetch = [];
 
 		selectedIds.forEach(termId => {
-			const term = store.getItem(termId);
+			const term = this.store.getItem(termId);
 			if (term) {
 				cachedTerms.push(term);
 			} else {
@@ -276,7 +264,8 @@
 		// Fetch missing terms if needed
 		if (needsFetch.length > 0) {
 			try {
-				const response = await store.fetch('terms', {
+
+				const response = await this.store.fetch({
 					filters: {
 						taxonomy: field.taxonomy,
 						termIDs: needsFetch.join(',')
@@ -285,7 +274,7 @@
 
 				if (response.terms) {
 					response.terms.forEach(term => {
-						store.setItem(term.id, term);
+						this.store.setItem(term.id, term);
 						this.addTermToDisplay(fieldId, term.id, term.name, term.path);
 					});
 				}
@@ -483,15 +472,16 @@
 		this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
 
 		// Get or create store for this taxonomy
-		this.activeStore = this.getOrCreateStore(this.currentConfig.taxonomy);
+		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
 
 		// Clear modal selection state
 		this.selectedTerms.clear();
 
 		// Copy field's current selections to modal state
 		if (this.currentConfig.selectedTerms) {
+			let termsToFetch = [];
 			this.currentConfig.selectedTerms.forEach(termId => {
-				const term = this.activeStore.getItem(termId);
+				const term = this.store.getItem(termId);
 				if (term) {
 					this.selectedTerms.set(termId, {
 						id: termId,
@@ -499,17 +489,26 @@
 						path: term.path
 					});
 				} else {
-					// If not in store, create minimal entry
-					this.selectedTerms.set(termId, {
-						id: termId,
-						name: `Term ${termId}`,
-						path: `Term ${termId}`
-					});
+					termsToFetch.push(termId);
 				}
 			});
+			if (termsToFetch.length > 0) {
+				let terms = this.fetchSpecificTerms(termsToFetch);
+				terms.forEach(term => {
+					this.selectedTerms.set(term.id, {
+						id: term.id,
+						name: term.name,
+						path: term.path
+					});
+				});
+			}
 		}
 	}
 
+	fetchSpecificTerms(terms) {
+		return [];
+	}
+
 	/**
 	 * Handle clicks within modal
 	 */
@@ -1183,16 +1182,11 @@
 		// Clear intervals and cleanup
 		this.observer?.disconnect();
 
-		// Unsubscribe from all stores
-		this.storeSubscriptions.forEach(unsubscribe => unsubscribe());
-
 		// Destroy all stores
-		this.stores.forEach(store => store.destroy());
+		this.store.destroy();
 
 		// Clear all maps
 		this.fields.clear();
-		this.stores.clear();
-		this.storeSubscriptions.clear();
 		this.selectedTerms.clear();
 	}
 }
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index 8328177..7144428 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -4,60 +4,88 @@
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
-		this.notifications = window.jvbNotifications;
 
 		//Load Datastore
-		this.initDB();
+		this.fieldStore = new window.jvbStore({
+			name: 'upload_fields',
+			storeName: 'fieldStates',
+			keyPath: 'id',
+			version: 2,
 
-		//State management
+			indexes: [
+				{ name: 'fieldId', keyPath: 'fieldId' },
+				{ name: 'timestamp', keyPath: 'timestamp' },
+				{ name: 'content', keyPath: 'content' },
+				{ name: 'itemId', keyPath: 'itemId' },
+				{ name: 'status', keyPath: 'status' }
+			],
+
+			stripDOMReferences: true,
+			TTL: 86400000*7 // 24 hours -> 1 week
+		});
+
+		this.uploadStore = new window.jvbStore({
+			name: 'uploads',
+			storeName: 'uploads',
+			keyPath: 'id',
+			storeBlobs: true,
+
+			indexes: [
+				{ name: 'fieldId', keyPath: 'fieldId' },
+				{ name: 'status', keyPath: 'status' },
+				{ name: 'groupId', keyPath: 'groupId' },
+				{ name: 'attachmentId', keyPath: 'attachmentId' }
+			],
+		});
+
+		// Subscribe to store events
+		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+		//Load Worker
+		this.initWorker();
+
+		// Core data structures
 		this.fields = new Map();
 		this.uploads = new Map();
 		this.uploadBlobs = new Map();
-		this.timeouts = new Map();
-		this.selected = new Map();
-
-		//Worker
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
-			settings: {
-				timeout: 10000, //10 seconds per image
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-
-		//Groups!
-		this.touch = {
-			x: null,
-			y: null
-		}
-		this.hasBulkContext = document.querySelector('details.uploader')!==null;
-		this.isTouching = false;
 		this.groups = new Map();
-
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.previewUrls = new Set();
 		//Notification and Subscribers
 		this.subscribers = new Set();
 
-		this.settings = {
-			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-			maxFileSize: 5242880,
-			maxProcessingTime: 120000, // 2 minutes max for processing
-			processingCheckInterval: 5000, // Check every 5 seconds
-			smartCompression: true,
-			fieldTypes: {
-				'single': { maxFiles: 1, allowMultiple: false },
-				'gallery': { maxFiles: 20, allowMultiple: true },
-				'groupable': { maxFiles: 20, allowMultiple: true }
+		// Controllers (will be initialized based on features)
+		this.dragController = null;
+
+		// Selectors
+		this.selectors = {
+			field: {
+				field: '[data-upload-field]',
+				input: 'input[type="file"]',
+				hiddenValue: 'input[type="hidden"]',
+				dropZone: '.file-upload-container',
+				preview: '.item-grid.preview',
+				progress: '.image-progress'
+			},
+			groups: {
+				container: '.upload-group',
+				grid: '.item-grid.group',
+				header: '.group-header',
+				selectAll: '[name="select-all-group"]',
+				actions: '.group-actions',
+				count: '.selection-controls .info'
+			},
+			items: {
+				item: '[data-upload-id]',
+				checkbox: '[name*="select-item"]',
+				featured: '[name="featured"]',
+				details: 'details'
 			}
 		};
 
+
 		this.statusMapping = {
 			'received': 'Image Received',
 			'local_processing': 'Processing Image...',
@@ -74,192 +102,421 @@
 	}
 
 	async init() {
-		this.initElements();
+		// Load existing data
+		await this.loadFields();
+		await this.loadUploads();
+		// Initialize fields
+		this.initializeFields();
+
+		// Set up core listeners
 		this.initListeners();
-		this.initCompressionWorker();
+
 		this.queue.subscribe((event, operation) => {
-			console.log('Operation Endpoint: ', operation.endpoint);
-			if (operation.endpoint !== 'uploads') {
+			if (operation.endpoint !== 'uploads' && operation.endpoint !== 'uploads/meta') {
 				return;
 			}
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data.fieldId;
 			switch(event) {
 				case 'cancel-operation':
-					this.clearField(operation.data.get('field_key'));
+					if (fieldId) {
+						this.clearField(fieldId);
+					}
 					break;
 				case 'operation-status':
-					console.log('Operation Data: ',operation.data);
-					const fieldId = operation.data?.field_key ||
-						(operation.data instanceof FormData ?
-							operation.data.get('field_key') : null);
-
 					if (fieldId) {
-						console.log('Updating field status:', fieldId, operation.status);
 						this.updateFieldStatus(fieldId, operation.status);
 					}
 					break;
+				case 'operation-complete':
+					const results = operation.result?.data || [];
+					results.forEach(result => {
+						const upload = this.uploads.get(result.upload_id);
+						if (upload) {
+							upload.attachmentId = result.attachment_id;
+							upload.status = 'completed';
+							this.uploads.set(upload.id, upload);
+						}
+					});
+					if (fieldId) {
+						this.cleanField(fieldId);
+					}
+					break;
 			}
+
 		});
-		await this.checkPendingUploads();
-		this.scanFields();
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
 	}
 
-	initElements() {
-		this.selectors = {
-			field: {
-				field: '.field.image',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				hiddenValue: 'input[type="hidden"]',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
+	initWorker() {
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: {
+				count: 0,
+				max: 3,
 			},
-			item: {
-				img: 'img',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-				status: '.status',
-				select: '[name*="select-item"]',
-				actions: '.item-actions',
-				featured: '[name="featured"]',
-				meta: '.upload-meta'
-			},
-			groups: {
-				container: '.item-grid.groups',
-				display: '.group-display',
-				selectAll: '#select-all-uploads',
-				actions: '.selection-actions',
-				info: '.selection-controls .info',
-				count: '.selection-count',
-				group: '.upload-group',
-				empty: '.empty-group'
+			settings: {
+				timeout: 10000, //10 seconds per image
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
 			}
 		};
-		this.ui = {};
 	}
 
-	scanFields() {
-		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
+	/**
+	 * Initialize all upload fields on the page
+	 */
+	initializeFields() {
+		const fields = document.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => {
 			this.registerUploader(uploader);
 		});
 	}
 
-	/**
-	 *
-	 * @param {HTMLElement} uploader
-	 * @param {object} options
-	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
-	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
-	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
-	 * @param {boolean} options.multiple Whether to allow multiple uploads
-	 * @param {number} options.itemID The post or term ID this is for.
-	 * @param {string} options.mode
-	 * @returns {string}
-	 */
-	registerUploader(uploader, options = {}) {
-		//Determine if this is for a post, term, content uploader, or option
-		let key = uploader.dataset['uploader']??this.determineKey(uploader);
+	scanFields(container) {
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => {
+			this.registerUploader(uploader);
+		});
+	}
 
-		uploader.dataset['uploader'] = key;
+	registerUploader(uploader) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader);
 
-		if (!this.fields.has(key)) {
-			let type = uploader.dataset.type;
+		// Create field data structure
+		const field = {
+			id: fieldId,
+			config: config,
+			element: uploader,
+			ui: this.buildFieldUI(uploader),
+			uploads: new Set(),
+			groups: new Set(),
+			state: 'ready',
+		};
 
-			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
-			let config = {
-				key: key,
-				name: uploader.dataset.field,
-				ui: {},
-				type: type,
-				maxFiles: typeConfig.maxFiles,
-				multiple: typeConfig.allowMultiple,
-				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
-				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
-				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
-				mode: uploader.dataset.mode??'direct',
-				... options
-			};
+		this.fields.set(fieldId, field);
+		uploader.dataset.uploader = fieldId;
+		this.addFieldSelectionHandler(fieldId);
 
-			config.ui = window.uiFromSelectors(this.selectors, uploader);
-			config.ui.groups.groups = new Map();
-
-			this.selected.set(key, new Set());
-			this.fields.set(key, config);
-			if(config.type === 'groupable' && !this.hasGroups) {
-				this.initGroupListeners();
-			}
+		if (config.destination === 'post_group' && !this.dragController) {
+			this.initGroupFeatures();
 		}
-		return key;
+
+		return fieldId;
 	}
 
 	/**
-	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
-	 * @param uploader
-	 * @returns {string}
+	 * Extract configuration from field element
 	 */
-	determineKey(uploader) {
-		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
-		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
-		let field = uploader.dataset.field;
-		return `${content}_${itemID}_${field}`;
+	extractFieldConfig(fieldElement) {
+		return {
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,  // Field name for meta
+			itemID: fieldElement.dataset.itemId || 0,  // Post/term/user ID
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
 	}
 
 	/**
-	 *
-	 * @param {HTMLElement} element
+	 * Build UI element references for a field
 	 */
-	getFieldIdFromElement(element) {
-		let field = element.closest('.field.image');
-		if (!field) {
+	buildFieldUI(fieldElement) {
+		let UI = {
+			field: fieldElement,
+			input: fieldElement.querySelector(this.selectors.field.input),
+			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+			preview: fieldElement.querySelector(this.selectors.field.preview),
+			progress: {
+				progress: fieldElement.querySelector(this.selectors.field.progress),
+				bar: fieldElement.querySelector('.bar'),
+				fill: fieldElement.querySelector('.fill'),
+				details: fieldElement.querySelector('.details'),
+				text: fieldElement.querySelector('.details .text'),
+				count: fieldElement.querySelector('.details .count')
+			}
+		};
+
+		let display = fieldElement.querySelector('.group-display');
+		if (display) {
+			UI.groups = {
+				display: display,
+				container: fieldElement.querySelector('.item-grid.groups'),
+				empty: fieldElement.querySelector('.empty-group'),
+				groups: new Map()
+			};
+		}
+
+		return UI;
+	}
+
+	/**
+	 * Set up core event listeners
+	 */
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+
+		// External file drops
+		this.dragEnterHandler 	= this.handleExternalDragEnter.bind(this);
+		this.dragLeaveHandler 	= this.handleExternalDragLeave.bind(this);
+		this.dragOverHandler 	= this.handleExternalDragOver.bind(this);
+		this.dropHandler 		= this.handleExternalDrop.bind(this);
+
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
+	}
+
+	/**
+	 * Initialize group-specific features (drag & drop for rearranging)
+	 */
+	initGroupFeatures() {
+		// Initialize drag controller for rearranging items
+		this.dragController = new window.jvbDragHandler({
+			// What can be dragged
+			draggableSelector: this.selectors.items.item,
+
+			// Where items can be dropped
+			dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
+
+			// Don't start drag on interactive elements
+			ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
+			previewElement: 'img, video, .icon',
+
+			// Extract upload ID from element
+			getItemId: (element) => {
+				return element.dataset.uploadId;
+			},
+
+			// Get selected items for multi-drag
+			getSelectedItems: (element) => {
+				const fieldId = this.getFieldIdFromElement(element);
+				const uploadId = element.dataset.uploadId;
+				const selected = this.getCurrentSelection(fieldId);
+
+				if (selected && selected.includes(uploadId)) {
+					return selected;
+				}
+
+				return [uploadId];
+			},
+
+			// Validate drop location
+			validateDrop: (itemIds, targetElement) => {
+				const targetFieldId = this.getFieldIdFromElement(targetElement);
+				const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
+				const itemFieldId = this.getFieldIdFromElement(itemElement);
+
+				return targetFieldId === itemFieldId;
+			},
+
+			// Handle successful drop
+			onDrop: (itemIds, targetElement) => {
+				this.handleItemDrop(itemIds, targetElement);
+				targetElement.scrollIntoView({behavior:'smooth', block:'center'});
+			},
+
+			// Optional callbacks
+			onDragStart: (itemIds) => {
+			},
+
+			onDragEnd: (itemIds, success) => {
+				if (success) {
+					// Clear selection after successful move
+					const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
+					const fieldId = this.getFieldIdFromElement(itemElement);
+					const handler = this.selectionHandlers.get(fieldId);
+					handler?.clearSelection();
+				}
+			},
+
+			// Preview options
+			previewOptions: {
+				multiOffset: { x: -60, y: -80 },
+				singleOffset: { x: -50, y: -60 },
+				showCount: true
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop)
+	 *******************************************************************************/
+
+	handleExternalDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
+		}
+	}
+	handleExternalDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) {
 			return;
 		}
-		return field.dataset.uploader??this.determineKey(field);
+
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
 	}
 
-	getFieldFromElement(element) {
-		let id = this.getFieldIdFromElement(element);
-		return (this.fields.has(id)) ? this.fields.get(id) : false;
+	handleExternalDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
 	}
 
-	getUploadFromElement(element) {
-		let id = this.getUploadIdFromElement(element);
-		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
+	handleExternalDrop(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+
+		if (!dropZone) return;
+
+		e.preventDefault();
+		dropZone.classList.remove('dragover');
+
+		const files = Array.from(e.dataTransfer.files);
+
+		if (files.length === 0) return;
+
+		const fieldId = this.getFieldIdFromElement(dropZone);
+
+		if (fieldId) {
+			this.processFiles(fieldId, files);
+			this.a11y.announce(`${files.length} file(s) dropped for upload`);
+		} else {
+			console.error('No field ID found for drop zone');
+		}
 	}
 
-	getUploadIdFromElement(element) {
-		let upload = element.closest('[data-upload-id]');
-		return upload?.dataset.uploadId || null;
-	}
+	/*******************************************************************************
+	 * ITEM DROP HANDLER (for rearranging existing uploads)
+	 *******************************************************************************/
 
-	getGroupFromElement(element) {
-		let groupId = this.getGroupIdFromElement(element);
-		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
-	}
-	getGroupIdFromElement(element) {
-		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
-	}
+	/**
+	 * Handle items being dropped (called by DragController)
+	 */
+	handleItemDrop(itemIds, targetElement) {
+		const isPreviewDrop = targetElement.classList.contains('preview');
+		let actualTarget = targetElement;
 
-	getModalType(field) {
-		// Safety check for field.ui
-		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
-			return null;
+		// Handle drop on empty group placeholder
+		if (targetElement.classList.contains('empty-group')) {
+			const fieldId = this.getFieldIdFromElement(targetElement);
+			const group = this.createGroup(fieldId);
+
+			if (!group) {
+				console.error('Failed to create group');
+				return;
+			}
+
+			actualTarget = group.grid;
 		}
 
-		const dialog = field.ui.field.field.closest('dialog');
-		if (!dialog) return null;
+		// Move each item to target
+		itemIds.forEach(uploadId => {
+			if (isPreviewDrop) {
+				// Moving back to preview (ungrouping)
+				this.removeFromGroup(uploadId);
+			} else {
+				// Moving to a group
+				this.addToGroup(uploadId, actualTarget);
+			}
+		});
 
-		if (dialog.classList.contains('edit')) return 'edit';
-		if (dialog.classList.contains('create')) return 'create';
-		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
+		// Persist state
+		const fieldId = this.getFieldIdFromElement(targetElement);
+		this.schedulePersistance(fieldId);
 
-		return dialog.className;
+		// Announce for accessibility
+		const message = itemIds.length > 1
+			? `Moved ${itemIds.length} items`
+			: 'Moved item';
+		this.a11y.announce(message);
+	}
+
+	/*******************************************************************************
+	 * CLICK HANDLERS
+	 *******************************************************************************/
+
+	handleClick(e) {
+		// File input triggers
+		if (e.target.matches(this.selectors.field.dropZone) ||
+			e.target.closest(this.selectors.field.dropZone)) {
+			const dropZone = e.target.closest(this.selectors.field.dropZone);
+			if (dropZone && !e.target.matches('input, button, a')) {
+				const input = dropZone.querySelector(this.selectors.field.input);
+				input?.click();
+			}
+		}
+
+		// Group actions
+		const actionButton = e.target.closest('[data-action]');
+		if (actionButton) {
+			this.handleAction(actionButton);
+		}
+	}
+
+	handleChange(e) {
+		const fieldId = this.getFieldIdFromElement(e.target);
+		// File input change
+		if (e.target.matches(this.selectors.field.input)) {
+			const fieldId = this.getFieldIdFromElement(e.target);
+			const files = Array.from(e.target.files);
+
+			if (files.length > 0 && fieldId) {
+				this.processFiles(fieldId, files);
+			}
+		}
+
+		// Meta field changes
+		if (fieldId) {
+			if (this.fields.get(fieldId).config.destination === 'post_group') {
+				this.handleGroupMetaChange(e.target);
+			} else {
+				this.queueUploadMeta(e);
+			}
+		}
+	}
+
+	/********************************************************************************
+	 UTILITY
+	********************************************************************************/
+	getCurrentSelection(fieldId) {
+		let selected = [];
+		for (let [key, handler] of this.selectionHandlers) {
+			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
+				selected = selected.concat([... handler.selectedItems]);
+			}
+		}
+		return selected;
+	}
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
 	}
 
 	getStatusText(status) {
@@ -270,7 +527,6 @@
 		return window.getIcon(this.queue.icons[status]);
 	}
 	getStatusProgress(status) {
-		console.log('Getting status progress for: ', status);
 		switch (status) {
 			case 'local_processing':
 				return 28;
@@ -289,1133 +545,518 @@
 		}
 	}
 
-	/******************************************************************************
-	 LISTENERS
-	******************************************************************************/
-	initListeners() {
-		this.clickHandler 		= this.handleClick.bind(this);
-		this.changeHandler 		= this.handleChange.bind(this);
-
-		if (this.hasBulkContext) {
-			this.pasteHandler 		= this.handlePaste.bind(this);
-			document.addEventListener('paste', this.pasteHandler);
+	getModalType(field) {
+		// Return cached value if available
+		if (field._cachedModalType !== undefined) {
+			return field._cachedModalType;
 		}
 
-
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
-	}
-	clearListeners() {
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		if (this.hasBulkContext) {
-			document.removeEventListener('paste', this.pasteHandler);
-		}
-	}
-
-	initGroupListeners() {
-		this.hasGroups = true;
-
-		this.dragStartHandler 	= this.handleDragStart.bind(this);
-		this.dragEndHandler 	= this.handleDragEnd.bind(this);
-		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
-		this.dragOverHandler 	= this.handleDragOver.bind(this);
-		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
-		this.dropHandler 		= this.handleDrop.bind(this);
-
-		this.touchStartHandler 	= this.handleTouchStart.bind(this);
-		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
-		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
-		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
-
-		document.addEventListener('dragstart', this.dragStartHandler);
-		document.addEventListener('dragend', this.dragEndHandler);
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('drop', this.dropHandler);
-
-		document.addEventListener('touchstart', this.touchStartHandler);
-		document.addEventListener('touchmove', this.touchMoveHandler);
-		document.addEventListener('touchend', this.touchEndHandler);
-		document.addEventListener('touchcancel', this.touchCancelHandler);
-	}
-	clearGroupListeners() {
-		document.removeEventListener('dragstart', this.dragStartHandler);
-		document.removeEventListener('dragend', this.dragEndHandler);
-		document.removeEventListener('dragenter', this.dragEnterHandler);
-		document.removeEventListener('dragover', this.dragOverHandler);
-		document.removeEventListener('dragleave', this.dragLeaveHandler);
-		document.removeEventListener('drop', this.dropHandler);
-
-		document.removeEventListener('touchstart', this.touchStartHandler);
-		document.removeEventListener('touchmove', this.touchMoveHandler);
-		document.removeEventListener('touchend', this.touchEndHandler);
-		document.removeEventListener('touchcancel', this.touchCancelHandler);
-	}
-
-	handleClick(e) {
-		if (!e.target.closest(this.selectors.field.field)) {
-			return;
+		// Safety check for field.element
+		if (!field || !field.element) {
+			field._cachedModalType = null;
+			return null;
 		}
 
-		if (window.targetCheck(e, '.restart-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.restartUploads(fieldId);
-		} else if (window.targetCheck(e, '.dismiss-cache-restore')) {
-			e.preventDefault();
-			const notification = e.target.closest('.upload-recovery-notification');
-			if (notification) notification.remove();
-		} else if (window.targetCheck(e, '#select-all-uploads')) {
-			e.preventDefault();
-			this.handleSelectAll(e.target);
-		} else if (window.targetCheck(e, '.upload-select')) {
-			const isShiftClick = e.shiftKey && this.lastClickedUpload;
-			if (isShiftClick) {
-				e.preventDefault();
-				this.handleRangeSelection(e.target, e);
-			} else {
-				this.updateSelection(e);
-			}
-		} else if (window.targetCheck(e, '.create-from-selection')) {
-			e.preventDefault();
-			let group = this.createGroup(this.getFieldFromElement(e.target));
-			this.addSelectionToGroup(group);
-		} else if (window.targetCheck(e, '.remove-selection')) {
-			e.preventDefault();
-			this.removeSelection(e.target);
-		} else if (window.targetCheck(e, '.add-to-group, .add-selection-to-group')) {
-			e.preventDefault();
-			this.addSelectionToGroup(e.target);
-		} else if (window.targetCheck(e, '.remove-group')) {
-			e.preventDefault();
-			const groupElement = e.target.closest('.upload-group');
-			if (groupElement) {
-				let field = this.getFieldFromElement(groupElement);
-				this.removeGroup(groupElement, true);
-			}
-		} else if (window.targetCheck(e, '.remove')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			const fieldId = this.getFieldIdFromElement(e.target);
-			if (uploadId && fieldId) {
-				this.removeUpload(fieldId, uploadId);
-			}
-		} else if (window.targetCheck(e, '.submit-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.submitUploads(fieldId);
-		} else if (window.targetCheck(e, '.retry-upload')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			this.retryUpload(uploadId);
-		}
-	}
-	handleChange(e) {
-		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
-			return;
-		}
-		e.preventDefault();
-
-		if (window.targetCheck(e, '[type="file"]')) {
-			console.log(this.fields);
-			let field = this.getFieldFromElement(e.target);
-			console.log(field);
-			if (!field) {
-				console.warn('File change on unregistered field: ', field.key)
-				return;
-			}
-
-			const files = Array.from(e.target.files);
-			if (files.length === 0) return;
-
-			this.processFiles(field.key, files);
-			e.target.value = '';
-		} else if (e.target.name.includes('select-')) {
-			this.updateSelection(e);
-		} else if (e.target.closest('.upload-meta')) {
-			e.preventDefault();
-			let name = e.target.name;
-			let value = e.target.value;
-			let upload = this.getUploadFromElement(e.target);
-			upload.changes[name] = value;
-			this.uploads.set(upload.id, upload);
-			this.persistFieldState(upload.fieldId);
-
-			//It's meta!
-			//TODO:
-			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
-			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
-			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
-			//Send to server for processing
-		} else if (e.target.closest('.group.fields')) {
-			let group = this.getGroupFromElement(e.target);
-			let name = e.target.name;
-			group.changes[name] = e.target.value;
-
-			this.persistFieldState(group.fieldId);
-			this.groups.set(group.id, group);
-		}
-	}
-
-	handlePaste(e) {
-		window.debouncer.schedule(
-			'imagePaste',
-			() => {
-				const items = Array.from(e.clipboardData.items);
-				const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-				if (imageItems.length === 0) return;
-
-				e.preventDefault();
-
-				const fieldId = this.getFieldIdFromElement(e.target);
-				if (!fieldId) return;
-
-				// Convert clipboard items to files
-				const files = [];
-				imageItems.forEach((item, index) => {
-					const file = item.getAsFile();
-					if (file) {
-						// Rename for clarity
-						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-							type: file.type,
-							lastModified: Date.now()
-						});
-						files.push(newFile);
-					}
-				});
-
-				if (files.length > 0) {
-					this.processFiles(fieldId, files);
-				}
-			},
-			100
-		);
-	}
-
-	isTouchOnFormElement(target) {
-		// Check if target is a form element or inside one
-		const formElements = [
-			'input', 'button', 'label', 'select', 'textarea',
-		];
-
-		return formElements.some(selector => {
-			return target.matches(selector) || target.closest(selector);
-		});
-	}
-	/**** DRAG AND TOUCH *****/
-	startDragOperation(config) {
-		const {
-			primaryElement,
-			sourceType,
-			startPosition,
-			event
-		} = config;
-
-		const uploadId = this.getUploadIdFromElement(primaryElement);
-		const fieldId = this.getFieldIdFromElement(primaryElement);
-
-		// Determine what items to drag
-		const draggedItems = this.getDraggedItems(primaryElement);
-
-		// Initialize drag state
-		this.dragState = {
-			primaryItem: uploadId,
-			draggedItems: draggedItems,
-			isDragging: true,
-			isMultiDrag: draggedItems.length > 1,
-			fieldId: fieldId,
-			sourceType: sourceType,
-			startTime: Date.now(),
-			startPosition: startPosition,
-			currentPosition: startPosition,
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-			touchMoved: false
-		};
-
-		// Create drag preview
-		this.createDragPreview(primaryElement);
-
-		// Apply dragging state
-		this.applyDraggingState(true);
-
-		const announceText = this.dragState.isMultiDrag
-			? `Started dragging ${draggedItems.length} items`
-			: 'Started dragging item';
-
-		this.a11y.announce(announceText);
-		this.provideDragFeedback('start');
-
-		return true;
-	}
-
-	updateDragOperation(position, elementUnderPointer) {
-		if (!this.dragState.isDragging) return;
-
-		const { sourceType, startPosition } = this.dragState;
-
-		// Update position
-		this.dragState.currentPosition = position;
-
-		// Check for significant movement (touch)
-		if (sourceType === 'touch' && !this.dragState.touchMoved) {
-			const deltaX = Math.abs(position.x - startPosition.x);
-			const deltaY = Math.abs(position.y - startPosition.y);
-
-			if (deltaX > 10 || deltaY > 10) {
-				this.dragState.touchMoved = true;
-			}
+		const dialog = field.element.closest('dialog');
+		if (!dialog) {
+			field._cachedModalType = null;
+			return null;
 		}
 
-		// Update preview and target
-		this.updateDragPreview(position);
-		this.updateDropTarget(elementUnderPointer);
-	}
+		let modalType = null;
+		if (dialog.classList.contains('edit')) modalType = 'edit';
+		else if (dialog.classList.contains('create')) modalType = 'create';
+		else if (dialog.classList.contains('bulkEdit')) modalType = 'bulkEdit';
+		else modalType = dialog.className;
 
-	endDragOperation(elementUnderPointer = null) {
-		if (!this.dragState.isDragging) return;
-
-		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-			this.dragState.validTarget;
-
-		// Process drop if valid - but only here, not in handleDrop
-		if (wasSuccessful && this.dragState.validTarget) {
-			this.processItemDrop({
-				itemIds: this.dragState.draggedItems,
-				targetElement: this.dragState.validTarget,
-				fieldId: this.dragState.fieldId,
-				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-				sourceType: this.dragState.sourceType
-			});
-		}
-
-		// Cleanup
-		this.cleanupDragOperation();
-
-		const announceText = wasSuccessful
-			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-			: 'Drag cancelled';
-
-		this.a11y.announce(announceText);
-	}
-
-	/**
-	 * Shared method to process any drop operation (drag or touch)
-	 * @param {Object} dropData - Standardized drop data
-	 * @returns {boolean} Success status
-	 */
-	processItemDrop(dropData) {
-		const {
-			itemIds,
-			targetElement,
-			fieldId,
-			dropType,
-			sourceType
-		} = dropData;
-
-		if (!itemIds?.length || !targetElement || !fieldId) {
-			return false;
-		}
-
-		// Determine if it's a preview drop
-		let isPreviewDrop = targetElement.classList.contains('item-grid') && targetElement.classList.contains('preview');
-
-		// Handle empty group drops by creating the group element
-		let actualTarget = targetElement;
-		if (targetElement.classList.contains('empty-group')) {
-			let group = this.createGroup(fieldId);
-			actualTarget = group.querySelector('.item-grid');
-			isPreviewDrop = false;
-		}
-
-		// Use existing addImageToGroup method for each item
-		// This method already handles:
-		// - removeImageFromCurrentLocation (cleanup of old location)
-		// - Adding to new location
-		// - Updating field.posts data structure
-		// - Caching the data
-		itemIds.forEach(uploadId => {
-			this.addImageToGroup(uploadId, actualTarget, isPreviewDrop);
-		});
-
-		// Clear selections for multi-drops
-		if (dropType === 'multiple') {
-			const field = this.fields.get(fieldId);
-			this.clearAllSelections(field);
-		}
-
-		// Announce completion
-		const announceText = dropType === 'multiple'
-			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-		this.a11y.announce(announceText);
-		this.provideFeedback(sourceType, 'success', {
-			count: itemIds.length,
-			isMultiple: dropType === 'multiple'
-		});
-
-		return true;
-	}
-
-	clearAllSelections(field) {
-		// Clear all selection checkboxes in the entire field container
-		const allCheckboxes = field.container.querySelectorAll('[name*="select-item"]');
-		allCheckboxes.forEach(checkbox => {
-			checkbox.checked = false;
-		});
-
-		// Update the select all state
-		if (field.selectAll) {
-			field.selectAll.checked = false;
-			const label = field.selectAll.nextElementSibling;
-			if (label) {
-				label.textContent = 'Select All';
-			}
-		}
-
-		// Hide selection controls
-		if (field.selectActions) field.selectActions.hidden = true;
-		if (field.selectInfo) field.selectInfo.hidden = true;
-	}
-
-	cleanupDragOperation() {
-		if (this.dragState.dragPreview) {
-			this.dragState.dragPreview.remove();
-		}
-
-		this.applyDraggingState(false);
-		this.clearDropTargetStates();
-
-		// Reset state
-		this.dragState.isDragging = false;
-		this.dragState.dragPreview = null;
-		this.dragState.draggedItems = [];
-	}
-
-	/**
-	 * Determine what items to drag (single or multiple selection)
-	 */
-	getDraggedItems(element) {
-		const selectedUploads = this.getSelectedUploads(element);
-		const primaryUploadId = element.dataset.uploadId;
-
-		// If we have multiple selections and primary is selected, drag all
-		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-			return selectedUploads;
-		}
-
-		// Otherwise, just drag the primary item
-		return [primaryUploadId];
-	}
-
-	/**
-	 * Apply/remove dragging visual state to items
-	 */
-	applyDraggingState(isDragging) {
-		this.dragState.draggedItems.forEach(uploadId => {
-			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (element) {
-				element.classList.toggle('dragging', isDragging);
-			}
-		});
-	}
-
-	/**
-	 * Create drag preview element
-	 */
-	createDragPreview(originalElement) {
-		const { isMultiDrag, draggedItems } = this.dragState;
-
-		if (isMultiDrag) {
-			this.dragState.dragPreview = this.createMultiDragPreview(originalElement, draggedItems);
-		} else {
-			this.dragState.dragPreview = this.createSingleDragPreview(originalElement);
-		}
-
-		this.updateDragPreview(this.dragState.startPosition);
-		document.body.appendChild(this.dragState.dragPreview);
-	}
-
-	/**
-	 * Create single item drag preview
-	 */
-	createSingleDragPreview(originalElement) {
-		const preview = originalElement.cloneNode(true);
-		preview.dataset.uploadId = preview.dataset.uploadId+'-dragging';
-		this.styleDragPreview(preview, false);
-		return preview;
-	}
-
-	styleDragPreview(preview, isMulti = false) {
-		preview.style.cssText = `
-        position: fixed;
-        z-index: 10000;
-        pointer-events: none;
-        opacity: 0.9;
-        transform: scale(1.05);
-        transition: transform 0.2s ease;
-        ${isMulti ? `
-            width: 120px;
-            height: 120px;
-            background: white;
-            border-radius: 8px;
-            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
-            padding: 4px;
-        ` : `
-            border-radius: 4px;
-            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
-        `}
-    `;
-
-		// Add dragging class for additional styling
-		preview.classList.add('drag-preview', 'is-dragging');
-		if (isMulti) {
-			preview.classList.add('multi-item');
-		}
-	}
-
-	/**
-	 * Create multiple items drag preview
-	 */
-	createMultiDragPreview(originalElement, draggedItems) {
-		const container = document.createElement('div');
-		container.className = 'drag-preview multi-item';
-
-		// Create stacked effect with up to 3 items
-		const displayCount = Math.min(draggedItems.length, 3);
-
-		for (let i = 0; i < displayCount; i++) {
-			const uploadId = draggedItems[i];
-			const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-			if (uploadElement) {
-				const stackedItem = uploadElement.cloneNode(true);
-				stackedItem.dataset.uploadId = uploadId + '_dragging';
-
-				stackedItem.style.cssText = `
-				position: absolute;
-				top: ${i * 4}px;
-				left: ${i * 4}px;
-				width: calc(100% - ${i * 4}px);
-				height: calc(100% - ${i * 4}px);
-				opacity: ${1 - (i * 0.15)};
-				transform: rotate(${(i - 1) * 2}deg);
-				z-index: ${10 - i};
-				border-radius: 4px;
-				overflow: hidden;
-			`;
-				container.appendChild(stackedItem);
-			}
-		}
-
-		// Add count badge
-		if (draggedItems.length > 1) {
-			const badge = this.createCountBadge(draggedItems.length);
-			container.appendChild(badge);
-		}
-
-		this.styleDragPreview(container, true);
-		return container;
-	}
-	/**
-	 * Update drag preview position
-	 */
-	updateDragPreview(position) {
-		if (!this.dragState.dragPreview) return;
-
-		// Calculate offset based on preview type and source
-		let offset;
-		if (this.dragState.sourceType === 'touch') {
-			offset = this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 };
-		} else {
-			offset = this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 };
-		}
-
-		const deltaX = position.x - this.dragState.startPosition.x;
-		const deltaY = position.y - this.dragState.startPosition.y;
-
-		this.dragState.dragPreview.style.transform = `translate(${deltaX + offset.x}px, ${deltaY + offset.y}px) scale(1.05)`;
-	}
-
-	/**
-	 * Update drop target highlighting
-	 */
-	updateDropTarget(elementUnderPointer) {
-		// Clear previous target
-		if (this.dragState.currentTarget) {
-			this.clearDropTargetState(this.dragState.currentTarget);
-		}
-
-		// Find valid drop target
-		const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-		// Update state
-		this.dragState.currentTarget = elementUnderPointer;
-		this.dragState.validTarget = validTarget;
-
-		// Apply visual feedback
-		if (validTarget) {
-			this.applyDropTargetState(validTarget);
-
-			// Haptic feedback for touch
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-				navigator.vibrate(pattern);
-			}
-		}
-	}
-
-	/**
-	 * Find valid drop target from element
-	 */
-	findValidDropTarget(element) {
-		if (!element) return null;
-
-		const postContainer = element.closest('.item-grid.group, .empty-group, .item-grid.preview');
-		if (postContainer) {
-			const fieldId = this.getFieldIdFromElement(postContainer);
-			if (fieldId === this.dragState.fieldId) {
-				return postContainer;
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * Apply drop target visual state
-	 */
-	applyDropTargetState(target) {
-		target.classList.add('dragover');
-
-		if (this.dragState.isMultiDrag) {
-			target.classList.add('multi-drop');
-			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-		}
-	}
-
-	/**
-	 * Clear drop target state from element
-	 */
-	clearDropTargetState(target) {
-		target.classList.remove('dragover', 'multi-drop');
-		target.removeAttribute('data-item-count');
-	}
-
-	/**
-	 * Clear all drop target states
-	 */
-	clearDropTargetStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-
-	/**
-	 * Create count badge for multi-item preview
-	 */
-	createCountBadge(count) {
-		const badge = document.createElement('div');
-		badge.className = 'selection-count-badge';
-		badge.textContent = count.toString();
-		badge.style.cssText = `
-			position: absolute;
-			top: -8px;
-			right: -8px;
-			background: var(--accent-primary);
-			color: white;
-			border-radius: 50%;
-			width: 24px;
-			height: 24px;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			font-size: 12px;
-			font-weight: bold;
-			box-shadow: 0 2px 8px rgba(0,0,0,0.3);
-			z-index: 20;
-		`;
-		return badge;
-	}
-
-	/**
-	 * Provide feedback for drag operations
-	 */
-	provideDragFeedback(type) {
-		const hapticPatterns = {
-			start: [50],
-			success: this.dragState.isMultiDrag ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			cancel: [100]
-		};
-
-		if (this.dragState.sourceType === 'touch' && navigator.vibrate && hapticPatterns[type]) {
-			navigator.vibrate(hapticPatterns[type]);
-		}
-	}
-
-	/**
-	 * Provide consistent feedback for different input methods
-	 */
-	provideFeedback(sourceType, feedbackType, data = {}) {
-		const hapticPatterns = {
-			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			error: [100, 50, 100]
-		};
-
-		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-			navigator.vibrate(hapticPatterns[feedbackType]);
-		}
-	}
-
-	clearDragoverStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-	/*********
-	 *  DRAG HANDLERS
-	 ********/
-	handleDragEnter(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		// Only handle external files
-		if (e.dataTransfer.types.includes('Files')) {
-			e.preventDefault();
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				uploadContainer.classList.add('dragover');
-			}
-		}
-	}
-	handleDragLeave(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-			uploadContainer.classList.remove('dragover');
-		}
-	}
-	handleDragStart(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'drag',
-			startPosition: { x: e.clientX, y: e.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-			e.dataTransfer.effectAllowed = 'move';
-		} else {
-			e.preventDefault();
-		}
-	}
-
-	handleDragOver(e) {
-		if (!this.dragState.isDragging) return;
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		e.preventDefault();
-		this.updateDragOperation({ x: e.clientX, y: e.clientY }, e.target);
-	}
-
-	handleDrop(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-
-		e.preventDefault();
-		this.clearDragoverStates();
-
-		// Handle external files (new uploads)
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer) {
-			const files = Array.from(e.dataTransfer.files);
-			if (files.length > 0) {
-				const fieldId = this.getFieldIdFromElement(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-	}
-
-	handleDragEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		// Find the element under the final drop position
-		const elementUnderDrop = document.elementFromPoint(
-			this.dragState.currentPosition?.x || e.clientX,
-			this.dragState.currentPosition?.y || e.clientY
-		);
-
-		this.endDragOperation(elementUnderDrop);
-	}
-	/*********
-	 * TOUCH HANDLERS
-	 ********/
-	handleTouchStart(e) {
-		if (!window.targetCheck(e, '.image.field')) return;
-		if (this.isTouchOnFormElement(e.target)) {
-			return;
-		}
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const touch = e.touches[0];
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'touch',
-			startPosition: { x: touch.clientX, y: touch.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.preventDefault(); // Prevent scrolling
-		}
-	}
-
-	handleTouchMove(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.touches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.updateDragOperation({ x: touch.clientX, y: touch.clientY }, elementUnderTouch);
-	}
-
-	handleTouchEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.changedTouches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.endDragOperation(elementUnderTouch);
-	}
-
-	handleTouchCancel(e) {
-		if (this.dragState.isDragging) {
-			this.cleanupDragOperation();
-			this.a11y.announce('Drag cancelled');
-		}
+		// Cache the result
+		field._cachedModalType = modalType;
+		return modalType;
 	}
 	/*******************************************************************************
-	 QUEUE INTEGRATION
+	 * GROUP ACTIONS
 	 *******************************************************************************/
-	async submitUploads(fieldId) {
+
+	handleAction(button) {
+		const action = button.dataset.action;
+		const fieldId = this.getFieldIdFromElement(button);
+		switch(action) {
+			case 'add-to-group':
+				this.handleAddToGroup(button);
+				break;
+			case 'delete-group':
+				this.handleDeleteGroup(button);
+				break;
+			case 'delete-upload':
+			case 'remove-from-group':
+				this.handleRemoveItem(button);
+				break;
+			case 'upload':
+				//upload groups
+				let field = this.fields.get(fieldId);
+				field.element.closest('details').open = false;
+				document.body.classList.add('uploading');
+
+				this.submitUploads(fieldId);
+				break;
+			case 'restore':
+				this.handleRestoreUploads().then(()=>{});
+				break;
+			case 'clear-cache':
+				if (!confirm(`Save these uploads for later?`)) {
+					this.cleanupStoredUploads();
+				}
+				this.cleanupRestore();
+				break;
+		}
+	}
+
+	handleAddToGroup(button) {
+		const fieldElement = button.closest(this.selectors.field.field);
+		const fieldId = fieldElement?.dataset.uploader;
+
+		if (!fieldId) return;
+
+		const selected = this.selected.get(fieldId);
+
+		if (!selected || selected.size === 0) {
+			// Create empty group
+			this.createGroup(fieldId);
+		} else {
+			// Create group with selected items
+			const group = this.createGroup(fieldId);
+			if (!group) return;
+
+			selected.forEach(uploadId => {
+				this.addToGroup(uploadId, group.grid);
+			});
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+
+			this.a11y.announce(`Created group with ${selected.size} items`);
+		}
+
+		this.schedulePersistance(fieldId);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.groups.container);
+		if (!group) return;
+
+		const groupId = group.dataset.groupId;
+		const fieldId = this.getFieldIdFromElement(group);
+
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		// Move items back to preview
+		const items = group.querySelectorAll(this.selectors.items.item);
+		items.forEach(item => {
+			const uploadId = item.dataset.uploadId;
+			this.removeFromGroup(uploadId);
+		});
+
+		// Remove group
+		this.deleteGroup(groupId);
+
+		this.a11y.announce('Group deleted, items returned to upload area');
+		this.schedulePersistance(fieldId);
+	}
+
+	handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		const uploadId = item.dataset.uploadId;
+		const fieldId = this.getFieldIdFromElement(item);
+
+		if (!confirm('Remove this item?')) {
+			return;
+		}
+
+		this.removeUpload(fieldId, uploadId);
+		this.a11y.announce('Item removed');
+		this.schedulePersistance(fieldId);
+	}
+
+	/*******************************************************************************
+	 * SELECTION MANAGEMENT
+	 *******************************************************************************/
+
+	/**
+	 * Add selection handler for a field
+	 */
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) {
+			return this.selectionHandlers.get(fieldId);
+		}
+
 		const field = this.fields.get(fieldId);
 		if (!field) return;
 
-		// Check if there are uploads to submit
-		const pendingUploads = Array.from(field.uploads || [])
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload &&
-				(upload.status === 'processed' ||
-					upload.status === 'processed-original'));
+		const container = field.ui.field;
+		if (!container) return;
 
-		if (pendingUploads.length === 0) {
-			this.notifications.add('No uploads ready to submit', 'warning');
-			return;
+		const handler = new window.jvbHandleSelection({
+			container: container,
+			ui: {
+				selectAll: container.querySelector('[name="select-all-uploads"]'),
+				bulkControls: container.querySelector('.selection-actions'),
+				count: container.querySelector('.selection-count')
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		// Subscribe to selection changes
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(fieldId, handler);
+		return handler;
+	}
+
+	/**
+	 * Add selection handler for a group
+	 */
+	addGroupSelectionHandler(fieldId, groupId) {
+		const handlerKey = `${fieldId}_${groupId}`;
+
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
 		}
 
-		// Queue the uploads
-		try {
+		const group = this.groups.get(groupId);
+		if (!group) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: group.element,
+			ui: {
+				selectAll: group.element.querySelector(this.selectors.groups.selectAll),
+				bulkControls: group.element.querySelector(this.selectors.groups.actions),
+				count: group.element.querySelector(this.selectors.groups.count)
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(handlerKey, handler);
+		return handler;
+	}
+
+	handleSelectAll(container, selected) {
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	determineFieldId(fieldElement) {
+		const content = fieldElement.dataset.content ||
+			fieldElement.closest('dialog')?.dataset.content ||
+			fieldElement.closest('form')?.dataset.save || '';
+		const itemID = fieldElement.dataset.itemId ||
+			fieldElement.closest('dialog')?.dataset.itemId || '';
+		const field = fieldElement.dataset.field || '';
+
+		return `${content}_${itemID}_${field}`;
+	}
+
+	getFromElement(element, type) {
+		const map = {
+			'field': { selector: this.selectors.field.field, key: 'uploader', store: this.fields },
+			'upload': { selector: this.selectors.items.item, key: 'uploadId', store: this.uploads },
+			'group': { selector: this.selectors.groups.container, key: 'groupId', store: this.groups }
+		};
+
+		const config = map[type];
+		if (!config) return null;
+
+		const el = element.closest(config.selector);
+		if (!el) return null;
+
+		const id = el.dataset[config.key];
+		return config.store.get(id);
+	}
+	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+	getFieldIdFromElement(el) { return this.getFromElement(el, 'field')?.id ?? null};
+	getUploadIdFromElement(el) {return this.getFromElement(el, 'upload')?.id ?? null};
+	getGroupIdFromElement(el) {return this.getFromElement(el, 'group')?.id ?? null};
+
+
+	/*******************************************************************************
+	 * FILE PROCESSING
+	 *******************************************************************************/
+	async processFiles(fieldId, files) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Hide upload container, show group display
+		if (field.ui.dropZone) {
+			field.ui.dropZone.hidden = true;
+		}
+		if (field.ui.groups.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+		// Process files
+		const processPromises = Array.from(files).map(async (file, index) => {
+			try {
+
+				// Create upload ID
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create upload data
+				const uploadData = {
+					id: uploadId,
+					attachment_id: null,
+					fieldId: fieldId,
+					originalFile: file,
+					processedFile: null,
+					preview: null,
+					status: 'local_processing',
+					element: null,
+					location: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Create preview URL
+				uploadData.preview = this.createPreviewUrl(file);
+
+				// Process the file (resize if image)
+				if (file.type.startsWith('image/')) {
+					uploadData.processedFile = await this.processImage(file, field.subtype);
+				} else {
+					uploadData.processedFile = file;
+				}
+
+				// Store blob data separately in IndexedDB
+				await this.uploadStore.saveBlob(uploadId, uploadData.processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				uploadData.element = this.createUploadElement({
+					...uploadData,
+					subtype: subtype
+				}, field.config.destination === 'post_group');
+
+				// Show progress on the item
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Add to preview grid
+				if (field.ui.preview) {
+					field.ui.preview.appendChild(uploadData.element);
+					uploadData.location = field.ui.preview;
+				}
+
+				// Store upload
+				this.uploads.set(uploadId, uploadData);
+				field.uploads.add(uploadId);
+
+				// Update progress
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+				uploadData.status = 'processed';
+
+				// Fade out item progress after a moment
+				setTimeout(() => {
+					this.showUploadProgress(uploadId, false);
+				}, 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		// Wait for all files to process
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		// Cache the state (now without DOM references)
+		await this.schedulePersistance(fieldId);
+
+		// Queue for upload if in direct mode
+		if (field.config.destination !== 'post_group') {
 			await this.queueUpload(fieldId);
-			this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitUploads',
-				fieldId
-			});
-			this.notifications.add('Failed to submit uploads', 'error');
+			// Lock uploads if max reached
+			this.maybeLockUploads(fieldId);
+		}
+
+	}
+
+	updateFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.ui.field) return;
+
+		const container = field.ui.field;
+		const uploadCount = field.uploads?.size || 0;
+		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		// Set data attributes for CSS targeting
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		// Update ARIA labels for accessibility
+		if (field.ui.preview) {
+			field.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
 		}
 	}
-	async retryUpload(uploadId) {
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field?.ui?.progress?.progress) return;
+
+		const progress = field.ui.progress;
+		const percent = total > 0 ? (current / total) * 100 : 0;
+
+		if (progress.fill) {
+			progress.fill.style.width = `${percent}%`;
+		}
+		if (progress.text) {
+			progress.text.textContent = message;
+		}
+		if (progress.count) {
+			progress.count.textContent = `${current}/${total}`;
+		}
+
+		progress.progress.hidden = (current === total);
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		field.state = status;
+		// Update UI based on status
+	}
+
+	updateUploadStatus(uploadId, status) {
 		const upload = this.uploads.get(uploadId);
 		if (!upload) return;
 
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		try {
-			// Reset status
-			this.updateUploadStatus(uploadId, 'received');
-
-			// If we have the processed file, skip to queuing
-			if (upload.processedFile) {
-				this.updateUploadStatus(uploadId, 'processed');
-				await this.queueUpload(upload.fieldId);
-			} else if (upload.originalFile) {
-				// Reprocess the file
-				const reprocessed = await this.processFile(upload.fieldId, upload.originalFile);
-				if (reprocessed) {
-					await this.queueUpload(upload.fieldId);
-				}
-			} else {
-				throw new Error('No file data available for retry');
-			}
-
-			this.notifications.add('Retrying upload...', 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'retryUpload',
-				uploadId
-			});
-			this.notifications.add('Failed to retry upload', 'error');
-		}
-	}
-	async restartUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const failedUploads = Array.from(field.uploads)
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload && upload.status === 'failed');
-
-		if (failedUploads.length === 0) {
-			this.notifications.add('No failed uploads to restart', 'info');
-			return;
-		}
-
-		for (const upload of failedUploads) {
-			await this.retryUpload(upload.id);
-		}
-
-		this.notifications.add(`Restarting ${failedUploads.length} upload(s)`, 'info');
-	}
-	async queueUpload(fieldId) {
-		//Further cache it, or is it already cached at this point?
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
-
-		const data = this.prepareUploadData(field, uploads);
-		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'image' : 'images';
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
-			append: '_upload'
-		}
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
-				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
-			});
-			field.operationId = operationId;
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		} finally {
-			this.persistFieldState(field.key);
-		}
+		upload.status = status;
+		this.updateUploadUI(uploadId);
 	}
 
-	prepareUploadData(field, uploads) {
-		console.log('Preparing Upload:', field);
-		const formData = new FormData();
-		formData.append('content', field.content);
-		formData.append('mode', field.mode);
-		formData.append('field_name', field.name);
-		formData.append('field_key', field.key);
-		formData.append('field_type', field.type);
-		formData.append('item_id', field.itemID);		//post, term, or user id
-		formData.append('context', field.context);	//post, term, or user
-		let uploadMap = [];
-		uploads.forEach(uploadId => {
-			let upload = this.uploads.get(uploadId);
-			if (upload) {
-				const fileToUpload = upload.processedFile || upload.originalFile;
-				if (fileToUpload) {
-					formData.append('files[]', fileToUpload);
-					uploadMap.push(upload.id);
-				} else {
-					console.warn(`No file for upload ${uploadId}`);
-				}
-			} else {
-				console.warn(`Upload ${uploadId} not found in uploads map`);
-			}
-		});
-		formData.append('upload_map', uploadMap);
+	updateUploadUI(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload?.element) return;
 
-		console.log('Final FormData:');
-		for (let pair of formData.entries()) {
-			console.log(pair[0], pair[1]);
-		}
+		// Update status classes
+		upload.element.className = upload.element.className.replace(/status-[\w-]+/g, '');
+		upload.element.classList.add(`status-${upload.status}`);
 
-		return formData;
-	}
-
-	async queueImageMeta(e) {
-		const upload = this.getUploadFromElement(element);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Collect meta data from the form
-		const metaContainer = element.closest('.upload-meta');
-		if (!metaContainer) return;
-
-		const metaData = {
-			title: metaContainer.querySelector('[name="title"]')?.value || '',
-			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
-			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
-			description: metaContainer.querySelector('[name="description"]')?.value || ''
-		};
-
-		// Update upload meta
-		upload.meta = { ...upload.meta, ...metaData };
-		this.uploads.set(upload.id, upload);
-
-		// Mark that we have meta changes
-		this.hasMetaChanges = true;
-
-		// Determine if upload has been sent to server
-		const isOnServer = upload.status === 'completed' && upload.attachmentId;
-
-		if (isOnServer) {
-			// Queue immediate update
-			await this.sendMetaUpdate(upload);
-		} else if (upload.operationId) {
-			// Wait for upload to complete, then send meta
-			this.queueDependentMetaUpdate(upload);
-		} else {
-			// Upload hasn't been queued yet, meta will be sent with initial upload
-			this.persistFieldState(field.key);
+		// Update progress if showing
+		const progress = upload.element.querySelector('.progress');
+		if (progress) {
+			this.updateUploadItemProgress(uploadId,
+				this.getStatusProgress(upload.status),
+				upload.status
+			);
 		}
 	}
 
 	/**
-	 * Send meta update to server
+	 * Show/hide progress indicator on individual upload items
 	 */
-	async sendMetaUpdate(upload) {
-		const formData = new FormData();
-		formData.append('attachment_id', upload.attachmentId);
-		formData.append('title', upload.meta.title);
-		formData.append('alt_text', upload.meta.alt_text);
-		formData.append('caption', upload.meta.caption);
-		formData.append('description', upload.meta.description);
+	showUploadProgress(uploadId, show = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
 
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: formData,
-			title: `Updating metadata for ${upload.meta.originalName}`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
+		const progressEl = upload.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => {
+					progressEl.hidden = true;
+				}, 300);
 			}
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-			this.notifications.add('Metadata updated', 'success');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
 		}
 	}
 
 	/**
-	 * Queue meta update that depends on upload completion
+	 * Update individual upload progress bar
 	 */
-	queueDependentMetaUpdate(upload) {
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			dependencies: [upload.operationId],
-			data: () => {
-				// This function will be called when dependencies are resolved
-				const formData = new FormData();
-				formData.append('operation_id', upload.operationId);
-				formData.append('upload_id', upload.id);
-				formData.append('title', upload.meta.title);
-				formData.append('alt_text', upload.meta.alt_text);
-				formData.append('caption', upload.meta.caption);
-				formData.append('description', upload.meta.description);
-				return formData;
-			},
-			title: `Updating metadata after upload`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
 
-		this.queue.addToQueue(operation);
+		const progressEl = upload.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) {
+			fill.style.width = `${percent}%`;
+		}
+
+		if (status && details) {
+			details.textContent = this.getStatusText(status);
+		}
+
+		if (status && icon) {
+			icon.innerHTML = this.getStatusIcon(status).outerHTML;
+		}
 	}
-	/*******************************************************************************
-	 IMAGE PROCESSING
-	*******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if(!field) return;
-
-		//Validate Files
-		const validFiles = files.filter(file=>this.validateFile(file, field));
-		if (validFiles.length === 0) return;
-
-		if (!this.checkFieldLimits(fieldId, validFiles.length)) {
-			// this.notify(`Cannot add ${validFiles.length} files. Field limit exceeded.`, 'warning');
-			return;
-		}
-		const processedUploads = await this.processBatch(fieldId, validFiles);
-
-		this.maybeLockUploads(fieldId);
-
-		if (field.groupDisplay) {
-			field.groupDisplay.hidden = false;
-		}
-		if (processedUploads.length > 0) {
-			await this.queueUpload(fieldId);
-		}
-
-		this.hideUploadProgress(fieldId);
-
-		this.a11y.announce(`Processed ${processedUploads.length} of ${validFiles.length} files`);
-	}
-
 	checkFieldLimits(fieldId, additionalFiles) {
 		const field = this.fields.get(fieldId);
 		if (!field) return false;
@@ -1423,18 +1064,9 @@
 		const currentCount = field.uploads?.size || 0;
 		const totalCount = currentCount + additionalFiles;
 
-		if (totalCount > field.maxFiles) {
-			this.notifications.add(
-				`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
-				'warning'
-			);
-			return false;
-		}
+		return totalCount <= field.maxFiles;
 
-		return true;
-	}
-	generateUploadId() {
-		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
 	}
 	validateFile(file, field) {
 		// Type validation
@@ -1464,101 +1096,14 @@
 		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
 	}
 
-	async processBatch(fieldId, files) {
-		const results = [];
-		const processingQueue = [];
-		const maxConcurrent = this.worker.settings.maxConcurrent;
-
-		let total = files.length;
-		for (let i = 0; i < files.length; i++) {
-			this.updateUploadProgress(fieldId, i, total);
-			// Wait if we've reached max concurrent processing
-			if (processingQueue.length >= maxConcurrent) {
-				await Promise.race(processingQueue);
-			}
-
-			const processPromise = this.processFile(fieldId, files[i])
-				.then(upload => {
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-
-					if (upload) results.push(upload);
-					return upload;
-				})
-				.catch(error => {
-					console.error(`Failed to process ${files[i].name}:`, error);
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-					return null;
-				});
-
-			processingQueue.push(processPromise);
+	shouldProcessClientSide(file, subtype) {
+		// Only process images client-side
+		if (subtype === 'image' && file.type.startsWith('image/')) {
+			return true;
 		}
 
-		// Wait for remaining files
-		await Promise.all(processingQueue);
-		return results;
-	}
-
-	async processFile(fieldId, file) {
-		const field = this.fields.get(fieldId);
-
-		const upload = await this.setUpload(fieldId, file);
-		const uploadId = upload.id;
-		try {
-			// Update UI immediately
-			this.addImageToGroup(uploadId);
-			this.updateUploadStatus(uploadId, 'local_processing');
-
-			// Attempt to process the image
-			let processedFile = null;
-			let processingFailed = false;
-
-			try {
-				processedFile = await this.processImage(file, uploadId);
-			} catch (error) {
-				console.warn(`Processing failed for ${file.name}, using original:`, error);
-				processingFailed = true;
-				processedFile = file; // Use original
-			}
-
-			// Update upload with processed file
-			upload.processedFile = processedFile;
-			upload.processingFailed = processingFailed;
-
-			// Update status
-			this.updateUploadStatus(uploadId, 'processed');
-
-			// Save to uploads map
-			this.uploads.set(uploadId, upload);
-
-			// Persist state
-			if (field && field.key) {
-				await this.persistFieldState(field.key);
-			}
-
-			const message = processingFailed
-				? `${file.name} added (original format)`
-				: `${file.name} processed and ready`;
-			this.a11y.announce(message);
-
-			return upload;
-
-		} catch (error) {
-			// Clean up failed upload
-			this.cleanupFailedUpload(uploadId, field.key);
-
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'processFile',
-				uploadId,
-				fileName: file.name
-			});
-
-			return null;
-		}
+		// Videos and documents go straight to server
+		return false;
 	}
 
 	async processImage(file, uploadId) {
@@ -1705,7 +1250,7 @@
 			};
 
 			try {
-				objectUrl = URL.createObjectURL(file);
+				objectUrl = this.createPreviewUrl(file);
 				img.src = objectUrl;
 			} catch (error) {
 				cleanup();
@@ -1833,8 +1378,6 @@
 	 * Restart compression worker
 	 */
 	restartCompressionWorker() {
-		console.log('Restarting compression worker...');
-
 		// Terminate existing worker
 		if (this.worker.worker) {
 			this.worker.worker.terminate();
@@ -1912,7 +1455,7 @@
         `;
 
 			const blob = new Blob([workerScript], { type: 'application/javascript' });
-			this.worker.worker = new Worker(URL.createObjectURL(blob));
+			this.worker.worker = new Worker(this.createPreviewUrl(blob));
 
 		} catch (error) {
 			console.warn('Failed to initialize compression worker:', error);
@@ -1949,120 +1492,62 @@
 		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
 	}
 
-	/**
-	 * Clean up failed upload
-	 */
-	cleanupFailedUpload(uploadId, fieldId) {
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			field.uploads.delete(uploadId);
-		}
-
-		const upload = this.uploads.get(uploadId);
-		if (upload) {
-			// Clean up preview URL
-			if (upload.preview?.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove element
-			upload.element?.remove();
-
-			// Remove from uploads
-			this.uploads.delete(uploadId);
-		}
-
-		// Remove from active tasks
-		this.worker.tasks.delete(uploadId);
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		// Track for cleanup
+		if (!this.previewUrls) this.previewUrls = new Set();
+		this.previewUrls.add(url);
+		return url;
 	}
-	/*******************************************************************************
-	 UI FUNCTIONALITY
-	*******************************************************************************/
-	/**
-	 * Update upload status correctly
-	 */
-	updateUploadStatus(uploadId, status) {
-		console.log('Updating upload status for: ', uploadId);
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
-			return;
-		}
-		upload.status = status;
 
-		this.updateImageUI(upload.id);
-		this.persistFieldState(upload.fieldId);
-	}
-	updateImageUI(uploadId) {
-		console.log('Updating image UI: ', uploadId);
-		const upload = this.uploads.get(uploadId);
-		console.log(upload);
-		if (!upload?.element) return;
-
-
-		const progressEl = upload.element.querySelector('.progress');
-		const itemEl = upload.element;
-
-		console.log('Updating Upload UI:', upload);
-		// Update status class on item for CSS styling
-		if (itemEl) {
-			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
-			itemEl.classList.add(`status-${upload.status}`);
-		}
-
-		if (progressEl) {
-			let icon = this.getStatusIcon(upload.status);
-			let message = this.getStatusText(upload.status);
-			let progress = this.getStatusProgress(upload.status);
-
-			const fill = progressEl.querySelector('.fill');
-			const itemIcon = progressEl.querySelector('span.icon');
-			const itemMessage = progressEl.querySelector('span.details');
-
-			if (fill) {
-				fill.style.width = `${progress}%`;
-			}
-			if (itemMessage) itemMessage.textContent = message;
-			if (itemIcon) {
-				window.removeChildren(itemIcon);
-				itemIcon.append(icon);
-			}
-
-			if (upload.status === 'completed') {
-				setTimeout(() => {
-					if (progressEl) {
-						window.fade(progressEl, false);
-					}
-				}, 1000);
-			}
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls?.delete(url);
 		}
 	}
-	/**
-	 * Hide the uploader drop zone if we have reached our limit
-	 */
+
 	maybeLockUploads(fieldId) {
 		const field = this.fields.get(fieldId);
-		if (!field) return;
+		if (!field?.ui?.dropZone) return;
 
-		// Hide/show drop zone based on file count
-		if (field.ui.field.dropZone) {
-			field.ui.field.dropZone.hidden = field.uploads && field.uploads.size >= field.maxFiles;
+		if (field.config.destination === 'post_group') {
+			return;
 		}
+
+		const uploadCount = field.uploads?.size || 0;
+		const maxFiles = field.config?.maxFiles || 999;
+
+		// Hide dropzone if at max files
+		field.ui.dropZone.hidden = uploadCount >= maxFiles;
+
+		// Update field state
+		field.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
 	}
-	createImageElement(upload, draggable = false) {
+	createUploadElement(upload, draggable = false) {
 		let image = window.getTemplate('uploadItem');
 		if (!image) {
 			console.error('Image template not found');
 			return;
 		}
 		image.dataset.uploadId = upload.id;
+		if (upload.originalFile) {
+			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
+		}
+
+
 		image.querySelector('[name="featured"]').value = upload.id;
 		let [
 			featured,
 			img,
+			video,
+			preview,
 			details
 		] = [
 			image.querySelector('[name="featured"]'),
 			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
 			image.querySelector('details')
 		];
 		[
@@ -2074,6 +1559,45 @@
 			upload.preview,
 			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
 		];
+
+		switch (image.dataset.subtype) {
+			case 'image':
+				[
+					img.src,
+					img.alt
+				] = [
+					upload.preview,
+					upload.originalFile?.name ?? upload.meta?.originalName?? ''
+				];
+				video.remove();
+				preview.remove();
+				break;
+			case 'video':
+				video.src = upload.preview;
+				img.remove();
+				preview.remove();
+				break;
+			case 'document':
+				const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
+				const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
+				const iconMap = {
+					'pdf': 'file-pdf',
+					'csv': 'file-csv',
+					'doc': 'file-doc',
+					'docx': 'file-doc',
+					'txt': 'file-txt',
+					'xls': 'file-xls',
+					'xlsx': 'file-xls'
+				};
+
+				const icon = window.getIcon(iconMap[extension] || 'file');
+
+				preview.innerText = upload.originalFile.name;
+				preview.prepend(icon);
+				img.remove();
+				video.remove();
+				break;
+		}
 		if (details) {
 			let template = window.getTemplate('uploadMeta');
 			if (template){
@@ -2097,345 +1621,737 @@
 
 		return image;
 	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		let progressBar = field.ui.field.progress.progress;
-
-		// Create progress bar if it doesn't exist
-		if (!progressBar) {
-			progressBar = window.getTemplate('imageProgress');
-
-			// Insert after drop zone or at top of container
-			const insertAfter = field.dropZone || field.container.firstElementChild;
-			if (insertAfter) {
-				insertAfter.insertAdjacentElement('afterend', progressBar);
-			} else {
-				field.container.prepend(progressBar);
-			}
-		}
-
-		// Update progress bar
-		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-		const progressFill = field.ui.field.progress.fill;
-		const progressMessage = field.ui.field.progress.details;
-		const progressCount = field.ui.field.progress.count;
-
-		if (progressFill) {
-			progressFill.style.width = `${progressPercent}%`;
-		}
-
-		if (progressMessage) {
-			progressMessage.textContent = message;
-		}
-
-		if (progressCount) {
-			progressCount.textContent = `${current}/${total}`;
-		}
-
-		// Add completion styling
-		if (current === total) {
-			progressBar.classList.add('completed');
-		}
-	}
-
-	hideUploadProgress(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const progressBar = field.ui.field.progress.progress;
-		if (progressBar) {
-			window.fade(progressBar, false);
-		}
-	}
 	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	*******************************************************************************/
-	async initDB() {
-		if (!('indexedDB' in window)) return;
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+	async submitUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads || field.uploads.size === 0) {
+			return;
+		}
 
-		const request = indexedDB.open(`jvb_uploads_db`, 1);
+		let uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			this.error.log('No uploads to upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
 
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-			if (!db.objectStoreNames.contains('fieldStates')) {
-				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
-				store.createIndex('timestamp', 'timestamp', { unique: false });
-				store.createIndex('content', 'content', { unique: false });
-				store.createIndex('itemId', 'itemId', { unique: false });
+		const fieldGroups = this.getFieldGroups(fieldId);
+
+		if (fieldGroups.length === 0) {
+			this.error.log('No groups created for post_group upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
+
+		// Build posts array from groups
+		const posts = [];
+		const formData = new FormData();
+		let uploadMap = [];
+
+		uploads = uploads.map((upload) => {
+			return this.uploads.get(upload);
+		});
+
+		fieldGroups.forEach((group, groupIndex) => {
+			const post = {
+				images: [],
+				fields: {}
+			};
+			for (let [name, value] of Object.entries(group.changes)) {
+				post.fields[name] = value;
 			}
 
-			// Blob storage remains separate for performance
-			if (!db.objectStoreNames.contains('uploadBlobs')) {
-				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
+			let groupUploads = uploads.filter((upload) => {
+				return upload['groupId'] === group.id;
+			});
+
+			groupUploads.forEach((upload) => {
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+
+						const imageData = {
+							upload_id: upload.id,
+							index: uploadMap.length
+						};
+						post.images.push(imageData);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+			// Add images for this group
+			// group.uploads.forEach(uploadId => {
+			// 	const upload = this.uploads.get(uploadId);
+			// 	if (upload) {
+			// 		const fileToUpload = upload.processedFile || upload.originalFile;
+			// 		if (fileToUpload) {
+			// 			formData.append('files[]', fileToUpload);
+			//
+			// 			const imageData = {
+			// 				upload_id: upload.id,
+			// 				index: uploadMap.length
+			// 			};
+			//
+			// 			// Check if this is the featured image
+			// 			const radioInput = upload.element?.querySelector('[name="featured"]');
+			// 			if (radioInput?.checked) {
+			// 				post.fields.featured = upload.id;
+			// 			}
+			//
+			// 			post.images.push(imageData);
+			// 			uploadMap.push(upload.id);
+			// 		}
+			// 	}
+			// });
+
+			posts.push(post);
+		});
+
+		//Each remaining upload (without a groupId) becomes its own post
+		let remainingUploads = uploads.filter((upload) => {
+			return !Object.hasOwn(upload, 'groupId');
+		});
+
+		remainingUploads.forEach((upload) => {
+			if (upload) {
+
+				const post = {
+					images: [],
+					fields: {}
+				};
+				const fileToUpload = upload.processedFile || upload.originalFile;
+				if (fileToUpload) {
+					formData.append('files[]', fileToUpload);
+
+					const imageData = {
+						upload_id: upload.id,
+						index: uploadMap.length
+					};
+					post.images.push(imageData);
+					uploadMap.push(upload.id);
+				}
+				posts.push(post);
+			}
+		});
+
+
+		// Add metadata to FormData
+		formData.append('content', field.config.content);
+		formData.append('user', field.config.itemID); // Assuming itemID is user ID
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			title: `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload',
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			field.operationId = operationId;
+			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
+
+			return operationId;
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			throw error;
+		} finally {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	async queueUpload(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads) return;
+
+		const uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			return;
+		}
+
+		const data = this.prepareUploadData(field, uploads);
+		this.a11y.announce('Queuing for upload');
+		let img = (uploads.length === 1) ? 'file' : 'files';
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} ${img} to server...`,
+			popup: `Uploading ${uploads.length} ${img}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload'
+		}
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (!upload) {
+					return;
+				}
+				upload.operationId = operationId;
+				this.updateUploadStatus(uploadId, 'queued');
+			});
+			field.operationId = operationId;
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		} finally {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	prepareUploadData(field, uploads) {
+
+		const formData = new FormData();
+		formData.append('content', field.config.content);
+		formData.append('mode', field.config.mode);
+		formData.append('field_name', field.config.name);
+		formData.append('fieldId', field.id);
+		formData.append('field_type', field.config.type);
+		formData.append('subtype', field.config.subtype);
+		formData.append('item_id', field.config.itemID);		//post, term, or user id
+		formData.append('destination', field.config.destination || 'meta'); //meta, post, post_group
+		let uploadMap = [];
+
+		const fieldGroups = this.getFieldGroups(field.id);
+		if (field.config.destination === 'post_group' && fieldGroups.length > 0) {
+			// User has created groups
+			let groups = [];
+			let titles = [];
+			let featuredImages = [];
+
+			fieldGroups.forEach(group => {
+				let groupUploadIndices = [];
+				let featuredIndex = null;
+
+				group.uploads.forEach(uploadId => {
+					let upload = this.uploads.get(uploadId);
+					if (upload) {
+						const fileToUpload = upload.processedFile || upload.originalFile;
+						if (fileToUpload) {
+							formData.append('files[]', fileToUpload);
+							const fileIndex = uploadMap.length;
+							uploadMap.push(upload.id);
+							groupUploadIndices.push(upload.id);
+
+							// Check if this is the featured image
+							const radioInput = upload.element?.querySelector('[name="featured"]');
+							if (radioInput?.checked) {
+								featuredIndex = upload.id;
+							}
+						}
+					}
+				});
+
+				groups.push(groupUploadIndices);
+				titles.push(group.title || '');
+				featuredImages.push(featuredIndex);
+			});
+
+			formData.append('groups', JSON.stringify(groups));
+			formData.append('group_titles', JSON.stringify(titles));
+			formData.append('featured_images', JSON.stringify(featuredImages));
+		} else {
+			// No groups - just append all files
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+		}
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		// console.log('Final FormData:');
+		// for (let pair of formData.entries()) {
+		// 	console.log(pair[0], pair[1]);
+		// }
+
+		return formData;
+	}
+
+	getFieldGroups(fieldId) {
+		const groups = [];
+
+		this.groups.forEach((groupData, groupId) => {
+			if (groupData.fieldId === fieldId) {
+				const field = this.fields.get(fieldId);
+				const groupElement = field?.ui?.groups?.groups?.get(groupId);
+
+				groups.push({
+					id: groupId,
+					uploads: Array.from(groupData.uploads || new Set()),
+					changes: groupData.changes || {},
+					element: groupElement || null
+				});
+			}
+		});
+
+		return groups;
+	}
+
+	async queueUploadMeta(e) {
+		const upload = this.getUploadFromElement(e.target);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		const container = e.target.closest('.upload-meta');
+		if (!container) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.meta = {
+			...upload.meta,
+			... data
+		};
+
+		let queueData = {};
+		//If there is an attachment ID, use that: else, use our generated upload id
+		queueData[upload.attachmentId??upload.id] = upload.meta;
+
+		const operation = {
+			endpoint:	'uploads/meta',
+			method:		'POST',
+			data:		queueData,
+			title:		`Updating meta`,
+			canMerge:	true,
+			headers:	{
+				'action_nonce': jvbSettings.dash
 			}
 		};
 
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFields();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
-	}
-
-	async loadFields() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
-			const fieldStates = tx.objectStore('fieldStates');
-			const blobStore = tx.objectStore('uploadBlobs');
-			const request = fieldStates.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(field => {
-					let uploads = field.uploads;
-					let uploadIds = uploads.map(upload => upload.id);
-					field.uploads = new Set(uploadIds);
-					this.fields.set(field.key, field);
-					uploads.forEach(upload => {
-						this.uploads.set(upload.id, upload);
-					});
-				});
-				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
-				resolve();
-			};
-
-			const blobRequest = blobStore.getAll();
-
-			blobRequest.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.uploadBlobs.set(item.id, item);
-				});
-				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
-				resolve();
-			};
-		});
-	}
-
-	getUpload(uploadId) {
-		return this.uploads.get(uploadId);
-	}
-
-	clearField(fieldId) {
-		let uploads = Array.from(this.fields.uploads);
-		uploads.forEach(upload => {
-			this.uploads.delete(upload);
-		});
-		this.fields.delete(fieldId);
-		if (this.db) {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-			tx.objectStore('fieldStates').delete(fieldId);
-			uploads.forEach(upload => {
-				tx.objectStore('uploadBlobs').delete(upload);
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
 			});
 		}
 	}
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
 
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.uploads.forEach(upload => {
-			console.log('Attempting to set upload to status: ', status);
-			this.updateUploadStatus(upload, status);
-		});
-
-		// Update UI based on status
-		const container = field.ui.field.field;
-		if (container) {
-			container.dataset.uploadStatus = status;
-
-			// Show/hide relevant UI elements
-			const submitBtn = container.querySelector('.submit-uploads');
-			if (submitBtn) {
-				submitBtn.disabled = status === 'uploading' || status === 'processing';
-			}
-		}
-	}
-
-	/**
-	 * Handle successful upload completion
-	 */
-	handleUploadComplete(operation) {
-		const response = operation.response;
-		if (!response?.uploads) return;
-
-		// Map server IDs to uploads
-		response.uploads.forEach(serverUpload => {
-			const upload = this.uploads.get(serverUpload.upload_id);
-			if (upload) {
-				upload.attachmentId = serverUpload.attachment_id;
-				this.updateUploadStatus(serverUpload.upload_id, 'completed');
-				this.uploads.set(upload.id, upload);
-
-				// Clear from cache since it's now on server
-				this.clearUpload(upload.id);
-			}
-		});
-
-		// Persist updated field state
-		const fieldKey = operation.data.get('field_key');
-		if (fieldKey) {
-			this.persistFieldState(fieldKey);
-		}
-	}
-
-	/**
-	 * Clear individual upload from cache after successful server upload
-	 */
-	async clearUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		// Clean up preview URL
-		if (upload.preview?.startsWith('blob:')) {
-			URL.revokeObjectURL(upload.preview);
+	createGroup(fieldKey, groupId = null) {
+		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Field not found:', fieldKey);
+			return null;
 		}
 
-		this.persistFieldState(upload.fieldId);
-		// Remove from memory
-		this.uploads.delete(uploadId);
-		this.uploadBlobs.delete(uploadId);
-
-		// Remove from IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').delete(uploadId);
+		if (!groupId) {
+			groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 		}
-	}
 
-	/**
-	 * Store upload with DataStore integration
-	 */
-	async setUpload(fieldId, file, uploadId = null) {
-		if (!uploadId) {
-			uploadId = this.generateUploadId();
+		const groupElement = this.createGroupElement(groupId, fieldKey);
+		if (!groupElement) {
+			console.error('Failed to create group element');
+			return null;
 		}
-		const upload = {
-			id: uploadId,
-			fieldId: fieldId,
-			groupId: null,
-			originalFile: file,
-			processedFile: null,
-			status: 'received',
-			progress: { percent: 0, message: 'Received...' },
-			preview: URL.createObjectURL(file),
-			createdAt: Date.now(),
-			meta: {
-				title: '',
-				alt_text: '',
-				caption: '',
-				originalName: file.name,
-				originalType: file.type,
-				originalSize: file.size
-			},
+
+		// Store in field UI Map
+		if (!field.ui.groups) {
+			field.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		field.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (field.ui.groups.container && field.ui.groups.empty) {
+			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
+		} else if (field.ui.groups.container) {
+			field.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Create group object
+		const group = {
+			id: groupId,
+			fieldId: fieldKey,
+			element: groupElement,
+			grid: groupElement.querySelector('.item-grid.group'),
+			uploads: new Set(),
 			changes: {}
 		};
 
-		// Add to field
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			console.error(`Field ${fieldId} not found`);
-			return null;
-		}
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadId);
+		// Store group
+		this.groups.set(groupId, group);
 
-		upload.element = this.createImageElement(upload, field.type==='groupable');
-		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
+		// Initialize selection handler for this group
+		this.addGroupSelectionHandler(fieldKey, groupId);
 
-		// Store in memory
-		this.uploads.set(uploadId, upload);
-		this.updateImageUI(uploadId);
+		// Persist state
+		this.schedulePersistance(fieldKey);
 
-		// Persist to DataStore
-		await this.persistFieldState(fieldId);
-
-		return upload;
+		return group;
 	}
 
-	getFieldUploads(fieldId, stripElements) {
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			// Set unique IDs and names for form fields
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+			let field = this.fields.get(fieldId);
+			if (field.config.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				summary.textContent = field.config.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details').remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	deleteGroup(groupId, confirm = true) {
+		let group = this.groups.get(groupId);
+		if (!group) {
+			return;
+		}
+
+		let keepUploads = true;
+		if (confirm && group.uploads && group.uploads.size > 0) {
+			keepUploads = !window.confirm('Delete uploads in group?');
+		}
+
+		if (confirm && keepUploads) {
+			// Move any remaining uploads back to preview
+			if (group.uploads && group.uploads.size > 0) {
+				Array.from(group.uploads).forEach(uploadId => {
+					this.addImageToGroup(uploadId, null, false);
+				});
+			}
+		}
+
+		// Remove from groups Map
+		this.groups.delete(groupId);
+
+		// Remove DOM element
+		let groupElement = group.element;
+		if (groupElement) {
+			groupElement.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		this.schedulePersistance(group.fieldId);
+	}
+
+	addToGroup(uploadId, target = null, persist = true) {
+		let upload = this.uploads.get(uploadId);
+		if(!upload) {
+			return;
+		}
+		let field = this.fields.get(upload.fieldId);
+		if (!field) {
+			return;
+		}
+
+		//Already in the Preview Grid, or already in the group we're moving to
+		if ((!target && upload.location === field.ui.preview) || target === upload.location) {
+			return;
+		}
+
+		// Remove from previous location
+		if (upload.location) {
+			let groupId = upload.location.dataset.groupId;
+			if (groupId) {
+				let group = this.groups.get(groupId);
+				if (group && group.uploads) {
+					group.uploads.delete(uploadId);
+
+					if (group.uploads.size === 0) {
+						this.deleteGroup(groupId);
+					}
+				}
+			}
+		}
+
+		const checkbox = upload.element.querySelector('[name*="select-item"]');
+		if (checkbox) {
+			checkbox.checked = false;
+		}
+
+		let featured = upload.element.querySelector('[name="featured"]');
+		featured.hidden = !target;
+
+
+		//If no target, it's going to the preview grid
+		if (!target) {
+			target = field.ui.preview;
+			upload.groupId = null;
+		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
+			// It's a group target
+			let groupId = target.dataset.groupId;
+			featured.name = groupId+'_'+featured.name;
+			let group = this.groups.get(groupId);
+			if (!group) {
+				group = this.createGroup(upload.fieldId);
+				target = group.grid;
+				groupId = group.id;
+			}
+			if (group) {
+				group.uploads.add(uploadId);
+				upload.groupId = groupId;
+			}
+
+		}
+
+		upload.location = target;
+		target.append(upload.element);
+
+		if (persist) {
+			this.schedulePersistance(field.id);
+		}
+	}
+
+	removeFromGroup(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		// Remove from current group if in one
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group?.uploads) {
+				group.uploads.delete(uploadId);
+
+				// Delete empty group
+				if (group.uploads.size === 0) {
+					this.deleteGroup(upload.groupId, false);
+				}
+			}
+			upload.groupId = null;
+		}
+
+		// Move back to preview
+		if (field.ui?.preview) {
+			field.ui.preview.appendChild(upload.element);
+			upload.location = field.ui.preview;
+		}
+
+		// Hide featured radio
+		const featured = upload.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = true;
+			featured.checked = false;
+		}
+	}
+
+	removeUpload(fieldId, uploadId) {
 		const field = this.fields.get(fieldId);
-		console.log('Got field uploads: ', field);
-		if (!field?.uploads) return [];
+		const upload = this.uploads.get(uploadId);
+
+		if (!field || !upload) return;
+
+		// Remove from field
+		field.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group && group.uploads) {
+				group.uploads.delete(uploadId);
+
+				if (group.uploads.size === 0) {
+					this.removeGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		upload.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state after removal
+		this.updateFieldState(fieldId);
+
+		// Update UI
+		this.maybeLockUploads(fieldId);
+		const handler = this.selectionHandlers.get(field.id);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	/*******************************************************************************
+	 * STATE MANAGEMENT
+	 *******************************************************************************/
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			1000
+		);
+	}
+
+	async persistFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Convert Sets to Arrays for storage
+		const fieldData = {
+			...field,
+			id: fieldId, // Use as primary key
+			fieldId: fieldId,
+			uploads: Array.from(field.uploads || []).map(uploadId => {
+				return this.uploads.get(uploadId);;
+			}),
+			groups: Array.from(this.groups.entries())
+				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
+				.map(([id, data]) => ({
+					id: data.id,
+					uploads: Array.from(data.uploads),
+					changes: data.changes || {}
+				})),
+
+			// Context for restoration
+			context: {
+				url: this.normalizeUrl(window.location.href),
+				fullUrl: window.location.href,
+				modalType: this.getModalType(field),
+				formId: field.formId,
+				fieldSelector: `.field.upload[data-field="${field.config.name}"]`
+			},
+			timestamp: Date.now()
+		};
+
+		// Save to store
+		await this.fieldStore.save(fieldData);
+	}
+	normalizeUrl(url) {
+		try {
+			const urlObj = new URL(url);
+			// Return just the origin + pathname (no query string or hash)
+			return urlObj.origin + urlObj.pathname;
+		} catch (e) {
+			return url;
+		}
+	}
+
+	/**
+	 * Get uploads for a field, optionally cleaned for storage
+	 * @param {string} fieldId
+	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
+	 * @returns {Array}
+	 */
+	getFieldUploads(fieldId, clean = false) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.uploads) return [];
 
 		return Array.from(field.uploads)
-			.map(id => {
-				let upload = this.uploads.get(id);
+			.map(uploadId => {
+				const upload = this.uploads.get(uploadId);
 				if (!upload) return null;
-				if (stripElements) {
-					// Create a clean copy without DOM references
-					const { element, ui, ...cleanUpload } = upload;
-					upload = cleanUpload;
+
+				if (clean) {
+					// Return cleaned version without DOM references or blob URLs
+					return {
+						id: upload.id,
+						fieldId: upload.fieldId,
+						status: upload.status,
+						// DON'T include preview (blob URL)
+						// DON'T include originalFile or processedFile (in blob storage)
+						attachmentId: upload.attachmentId,
+						operationId: upload.operationId,
+						groupId: upload.groupId || null,
+						changes: upload.changes || {}, // ← ADD: Include changes
+						meta: {
+							originalName: upload.meta?.originalName || upload.originalFile?.name,
+							size: upload.meta?.size || upload.originalFile?.size,
+							type: upload.meta?.type || upload.originalFile?.type,
+							title: upload.meta?.title,
+							alt: upload.meta?.alt,
+							caption: upload.meta?.caption
+						}
+					};
 				}
+
+				// Return full upload object
 				return upload;
 			})
 			.filter(Boolean);
 	}
 
-	/**
-	 * Persist upload to DataStore
-	 */
-	async persistFieldState(fieldId) {
-		if (!this.db) return;
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Create clean field config without UI references
-		const { ui, container, dropZone, previewGrid, selectAll, selectActions, selectInfo, selectCount, groupDisplay, ...cleanConfig } = field;
-
-		const fieldState = {
-			fieldId: fieldId,
-			timestamp: Date.now(),
-
-			config: {
-				key: cleanConfig.key,
-				id: cleanConfig.id,
-				name: cleanConfig.name,
-				type: cleanConfig.type,
-				content: cleanConfig.content,
-				itemID: cleanConfig.itemID,
-				context: cleanConfig.context,
-				mode: cleanConfig.mode,
-				maxFiles: cleanConfig.maxFiles,
-				multiple: cleanConfig.multiple
-			},
-
-			// Recovery context
-			context: {
-				url: window.location.href,
-				modalType: this.getModalType(field),
-				formId: field.formId
-			},
-
-			// Uploads with their group associations (cleaned)
-			uploads: this.getFieldUploads(fieldId, true),
-
-			// Groups structure (ensure these are also cleaned)
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					meta: data.meta,
-					changes: data.changes
-				}))
-		};
-
-		const tx = this.db.transaction(['fieldStates'], 'readwrite');
-		await tx.objectStore('fieldStates').put(fieldState);
-	}
-	/*******************************************************************************
-	 RESTORE FUNCTIONALITY
-	*******************************************************************************/
-	async checkPendingUploads() {
+	async checkForStoredUploads() {
 		if (!this.db) return;
 
 		const tx = this.db.transaction(['fieldStates'], 'readonly');
@@ -2446,12 +2362,29 @@
 			request.onsuccess = () => resolve(request.result);
 		});
 
+		//
+		// allFieldStates.forEach(field => {
+		// 	console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
+		// 	field.uploads.forEach((upload, idx) => {
+		// 		console.log(`  Upload ${idx}:`, {
+		// 			id: upload.id,
+		// 			status: upload.status,
+		// 			operationId: upload.operationId,
+		// 			hasOperationId: !!upload.operationId
+		// 		});
+		// 	});
+		// });
+
 		// Filter for pending uploads (not yet sent to server)
 		const pendingFields = allFieldStates.filter(field =>
 			field.uploads.some(upload =>
-				upload.status === 'processing' ||
-				upload.status === 'processed' ||
-				upload.status === 'pending'
+				// If no operationId, it hasn't been sent to server yet
+				!upload.operationId &&
+				// And it's been processed locally
+				(upload.status === 'completed' ||
+					upload.status === 'processed' ||
+					upload.status === 'local_processing' ||
+					upload.status === 'processed-original')
 			)
 		);
 
@@ -2461,132 +2394,359 @@
 		this.showRecoveryNotification(pendingFields);
 	}
 
-	showRecoveryNotification(pendingFields) {
-		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
 
-		let notification = window.getTemplate('restoreNotification');
-		[
-			notification.querySelector('.restore-details').textContent,
-		] = [
-			`${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`
-		];
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
+		if (selectedUploads.length === 0) {
+			return;
+		}
+		await this.restoreSelectedUploads(selectedUploads);
 
-		pendingFields.forEach(field => {
-			console.log(field);
-			let template = window.getTemplate('restoreField');
-			field.uploads.forEach(upload => {
-				let uploadItem = window.getTemplate('restoreItem');
-				[
-					uploadItem.querySelector('img').src
-				] = [
-					upload.preview
-				];
-				template.append(uploadItem);
-			});
-			notification.append(template);
-		});
-
-
-		// Add event handlers
-		notification.querySelector('[data-action="restore"]').addEventListener('click', () => {
-			this.restoreFieldStates(pendingFields);
-			notification.remove();
-		});
-
-		notification.querySelector('[data-action="dismiss"]').addEventListener('click', () => {
-			this.notifications.add('Uploads saved for later restoration', 'info');
-			notification.remove();
-		});
-
-		notification.querySelector('[data-action="clear"]').addEventListener('click', () => {
-			this.clearCachedUploads(pendingFields);
-			notification.remove();
-		});
-
-		document.body.appendChild(notification);
+		this.cleanupRestore();
 	}
 
-	async restoreFieldStates(fieldStates) {
-		// Group by URL
-		const byUrl = new Map();
-		fieldStates.forEach(field => {
-			if (!byUrl.has(field.context.url)) {
-				byUrl.set(field.context.url, []);
+	getSelectedRestorationUploads(notificationEl) {
+		let selected = [];
+		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
 			}
-			byUrl.get(field.context.url).push(field);
 		});
 
-		// If all on current page, restore directly
-		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
-			for (const fieldState of fieldStates) {
+		return selected;
+	}
+
+	handleGroupMetaChange(input) {
+		let group = this.getGroupFromElement(input);
+		if (!group) {
+			return;
+		}
+		if (!Object.hasOwn(group, 'changes')) {
+			group.changes = {};
+		}
+		let name = input.name;
+		if (name.includes('group')) {
+			let replace = group.id+'_';
+			let replace2 = group.id+'[';
+			name = name.replace(replace, '').replace(replace2,'').replace(']', '');
+		}
+		group.changes[`${name}`] = input.value;
+		this.groups.set(group.id, group);
+		this.schedulePersistance(group.fieldId);
+	}
+
+
+	/*******************************************************************************
+	 * RESTORING UPLOADS
+	 *******************************************************************************/
+	async showRecoveryNotification(pendingFields) {
+		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+		const totalGroups = pendingFields.reduce((sum, field) =>
+			sum + (field.groups?.length || 0), 0);
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			console.error('Restore notification template not found');
+			return;
+		}
+
+		// Build appropriate message
+		let message;
+		if (totalGroups > 0) {
+			let group = totalGroups > 1 ? 'groups' : 'group';
+			let upload = totalUploads > 1 ? 'uploads' : 'upload';
+			message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+		} else {
+			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+		}
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) {
+			detailsEl.textContent = message;
+		}
+
+		// Build the restoration preview
+		for (const field of pendingFields) {
+			let fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			// Set field name/title
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) {
+				titleEl.textContent = field.config.name || 'Unnamed Field';
+			}
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			// Process each upload
+			for (const upload of field.uploads) {
+
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const blobData = await this.uploadStore.getBlob(upload.id);
+
+
+				if (blobData) {
+					try {
+						// Create new blob URL from stored data
+						const blob = new Blob([blobData.data], { type: blobData.type });
+						const previewUrl = this.createPreviewUrl(blob);
+
+						let [
+							featured,
+							img,
+							video,
+							preview,
+							details
+						] = [
+							uploadItem.querySelector('[name="featured"]'),
+							uploadItem.querySelector('img'),
+							uploadItem.querySelector('video'),
+							uploadItem.querySelector('label > span'),
+							uploadItem.querySelector('details')
+						];
+
+						uploadItem.dataset.uploadId = upload.id;
+
+
+						uploadItem.dataset.fieldId = field.id;
+
+						let subtype = this.getSubtypeFromMime(blobData.type);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									upload.originalFile?.name ?? upload.meta?.originalName?? ''
+								];
+								video.remove();
+								preview.remove();
+								break;
+							case 'video':
+								video.src = previewUrl;
+								img.remove();
+								preview.remove();
+								break;
+							case 'document':
+								let extension = '';
+								let icon;
+								switch (extension) {
+									case 'pdf':
+										icon = window.getIcon('file-pdf');
+										break;
+									case 'csv':
+										icon = window.getIcon('file-csv');
+										break;
+									case 'doc':
+										icon = window.getIcon('file-doc');
+										break;
+									case 'txt':
+										icon = window.getIcon('file-txt');
+										break;
+									case 'xls':
+										icon = window.getIcon('file-xls');
+										break;
+									default:
+										icon = window.getIcon('file');
+										break;
+								}
+
+								preview.innerText = upload.originalFile.name;
+								preview.prepend(icon);
+								img.remove();
+								video.remove();
+								break;
+						}
+
+						// Store URL for cleanup later
+						uploadItem.dataset.previewUrl = previewUrl;
+					} catch (error) {
+						console.warn('Failed to create preview for upload:', upload.id, error);
+					}
+				}
+
+				// Set upload metadata
+				const nameEl = uploadItem.querySelector('summary span');
+				if (nameEl) {
+					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+				}
+
+				const metaEl = uploadItem.querySelector('details');
+				if (metaEl && upload.meta) {
+					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+				}
+
+				// Update input IDs safely
+				uploadItem.querySelectorAll('input').forEach(input => {
+					let id = input.id;
+					if (id) {
+						let newId = id + upload.id;
+						let label = input.parentNode.querySelector(`label[for="${id}"]`);
+						input.id = newId;
+						if (label) {
+							label.htmlFor = newId;
+						}
+					}
+				});
+
+				if (itemGrid) {
+					itemGrid.appendChild(uploadItem);
+				}
+			}
+
+			notification.querySelector('.wrap').appendChild(itemGrid);
+		}
+
+		document.querySelector('.field.upload').appendChild(notification);
+		notification = document.querySelector('dialog.restore-uploads');
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			ui: {
+				selectAll: notification.querySelector('#select-all-restore'),
+				count: notification.querySelector('.selection-count'),
+			},
+		});
+
+		this.restoreModal.handleOpen();
+
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		// Group by field
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		// Get full field states from IndexedDB
+		if (!this.db) {
+			// this.notifications.add('Cannot restore: Database not available', 'error');
+			return;
+		}
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const store = tx.objectStore('fieldStates');
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const request = store.get(fieldId);
+			const fieldState = await new Promise(resolve => {
+				request.onsuccess = () => resolve(request.result);
+				request.onerror = () => resolve(null);
+			});
+
+			if (fieldState) {
+				// Filter to only selected uploads
+				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
 				await this.restoreField(fieldState);
 			}
-			this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
-		} else {
-			// Store intent to restore and navigate
-			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
-
-			// Navigate to first URL
-			const firstUrl = byUrl.keys().next().value;
-			if (window.location.href !== firstUrl) {
-				window.location.href = firstUrl;
-			}
 		}
+
+		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
 	}
 
 	async restoreField(fieldState) {
-		const { config, context, uploads, groups } = fieldState;
+		const { config, context, uploads, groups, id } = fieldState;  // ← Use 'id'
 
 		// If in a modal, open it first
 		if (context.modalType) {
 			await this.openModalForRestore(context);
 		}
 
-		// Find the field element
-		const fieldElement = document.querySelector(
-			`.field.image[data-field-id="${config.id}"]`
-		);
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
 
 		if (!fieldElement) {
-			console.warn(`Field ${config.id} not found for restoration`);
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
 			return;
 		}
 
-		// Register the field
-		const fieldKey = this.registerUploader(fieldElement, config);
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fields.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement, config);
+		}
+
 		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// Merge saved state back into field
+		field.state = fieldState.state || 'ready';
+
+		// Rebuild UI references
+		field.ui = this.buildFieldUI(fieldElement);
+
+		if (field.ui.groups?.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		// Restore groups
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(fieldKey, groups);
+		}
 
 		// Restore uploads
 		for (const uploadData of uploads) {
 			await this.restoreUpload(field, uploadData);
 		}
 
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(field, groups, uploads);
-		}
-
 		// Update UI
+		this.updateFieldState(fieldKey);
 		this.maybeLockUploads(fieldKey);
 
 		// Queue for upload if needed
-		if (config.mode === 'direct') {
+		if (config.mode === 'direct' && config.destination !== 'post_group') {
 			await this.queueUpload(fieldKey);
 		}
 	}
 
 	async restoreUpload(field, uploadData) {
-		// Reconstruct the file from blob data
-		const blobData = await this.getBlobData(uploadData.id);
-		let file = null;
+		// Try to get blob data from IndexedDB
+		const blobData = await this.uploadStore.getBlob(uploadData.id);
 
 		if (blobData) {
-			file = new File(
-				[blobData.data],
-				blobData.name,
-				{ type: blobData.type, lastModified: blobData.lastModified }
-			);
+			const file = blobData.data instanceof File
+				? blobData.data
+				: new File(
+					[blobData.data],
+					blobData.name,
+					{ type: blobData.type, lastModified: blobData.lastModified }
+				);
+
+			uploadData.originalFile = file;
 			uploadData.processedFile = file;
+			uploadData.preview = this.createPreviewUrl(file);
+		} else {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return; // Skip this upload if we can't restore the file
 		}
 
 		// Add to field
@@ -2594,61 +2754,59 @@
 		field.uploads.add(uploadData.id);
 
 		// Recreate DOM element
-		uploadData.element = this.createImageElement(uploadData, field.type === 'groupable');
+		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
+		uploadData.element = this.createUploadElement({
+			...uploadData,
+			subtype: subtype
+		}, field.config.destination === 'post_group');
 
 		// Restore to correct location
-		const location = uploadData.groupId
-			? field.ui.groups.groups.get(uploadData.groupId)
-			: field.ui.field.preview;
+		let location;
+		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
+			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
+		} else {
+			location = field.ui.preview;
+		}
 
 		if (location) {
-			location.append(uploadData.element);
+			location.appendChild(uploadData.element);
 			uploadData.location = location;
 		}
 
 		// Store in memory
 		this.uploads.set(uploadData.id, uploadData);
-	}
-
-	async restoreGroups(field, groups, uploads) {
-		for (const groupData of groups) {
-			// Create group element
-			const groupElement = this.createGroupElement(groupData.id, field.key);
-			field.ui.groups.groups.set(groupData.id, groupElement);
-			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-
-			// Create group Set
-			const groupSet = new Set(groupData.uploadIds);
-			this.groups.set(groupData.id, groupSet);
-
-			// Restore group metadata
-			if (groupData.meta) {
-				this.groupsMeta.set(groupData.id, groupData.meta);
-				// TODO: Populate meta fields in groupElement
+		if (uploadData.groupId) {
+			const group = this.groups.get(uploadData.groupId);
+			if (group && group.uploads) {
+				group.uploads.add(uploadData.id);
 			}
-
-			// Move uploads to group
-			groupData.uploadIds.forEach(uploadId => {
-				const upload = uploads.find(u => u.id === uploadId);
-				if (upload && upload.element) {
-					groupElement.querySelector('.item-grid').append(upload.element);
-					upload.location = groupElement.querySelector('.item-grid');
-					upload.groupId = groupData.id;
-				}
-			});
 		}
 	}
 
-	async getBlobData(uploadId) {
-		if (!this.db) return null;
+	async restoreGroups(fieldKey, groups) {
+		for (const groupData of groups) {
+			// Use createGroup which properly initializes EVERYTHING including selection handlers
+			const group = this.createGroup(fieldKey, groupData.id);
 
-		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
-		const request = tx.objectStore('uploadBlobs').get(uploadId);
+			if (group) {
+				// Update the group metadata from saved state
+				if (groupData.meta) {
+					group.meta = { ...groupData.meta };
+				}
+				if (groupData.changes) {
+					group.changes = { ...groupData.changes };
+				}
 
-		return new Promise(resolve => {
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
+
+				// If you saved group titles, restore them
+				if (groupData.title) {
+					const titleInput = group.element.querySelector('[name*="post_title"]');
+					if (titleInput) {
+						titleInput.value = groupData.title;
+					}
+				}
+			}
+		}
 	}
 
 	async openModalForRestore(context) {
@@ -2678,370 +2836,65 @@
 		}
 	}
 
-	async clearCachedUploads(fieldStates) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-
-		for (const field of fieldStates) {
-			// Delete field state
-			await tx.objectStore('fieldStates').delete(field.fieldId);
-
-			// Delete all associated blobs
-			for (const upload of field.uploads) {
-				await tx.objectStore('uploadBlobs').delete(upload.id);
-
-				// Clean up preview URLs
-				if (upload.preview?.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-			}
-		}
-
-		this.notifications.add('Cached uploads cleared', 'info');
-	}
-
-// Check for restoration intent on page load
-	async checkRestorationIntent() {
-		const restoreData = sessionStorage.getItem('jvb_restore_uploads');
-		if (!restoreData) return;
-
-		const fieldStates = JSON.parse(restoreData);
-		const currentUrlFields = fieldStates.filter(f => f.context.url === window.location.href);
-
-		if (currentUrlFields.length > 0) {
-			for (const fieldState of currentUrlFields) {
-				await this.restoreField(fieldState);
-			}
-
-			// Remove restored fields from session storage
-			const remaining = fieldStates.filter(f => f.context.url !== window.location.href);
-			if (remaining.length > 0) {
-				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(remaining));
-			} else {
-				sessionStorage.removeItem('jvb_restore_uploads');
-			}
-
-			this.notifications.add(`Restored ${currentUrlFields.length} field(s)`, 'success');
-		}
-	}
 	/*******************************************************************************
-	 GROUP FUNCTIONALITY
-	 Includes selection, dragging, and grouping logic
-	*******************************************************************************/
-	/**
-	 *
-	 * @param {string} uploadId as defined by setUpload
-	 * @param {HTMLElement|null} target The target location
-	 * @param {boolean} persist whethet to cache this change
-	 */
-	addImageToGroup(uploadId, target = null, persist = true) {
-		let upload = this.getUpload(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-		//Already in the Preview Grid, or already in the group we're moving to
-		if (!target && upload.location === field.ui.field.preview || target === upload.location) {
-			return;
-		}
+	 INDEXEDDB CACHE FUNCTIONALITY
+	 *******************************************************************************/
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
 
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group) {
-					group.delete(uploadId);
-					if (group.size === 0) {
-						this.removeGroup(groupId);
-					}
-				}
-			}
+				break;
+			case 'item-saved':
+				console.log(`Field state saved: ${data.key}`);
+				break;
 		}
+	}
 
-		upload.element.querySelector('[name="featured"]').hidden = !target;
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.field.preview;
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.checkForStoredUploads();
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+	async saveUpload(upload) {
+		// Handle blob data separately
+		if (upload.file instanceof File || upload.file instanceof Blob) {
+			await this.uploadStore.saveBlob(upload.id, upload.file);
+			// Don't store the file in the main store
+			const { file, originalFile, ...cleanUpload } = upload;
+			await this.uploadStore.save(cleanUpload);
 		} else {
-			let groupId = target.dataset.groupId;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
+			await this.uploadStore.save(upload);
+		}
+	}
+
+	async loadFields() {
+		// Load all field states from the store
+		const fields = await this.fieldStore.getAll();
+
+		fields.forEach(field => {
+			// Reconstruct upload sets
+			if (field.uploads && Array.isArray(field.uploads)) {
+				field.uploads = new Set(field.uploads.map(u => u.id));
 			}
-			group.uploads.add(uploadId);
-		}
-
-
-		target.append(upload.element);
-		if (persist) {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	addSelectionToGroup(target) {
-		let field = this.getFieldFromElement(target);
-		if (!field) {
-			return;
-		}
-		if (this.selected.get(field.key).size === 0) {
-			return;
-		}
-		let group = this.getGroupFromElement(target);
-		if (!group) {
-			group = this.createGroup(field.key);
-		}
-
-		Array.from(this.selected).forEach(uploadId => {
-			this.addImageToGroup(uploadId, group.grid, false);
-		});
-
-		this.persistFieldState(group.fieldId);
-	}
-
-
-	/**
-	 * Remove an empty group from the field
-	 * @param {string} groupId - The group to remove
-	 * @param {boolean} confirm - ask for confirmation
-	 */
-	removeGroup(groupId, confirm = false) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-		if (confirm) {
-			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
-				return;
-			}
-		}
-
-		if (group.uploads.size > 0) {
-			Array.from(group.uploads).forEach(upload => {
-				this.addImageToGroup(upload);
-			});
-		}
-
-		let groupElement = group.element;
-		// Remove DOM element
-		if (groupElement) {
-			window.fade(groupElement, false);
-			this.a11y.announce('Empty group removed');
-		}
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	createGroup(fieldId) {
-		let field = this.fields.get(fieldId);
-		if(!field) {
-			return;
-		}
-		let index = field.ui.groups.size;
-		field.ui.groups.groups.set(`group-${index}`, this.createGroupElement(`group-${index}`, fieldId));
-		let group = field.ui.groups.groups.get(`group-${index}`);
-		field.ui.groups.container.insertAfter(group, field.ui.groups.empty);
-		let groupConfig = {
-			fieldId: field.key,
-			id: `group-${index}`,
-			element: group,
-			grid: group.querySelector('.item-grid'),
-			uploads: new Set(),
-			meta: {
-				post_title: '',
-				post_excerpt: '',
-			},
-			changes: {},
-		};
-		this.groups.set(`group-${index}`, groupConfig);
-		return groupConfig;
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let post = window.getTemplate('imageGroup');
-		if (!post) {
-			return;
-		}
-		post.dataset.groupId = groupId;
-		post.dataset.fieldId = fieldId;
-		let fields = window.getTemplate('groupMetaData');
-		post.querySelector('.fields')?.append(fields);
-
-		return post;
-	}
-
-	/**
-	 * Handle select all functionality
-	 */
-	handleSelectAll(element, checked = null) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return;
-
-		// Use element's checked state if not provided
-		if (checked === null) {
-			checked = element.checked;
-		}
-
-		const target = field.previewGrid;
-		const previewItems = target.querySelectorAll('[data-upload-id]') || [];
-
-		previewItems.forEach(item => {
-			const checkbox = item.querySelector('[name*="select-item"]');
-			if (checkbox) {
-				checkbox.checked = checked;
-			}
-		});
-
-		this.updateSelectAll(element);
-		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-
-		// Clear last clicked since we're selecting/deselecting all
-		this.lastClickedUpload = null;
-	}
-
-	updateSelection(e) {
-		let field = this.getFieldFromElement(e.target);
-		let upload = this.getUploadFromElement(e.target);
-		if (!field || ! upload) {
-			console.log('No field or upload found...');
-			return;
-		}
-
-		this.lastClickedUpload = upload.id;
-		let action = e.target.checked;
-		if (action) {
-			this.selected.get(field.key).add(upload.id);
-		} else {
-			this.selected.get(field.key).delete(upload.id);
-		}
-	}
-
-	updateSelectAll(element) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return;
-
-		const selected = this.getSelectedUploads(element);
-		if (selected.length > 0 ) {
-			field.selectActions.hidden = false;
-			field.selectInfo.hidden = false;
-			field.selectCount.textContent = `${selected.length}`;
-		} else {
-			field.selectActions.hidden = true;
-			field.selectInfo.hidden = true;
-		}
-		let selectAll = selected.length === field.container.querySelectorAll('.item-grid.preview .upload-item').length;
-		field.selectAll.checked = selectAll;
-		field.selectAll.nextElementSibling.textContent = (selectAll) ? 'Clear Selection' : 'Select All';
-	}
-
-	getSelectedUploads(element) {
-		let field = this.getFieldFromElement(element);
-		if (!field) {
-			return;
-		}
-		return Array.from(this.selected.get(field.key)??[]);
-	}
-
-
-	handleRangeSelection(currentElement, event) {
-		const field = this.getFieldFromElement(currentElement);
-		if (!field) return;
-
-		const currentUploadId = this.getUploadIdFromElement(currentElement);
-		if (!currentUploadId || !this.lastClickedUpload) return;
-
-		// Get all upload items in the preview grid
-		const container = currentElement.closest('.item-grid');
-		const allItems = Array.from(container.querySelectorAll('[data-upload-id]'));
-
-		// Find indices of first and current items
-		const firstIndex = allItems.findIndex(item =>
-			item.dataset.uploadId === this.lastClickedUpload
-		);
-		const currentIndex = allItems.findIndex(item =>
-			item.dataset.uploadId === currentUploadId
-		);
-
-		if (firstIndex === -1 || currentIndex === -1) return;
-
-		// Determine range (handle both directions)
-		const startIndex = Math.min(firstIndex, currentIndex);
-		const endIndex = Math.max(firstIndex, currentIndex);
-
-		// Select all items in range (including the clicked one!)
-		for (let i = startIndex; i <= endIndex; i++) {
-			const item = allItems[i];
-			const checkbox = item.querySelector('[name*="select-item"]');
-			if (checkbox) {
-				checkbox.checked = true;
-			}
-		}
-
-		currentElement.checked = true;
-		// Update selection UI
-		this.updateSelectAll(currentElement);
-
-		// Announce the range selection
-		const selectedCount = endIndex - startIndex + 1;
-		this.a11y.announce(`Selected ${selectedCount} items in range`);
-
-		// Update the last clicked item to the current one
-		this.lastClickedUpload = currentUploadId;
-	}
-
-	removeSelection(button) {
-		let fieldId = this.getFieldIdFromElement(button);
-
-		const selectedUploads = this.getSelectedUploads(button);
-		if (selectedUploads.length === 0) {
-			this.notify('No uploads selected', 'warning');
-			return;
-		}
-
-		selectedUploads.forEach(upload => {
-			this.removeUpload(fieldId, upload);
+			this.fields.set(field.fieldId, field);
 		});
 	}
 
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			group?.delete(uploadId);
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		this.updateSelectAll(field.ui.field.field);
-
-		this.a11y.announce('Upload removed');
+	async loadUploads() {
+		const uploads = await this.uploadStore.getAll();
+		uploads.forEach(upload => {
+			this.uploads.set(upload.id, upload);
+		});
 	}
 
 	/**************************************************************************
-	 META
-	 Handled separately, in case it is edited in the middle of processing images
-	**************************************************************************/
-
-	/**************************************************************************
 	 SUBSCRIBERS
-	**************************************************************************/
+	 **************************************************************************/
 	/**
 	 * Event system
 	 */
@@ -3053,35 +2906,111 @@
 	notify(event, data) {
 		this.subscribers.forEach(cb => cb(event, data));
 	}
+	/*******************************************************************************
+	 * CLEANUP
+	 *******************************************************************************/
 
-	handleBeforeUnload(e) {
-		// Check for any uploads in processing or pending state
-		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
-			upload.status === 'processing' ||
-			upload.status === 'pending' ||
-			upload.status === 'uploading'
-		);
+	destroy() {
+		// Remove core listeners
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		document.removeEventListener('dragenter', this.dragEnterHandler);
+		document.removeEventListener('dragleave', this.dragLeaveHandler);
+		document.removeEventListener('dragover', this.dragOverHandler);
+		document.removeEventListener('drop', this.dropHandler);
 
-		if (unsavedUploads.length > 0) {
-			const message = 'You have uploads in progress. Are you sure you want to leave?';
-			e.preventDefault();
-			e.returnValue = message;
-			return message;
+		// Destroy drag controller
+		if (this.dragController) {
+			this.dragController.destroy();
 		}
-	}
-	/**************************************************************************
-	 CLEANUP
-	**************************************************************************/
-	cleanup() {
-		this.clearListeners();
-		if (this.hasGroups) {
-			this.clearGroupListeners();
-		}
-		this.compressionWorker = null;
+
+		// Destroy selection handlers
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		// Clear data
+		this.fields.clear();
+		this.uploads.clear();
+		this.groups.clear();
+		this.selected.clear();
 		this.subscribers.clear();
 	}
+
+	cleanupRestore() {
+		this.restoreModal.handleClose();
+		this.restoreSelection.destroy();
+		this.restoreSelection = null;
+		this.restoreModal.destroy();
+		this.restoreModal.modal.remove();
+		this.restoreModal = null;
+	}
+
+	async cleanupStoredUploads() {
+		this.fieldStore.clear();
+		this.uploadStore.clear();
+	}
+
+	/**
+	 * Clear all uploads for a field and cleanup resources
+	 */
+	async clearField(fieldId) {
+		// Clear from stores
+		await this.fieldStore.delete(fieldId);
+
+		// Clear related uploads
+		const field = this.fields.get(fieldId);
+		if (field?.uploads) {
+			for (const uploadId of field.uploads) {
+				await this.uploadStore.delete(uploadId);
+			}
+		}
+
+		// Clear from memory
+		this.fields.delete(fieldId);
+	}
+
+	async clearUpload(uploadId, persist = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		// Clean up preview URL using helper
+		this.revokePreviewUrl(upload.preview);
+
+		// Clean up element preview URL
+		if (upload.element) {
+			const previewUrl = upload.element.dataset.previewUrl;
+			this.revokePreviewUrl(previewUrl);
+			delete upload.element.dataset.previewUrl;
+		}
+
+		if (persist) {
+			await this.schedulePersistance(upload.fieldId);
+		}
+
+		// Remove from memory
+		this.uploads.delete(uploadId);
+
+		// Remove from IndexedDB
+		this.uploadStore.delete(uploadId);
+		this.uploadStore.delete(uploadId, 'blobs');
+	}
+	cleanupAllPreviewUrls() {
+		if (this.previewUrls) {
+			this.previewUrls.forEach(url => {
+				try {
+					URL.revokeObjectURL(url);
+				} catch (e) {
+					// Ignore errors during cleanup
+				}
+			});
+			this.previewUrls.clear();
+		}
+	}
 }
 
+// Initialize when DOM is ready
 document.addEventListener('DOMContentLoaded', () => {
 	window.jvbUploads = new UploadManager();
 });
diff --git a/assets/js/concise/UploadManagerOld.js b/assets/js/concise/UploadManagerOld.js
new file mode 100644
index 0000000..7e34eab
--- /dev/null
+++ b/assets/js/concise/UploadManagerOld.js
@@ -0,0 +1,4114 @@
+class UploadManager {
+	constructor() {
+		//Load dependencies
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+		this.notifications = window.jvbNotifications;
+
+		//Load Datastore
+		this.initDB();
+
+		//State management
+		this.fields = new Map();
+		this.uploads = new Map();
+		this.uploadBlobs = new Map();
+		this.timeouts = new Map();
+		this.selected = new Map();
+		this.dragState = {
+			isDragging: false,
+			primaryItem: null,
+			draggedItems: [],
+			isMultiDrag: false,
+			fieldId: null,
+			sourceType: null,
+			startTime: null,
+			startPosition: { x: 0, y: 0 },
+			currentPosition: { x: 0, y: 0 },
+			currentTarget: null,
+			validTarget: null,
+			dragPreview: null,
+			touchId: null,
+			touchMoved: false
+		};
+		this.hasGroups = false;
+
+		this.selectionHandlers = new Map();
+
+		//Worker
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: {
+				count: 0,
+				max: 3,
+			},
+			settings: {
+				timeout: 10000, //10 seconds per image
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+
+		//Groups!
+		this.touch = {
+			x: null,
+			y: null
+		}
+		this.hasBulkContext = document.querySelector('details.uploader')!==null;
+		this.isTouching = false;
+		this.groups = new Map();
+		this.groupsMeta = new Map();
+
+		//Notification and Subscribers
+		this.subscribers = new Set();
+
+		this.settings = {
+			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
+			maxFileSize: 5242880,
+			maxProcessingTime: 120000, // 2 minutes max for processing
+			processingCheckInterval: 5000, // Check every 5 seconds
+			smartCompression: true,
+			fieldTypes: {
+				'single': { maxFiles: 1, allowMultiple: false },
+				'gallery': { maxFiles: 20, allowMultiple: true },
+				'groupable': { maxFiles: 20, allowMultiple: true }
+			}
+		};
+
+		this.acceptedTypes = {
+			image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
+			video: ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'],
+			document: [
+				'application/pdf',
+				'application/msword',
+				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+				'text/plain',
+				'text/csv'
+			]
+		};
+
+		this.maxSizes = {
+			image: 5 * 1024 * 1024,    // 5MB
+			video: 100 * 1024 * 1024,  // 100MB
+			document: 10 * 1024 * 1024 // 10MB
+		};
+
+		this.statusMapping = {
+			'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'
+		};
+
+		this.init();
+	}
+
+	async init() {
+		this.initElements();
+		this.initListeners();
+		this.initCompressionWorker();
+		this.queue.subscribe((event, operation) => {
+			if (operation.endpoint !== 'uploads') {
+				return;
+			}
+			switch(event) {
+				case 'cancel-operation':
+					this.clearField(operation.data.get('field_key'));
+					break;
+				case 'operation-status':
+					const fieldId = operation.data?.field_key ||
+						(operation.data instanceof FormData ?
+							operation.data.get('field_key') : null);
+
+					if (fieldId) {
+						this.updateFieldStatus(fieldId, operation.status);
+					}
+					break;
+			}
+		});
+		this.scanFields();
+	}
+
+	initElements() {
+		this.selectors = {
+			field: {
+				field: '.field.upload',
+				dropZone: '.file-upload-container',
+				preview: '.item-grid.preview',
+				previewWrap: '.preview-wrap',
+				selectAll: '[type=checkbox]#select-all-uploads',
+				selectActions: '.selection-actions',
+				selectCount: '.selected .info',
+				hiddenValue: 'input[type="hidden"]',
+				progress: {
+					progress: '.progress',
+					details: '.progress .details',
+					fill: '.progress .fill',
+					count: '.progress .count'
+				},
+			},
+			item: {
+				img: 'img',
+				progress: {
+					progress: '.progress',
+					details: '.progress .details',
+					fill: '.progress .fill',
+					count: '.progress .count'
+				},
+				status: '.status',
+				select: '[name*="select-item"]',
+				actions: '.item-actions',
+				featured: '[name="featured"]',
+				meta: '.upload-meta'
+			},
+			groups: {
+				container: '.item-grid.groups',
+				display: '.group-display',
+				selectAll: '#select-all-group',
+				actions: '.selection-actions',
+				info: '.selection-controls .info',
+				count: '.selection-count',
+				group: '.upload-group',
+				empty: '.empty-group'
+			}
+		};
+		this.ui = {};
+	}
+
+	scanFields() {
+		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
+			this.registerUploader(uploader);
+		});
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} uploader
+	 * @param {object} options
+	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
+	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
+	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
+	 * @param {boolean} options.multiple Whether to allow multiple uploads
+	 * @param {number} options.itemID The post or term ID this is for.
+	 * @param {string} options.mode
+	 * @returns {string}
+	 */
+	registerUploader(uploader, options = {}) {
+		//Determine if this is for a post, term, content uploader, or option
+		let key = uploader.dataset['uploader']??this.determineKey(uploader);
+
+		uploader.dataset['uploader'] = key;
+
+		if (!this.fields.has(key)) {
+			let type = uploader.dataset.type??'single';
+
+			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
+			let config = {
+				key: key,
+				name: uploader.dataset.field,
+				ui: {},
+				type: type,
+				subtype: uploader.dataset.subtype??'image',
+				maxFiles: typeConfig.maxFiles,
+				multiple: typeConfig.allowMultiple,
+				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
+				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
+				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
+				mode: uploader.dataset.mode??'direct',
+				destination: uploader.dataset.destination ?? 'meta',
+				... options
+			};
+
+			config.ui = window.uiFromSelectors(this.selectors, uploader);
+			config.ui.groups.groups = new Map();
+
+			this.selected.set(key, new Set());
+			this.fields.set(key, config);
+			if(config.destination === 'post_group' && !this.hasGroups) {
+				this.initGroupListeners();
+			}
+			// Initialize selection handler for this field
+			this.initSelectionHandler(key, config);
+		}
+		return key;
+	}
+
+	initSelectionHandler(fieldKey) {
+		const field = this.fields.get(fieldKey);
+		if (!field) return;
+
+		// Don't reinitialize if already exists
+		if (this.selectionHandlers.has(fieldKey)) {
+			return this.selectionHandlers.get(fieldKey);
+		}
+
+		// Get the container - use preview for uploads in preview, or field for all uploads
+		const container = field.ui.field.previewWrap;
+		if (!container) {
+			console.warn('No container found for selection handler:', fieldKey);
+			return;
+		}
+
+		const handler = new window.jvbHandleSelection({
+			container: container,
+			ui: {
+				selectAll: field.ui.field.selectAll,
+				bulkControls: field.ui.field.selectActions,
+				count: field.ui.field.selectCount
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]',
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldKey, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(fieldKey, handler);
+
+		return handler;
+	}
+
+	addGroupSelectionHandler(fieldId, groupId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		const group = this.groups.get(groupId);
+		if (!group) return;
+
+		let handlerKey = fieldId+'_'+groupId;
+		// Don't reinitialize if already exists
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
+		}
+
+		// Get the container - use preview for uploads in preview, or field for all uploads
+		const container = group.element;
+		if (!container) {
+			console.warn('No container found for selection handler:', fieldKey);
+			return;
+		}
+
+		const handler = new window.jvbHandleSelection({
+			container: container,
+			ui: {
+				selectAll: container.querySelector(this.selectors.groups.selectAll),
+				bulkControls: container.querySelector(this.selectors.groups.actions),
+				count: container.querySelector(this.selectors.groups.count)
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]',
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(handlerKey, handler);
+		return handler;
+	}
+
+	removeSelectionHandler(fieldId, groupId = null) {
+		let key = fieldId;
+		if (groupId) {
+			key = key+'_'+groupId;
+		}
+		if (this.selectionHandlers.has(key)) {
+			let handler = this.selectionHandlers.get(key);
+			handler.destroy();
+			this.selectionHandlers.delete(key);
+		}
+	}
+
+	/**
+	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
+	 * @param uploader
+	 * @returns {string}
+	 */
+	determineKey(uploader) {
+		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
+		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
+		let field = uploader.dataset.field;
+		return `${content}_${itemID}_${field}`;
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} element
+	 */
+	getFieldIdFromElement(element) {
+		let field = element.closest(this.selectors.field.field);
+		if (!field) {
+			return;
+		}
+		return field.dataset.uploader??this.determineKey(field);
+	}
+
+	getFieldFromElement(element) {
+		let id = this.getFieldIdFromElement(element);
+		return (this.fields.has(id)) ? this.fields.get(id) : false;
+	}
+
+	getUploadFromElement(element) {
+		let id = this.getUploadIdFromElement(element);
+		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
+	}
+
+	getUploadIdFromElement(element) {
+		let upload = element.closest('[data-upload-id]');
+		return upload?.dataset.uploadId || null;
+	}
+
+	getGroupFromElement(element) {
+		let groupId = this.getGroupIdFromElement(element);
+		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
+	}
+	getGroupIdFromElement(element) {
+		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
+	}
+
+	getModalType(field) {
+		// Safety check for field.ui
+		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
+			return null;
+		}
+
+		const dialog = field.ui.field.field.closest('dialog');
+		if (!dialog) return null;
+
+		if (dialog.classList.contains('edit')) return 'edit';
+		if (dialog.classList.contains('create')) return 'create';
+		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
+
+		return dialog.className;
+	}
+
+	getStatusText(status) {
+		return this.statusMapping[status] || status;
+	}
+
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
+	}
+	getStatusProgress(status) {
+		switch (status) {
+			case 'local_processing':
+				return 28;
+			case 'queued':
+				return 50;
+			case 'uploading':
+				return 66;
+			case 'pending':
+				return 75;
+			case 'processing':
+				return 89;
+			case 'completed':
+				return 100;
+			default:
+				return 0;
+		}
+	}
+
+	/******************************************************************************
+	 LISTENERS
+	******************************************************************************/
+	initListeners() {
+		this.clickHandler 		= this.handleClick.bind(this);
+		this.changeHandler 		= this.handleChange.bind(this);
+
+		if (this.hasBulkContext) {
+			this.pasteHandler 		= this.handlePaste.bind(this);
+			document.addEventListener('paste', this.pasteHandler);
+		}
+
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
+	}
+	clearListeners() {
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		if (this.hasBulkContext) {
+			document.removeEventListener('paste', this.pasteHandler);
+		}
+	}
+
+	initGroupListeners() {
+		this.hasGroups = true;
+
+		this.dragStartHandler 	= this.handleDragStart.bind(this);
+		this.dragEndHandler 	= this.handleDragEnd.bind(this);
+		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
+		this.dragOverHandler 	= this.handleDragOver.bind(this);
+		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
+		this.dropHandler 		= this.handleDrop.bind(this);
+
+		this.touchStartHandler 	= this.handleTouchStart.bind(this);
+		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
+		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
+		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
+
+		document.addEventListener('dragstart', this.dragStartHandler);
+		document.addEventListener('dragend', this.dragEndHandler);
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('drop', this.dropHandler);
+
+		document.addEventListener('touchstart', this.touchStartHandler, { passive: false });
+		document.addEventListener('touchmove', this.touchMoveHandler, { passive: false });
+		document.addEventListener('touchend', this.touchEndHandler, { passive: false });
+		document.addEventListener('touchcancel', this.touchCancelHandler, { passive: false });
+
+		document.addEventListener('input', (e) => {
+			if (e.target.matches('.fields.group input, .fields.group textarea')) {
+				this.handleGroupMetadataChange(e);
+			}
+		});
+	}
+	handleGroupMetadataChange(e) {
+		if (!e.target.closest('.fields.group')) return;
+
+		const groupElement = e.target.closest('[data-group-id]');
+		if (!groupElement) return;
+
+		const fieldId = groupElement.dataset.fieldId;
+		this.persistFieldState(fieldId);
+	}
+	clearGroupListeners() {
+		document.removeEventListener('dragstart', this.dragStartHandler);
+		document.removeEventListener('dragend', this.dragEndHandler);
+		document.removeEventListener('dragenter', this.dragEnterHandler);
+		document.removeEventListener('dragover', this.dragOverHandler);
+		document.removeEventListener('dragleave', this.dragLeaveHandler);
+		document.removeEventListener('drop', this.dropHandler);
+
+		document.removeEventListener('touchstart', this.touchStartHandler, { passive: false });
+		document.removeEventListener('touchmove', this.touchMoveHandler, { passive: false });
+		document.removeEventListener('touchend', this.touchEndHandler, { passive: false });
+		document.removeEventListener('touchcancel', this.touchCancelHandler, { passive: false });
+	}
+
+	handleClick(e) {
+		if (!e.target.closest(this.selectors.field.field)) {
+			return;
+		}
+		let actionButton = window.targetCheck(e, '[data-action]');
+
+		if (!actionButton) {
+			return;
+		}
+		let action = actionButton.dataset.action;
+
+		let field = this.getFieldFromElement(actionButton);
+		let selected = this.getCurrentSelection(field.key);
+		let group = this.getGroupFromElement(actionButton);
+		let groupId = (group) ? group.id : false;
+		let isItem = actionButton.closest('[data-upload-id]');
+		let items = 'upload';
+		let reference = 'it';
+		if (isItem) {
+			selected = [isItem.dataset.uploadId];
+		} else {
+			if (selected.length > 1) {
+				items = 'uploads';
+				reference = 'them';
+			}
+		}
+
+		let deleteUploads;
+
+		switch (action) {
+			case 'add-to-group':
+				//Create from selection
+				//Check for groupId, if no group id, create new group with selection
+				if (selected.length === 0) {
+					//Nothing to move
+					return;
+				}
+				if (!groupId) {
+					group = this.createGroup(field.key);
+					groupId = group.id;
+				}
+				this.addSelectionToGroup(group.element);
+
+				break;
+			case 'remove-from-group':
+				if (selected.length === 0) {
+					return;
+				}
+				//confirm if they want to keep uploads
+				//remove selection from group
+
+				deleteUploads = !confirm(`Would you like to keep the ${items}, just remove ${reference} from this group?`);
+				selected.forEach(upload => {
+					this.removeFromGroup(field.key, upload, groupId);
+					if (deleteUploads) {
+						this.removeUpload(field.key, upload);
+					}
+				});
+				break;
+			case 'delete-upload':
+				if (selected.length === 0) {
+					return;
+				}
+				//delete selection
+				deleteUploads = false;
+				reference = (reference === 'them') ? 'these' : 'this';
+				if (confirm(`Are you sure you want to delete ${reference} ${items}?`)) {
+					deleteUploads = true;
+				}
+				selected.forEach(upload => {
+					this.removeFromGroup(field.key, upload, groupId);
+					if (deleteUploads) {
+						this.removeUpload(field.key, upload);
+					}
+				});
+				break;
+			case 'delete-group':
+				//delete entire group
+				if (group.uploads.length > 0) {
+
+					deleteUploads = confirm(`Do you want to remove all uploads in the group, too?`);
+					if (deleteUploads) {
+						group.uploads.forEach(upload => {
+							this.removeUpload(field.key, upload);
+						});
+					} else {
+						group.uploads.forEach(upload => {
+							this.addImageToGroup(upload);
+						})
+					}
+				}
+				this.removeGroup(groupId, false);
+				break;
+			case 'upload':
+				//upload groups
+				e.preventDefault();
+				this.submitUploads(field.key);
+				break;
+			case 'restore':
+				let notification = document.querySelector('dialog.restore-uploads');
+				if (!notification) {
+					return;
+				}
+				//restore selected uploads
+				const selectedUploads = this.getSelectedRestorationUploads(notification);
+				if (selectedUploads.length === 0) {
+					// this.notifications.add('No uploads selected for restoration', 'warning');
+					return;
+				}
+				this.restoreSelectedUploads(selectedUploads);
+
+				this.restoreModal.handleClose();
+				this.restoreSelection.destroy();
+				this.restoreSelection = null;
+				// Clean up blob URLs before removing notification
+				this.cleanupRestoreNotificationUrls(notification);
+				notification.remove();
+				break;
+			case 'clear-cache':
+				if (!confirm(`Save these uploads for later?`)) {
+					//clear cached uploads
+					this.cleanupStoredRestoration();
+				}
+
+				this.restoreModal.handleClose();
+				this.restoreSelection.destroy();
+				this.restoreSelection = null;
+				this.restoreModal.destroy();
+				this.restoreModal.modal.remove();
+
+				break;
+		}
+	}
+	handleChange(e) {
+		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
+			return;
+		}
+		e.preventDefault();
+
+		if (window.targetCheck(e, '[type="file"]')) {
+			let field = this.getFieldFromElement(e.target);
+			if (!field) {
+				console.warn('File change on unregistered field: ', field.key)
+				return;
+			}
+
+			const files = Array.from(e.target.files);
+			if (files.length === 0) return;
+
+			this.processFiles(field.key, files);
+			e.target.value = '';
+		} else if (e.target.closest('.upload-meta')) {
+			e.preventDefault();
+			let name = e.target.name;
+			let value = e.target.value;
+			let upload = this.getUploadFromElement(e.target);
+			upload.changes[name] = value;
+			this.uploads.set(upload.id, upload);
+			this.persistFieldState(upload.fieldId);
+
+			//It's meta!
+			//TODO:
+			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
+			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
+			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
+			//Send to server for processing
+		} else if (e.target.closest('.group.fields')) {
+			let group = this.getGroupFromElement(e.target);
+			let name = e.target.name;
+			group.changes[name] = e.target.value;
+
+			this.persistFieldState(group.fieldId);
+			this.groups.set(group.id, group);
+		}
+	}
+
+	handlePaste(e) {
+		window.debouncer.schedule(
+			'imagePaste',
+			() => {
+				const items = Array.from(e.clipboardData.items);
+				const imageItems = items.filter(item => item.type.startsWith('image/'));
+
+				if (imageItems.length === 0) return;
+
+				e.preventDefault();
+
+				const fieldId = this.getFieldIdFromElement(e.target);
+				if (!fieldId) return;
+
+				// Convert clipboard items to files
+				const files = [];
+				imageItems.forEach((item, index) => {
+					const file = item.getAsFile();
+					if (file) {
+						// Rename for clarity
+						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
+							type: file.type,
+							lastModified: Date.now()
+						});
+						files.push(newFile);
+					}
+				});
+
+				if (files.length > 0) {
+					this.processFiles(fieldId, files);
+				}
+			},
+			100
+		);
+	}
+
+	isTouchOnFormElement(target) {
+		// Check if target is a form element or inside one
+		const formElements = [
+			'input', 'button', 'label', 'select', 'textarea',
+		];
+
+		return formElements.some(selector => {
+			return target.matches(selector) || target.closest(selector);
+		});
+	}
+	/**** DRAG AND TOUCH *****/
+	startDragOperation(config) {
+		const {
+			primaryElement,
+			sourceType,
+			startPosition,
+			event
+		} = config;
+
+		const uploadId = this.getUploadIdFromElement(primaryElement);
+		const fieldId = this.getFieldIdFromElement(primaryElement);
+
+		// Determine what items to drag
+		const draggedItems = this.getDraggedItems(primaryElement);
+
+		// Initialize drag state
+		this.dragState = {
+			primaryItem: uploadId,
+			draggedItems: draggedItems,
+			isDragging: true,
+			isMultiDrag: draggedItems.length > 1,
+			fieldId: fieldId,
+			sourceType: sourceType,
+			startTime: Date.now(),
+			startPosition: startPosition,
+			currentPosition: startPosition,
+			currentTarget: null,
+			validTarget: null,
+			dragPreview: null,
+			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
+			touchMoved: false
+		};
+
+		// Create drag preview
+		this.createDragPreview(primaryElement);
+
+		// Apply dragging state
+		this.applyDraggingState(true);
+
+		const announceText = this.dragState.isMultiDrag
+			? `Started dragging ${draggedItems.length} items`
+			: 'Started dragging item';
+
+		this.a11y.announce(announceText);
+		this.provideDragFeedback('start');
+
+		return true;
+	}
+
+	updateDragOperation(position, elementUnderPointer) {
+		if (!this.dragState.isDragging) return;
+
+		const { sourceType, startPosition } = this.dragState;
+
+		// Update position
+		this.dragState.currentPosition = position;
+
+		// Check for significant movement (touch)
+		if (sourceType === 'touch' && !this.dragState.touchMoved) {
+			const deltaX = Math.abs(position.x - startPosition.x);
+			const deltaY = Math.abs(position.y - startPosition.y);
+
+			if (deltaX > 10 || deltaY > 10) {
+				this.dragState.touchMoved = true;
+			}
+		}
+
+		// Update preview and target
+		this.updateDragPreview(position);
+		this.updateDropTarget(elementUnderPointer);
+	}
+
+	endDragOperation(elementUnderPointer = null) {
+		if (!this.dragState.isDragging) return;
+
+		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
+			this.dragState.validTarget;
+
+		// Process drop if valid - but only here, not in handleDrop
+		if (wasSuccessful && this.dragState.validTarget) {
+			this.processItemDrop({
+				itemIds: this.dragState.draggedItems,
+				targetElement: this.dragState.validTarget,
+				fieldId: this.dragState.fieldId,
+				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
+				sourceType: this.dragState.sourceType
+			});
+		}
+
+		// Cleanup
+		this.cleanupDragOperation();
+
+		const announceText = wasSuccessful
+			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
+			: 'Drag cancelled';
+
+		this.a11y.announce(announceText);
+	}
+
+	/**
+	 * Shared method to process any drop operation (drag or touch)
+	 * @param {Object} dropData - Standardized drop data
+	 * @returns {boolean} Success status
+	 */
+	processItemDrop(dropData) {
+		const { itemIds, targetElement, fieldId, dropType, sourceType } = dropData;
+
+		if (!itemIds?.length || !targetElement || !fieldId) {
+			return false;
+		}
+
+		let isPreviewDrop = targetElement.classList.contains('preview') &&
+			targetElement.classList.contains('item-grid');
+		let actualTarget = targetElement;
+
+		// Handle empty group drops
+		if (targetElement.classList.contains('empty-group')) {
+			let group = this.createGroup(fieldId);
+			if (!group) {
+				console.error('Failed to create group');
+				return false;
+			}
+			actualTarget = group.grid;
+			isPreviewDrop = false;
+		}
+
+		itemIds.forEach(uploadId => {
+			this.addImageToGroup(uploadId, isPreviewDrop ? null : actualTarget, false);
+		});
+
+		const field = this.fields.get(fieldId);
+		if (field) {
+			this.clearAllSelections(field);
+		}
+
+		this.persistFieldState(fieldId);
+
+		const announceText = dropType === 'multiple'
+			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
+			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
+
+		this.a11y.announce(announceText);
+		this.provideFeedback(sourceType, 'success', {
+			count: itemIds.length,
+			isMultiple: dropType === 'multiple'
+		});
+
+		return true;
+	}
+
+
+
+	cleanupDragOperation() {
+		if (this.dragState.dragPreview) {
+			this.dragState.dragPreview.remove();
+		}
+
+		this.applyDraggingState(false);
+		this.clearDropTargetStates();
+
+		// Reset state
+		this.dragState.isDragging = false;
+		this.dragState.dragPreview = null;
+		this.dragState.draggedItems = [];
+	}
+
+	/**
+	 * Determine what items to drag (single or multiple selection)
+	 */
+	getDraggedItems(element) {
+		const selectedUploads = this.getSelectedUploads(element);
+		const primaryUploadId = element.dataset.uploadId;
+
+		// If we have multiple selections and primary is selected, drag all
+		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
+			return selectedUploads;
+		}
+
+		// Otherwise, just drag the primary item
+		return [primaryUploadId];
+	}
+
+	/**
+	 * Apply/remove dragging visual state to items
+	 */
+	applyDraggingState(isDragging) {
+		this.dragState.draggedItems.forEach(uploadId => {
+			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
+			if (element) {
+				element.classList.toggle('dragging', isDragging);
+			}
+		});
+	}
+
+	/**
+	 * Create drag preview element
+	 */
+	/**
+	 * Create drag preview element from template
+	 */
+	createDragPreview() {
+		const { draggedItems, sourceType } = this.dragState;
+
+		// Get the template
+		const template = window.getTemplate('dragPreview');
+		if (!template) {
+			console.error('Drag preview template not found');
+			return;
+		}
+
+		this.dragState.dragPreview = template;
+		const itemsContainer = template.querySelector('.drag-items');
+		const countBadge = template.querySelector('.drag-count');
+
+		// Set data attributes for CSS targeting
+		template.dataset.source = sourceType;
+
+		// Handle single vs multi-item
+		const itemCount = draggedItems.length;
+
+		if (itemCount > 1) {
+			// Multi-item: show count and stack up to 3 items
+			template.dataset.count = itemCount;
+			countBadge.dataset.count = itemCount;
+			countBadge.hidden = false;
+
+			const displayCount = Math.min(itemCount, 3);
+			for (let i = 0; i < displayCount; i++) {
+				const uploadId = draggedItems[i];
+				const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
+
+				if (uploadElement) {
+					const clonedItem = uploadElement.cloneNode(true);
+					clonedItem.dataset.uploadId = `${uploadId}-preview`;
+					// Remove interactive elements from clone
+					clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
+					itemsContainer.appendChild(clonedItem);
+				}
+			}
+		} else {
+			// Single item: just clone it
+			const uploadElement = document.querySelector(`[data-upload-id="${draggedItems[0]}"]`);
+			if (uploadElement) {
+				const clonedItem = uploadElement.cloneNode(true);
+				clonedItem.dataset.uploadId = `${draggedItems[0]}-preview`;
+				// Remove interactive elements from clone
+				clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
+				itemsContainer.appendChild(clonedItem);
+			}
+		}
+
+		// Add to DOM
+		document.body.appendChild(this.dragState.dragPreview);
+
+		// Position immediately at start position
+		this.updateDragPreview(this.dragState.startPosition);
+	}
+
+	/**
+	 * Update drag preview position
+	 */
+	updateDragPreview(position) {
+		if (!this.dragState.dragPreview) return;
+
+		const preview = this.dragState.dragPreview;
+
+		// Determine offset based on source type
+		let offset;
+		if (this.dragState.sourceType === 'touch') {
+			// For touch, offset up and to the left so finger doesn't cover preview
+			offset = this.dragState.isMultiDrag
+				? { x: -60, y: -80 }
+				: { x: -50, y: -60 };
+		} else {
+			// For mouse, smaller offset
+			offset = this.dragState.isMultiDrag
+				? { x: 15, y: 15 }
+				: { x: 10, y: 10 };
+		}
+
+		// Position the preview at the current pointer position with offset
+		preview.style.left = `${position.x + offset.x}px`;
+		preview.style.top = `${position.y + offset.y}px`;
+	}
+
+	/**
+	 * Update drop target highlighting
+	 */
+	updateDropTarget(elementUnderPointer) {
+		// Clear previous target
+		if (this.dragState.currentTarget) {
+			this.clearDropTargetState(this.dragState.currentTarget);
+		}
+
+		// Find valid drop target
+		const validTarget = this.findValidDropTarget(elementUnderPointer);
+
+		// Update state
+		this.dragState.currentTarget = elementUnderPointer;
+		this.dragState.validTarget = validTarget;
+
+		// Apply visual feedback
+		if (validTarget) {
+			this.applyDropTargetState(validTarget);
+
+			// Haptic feedback for touch
+			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
+				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
+				navigator.vibrate(pattern);
+			}
+		}
+	}
+
+	/**
+	 * Find valid drop target from element
+	 */
+	findValidDropTarget(element) {
+		const target = element?.closest('.item-grid.group, .empty-group, .item-grid.preview');
+		return target && this.getFieldIdFromElement(target) === this.dragState.fieldId ? target : null;
+	}
+
+	/**
+	 * Apply drop target visual state
+	 */
+	applyDropTargetState(target) {
+		target.classList.add('dragover');
+
+		if (this.dragState.isMultiDrag) {
+			target.classList.add('multi-drop');
+			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
+		}
+	}
+
+	/**
+	 * Clear drop target state from element
+	 */
+	clearDropTargetState(target) {
+		target.classList.remove('dragover', 'multi-drop');
+		target.removeAttribute('data-item-count');
+	}
+
+	/**
+	 * Clear all drop target states
+	 */
+	clearDropTargetStates() {
+		document.querySelectorAll('.dragover').forEach(el => {
+			el.classList.remove('dragover', 'multi-drop');
+			el.removeAttribute('data-item-count');
+		});
+	}
+
+
+	/**
+	 * Provide feedback for drag operations
+	 */
+	provideDragFeedback(type) {
+		const hapticPatterns = {
+			start: [50],
+			success: this.dragState.isMultiDrag ? [30, 20, 30] : [50],
+			error: [100, 50, 100],
+			warning: [50]
+		};
+
+		// Haptic feedback (vibration on supported devices)
+		if (navigator.vibrate && hapticPatterns[type]) {
+			navigator.vibrate(hapticPatterns[type]);
+		}
+
+		// Visual feedback
+		const feedback = document.createElement('div');
+		feedback.className = `drag-feedback ${type}`;
+		feedback.style.cssText = `
+		position: fixed;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		padding: 1rem 2rem;
+		background: var(--${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'});
+		color: white;
+		border-radius: var(--radius);
+		z-index: 10001;
+		animation: feedbackPulse 0.3s ease;
+		pointer-events: none;
+	`;
+
+		const icons = {
+			start: '↕️',
+			success: '✓',
+			error: '✗',
+			warning: '⚠'
+		};
+
+		feedback.textContent = icons[type] || '';
+		document.body.appendChild(feedback);
+
+		setTimeout(() => {
+			feedback.style.animation = 'fadeOut 0.3s ease';
+			setTimeout(() => feedback.remove(), 300);
+		}, 500);
+	}
+
+	/**
+	 * Provide consistent feedback for different input methods
+	 */
+	provideFeedback(sourceType, feedbackType, data = {}) {
+		const hapticPatterns = {
+			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
+			error: [100, 50, 100]
+		};
+
+		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
+			navigator.vibrate(hapticPatterns[feedbackType]);
+		}
+	}
+
+	clearDragoverStates() {
+		document.querySelectorAll('.dragover').forEach(el => {
+			el.classList.remove('dragover', 'multi-drop');
+			el.removeAttribute('data-item-count');
+		});
+	}
+	/*********
+	 *  DRAG HANDLERS
+	 ********/
+	handleDragEnter(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		// Only handle external files
+		if (e.dataTransfer.types.includes('Files')) {
+			e.preventDefault();
+			const uploadContainer = e.target.closest('.file-upload-container');
+			if (uploadContainer) {
+				uploadContainer.classList.add('dragover');
+			}
+		}
+	}
+	handleDragLeave(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		const uploadContainer = e.target.closest('.file-upload-container');
+		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
+			uploadContainer.classList.remove('dragover');
+		}
+	}
+	handleDragStart(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		const uploadItem = e.target.closest('[data-upload-id]');
+		if (!uploadItem) return;
+
+		const result = this.startDragOperation({
+			primaryElement: uploadItem,
+			sourceType: 'drag',
+			startPosition: { x: e.clientX, y: e.clientY },
+			event: e
+		});
+
+		if (result) {
+			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
+			e.dataTransfer.effectAllowed = 'move';
+		} else {
+			e.preventDefault();
+		}
+	}
+
+	handleDragOver(e) {
+		if (!this.dragState.isDragging) return;
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		e.preventDefault();
+		e.dataTransfer.dropEffect = 'move';
+
+		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
+		this.updateDragOperation(
+			{ x: e.clientX, y: e.clientY },
+			elementUnderPointer
+		);
+	}
+
+	handleDrop(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		e.preventDefault();
+		this.clearDragoverStates();
+
+		// Handle external files (new uploads)
+		const uploadContainer = e.target.closest('.file-upload-container');
+		if (uploadContainer) {
+			const files = Array.from(e.dataTransfer.files);
+			if (files.length > 0) {
+				const fieldId = this.getFieldIdFromElement(uploadContainer);
+				if (fieldId) {
+					this.processFiles(fieldId, files);
+					this.a11y.announce(`${files.length} file(s) dropped for upload`);
+				}
+			}
+		}
+	}
+
+	handleDragEnd(e) {
+		if (!this.dragState.isDragging) return;
+
+		// Find the element under the final drop position
+		const elementUnderDrop = document.elementFromPoint(
+			this.dragState.currentPosition?.x || e.clientX,
+			this.dragState.currentPosition?.y || e.clientY
+		);
+
+		this.endDragOperation(elementUnderDrop);
+	}
+	/*********
+	 * TOUCH HANDLERS
+	 ********/
+	handleTouchStart(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+		if (this.isTouchOnFormElement(e.target)) {
+			return;
+		}
+
+		const uploadItem = e.target.closest('[data-upload-id]');
+		if (!uploadItem) return;
+
+		const touch = e.touches[0];
+
+		const result = this.startDragOperation({
+			primaryElement: uploadItem,
+			sourceType: 'touch',
+			startPosition: { x: touch.clientX, y: touch.clientY },
+			event: e
+		});
+
+		if (result) {
+			e.preventDefault(); // Prevent scrolling
+		}
+	}
+
+	handleTouchMove(e) {
+		if (!this.dragState.isDragging) return;
+
+		e.preventDefault();
+		const touch = e.touches[0];
+		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
+
+		this.updateDragOperation(
+			{ x: touch.clientX, y: touch.clientY },
+			elementUnderTouch
+		);
+	}
+
+	handleTouchEnd(e) {
+		if (!this.dragState.isDragging) return;
+
+		e.preventDefault();
+		const touch = e.changedTouches[0];
+		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
+
+		this.endDragOperation(elementUnderTouch);
+	}
+
+	handleTouchCancel(e) {
+		if (!this.dragState.isDragging) {
+			return;
+		}
+		if (this.dragState.isDragging) {
+			this.cleanupDragOperation();
+			this.a11y.announce('Drag cancelled');
+		}
+	}
+	/*******************************************************************************
+	 QUEUE INTEGRATION
+	 *******************************************************************************/
+	async submitUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Check if there are uploads to submit
+		const pendingUploads = Array.from(field.uploads || [])
+			.map(id => this.uploads.get(id))
+			.filter(upload => upload &&
+				(upload.status === 'processed' ||
+					upload.status === 'processed-original'));
+
+		if (pendingUploads.length === 0) {
+			// this.notifications.add('No uploads ready to submit', 'warning');
+			return;
+		}
+
+		// Queue the uploads
+		try {
+			await this.queueUpload(fieldId);
+			// this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitUploads',
+				fieldId
+			});
+			// this.notifications.add('Failed to submit uploads', 'error');
+		}
+	}
+	async retryUpload(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		try {
+			// Reset status
+			this.updateUploadStatus(uploadId, 'received');
+
+			// If we have the processed file, skip to queuing
+			if (upload.processedFile) {
+				this.updateUploadStatus(uploadId, 'processed');
+				await this.queueUpload(upload.fieldId);
+			} else if (upload.originalFile) {
+				// Reprocess the file
+				const reprocessed = await this.processFile(upload.originalFile, field);
+				if (reprocessed) {
+					await this.queueUpload(upload.fieldId);
+				}
+			} else {
+				throw new Error('No file data available for retry');
+			}
+
+			// this.notifications.add('Retrying upload...', 'info');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'retryUpload',
+				uploadId
+			});
+			// this.notifications.add('Failed to retry upload', 'error');
+		}
+	}
+
+	async queueUpload(fieldId) {
+		//Further cache it, or is it already cached at this point?
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads) return;
+
+		const uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			return;
+		}
+
+		const data = this.prepareUploadData(field, uploads);
+		this.a11y.announce('Queuing for upload');
+		let img = (uploads.length === 1) ? 'image' : 'images';
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} ${img} to server...`,
+			popup: `Uploading ${uploads.length} ${img}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload'
+		}
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (!upload) {
+					return;
+				}
+				upload.operationId = operationId;
+				this.updateUploadStatus(uploadId, 'queued');
+			});
+			field.operationId = operationId;
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		} finally {
+			this.persistFieldState(field.key);
+		}
+	}
+
+	prepareUploadData(field, uploads) {
+
+		const formData = new FormData();
+		formData.append('content', field.content);
+		formData.append('mode', field.mode);
+		formData.append('field_name', field.name);
+		formData.append('field_key', field.key);
+		formData.append('field_type', field.type);
+		formData.append('subtype', field.subtype);
+		formData.append('item_id', field.itemID);		//post, term, or user id
+		formData.append('context', field.context);	//post, term, or user
+		formData.append('destination', field.destination || 'meta'); //meta, post, post_group
+		let uploadMap = [];
+
+		const fieldGroups = this.getFieldGroups(field.key);
+		if (field.destination === 'post_group' && fieldGroups.length > 0) {
+			// User has created groups
+			let groups = [];
+			let titles = [];
+			let featuredImages = [];
+
+			fieldGroups.forEach(group => {
+				let groupUploadIndices = [];
+				let featuredIndex = null;
+
+				group.uploads.forEach(uploadId => {
+					let upload = this.uploads.get(uploadId);
+					if (upload) {
+						const fileToUpload = upload.processedFile || upload.originalFile;
+						if (fileToUpload) {
+							formData.append('files[]', fileToUpload);
+							const fileIndex = uploadMap.length;
+							uploadMap.push(upload.id);
+							groupUploadIndices.push(upload.id);
+
+							// Check if this is the featured image
+							const radioInput = upload.element?.querySelector('[name="featured"]');
+							if (radioInput?.checked) {
+								featuredIndex = upload.id;
+							}
+						}
+					}
+				});
+
+				groups.push(groupUploadIndices);
+				titles.push(group.title || '');
+				featuredImages.push(featuredIndex);
+			});
+
+			formData.append('groups', JSON.stringify(groups));
+			formData.append('group_titles', JSON.stringify(titles));
+			formData.append('featured_images', JSON.stringify(featuredImages));
+		} else {
+			// No groups - just append all files
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+		}
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		// console.log('Final FormData:');
+		// for (let pair of formData.entries()) {
+		// 	console.log(pair[0], pair[1]);
+		// }
+
+		return formData;
+	}
+
+	getFieldGroups(fieldId) {
+		const groups = [];
+
+		this.groups.forEach((groupData, groupId) => {
+			if (groupData.fieldId === fieldId) {
+				const field = this.fields.get(fieldId);
+				const groupElement = field?.ui?.groups?.groups?.get(groupId);
+
+				groups.push({
+					id: groupId,
+					uploads: Array.from(groupData.uploads || new Set()),
+					meta: this.groupsMeta.get(groupId) || {},
+					element: groupElement || null
+				});
+			}
+		});
+
+		return groups;
+	}
+
+	/**
+	 * Build groups data from field state
+	 */
+	buildGroupsData(field, uploads) {
+		const groups = [];
+		const titles = [];
+		const uploadMap = [];
+
+		if (field.groups && field.groups.length > 0) {
+			// User has explicitly created groups
+			field.groups.forEach(group => {
+				const groupUploads = [];
+				group.uploads.forEach(uploadId => {
+					groupUploads.push(uploadId);
+					uploadMap.push(uploadId);
+				});
+				groups.push(groupUploads);
+				titles.push(group.title || '');
+			});
+		} else {
+			// No explicit groups - treat all as one group
+			const allUploads = [];
+			uploads.forEach(uploadId => {
+				allUploads.push(uploadId);
+				uploadMap.push(uploadId);
+			});
+			groups.push(allUploads);
+			titles.push('');
+		}
+
+		return { groups, titles, uploadMap };
+	}
+
+	async queueImageMeta(e) {
+		const upload = this.getUploadFromElement(element);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		// Collect meta data from the form
+		const metaContainer = element.closest('.upload-meta');
+		if (!metaContainer) return;
+
+		const metaData = {
+			title: metaContainer.querySelector('[name="title"]')?.value || '',
+			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
+			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
+			description: metaContainer.querySelector('[name="description"]')?.value || ''
+		};
+
+		// Update upload meta
+		upload.meta = { ...upload.meta, ...metaData };
+		this.uploads.set(upload.id, upload);
+
+		// Mark that we have meta changes
+		this.hasMetaChanges = true;
+
+		// Determine if upload has been sent to server
+		const isOnServer = upload.status === 'completed' && upload.attachmentId;
+
+		if (isOnServer) {
+			// Queue immediate update
+			await this.sendMetaUpdate(upload);
+		} else if (upload.operationId) {
+			// Wait for upload to complete, then send meta
+			this.queueDependentMetaUpdate(upload);
+		} else {
+			// Upload hasn't been queued yet, meta will be sent with initial upload
+			this.persistFieldState(field.key);
+		}
+	}
+
+	/**
+	 * Send meta update to server
+	 */
+	async sendMetaUpdate(upload) {
+		const formData = new FormData();
+		formData.append('attachment_id', upload.attachmentId);
+		formData.append('title', upload.meta.title);
+		formData.append('alt_text', upload.meta.alt_text);
+		formData.append('caption', upload.meta.caption);
+		formData.append('description', upload.meta.description);
+		//TODO:
+		// Send an array of attachment IDs with the changes, similar to the post editing logic
+		/**
+		 *  let data = {
+		 *  	items: {
+		 *      	uploadID: {
+		 *          	title: '',
+		 *          	alt: '',
+		 *          	caption: '',
+		 *         		depends_on: ''  <-- only necessary if uploadID is the generated upload_id
+		 *      	}
+		 *  	},
+		 *  	user: userID
+		 *  }
+		 *
+		 *  WHERE uploadID = attachment_id (if already uploaded) or our generated upload_id if the file hasn't been processed yet
+		 *
+		 */
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: formData,
+			title: `Updating metadata for ${upload.meta.originalName}`,
+			canMerge: true,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			}
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+			// this.notifications.add('Metadata updated', 'success');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
+			});
+		}
+	}
+
+	/**
+	 * Queue meta update that depends on upload completion
+	 */
+	queueDependentMetaUpdate(upload) {
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			dependencies: [upload.operationId],
+			data: () => {
+				// This function will be called when dependencies are resolved
+				const formData = new FormData();
+				formData.append('operation_id', upload.operationId);
+				formData.append('upload_id', upload.id);
+				formData.append('title', upload.meta.title);
+				formData.append('alt_text', upload.meta.alt_text);
+				formData.append('caption', upload.meta.caption);
+				formData.append('description', upload.meta.description);
+				return formData;
+			},
+			title: `Updating metadata after upload`,
+			canMerge: true,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			}
+		};
+
+		this.queue.addToQueue(operation);
+	}
+	/*******************************************************************************
+	 IMAGE PROCESSING
+	*******************************************************************************/
+	async processFiles(fieldId, files) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Hide upload container, show group display
+		if (field.ui.field.dropZone) {
+			field.ui.field.dropZone.hidden = true;
+		}
+		if (field.ui.groups.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+		// Process files
+		const processPromises = Array.from(files).map(async (file, index) => {
+			try {
+				// Create upload ID
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create upload data
+				const uploadData = {
+					id: uploadId,
+					fieldId: fieldId,
+					originalFile: file,
+					processedFile: null,
+					preview: null,
+					status: 'local_processing',
+					element: null,
+					location: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Create preview URL
+				uploadData.preview = URL.createObjectURL(file);
+
+				// Process the file (resize if image)
+				if (file.type.startsWith('image/')) {
+					uploadData.processedFile = await this.processImage(file, field.subtype);
+				} else {
+					uploadData.processedFile = file;
+				}
+
+				// Store blob data separately in IndexedDB
+				if (this.db) {
+					try {
+						await this.storeBlobData(uploadId, uploadData.processedFile || file);
+					} catch (error) {
+						console.warn('Failed to store blob data:', error);
+					}
+				}
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				uploadData.element = this.createImageElement({
+					...uploadData,
+					subtype: subtype
+				}, field.destination === 'post_group');
+
+				// Show progress on the item
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Add to preview grid
+				if (field.ui.field.preview) {
+					field.ui.field.preview.appendChild(uploadData.element);
+					uploadData.location = field.ui.field.preview;
+				}
+
+				// Store upload
+				this.uploads.set(uploadId, uploadData);
+				field.uploads.add(uploadId);
+
+				// Update progress
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+				uploadData.status = 'processed';
+
+				// Fade out item progress after a moment
+				setTimeout(() => {
+					this.showUploadProgress(uploadId, false);
+				}, 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		// Wait for all files to process
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		// Cache the state (now without DOM references)
+		await this.persistFieldState(fieldId);
+
+		// Queue for upload if in direct mode
+		if (field.mode === 'direct' && field.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+		}
+
+		// Lock uploads if max reached
+		this.maybeLockUploads(fieldId);
+	}
+
+	updateFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.ui.field.field) return;
+
+		const container = field.ui.field.field;
+		const uploadCount = field.uploads?.size || 0;
+		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		// Set data attributes for CSS targeting
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		// Update ARIA labels for accessibility
+		if (field.ui.field.preview) {
+			field.ui.field.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	/**
+	 * Store file blob data in IndexedDB
+	 */
+	async storeBlobData(uploadId, file) {
+		if (!this.db) return;
+
+		const blobData = {
+			uploadId: uploadId,
+			data: file,
+			name: file.name,
+			type: file.type,
+			lastModified: file.lastModified,
+			timestamp: Date.now()
+		};
+
+		try {
+			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
+			await tx.objectStore('uploadBlobs').put(blobData);
+		} catch (error) {
+			console.error('Failed to store blob data:', error);
+			throw error;
+		}
+	}
+
+	/**
+	 * Show/hide progress indicator on individual upload items
+	 */
+	showUploadProgress(uploadId, show = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
+
+		const progressEl = upload.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => {
+					progressEl.hidden = true;
+				}, 300);
+			}
+		}
+	}
+
+	/**
+	 * Update individual upload progress bar
+	 */
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
+
+		const progressEl = upload.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) {
+			fill.style.width = `${percent}%`;
+		}
+
+		if (status && details) {
+			details.textContent = this.getStatusText(status);
+		}
+
+		if (status && icon) {
+			icon.innerHTML = this.getStatusIcon(status).outerHTML;
+		}
+	}
+	checkFieldLimits(fieldId, additionalFiles) {
+		const field = this.fields.get(fieldId);
+		if (!field) return false;
+
+		const currentCount = field.uploads?.size || 0;
+		const totalCount = currentCount + additionalFiles;
+
+		if (totalCount > field.maxFiles) {
+			// this.notifications.add(
+			// 	`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
+			// 	'warning'
+			// );
+			return false;
+		}
+
+		return true;
+	}
+	generateUploadId() {
+		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+	}
+	validateFile(file, field) {
+		// Type validation
+		if (!this.settings.allowedTypes.includes(file.type)) {
+			this.notify(`Invalid file type: ${file.type}`, 'error');
+			return false;
+		}
+
+		// Size validation
+		if (file.size > this.settings.maxFileSize) {
+			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
+			return false;
+		}
+
+		return true;
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+
+		const k = 1024;
+		const dm = decimals < 0 ? 0 : decimals;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+	}
+
+	shouldProcessClientSide(file, subtype) {
+		// Only process images client-side
+		if (subtype === 'image' && file.type.startsWith('image/')) {
+			return true;
+		}
+
+		// Videos and documents go straight to server
+		return false;
+	}
+
+	async processBatch(fieldId, files) {
+		const results = [];
+		const processingQueue = [];
+		const maxConcurrent = this.worker.settings.maxConcurrent;
+
+		let total = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+		let field = this.fields.get(fieldId);
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+
+		for (let i = 0; i < files.length; i++) {
+			this.showUploadProgress(uploadId, true);
+			this.updateUploadProgress(fieldId, i, total);
+			// Wait if we've reached max concurrent processing
+			if (processingQueue.length >= maxConcurrent) {
+				await Promise.race(processingQueue);
+			}
+
+			const processPromise = this.processFile(files[i], field)
+				.then(upload => {
+					// Remove from processing queue
+					const index = processingQueue.indexOf(processPromise);
+					if (index > -1) processingQueue.splice(index, 1);
+
+					if (upload) results.push(upload);
+					return upload;
+				})
+				.catch(error => {
+					console.error(`Failed to process ${files[i].name}:`, error);
+					// Remove from processing queue
+					const index = processingQueue.indexOf(processPromise);
+					if (index > -1) processingQueue.splice(index, 1);
+					return null;
+				});
+
+			processingQueue.push(processPromise);
+		}
+
+		// Wait for remaining files
+		await Promise.all(processingQueue);
+		return results;
+	}
+
+	async processFile(file, field, uploadId = null) {
+		if (!field || !file) {
+			console.error('Missing required parameters:', { file, field });
+			return null;
+		}
+
+		if (!this.shouldProcessClientSide(file, field.subtype)) {
+			return upload;
+		}
+
+		const id = uploadId || `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+		try {
+			// Create upload object
+			const upload = {
+				id,
+				fieldId: field.key,
+				originalFile: file,
+				processedFile: null,
+				preview: null,
+				status: 'local_processing',
+				element: null,
+				location: null,
+				groupId: null,
+				changes: {},
+				meta: {
+					originalName: file.name,
+					size: file.size,
+					type: file.type
+				}
+			};
+
+			// Create preview URL
+			upload.preview = URL.createObjectURL(file);
+
+			// Process the file
+			let processedFile = null;
+			let processingFailed = false;
+
+			if (file.type.startsWith('image/')) {
+				try {
+					processedFile = await this.processImage(file, id);
+				} catch (error) {
+					console.warn(`Image processing failed for ${file.name}, using original:`, error);
+					processingFailed = true;
+					processedFile = file;
+				}
+			} else {
+				processedFile = file; // Videos/documents use original
+			}
+
+			upload.processedFile = processedFile;
+			upload.processingFailed = processingFailed;
+
+			// Store in uploads map
+			this.uploads.set(id, upload);
+
+			// Add to field's uploads
+			if (!field.uploads) {
+				field.uploads = new Set();
+			}
+			field.uploads.add(id);
+
+			// Update status
+			this.updateUploadStatus(id, 'processed');
+
+			// Persist state
+			await this.persistFieldState(field.key);
+
+			// Announce to screen readers
+			const message = processingFailed
+				? `${file.name} added (original format)`
+				: `${file.name} processed and ready`;
+			this.a11y.announce(message);
+
+			return upload;
+
+		} catch (error) {
+			// Clean up failed upload
+			this.cleanupFailedUpload(id, field.key);
+
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'processFile',
+				uploadId: id,
+				fileName: file.name
+			});
+
+			return null;
+		}
+	}
+
+	async processImage(file, uploadId) {
+		const timeout = this.worker.settings.timeout;
+
+		return new Promise((resolve, reject) => {
+			let timeoutId;
+			let taskCompleted = false;
+
+			// Set timeout
+			timeoutId = setTimeout(() => {
+				if (!taskCompleted) {
+					taskCompleted = true;
+
+					// Remove from active tasks
+					this.worker.tasks.delete(uploadId);
+
+					// Maybe restart worker if configured
+					if (this.worker.settings.restartAfterTimeout) {
+						this.restartCompressionWorker();
+					}
+
+					reject(new Error(`Processing timeout for ${file.name}`));
+				}
+			}, timeout);
+
+			// Track this task
+			this.worker.tasks.set(uploadId, { file, timeoutId });
+
+			// Process image
+			this.handleProcess(file, uploadId)
+				.then(result => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						resolve(result);
+					}
+				})
+				.catch(error => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						reject(error);
+					}
+				});
+		});
+	}
+
+	async handleProcess(file, uploadId) {
+		// Skip non-images
+		if (!file.type.startsWith('image/')) {
+			return file;
+		}
+
+		const maxDimension = this.getMaxDimension();
+		const quality = 0.85;
+
+		// Try worker first if available
+		if (this.shouldUseWorker(file)) {
+			try {
+				// Ensure worker is initialized
+				if (!this.worker.worker) {
+					this.initCompressionWorker();
+				}
+
+				if (this.worker.worker) {
+					return await this.processWithWorker(file, uploadId, maxDimension, quality);
+				}
+			} catch (error) {
+				console.warn('Worker processing failed, falling back to main thread:', error);
+			}
+		}
+
+		// Fallback to main thread
+		return await this.processOnMainThread(file, maxDimension, quality);
+	}
+
+	/**
+	 * Process image on main thread with better error handling
+	 */
+	async processOnMainThread(file, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			const img = new Image();
+			const canvas = document.createElement('canvas');
+			const ctx = canvas.getContext('2d');
+			let objectUrl = null;
+
+			const cleanup = () => {
+				img.onload = null;
+				img.onerror = null;
+				if (objectUrl) {
+					URL.revokeObjectURL(objectUrl);
+					objectUrl = null;
+				}
+				// Explicitly clean up canvas
+				canvas.width = 1;
+				canvas.height = 1;
+				ctx.clearRect(0, 0, 1, 1);
+			};
+
+			img.onload = () => {
+				try {
+					const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
+					canvas.width = width;
+					canvas.height = height;
+
+					// Enhanced image smoothing
+					ctx.imageSmoothingEnabled = true;
+					ctx.imageSmoothingQuality = 'high';
+					ctx.drawImage(img, 0, 0, width, height);
+
+					const outputFormat = this.getOptimalFormat(file);
+					const outputQuality = this.getOptimalQuality(file, quality);
+
+					canvas.toBlob(
+						(blob) => {
+							cleanup();
+							if (blob) {
+								const processedFile = new File(
+									[blob],
+									this.getProcessedFileName(file, outputFormat),
+									{ type: outputFormat, lastModified: Date.now() }
+								);
+								resolve(processedFile);
+							} else {
+								reject(new Error('Canvas toBlob failed'));
+							}
+						},
+						outputFormat,
+						outputQuality
+					);
+
+				} catch (error) {
+					cleanup();
+					reject(new Error(`Canvas processing failed: ${error.message}`));
+				}
+			};
+
+			img.onerror = () => {
+				cleanup();
+				reject(new Error(`Failed to load image: ${file.name}`));
+			};
+
+			try {
+				objectUrl = URL.createObjectURL(file);
+				img.src = objectUrl;
+			} catch (error) {
+				cleanup();
+				reject(new Error(`Failed to create object URL: ${error.message}`));
+			}
+		});
+	}
+
+	/**
+	 * Get optimal output format
+	 */
+	getOptimalFormat(file) {
+		// Keep original format for certain types
+		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
+			return file.type;
+		}
+
+		// Use WebP if supported, otherwise JPEG
+		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+	}
+
+	/**
+	 * Get optimal quality setting
+	 */
+	getOptimalQuality(file, requestedQuality) {
+		// Higher quality for smaller files
+		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+		if (file.size < 2 * 1024 * 1024) return requestedQuality;
+
+		// Lower quality for very large files
+		return Math.min(requestedQuality, 0.8);
+	}
+
+	/**
+	 * Generate processed file name
+	 */
+	getProcessedFileName(originalFile, outputFormat) {
+		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
+
+		const extensions = {
+			'image/webp': '.webp',
+			'image/jpeg': '.jpg',
+			'image/png': '.png',
+			'image/gif': '.gif'
+		};
+
+		return baseName + (extensions[outputFormat] || '.jpg');
+	}
+
+	/**
+	 * Get maximum dimension based on device capabilities
+	 */
+	getMaxDimension() {
+		const screenWidth = window.screen.width;
+		const devicePixelRatio = window.devicePixelRatio || 1;
+
+		// Scale based on device capabilities
+		if (screenWidth * devicePixelRatio > 2560) return 2400;
+		if (screenWidth * devicePixelRatio > 1920) return 1920;
+		return 1200;
+	}
+
+	/**
+	 * Determine if we should use Web Worker
+	 */
+	shouldUseWorker(file) {
+		// Use worker for large files or when available
+		return this.worker.worker &&
+			file.size > 1024 * 1024 && // > 1MB
+			typeof OffscreenCanvas !== 'undefined';
+	}
+
+	async processWithWorker(file, uploadId, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			if (!this.worker.worker) {
+				reject(new Error('Worker not available'));
+				return;
+			}
+
+			// Create unique message ID for this task
+			const messageId = `${uploadId}_${Date.now()}`;
+
+			// Handler for this specific message
+			const messageHandler = (e) => {
+				if (e.data.messageId !== messageId) return;
+
+				// Remove handler
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+
+				if (e.data.success) {
+					const processedFile = new File(
+						[e.data.blob],
+						this.getProcessedFileName(file, e.data.format || 'image/webp'),
+						{ type: e.data.format || 'image/webp', lastModified: Date.now() }
+					);
+					resolve(processedFile);
+				} else {
+					reject(new Error(e.data.error || 'Worker processing failed'));
+				}
+			};
+
+			const errorHandler = (error) => {
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+				reject(new Error(`Worker error: ${error.message}`));
+			};
+
+			// Add handlers
+			this.worker.worker.addEventListener('message', messageHandler);
+			this.worker.worker.addEventListener('error', errorHandler);
+
+			// Send message to worker
+			this.worker.worker.postMessage({
+				messageId,
+				file,
+				maxDimension,
+				quality,
+				outputFormat: this.getOptimalFormat(file)
+			});
+		});
+	}
+
+	/**
+	 * Restart compression worker
+	 */
+	restartCompressionWorker() {
+		// Terminate existing worker
+		if (this.worker.worker) {
+			this.worker.worker.terminate();
+			this.worker.worker = null;
+		}
+
+		// Clear active tasks
+		this.worker.tasks.clear();
+
+		// Check restart limit
+		if (this.worker.restart.count >= this.worker.restart.max) {
+			console.error('Max worker restarts reached, disabling worker');
+			return;
+		}
+
+		this.worker.restart.count++;
+
+		// Reinitialize
+		this.initCompressionWorker();
+	}
+
+	/**
+	 * Initialize Web Worker for image compression
+	 */
+	initCompressionWorker() {
+		if (this.worker.worker || typeof Worker === 'undefined') return;
+
+		try {
+			const workerScript = `
+            self.onmessage = async function(e) {
+                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+
+                try {
+                    // Create ImageBitmap from file
+                    const bitmap = await createImageBitmap(file);
+
+                    // Calculate dimensions
+                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+                    const width = Math.round(bitmap.width * scale);
+                    const height = Math.round(bitmap.height * scale);
+
+                    // Create OffscreenCanvas
+                    const canvas = new OffscreenCanvas(width, height);
+                    const ctx = canvas.getContext('2d');
+
+                    // Draw and resize
+                    ctx.imageSmoothingEnabled = true;
+                    ctx.imageSmoothingQuality = 'high';
+                    ctx.drawImage(bitmap, 0, 0, width, height);
+
+                    // Clean up bitmap
+                    bitmap.close();
+
+                    // Convert to blob
+                    const blob = await canvas.convertToBlob({
+                        type: outputFormat,
+                        quality: quality
+                    });
+
+                    self.postMessage({
+                        messageId,
+                        success: true,
+                        blob: blob,
+                        format: outputFormat
+                    });
+
+                } catch (error) {
+                    self.postMessage({
+                        messageId,
+                        success: false,
+                        error: error.message
+                    });
+                }
+            };
+        `;
+
+			const blob = new Blob([workerScript], { type: 'application/javascript' });
+			this.worker.worker = new Worker(URL.createObjectURL(blob));
+
+		} catch (error) {
+			console.warn('Failed to initialize compression worker:', error);
+			this.worker.worker = null;
+		}
+	}
+
+	/**
+	 * Calculate optimal dimensions with aspect ratio preservation
+	 */
+	calculateOptimalDimensions(img, maxDimension) {
+		let { width, height } = img;
+
+		// Don't upscale
+		if (width <= maxDimension && height <= maxDimension) {
+			return { width, height };
+		}
+
+		// Calculate scale factor
+		const scale = Math.min(maxDimension / width, maxDimension / height);
+
+		return {
+			width: Math.round(width * scale),
+			height: Math.round(height * scale)
+		};
+	}
+
+
+	/**
+	 * Check WebP support
+	 */
+	supportsWebP() {
+		const canvas = document.createElement('canvas');
+		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
+	}
+
+	/**
+	 * Clean up failed upload
+	 */
+	cleanupFailedUpload(uploadId, fieldId) {
+		const field = this.fields.get(fieldId);
+		if (field?.uploads) {
+			field.uploads.delete(uploadId);
+		}
+
+		const upload = this.uploads.get(uploadId);
+		if (upload) {
+			// Clean up preview URL
+			if (upload.preview?.startsWith('blob:')) {
+				URL.revokeObjectURL(upload.preview);
+			}
+
+			// Remove element
+			upload.element?.remove();
+
+			// Remove from uploads
+			this.uploads.delete(uploadId);
+		}
+
+		// Remove from active tasks
+		this.worker.tasks.delete(uploadId);
+	}
+	/*******************************************************************************
+	 UI FUNCTIONALITY
+	*******************************************************************************/
+	/**
+	 * Update upload status correctly
+	 */
+	updateUploadStatus(uploadId, status) {
+		let upload = this.uploads.get(uploadId);
+		if(!upload) {
+			return;
+		}
+		upload.status = status;
+
+		this.updateImageUI(upload.id);
+		this.persistFieldState(upload.fieldId);
+	}
+	updateImageUI(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload?.element) return;
+
+
+		const progressEl = upload.element.querySelector('.progress');
+		const itemEl = upload.element;
+
+		// Update status class on item for CSS styling
+		if (itemEl) {
+			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
+			itemEl.classList.add(`status-${upload.status}`);
+		}
+
+		if (progressEl) {
+			let icon = this.getStatusIcon(upload.status);
+			let message = this.getStatusText(upload.status);
+			let progress = this.getStatusProgress(upload.status);
+
+			const fill = progressEl.querySelector('.fill');
+			const itemIcon = progressEl.querySelector('span.icon');
+			const itemMessage = progressEl.querySelector('span.details');
+
+			if (fill) {
+				fill.style.width = `${progress}%`;
+			}
+			if (itemMessage) itemMessage.textContent = message;
+			if (itemIcon) {
+				window.removeChildren(itemIcon);
+				itemIcon.append(icon);
+			}
+
+			if (upload.status === 'completed') {
+				setTimeout(() => {
+					if (progressEl) {
+						window.fade(progressEl, false);
+					}
+				}, 1000);
+			}
+		}
+	}
+	/**
+	 * Hide the uploader drop zone if we have reached our limit
+	 */
+	maybeLockUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (field.ui.field.dropZone) {
+			const hasUploads = field.uploads && field.uploads.size > 0;
+			const atMaxFiles = field.uploads && field.uploads.size >= field.maxFiles;
+
+			// Hide if we have uploads OR if we're at max files
+			field.ui.field.dropZone.hidden = hasUploads || atMaxFiles;
+		}
+	}
+	createImageElement(upload, draggable = false) {
+		let image = window.getTemplate('uploadItem');
+		if (!image) {
+			console.error('Image template not found');
+			return;
+		}
+		image.dataset.uploadId = upload.id;
+		if (upload.originalFile) {
+			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
+		}
+
+
+		image.querySelector('[name="featured"]').value = upload.id;
+		let [
+			featured,
+			img,
+			video,
+			preview,
+			details
+		] = [
+			image.querySelector('[name="featured"]'),
+			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
+			image.querySelector('details')
+		];
+		[
+			featured.value,
+			img.src,
+			img.alt
+		] = [
+			upload.id,
+			upload.preview,
+			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
+		];
+
+		switch (image.dataset.subtype) {
+			case 'image':
+				[
+					img.src,
+					img.alt
+				] = [
+					upload.preview,
+					upload.originalFile?.name ?? upload.meta?.originalName?? ''
+				];
+				video.remove();
+				preview.remove();
+				break;
+			case 'video':
+				video.src = upload.preview;
+				img.remove();
+				preview.remove();
+				break;
+			case 'document':
+				const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
+				const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
+				let icon;
+				switch (extension) {
+					case 'pdf':
+						icon = window.getIcon('file-pdf');
+						break;
+					case 'csv':
+						icon = window.getIcon('file-csv');
+						break;
+					case 'doc':
+						icon = window.getIcon('file-doc');
+						break;
+					case 'txt':
+						icon = window.getIcon('file-txt');
+						break;
+					case 'xls':
+						icon = window.getIcon('file-xls');
+						break;
+					default:
+						icon = window.getIcon('file');
+						break;
+				}
+
+				preview.innerText = upload.originalFile.name;
+				preview.prepend(icon);
+				img.remove();
+				video.remove();
+				break;
+		}
+		if (details) {
+			let template = window.getTemplate('uploadMeta');
+			if (template){
+				details.append(template);
+			}
+		}
+		image.draggable = draggable;
+
+		// Update input IDs safely
+		image.querySelectorAll('input').forEach(input => {
+			let id = input.id;
+			if (id) {
+				let newId = id + upload.id;
+				let label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) {
+					label.htmlFor = newId;
+				}
+			}
+		});
+
+		return image;
+	}
+
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		let progressBar = field.ui.field.progress.progress;
+
+		// Create progress bar if it doesn't exist
+		if (!progressBar) {
+			progressBar = window.getTemplate('imageProgress');
+
+			if (!progressBar) {
+				console.warn('Progress bar template not found');
+				return;
+			}
+
+			// Insert after drop zone or at top of container
+			const container = field.ui.field.field;
+			const insertAfter = field.ui.field.dropZone;
+
+			if (insertAfter) {
+				insertAfter.insertAdjacentElement('afterend', progressBar);
+			} else if (container) {
+				container.prepend(progressBar);
+			}
+
+			// Update the field UI reference to match actual structure
+			if (!field.ui.field.progress) {
+				field.ui.field.progress = {};
+			}
+			field.ui.field.progress = {
+				progress: progressBar,
+				bar: progressBar.querySelector('.bar'),
+				fill: progressBar.querySelector('.fill'),
+				details: progressBar.querySelector('.details'),
+				text: progressBar.querySelector('.details .text'),
+				count: progressBar.querySelector('.details .count')
+			};
+		}
+
+
+		progressBar.hidden = false;
+		progressBar.style.display = 'flex';
+		progressBar.style.animation = 'none';
+		progressBar.style.opacity = '1';
+
+		// Update progress bar
+		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
+		const progressFill = field.ui.field.progress.fill;
+		const progressText = field.ui.field.progress.text;
+		const progressCount = field.ui.field.progress.count;
+
+		if (progressFill) {
+			progressFill.style.width = `${progressPercent}%`;
+		}
+
+		if (progressText) {
+			progressText.textContent = message;
+		}
+
+		if (progressCount) {
+			progressCount.textContent = `${current}/${total}`;
+		}
+
+		// Hide when complete
+		if (current >= total) {
+			setTimeout(() => {
+				progressBar.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => {
+					progressBar.hidden = true;
+					progressBar.style.display = 'none';
+				}, 300);
+			}, 1000);
+		}
+	}
+
+	hideUploadProgress(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		const progressBar = field.ui.field.progress.progress;
+		if (progressBar) {
+			window.fade(progressBar, false);
+		}
+	}
+	/*******************************************************************************
+	 INDEXEDDB CACHE FUNCTIONALITY
+	*******************************************************************************/
+	async initDB() {
+		if (!('indexedDB' in window)) return;
+
+		const request = indexedDB.open(`jvb_uploads_db`, 1);
+
+		request.onupgradeneeded = (e) => {
+			const db = e.target.result;
+			if (!db.objectStoreNames.contains('fieldStates')) {
+				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
+				store.createIndex('timestamp', 'timestamp', { unique: false });
+				store.createIndex('content', 'content', { unique: false });
+				store.createIndex('itemId', 'itemId', { unique: false });
+			}
+
+			// Blob storage remains separate for performance
+			if (!db.objectStoreNames.contains('uploadBlobs')) {
+				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
+			}
+		};
+
+		request.onsuccess = (e) => {
+			this.db = e.target.result;
+			this.loadFields();
+			this.checkPendingUploads();
+		};
+
+		request.onerror = (e) => {
+			console.error('IndexedDB error:', e);
+		};
+	}
+
+	async loadFields() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
+			const fieldStates = tx.objectStore('fieldStates');
+			const blobStore = tx.objectStore('uploadBlobs');
+			const request = fieldStates.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(field => {
+					let uploads = field.uploads;
+					let uploadIds = uploads.map(upload => upload.id);
+					field.uploads = new Set(uploadIds);
+					this.fields.set(field.key, field);
+					uploads.forEach(upload => {
+						this.uploads.set(upload.id, upload);
+					});
+				});
+				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
+				resolve();
+			};
+
+			const blobRequest = blobStore.getAll();
+
+			blobRequest.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					this.uploadBlobs.set(item.id, item);
+				});
+				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
+				resolve();
+			};
+		});
+	}
+
+	getUpload(uploadId) {
+		return this.uploads.get(uploadId);
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		field.uploads.forEach(upload => {
+			this.updateUploadStatus(upload, status);
+		});
+
+		// Update UI based on status
+		const container = field.ui.field.field;
+		if (container) {
+			container.dataset.uploadStatus = status;
+
+			// Show/hide relevant UI elements
+			const submitBtn = container.querySelector('.submit-uploads');
+			if (submitBtn) {
+				submitBtn.disabled = status === 'uploading' || status === 'processing';
+			}
+		}
+	}
+
+	/**
+	 * Handle successful upload completion
+	 */
+	handleUploadComplete(operation) {
+		const response = operation.response;
+		if (!response?.uploads) return;
+
+		response.uploads.forEach(serverUpload => {
+			const upload = this.uploads.get(serverUpload.upload_id);
+			if (upload) {
+				upload.attachmentId = serverUpload.attachment_id;
+				this.updateUploadStatus(serverUpload.upload_id, 'completed');
+				this.uploads.set(upload.id, upload);
+
+				// **ADD: Cleanup after successful upload**
+				this.clearUpload(upload.id);
+			}
+		});
+
+		const fieldKey = operation.data.get('field_key');
+		if (fieldKey) {
+			// **ADD: Clear field cache after all uploads complete**
+			const field = this.fields.get(fieldKey);
+			const allComplete = Array.from(field.uploads).every(id => {
+				const upload = this.uploads.get(id);
+				return upload?.status === 'completed';
+			});
+
+			if (allComplete) {
+				this.clearField(fieldKey);
+			}
+		}
+	}
+
+	/**
+	 * Store upload with DataStore integration
+	 */
+	async setUpload(fieldId, file, uploadId = null) {
+		if (!uploadId) {
+			uploadId = this.generateUploadId();
+		}
+		const upload = {
+			id: uploadId,
+			fieldId: fieldId,
+			groupId: null,
+			originalFile: file,
+			processedFile: null,
+			status: 'received',
+			progress: { percent: 0, message: 'Received...' },
+			preview: URL.createObjectURL(file),
+			createdAt: Date.now(),
+			meta: {
+				title: '',
+				alt_text: '',
+				caption: '',
+				originalName: file.name,
+				originalType: file.type,
+				originalSize: file.size
+			},
+			changes: {}
+		};
+
+		// Add to field
+		const field = this.fields.get(fieldId);
+		if (!field) {
+			console.error(`Field ${fieldId} not found`);
+			return null;
+		}
+		if (!field.uploads) field.uploads = new Set();
+		field.uploads.add(uploadId);
+
+		upload.element = this.createImageElement(upload, field.type==='groupable');
+		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
+
+		// Store in memory
+		this.uploads.set(uploadId, upload);
+		this.updateImageUI(uploadId);
+
+		// Persist to DataStore
+		await this.persistFieldState(fieldId);
+
+		return upload;
+	}
+
+	/**
+	 * Get uploads for a field, optionally cleaned for storage
+	 * @param {string} fieldId
+	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
+	 * @returns {Array}
+	 */
+	getFieldUploads(fieldId, clean = false) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.uploads) return [];
+
+		return Array.from(field.uploads)
+			.map(uploadId => {
+				const upload = this.uploads.get(uploadId);
+				if (!upload) return null;
+
+				if (clean) {
+					// Return cleaned version without DOM references
+					return {
+						id: upload.id,
+						fieldId: upload.fieldId,
+						status: upload.status,
+						preview: upload.preview,
+						attachmentId: upload.attachmentId,
+						operationId: upload.operationId,
+						groupId: upload.groupId || null,
+						meta: {
+							originalName: upload.meta?.originalName || upload.originalFile?.name,
+							size: upload.meta?.size || upload.originalFile?.size,
+							type: upload.meta?.type || upload.originalFile?.type,
+							title: upload.meta?.title,
+							alt: upload.meta?.alt,
+							caption: upload.meta?.caption
+						}
+					};
+				}
+
+				// Return full upload object
+				return upload;
+			})
+			.filter(Boolean);
+	}
+
+	/**
+	 * Persist upload to DataStore
+	 */
+	async persistFieldState(fieldId) {
+		if (!this.db) return;
+
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Create clean field config
+		const { ui, ...cleanConfig } = field;
+
+		const fieldState = {
+			fieldId: fieldId,
+			timestamp: Date.now(),
+
+			config: {
+				...cleanConfig,
+				fieldName: field.name,
+				dataField: field.ui?.field?.field?.dataset?.field
+			},
+
+			// Recovery context with normalized URL
+			context: {
+				url: this.normalizeUrl(window.location.href),
+				fullUrl: window.location.href, // Keep for reference
+				modalType: this.getModalType(field),
+				formId: field.formId,
+				// **FIX**: Store additional identifiers
+				fieldSelector: `.field.upload[data-field="${field.name}"]`
+			},
+
+			// Uploads (cleaned of DOM references and blob URLs)
+			uploads: this.getFieldUploads(fieldId, true).map(upload => {
+				// **FIX**: Don't store blob URLs as they become invalid
+				const { preview, element, location, ...cleanUpload } = upload;
+				return cleanUpload;
+			}),
+
+			// Groups structure
+			groups: Array.from(this.groups.entries())
+				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
+				.map(([id, data]) => ({
+					id: data.id,
+					uploads: Array.from(data.uploads),
+					meta: data.meta || {},
+					changes: data.changes || {}
+				}))
+		};
+
+		try {
+			const tx = this.db.transaction(['fieldStates'], 'readwrite');
+			await tx.objectStore('fieldStates').put(fieldState);
+		} catch (error) {
+			console.error('Failed to persist field state:', error);
+		}
+	}
+
+	normalizeUrl(url) {
+		try {
+			const urlObj = new URL(url);
+			// Return just the origin + pathname (no query string or hash)
+			return urlObj.origin + urlObj.pathname;
+		} catch (e) {
+			return url;
+		}
+	}
+	/*******************************************************************************
+	 RESTORE FUNCTIONALITY
+	*******************************************************************************/
+	async checkPendingUploads() {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const fieldStore = tx.objectStore('fieldStates');
+
+		const allFieldStates = await new Promise(resolve => {
+			const request = fieldStore.getAll();
+			request.onsuccess = () => resolve(request.result);
+		});
+
+
+		allFieldStates.forEach(field => {
+			console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
+			field.uploads.forEach((upload, idx) => {
+				console.log(`  Upload ${idx}:`, {
+					id: upload.id,
+					status: upload.status,
+					operationId: upload.operationId,
+					hasOperationId: !!upload.operationId
+				});
+			});
+		});
+
+		// Filter for pending uploads (not yet sent to server)
+		const pendingFields = allFieldStates.filter(field =>
+			field.uploads.some(upload =>
+				// If no operationId, it hasn't been sent to server yet
+				!upload.operationId &&
+				// And it's been processed locally
+				(upload.status === 'completed' ||
+					upload.status === 'processed' ||
+					upload.status === 'local_processing' ||
+					upload.status === 'processed-original')
+			)
+		);
+
+		console.log('Pending Fields: ', pendingFields);
+
+		if (pendingFields.length === 0) return;
+
+		// Show recovery notification
+		this.showRecoveryNotification(pendingFields);
+	}
+
+	async showRecoveryNotification(pendingFields) {
+		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+		const totalGroups = pendingFields.reduce((sum, field) =>
+			sum + (field.groups?.length || 0), 0);
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			console.error('Restore notification template not found');
+			return;
+		}
+
+		// Build appropriate message
+		let message = '';
+		if (totalGroups > 0) {
+			let group = totalGroups > 1 ? 'groups' : 'group';
+			let upload = totalUploads > 1 ? 'uploads' : 'upload';
+			message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+		} else {
+			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+		}
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) {
+			detailsEl.textContent = message;
+		}
+
+		// Build the restoration preview
+		for (const field of pendingFields) {
+			let fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			// Set field name/title
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) {
+				titleEl.textContent = field.config.name || 'Unnamed Field';
+			}
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			// Process each upload
+			for (const upload of field.uploads) {
+
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+			//
+			// 	const imgEl = uploadItem.querySelector('img');
+			// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+			//
+				const blobData = await this.getBlobData(upload.id);
+
+
+				if (blobData) {
+					try {
+						// Create new blob URL from stored data
+						const blob = new Blob([blobData.data], { type: blobData.type });
+						const previewUrl = URL.createObjectURL(blob);
+
+						let [
+							featured,
+							img,
+							video,
+							preview,
+							details
+						] = [
+							uploadItem.querySelector('[name="featured"]'),
+							uploadItem.querySelector('img'),
+							uploadItem.querySelector('video'),
+							uploadItem.querySelector('label > span'),
+							uploadItem.querySelector('details')
+						];
+
+						uploadItem.dataset.uploadId = upload.id;
+						uploadItem.dataset.fieldId = field.config.key;
+
+						let subtype = this.getSubtypeFromMime(blobData.type);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									upload.originalFile?.name ?? upload.meta?.originalName?? ''
+								];
+								video.remove();
+								preview.remove();
+								break;
+							case 'video':
+								video.src = previewUrl;
+								img.remove();
+								preview.remove();
+								break;
+							case 'document':
+								let extension = '';
+								let icon;
+								switch (extension) {
+									case 'pdf':
+										icon = window.getIcon('file-pdf');
+										break;
+									case 'csv':
+										icon = window.getIcon('file-csv');
+										break;
+									case 'doc':
+										icon = window.getIcon('file-doc');
+										break;
+									case 'txt':
+										icon = window.getIcon('file-txt');
+										break;
+									case 'xls':
+										icon = window.getIcon('file-xls');
+										break;
+									default:
+										icon = window.getIcon('file');
+										break;
+								}
+
+								preview.innerText = upload.originalFile.name;
+								preview.prepend(icon);
+								img.remove();
+								video.remove();
+								break;
+						}
+
+						// Store URL for cleanup later
+						uploadItem.dataset.previewUrl = previewUrl;
+					} catch (error) {
+						console.warn('Failed to create preview for upload:', upload.id, error);
+					}
+				}
+
+				// Set upload metadata
+				const nameEl = uploadItem.querySelector('summary span');
+				if (nameEl) {
+					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+				}
+
+				const metaEl = uploadItem.querySelector('details');
+				if (metaEl && upload.meta) {
+					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+				}
+
+				// Update input IDs safely
+				uploadItem.querySelectorAll('input').forEach(input => {
+					let id = input.id;
+					if (id) {
+						let newId = id + upload.id;
+						let label = input.parentNode.querySelector(`label[for="${id}"]`);
+						input.id = newId;
+						if (label) {
+							label.htmlFor = newId;
+						}
+					}
+				});
+
+				if (itemGrid) {
+					itemGrid.appendChild(uploadItem);
+				}
+			}
+
+			notification.querySelector('.wrap').appendChild(itemGrid);
+		}
+
+		document.querySelector('.field.upload').appendChild(notification);
+		notification = document.querySelector('dialog.restore-uploads');
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			ui: {
+				selectAll: notification.querySelector('#select-all-restore'),
+				count: notification.querySelector('.selection-count'),
+			},
+		});
+
+		this.restoreModal.handleOpen();
+
+	}
+
+	async cleanupStoredRestoration() {
+		if (!this.db) return;
+
+		const notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) return;
+
+		// Get all upload IDs from the notification
+		const items = notification.querySelectorAll('[data-upload-id]');
+		const uploadIds = Array.from(items).map(item => item.dataset.uploadId);
+
+		// Clean up blob URLs in the notification
+		this.cleanupRestoreNotificationUrls(notification);
+
+		// **Delete blob data from IndexedDB**
+		if (uploadIds.length > 0) {
+			const tx = this.db.transaction(['uploadBlobs', 'fieldStates'], 'readwrite');
+
+			// Delete all blob data
+			uploadIds.forEach(uploadId => {
+				tx.objectStore('uploadBlobs').delete(uploadId);
+			});
+
+			// Also delete field states
+			const fieldIds = Array.from(items).map(item => item.dataset.fieldId);
+			const uniqueFieldIds = [...new Set(fieldIds)];
+
+			uniqueFieldIds.forEach(fieldId => {
+				if (fieldId) {
+					tx.objectStore('fieldStates').delete(fieldId);
+				}
+			});
+
+			await tx.complete;
+		}
+	}
+
+	cleanupRestoreNotificationUrls(notification) {
+		if (!notification) return;
+
+		// Find all elements with preview URLs
+		const items = notification.querySelectorAll('[data-preview-url]');
+		items.forEach(item => {
+			const url = item.dataset.previewUrl;
+			if (url && url.startsWith('blob:')) {
+				URL.revokeObjectURL(url);
+				delete item.dataset.previewUrl;
+			}
+		});
+	}
+
+	getSelectedRestorationUploads(notificationEl) {
+		let selected = [];
+		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		return selected;
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		// Group by field
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		// Get full field states from IndexedDB
+		if (!this.db) {
+			// this.notifications.add('Cannot restore: Database not available', 'error');
+			return;
+		}
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const store = tx.objectStore('fieldStates');
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const request = store.get(fieldId);
+			const fieldState = await new Promise(resolve => {
+				request.onsuccess = () => resolve(request.result);
+				request.onerror = () => resolve(null);
+			});
+
+			if (fieldState) {
+				// Filter to only selected uploads
+				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
+				await this.restoreField(fieldState);
+			}
+		}
+
+		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
+	}
+
+	async restoreField(fieldState) {
+		const { config, context, uploads, groups } = fieldState;
+
+		// If in a modal, open it first
+		if (context.modalType) {
+			await this.openModalForRestore(context);
+		}
+
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+		if (!fieldElement) {
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
+			return;
+		}
+
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fields.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement, config);
+		}
+
+		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		if (!field.ui.groups) {
+			field.ui.groups = {};
+		}
+		if (!field.ui.groups.groups) {
+			field.ui.groups.groups = new Map();
+		}
+
+		// Make sure we have the container and empty group references
+		if (!field.ui.groups.container) {
+			field.ui.groups.container = fieldElement.querySelector('.item-grid.groups');
+		}
+		if (!field.ui.groups.empty) {
+			field.ui.groups.empty = fieldElement.querySelector('.empty-group');
+		}
+		let display = fieldElement.querySelector('.group-display');
+		if (display) {
+			display.hidden = false;
+		}
+
+		// Restore uploads
+		for (const uploadData of uploads) {
+			await this.restoreUpload(field, uploadData);
+		}
+
+		// Restore groups
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(field, groups, uploads);
+		}
+
+		// Update UI
+		this.updateFieldState(fieldKey);
+		this.maybeLockUploads(fieldKey);
+
+		await this.persistFieldState(fieldKey);
+
+		// Queue for upload if needed (should not happen for post_group)
+		if (config.mode === 'direct' && config.destination !== 'post_group') {
+			await this.queueUpload(fieldKey);
+		}
+	}
+
+	async restoreUpload(field, uploadData) {
+		// Try to get blob data from IndexedDB
+		const blobData = await this.getBlobData(uploadData.id);
+
+		if (blobData) {
+			const file = blobData.data instanceof File
+				? blobData.data
+				: new File(
+					[blobData.data],
+					blobData.name,
+					{ type: blobData.type, lastModified: blobData.lastModified }
+				);
+
+			uploadData.originalFile = file;
+			uploadData.processedFile = file;
+			uploadData.preview = URL.createObjectURL(file);
+		} else {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return; // Skip this upload if we can't restore the file
+		}
+
+		// Add to field
+		if (!field.uploads) field.uploads = new Set();
+		field.uploads.add(uploadData.id);
+
+		// Recreate DOM element
+		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
+		uploadData.element = this.createImageElement({
+			...uploadData,
+			subtype: subtype
+		}, field.destination === 'post_group');
+
+		// Restore to correct location
+		let location;
+		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
+			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
+		} else {
+			location = field.ui.field.preview;
+		}
+
+		if (location) {
+			location.appendChild(uploadData.element);
+			uploadData.location = location;
+		}
+
+		// Store in memory
+		this.uploads.set(uploadData.id, uploadData);
+	}
+
+	async restoreFieldStates(fieldStates) {
+		// Group by URL
+		const byUrl = new Map();
+		fieldStates.forEach(field => {
+			if (!byUrl.has(field.context.url)) {
+				byUrl.set(field.context.url, []);
+			}
+			byUrl.get(field.context.url).push(field);
+		});
+
+		// If all on current page, restore directly
+		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
+			for (const fieldState of fieldStates) {
+				await this.restoreField(fieldState);
+			}
+			// this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
+		} else {
+			// Store intent to restore and navigate
+			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
+
+			// Navigate to first URL
+			const firstUrl = byUrl.keys().next().value;
+			if (window.location.href !== firstUrl) {
+				window.location.href = firstUrl;
+			}
+		}
+	}
+
+	async restoreGroups(field, groups, uploads) {
+		// Ensure the groups.groups Map exists
+		if (!field.ui.groups.groups) {
+			field.ui.groups.groups = new Map();
+		}
+
+		for (const groupData of groups) {
+			// Create group element
+			const groupElement = this.createGroupElement(groupData.id, field.key);
+
+			// Store in field UI Map
+			field.ui.groups.groups.set(groupData.id, groupElement);
+
+			// Insert into DOM
+			if (field.ui.groups.container && field.ui.groups.empty) {
+				field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
+			} else if (field.ui.groups.container) {
+				field.ui.groups.container.appendChild(groupElement);
+			}
+
+			this.groups.set(groupData.id, {
+				id: groupData.id,
+				fieldId: field.key,
+				element: groupElement,
+				uploads: new Set(groupData.uploads), // FIXED: was groupData.uploadIds
+				meta: groupData.meta || {},
+				changes: groupData.changes || {}
+			});
+
+			// Move uploads to group
+			groupData.uploads.forEach(uploadId => {
+				const upload = uploads.find(u => u.id === uploadId);
+				if (upload && upload.element) {
+					const groupGrid = groupElement.querySelector('.item-grid');
+					if (groupGrid) {
+						groupGrid.appendChild(upload.element);
+						upload.location = groupGrid;
+						upload.groupId = groupData.id;
+					}
+				}
+			});
+		}
+	}
+
+	async getBlobData(uploadId) {
+		if (!this.db) return null;
+
+		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
+		const request = tx.objectStore('uploadBlobs').get(uploadId);
+
+		return new Promise(resolve => {
+			request.onsuccess = () => resolve(request.result);
+			request.onerror = () => resolve(null);
+		});
+	}
+
+	async openModalForRestore(context) {
+		const { modalType, formId } = context;
+
+		// Find and click the appropriate button to open the modal
+		let trigger = null;
+
+		switch(modalType) {
+			case 'create':
+				trigger = document.querySelector('[data-action="create"]');
+				break;
+			case 'edit':
+				// Need to find the specific edit button
+				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
+				break;
+			case 'bulkEdit':
+				trigger = document.querySelector('[data-action="bulk-edit"]');
+				break;
+		}
+
+		if (trigger) {
+			trigger.click();
+
+			// Wait for modal to open
+			await new Promise(resolve => setTimeout(resolve, 300));
+		}
+	}
+	/*******************************************************************************
+	 GROUP FUNCTIONALITY
+	 Includes selection, dragging, and grouping logic
+	*******************************************************************************/
+	/**
+	 *
+	 * @param {string} uploadId as defined by setUpload
+	 * @param {HTMLElement|null} target The target location
+	 * @param {boolean} persist whethet to cache this change
+	 */
+	addImageToGroup(uploadId, target = null, persist = true) {
+		let upload = this.getUpload(uploadId);
+		if(!upload) {
+			return;
+		}
+		let field = this.fields.get(upload.fieldId);
+		if (!field) {
+			return;
+		}
+
+		//Already in the Preview Grid, or already in the group we're moving to
+		if ((!target && upload.location === field.ui.field.preview) || target === upload.location) {
+			return;
+		}
+
+		// Remove from previous location
+		if (upload.location) {
+			let groupId = upload.location.dataset.groupId;
+			if (groupId) {
+				let group = this.groups.get(groupId);
+				if (group && group.uploads) {
+					group.uploads.delete(uploadId);
+
+					if (group.uploads.size === 0) {
+						this.removeGroup(groupId);
+					}
+				}
+			}
+		}
+
+		const checkbox = upload.element.querySelector('[name*="select-item"]');
+		if (checkbox) {
+			checkbox.checked = false;
+		}
+
+		upload.element.querySelector('[name="featured"]').hidden = !target;
+
+		//If no target, it's going to the preview grid
+		if (!target) {
+			target = field.ui.field.preview;
+		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
+			// It's a group target
+			let groupId = target.dataset.groupId;
+			let group = this.groups.get(groupId);
+			if (!group) {
+				group = this.createGroup(upload.fieldId);
+				target = group.grid;
+			}
+			if (group) {
+				group.uploads.add(uploadId);
+			}
+		}
+
+		upload.location = target;
+		target.append(upload.element);
+
+		if (persist) {
+			this.persistFieldState(field.key);
+		}
+	}
+
+	addSelectionToGroup(target) {
+		let field = this.getFieldFromElement(target);
+		if (!field) {
+			return;
+		}
+		let currentSelection = this.getCurrentSelection(field.key);
+		if (currentSelection.length === 0 ) {
+			return;
+		}
+
+		let group = this.getGroupFromElement(target);
+		if (!group && target !== field.ui.field.preview) {
+			group = this.createGroup(field.key);
+		}
+
+		currentSelection.forEach(uploadId => {
+			this.addImageToGroup(uploadId, group.grid??null, false);
+		});
+
+		this.persistFieldState(group.fieldId);
+	}
+
+	getCurrentSelection(fieldId) {
+		let selected = [];
+		for (var [key, handler] of this.selectionHandlers) {
+			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
+				selected = selected.concat([... handler.selectedItems]);
+			}
+		}
+		return selected;
+	}
+
+	/**
+	 * Remove an empty group from the field
+	 * @param {string} groupId - The group to remove
+	 * @param {boolean} confirm - ask for confirmation
+	 */
+	removeGroup(groupId, confirm = false) {
+		let group = this.groups.get(groupId);
+		if (!group) {
+			return;
+		}
+
+		if (confirm && group.uploads && group.uploads.size > 0) {
+			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
+				return;
+			}
+		}
+
+		// Move any remaining uploads back to preview
+		if (group.uploads && group.uploads.size > 0) {
+			Array.from(group.uploads).forEach(uploadId => {
+				this.addImageToGroup(uploadId, null, false);
+			});
+		}
+
+		// Remove from groups Map
+		this.groups.delete(groupId);
+
+		// Remove DOM element
+		let groupElement = group.element;
+		if (groupElement) {
+			groupElement.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		this.persistFieldState(group.fieldId);
+	}
+
+	/**
+	 * Create a new group
+	 */
+	createGroup(fieldKey) {
+		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Field not found:', fieldKey);
+			return null;
+		}
+
+		const groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+		const groupElement = this.createGroupElement(groupId, fieldKey);
+		if (!groupElement) {
+			console.error('Failed to create group element');
+			return null;
+		}
+
+		// Store in field UI Map
+		if (!field.ui.groups) {
+			field.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		field.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (field.ui.groups.container && field.ui.groups.empty) {
+			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
+		} else if (field.ui.groups.container) {
+			field.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Create group object
+		const group = {
+			id: groupId,
+			fieldId: fieldKey,
+			element: groupElement,
+			grid: groupElement.querySelector('.item-grid.group'),
+			uploads: new Set(),
+			meta: {},
+			changes: {}
+		};
+
+		// Store group
+		this.groups.set(groupId, group);
+
+		// Initialize selection handler for this group
+		this.addGroupSelectionHandler(fieldKey, groupId);
+
+		// Persist state
+		this.persistFieldState(fieldKey);
+
+		return group;
+	}
+
+
+	/**
+	 * Remove upload from group
+	 */
+	removeFromGroup(fieldId, uploadId, groupId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const group = field.groups.find(g => g.id === groupId);
+		if (!group) return;
+
+		group.uploads = group.uploads.filter(id => id !== uploadId);
+
+		this.renderGroupUI(fieldId);
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Update group title
+	 */
+	updateGroupTitle(fieldId, groupId, title) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const group = field.groups.find(g => g.id === groupId);
+		if (!group) return;
+
+		group.title = title;
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Delete group
+	 */
+	deleteGroup(fieldId, groupId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		field.groups = field.groups.filter(g => g.id !== groupId);
+
+		this.renderGroupUI(fieldId);
+		this.removeSelectionHandler(fieldId, groupId);
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Render group UI
+	 */
+	renderGroupUI(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const container = field.ui.group.container;
+		if (!container) {
+			console.warn('Groups container not found for field:', fieldId);
+			return;
+		}
+
+		// Clear existing
+		window.removeChildren(container);
+
+		// Render each group
+		field.groups.forEach(group => {
+			const groupEl = this.createGroupElement(fieldId, group);
+			container.appendChild(groupEl);
+		});
+	}
+
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			// Set unique IDs and names for form fields
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+			let field = this.fields.get(fieldId);
+			if (field.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				summary.textContent = field.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details').remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	handleSelectAll(element, checked = null) {
+		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
+	}
+
+	clearAllSelections(field) {
+		const handler = this.selectionHandlers.get(field.key);
+		if (handler) {
+			handler.clearSelection();
+		}
+	}
+
+	getSelectedUploads(element) {
+		const field = this.getFieldFromElement(element);
+		if (!field) return [];
+
+		const handler = this.selectionHandlers.get(field.key);
+		return handler ? handler.getSelected() : [];
+	}
+
+	removeSelection(button) {
+		let fieldId = this.getFieldIdFromElement(button);
+
+		const selectedUploads = this.getSelectedUploads(button);
+		if (selectedUploads.length === 0) {
+			this.notify('No uploads selected', 'warning');
+			return;
+		}
+
+		selectedUploads.forEach(upload => {
+			this.removeUpload(fieldId, upload);
+		});
+	}
+
+	removeUpload(fieldId, uploadId) {
+		const field = this.fields.get(fieldId);
+		const upload = this.uploads.get(uploadId);
+
+		if (!field || !upload) return;
+
+		// Remove from field
+		field.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group && group.uploads) {
+				group.uploads.delete(uploadId);
+
+				if (group.uploads.size === 0) {
+					this.removeGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		upload.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state after removal
+		this.updateFieldState(fieldId);
+
+		// Update UI
+		this.maybeLockUploads(fieldId);
+		const handler = this.selectionHandlers.get(field.key);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	/**************************************************************************
+	 META
+	 Handled separately, in case it is edited in the middle of processing images
+	**************************************************************************/
+
+	/**************************************************************************
+	 SUBSCRIBERS
+	**************************************************************************/
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	handleBeforeUnload(e) {
+		// Check for any uploads in processing or pending state
+		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
+			upload.status === 'processing' ||
+			upload.status === 'pending' ||
+			upload.status === 'uploading'
+		);
+
+		if (unsavedUploads.length > 0) {
+			const message = 'You have uploads in progress. Are you sure you want to leave?';
+			e.preventDefault();
+			e.returnValue = message;
+			return message;
+		}
+	}
+	/**************************************************************************
+	 CLEANUP
+	**************************************************************************/
+	cleanup() {
+		this.clearListeners();
+		if (this.hasGroups) {
+			this.clearGroupListeners();
+		}
+		this.compressionWorker = null;
+		this.subscribers.clear();
+	}
+
+	/**
+	 * Clear individual upload from cache after successful server upload
+	 */
+	async clearUpload(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		// Clean up preview URL
+		if (upload.preview && upload.preview.startsWith('blob:')) {
+			URL.revokeObjectURL(upload.preview);
+			upload.preview = null;
+		}
+
+		// Clean up element preview URL
+		if (upload.element) {
+			const previewUrl = upload.element.dataset.previewUrl;
+			if (previewUrl && previewUrl.startsWith('blob:')) {
+				URL.revokeObjectURL(previewUrl);
+				delete upload.element.dataset.previewUrl;
+			}
+		}
+
+		this.persistFieldState(upload.fieldId);
+		// Remove from memory
+		this.uploads.delete(uploadId);
+		this.uploadBlobs.delete(uploadId);
+
+		// Remove from IndexedDB
+		if (this.db) {
+			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
+			await tx.objectStore('uploadBlobs').delete(uploadId);
+		}
+	}
+
+	/**
+	 * Clear all uploads for a field and cleanup resources
+	 */
+	clearField(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		const uploads = Array.from(field.uploads || []);
+
+		// Cleanup each upload's resources
+		uploads.forEach(uploadId => {
+			this.clearUpload(uploadId);
+			this.uploads.delete(uploadId);
+		});
+
+		// Clear field state
+		this.fields.delete(fieldId);
+
+		// Cleanup IndexedDB
+		if (this.db) {
+			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
+			tx.objectStore('fieldStates').delete(fieldId);
+			uploads.forEach(uploadId => {
+				tx.objectStore('uploadBlobs').delete(uploadId);
+			});
+		}
+	}
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+	window.jvbUploads = new UploadManager();
+});
diff --git a/assets/js/concise/UploadManagerOlder.js b/assets/js/concise/UploadManagerOlder.js
new file mode 100644
index 0000000..c51d8cd
--- /dev/null
+++ b/assets/js/concise/UploadManagerOlder.js
@@ -0,0 +1,4010 @@
+class UploadManager {
+	constructor() {
+		//Load dependencies
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+		this.notifications = window.jvbNotifications;
+
+		//Load Datastore
+		this.initDB();
+
+		//State management
+		this.fields = new Map();
+		this.uploads = new Map();
+		this.uploadBlobs = new Map();
+		this.timeouts = new Map();
+		this.selected = new Map();
+		this.dragState = null;
+		this.hasGroups = false;
+
+		this.selectionHandlers = new Map();
+
+		//Worker
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: {
+				count: 0,
+				max: 3,
+			},
+			settings: {
+				timeout: 10000, //10 seconds per image
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+
+		//Groups!
+		this.touch = {
+			x: null,
+			y: null
+		}
+		this.hasBulkContext = document.querySelector('details.uploader')!==null;
+		this.isTouching = false;
+		this.groups = new Map();
+
+		//Notification and Subscribers
+		this.subscribers = new Set();
+
+		this.settings = {
+			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
+			maxFileSize: 5242880,
+			maxProcessingTime: 120000, // 2 minutes max for processing
+			processingCheckInterval: 5000, // Check every 5 seconds
+			smartCompression: true,
+			fieldTypes: {
+				'single': { maxFiles: 1, allowMultiple: false },
+				'gallery': { maxFiles: 20, allowMultiple: true },
+				'groupable': { maxFiles: 20, allowMultiple: true }
+			}
+		};
+
+		this.acceptedTypes = {
+			image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
+			video: ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'],
+			document: [
+				'application/pdf',
+				'application/msword',
+				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+				'text/plain',
+				'text/csv'
+			]
+		};
+
+		this.maxSizes = {
+			image: 5 * 1024 * 1024,    // 5MB
+			video: 100 * 1024 * 1024,  // 100MB
+			document: 10 * 1024 * 1024 // 10MB
+		};
+
+		this.statusMapping = {
+			'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'
+		};
+
+		this.init();
+	}
+
+	async init() {
+		this.initElements();
+		this.initListeners();
+		this.initCompressionWorker();
+		this.queue.subscribe((event, operation) => {
+			console.log('Operation Endpoint: ', operation.endpoint);
+			if (operation.endpoint !== 'uploads') {
+				return;
+			}
+			switch(event) {
+				case 'cancel-operation':
+					this.clearField(operation.data.get('field_key'));
+					break;
+				case 'operation-status':
+					console.log('Operation Data: ',operation.data);
+					const fieldId = operation.data?.field_key ||
+						(operation.data instanceof FormData ?
+							operation.data.get('field_key') : null);
+
+					if (fieldId) {
+						console.log('Updating field status:', fieldId, operation.status);
+						this.updateFieldStatus(fieldId, operation.status);
+					}
+					break;
+			}
+		});
+		this.scanFields();
+	}
+
+	initElements() {
+		this.selectors = {
+			field: {
+				field: '.field.upload',
+				dropZone: '.file-upload-container',
+				preview: '.item-grid.preview',
+				hiddenValue: 'input[type="hidden"]',
+				progress: {
+					progress: '.progress',
+					details: '.progress .details',
+					fill: '.progress .fill',
+					count: '.progress .count'
+				},
+			},
+			item: {
+				img: 'img',
+				progress: {
+					progress: '.progress',
+					details: '.progress .details',
+					fill: '.progress .fill',
+					count: '.progress .count'
+				},
+				status: '.status',
+				select: '[name*="select-item"]',
+				actions: '.item-actions',
+				featured: '[name="featured"]',
+				meta: '.upload-meta'
+			},
+			groups: {
+				container: '.item-grid.groups',
+				display: '.group-display',
+				selectAll: '#select-all-uploads',
+				actions: '.selection-actions',
+				info: '.selection-controls .info',
+				count: '.selection-count',
+				group: '.upload-group',
+				empty: '.empty-group'
+			}
+		};
+		this.ui = {};
+	}
+
+	scanFields() {
+		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
+			this.registerUploader(uploader);
+		});
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} uploader
+	 * @param {object} options
+	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
+	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
+	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
+	 * @param {boolean} options.multiple Whether to allow multiple uploads
+	 * @param {number} options.itemID The post or term ID this is for.
+	 * @param {string} options.mode
+	 * @returns {string}
+	 */
+	registerUploader(uploader, options = {}) {
+		//Determine if this is for a post, term, content uploader, or option
+		let key = uploader.dataset['uploader']??this.determineKey(uploader);
+
+		uploader.dataset['uploader'] = key;
+
+		if (!this.fields.has(key)) {
+			let type = uploader.dataset.type??'single';
+
+			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
+			let config = {
+				key: key,
+				name: uploader.dataset.field,
+				ui: {},
+				type: type,
+				subtype: uploader.dataset.subtype??'image',
+				maxFiles: typeConfig.maxFiles,
+				multiple: typeConfig.allowMultiple,
+				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
+				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
+				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
+				mode: uploader.dataset.mode??'direct',
+				destination: uploader.dataset.destination ?? 'meta',
+				... options
+			};
+
+			config.ui = window.uiFromSelectors(this.selectors, uploader);
+			config.ui.groups.groups = new Map();
+
+			this.selected.set(key, new Set());
+			this.fields.set(key, config);
+			if(config.destination === 'post_group' && !this.hasGroups) {
+				this.initGroupListeners();
+			}
+			// Initialize selection handler for this field
+			this.initSelectionHandler(key, config);
+		}
+		return key;
+	}
+
+	initSelectionHandler(fieldKey) {
+		const field = this.fields.get(fieldKey);
+		if (!field) return;
+
+		// Don't reinitialize if already exists
+		if (this.selectionHandlers.has(fieldKey)) {
+			return this.selectionHandlers.get(fieldKey);
+		}
+
+		// Get the container - use preview for uploads in preview, or field for all uploads
+		const container = field.ui.field.preview || field.ui.field.field;
+		if (!container) {
+			console.warn('No container found for selection handler:', fieldKey);
+			return;
+		}
+
+		const handler = new window.jvbHandleSelection({
+			container: container,
+			ui: {
+				selectAll: field.ui.groups.selectAll,
+				bulkControls: field.ui.groups.actions,
+				count: field.ui.groups.count
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]',
+			onSelectionChange: (selectedItems) => {
+				// Sync with UploadManager's selected set
+				this.selected.set(fieldKey, selectedItems);
+
+				// Call any additional UI updates if needed
+				// this.onSelectionChanged(fieldKey, selectedItems);
+			}
+		});
+
+		this.selectionHandlers.set(fieldKey, handler);
+		return handler;
+	}
+
+	/**
+	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
+	 * @param uploader
+	 * @returns {string}
+	 */
+	determineKey(uploader) {
+		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
+		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
+		let field = uploader.dataset.field;
+		return `${content}_${itemID}_${field}`;
+	}
+
+	/**
+	 *
+	 * @param {HTMLElement} element
+	 */
+	getFieldIdFromElement(element) {
+		let field = element.closest('.field.upload');
+		if (!field) {
+			return;
+		}
+		return field.dataset.uploader??this.determineKey(field);
+	}
+
+	getFieldFromElement(element) {
+		let id = this.getFieldIdFromElement(element);
+		return (this.fields.has(id)) ? this.fields.get(id) : false;
+	}
+
+	getUploadFromElement(element) {
+		let id = this.getUploadIdFromElement(element);
+		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
+	}
+
+	getUploadIdFromElement(element) {
+		let upload = element.closest('[data-upload-id]');
+		return upload?.dataset.uploadId || null;
+	}
+
+	getGroupFromElement(element) {
+		let groupId = this.getGroupIdFromElement(element);
+		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
+	}
+	getGroupIdFromElement(element) {
+		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
+	}
+
+	getModalType(field) {
+		// Safety check for field.ui
+		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
+			return null;
+		}
+
+		const dialog = field.ui.field.field.closest('dialog');
+		if (!dialog) return null;
+
+		if (dialog.classList.contains('edit')) return 'edit';
+		if (dialog.classList.contains('create')) return 'create';
+		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
+
+		return dialog.className;
+	}
+
+	getStatusText(status) {
+		return this.statusMapping[status] || status;
+	}
+
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
+	}
+	getStatusProgress(status) {
+		console.log('Getting status progress for: ', status);
+		switch (status) {
+			case 'local_processing':
+				return 28;
+			case 'queued':
+				return 50;
+			case 'uploading':
+				return 66;
+			case 'pending':
+				return 75;
+			case 'processing':
+				return 89;
+			case 'completed':
+				return 100;
+			default:
+				return 0;
+		}
+	}
+
+	/******************************************************************************
+	 LISTENERS
+	 ******************************************************************************/
+	initListeners() {
+		this.clickHandler 		= this.handleClick.bind(this);
+		this.changeHandler 		= this.handleChange.bind(this);
+
+		if (this.hasBulkContext) {
+			this.pasteHandler 		= this.handlePaste.bind(this);
+			document.addEventListener('paste', this.pasteHandler);
+		}
+
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
+	}
+	clearListeners() {
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		if (this.hasBulkContext) {
+			document.removeEventListener('paste', this.pasteHandler);
+		}
+	}
+
+	initGroupListeners() {
+		this.hasGroups = true;
+
+		this.dragStartHandler 	= this.handleDragStart.bind(this);
+		this.dragEndHandler 	= this.handleDragEnd.bind(this);
+		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
+		this.dragOverHandler 	= this.handleDragOver.bind(this);
+		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
+		this.dropHandler 		= this.handleDrop.bind(this);
+
+		this.touchStartHandler 	= this.handleTouchStart.bind(this);
+		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
+		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
+		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
+
+		document.addEventListener('dragstart', this.dragStartHandler);
+		document.addEventListener('dragend', this.dragEndHandler);
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('drop', this.dropHandler);
+
+		document.addEventListener('touchstart', this.touchStartHandler);
+		document.addEventListener('touchmove', this.touchMoveHandler);
+		document.addEventListener('touchend', this.touchEndHandler);
+		document.addEventListener('touchcancel', this.touchCancelHandler);
+
+		document.addEventListener('input', (e) => {
+			if (e.target.matches('.fields.group input, .fields.group textarea')) {
+				this.handleGroupMetadataChange(e);
+			}
+		});
+	}
+	handleGroupMetadataChange(e) {
+		if (!e.target.closest('.fields.group')) return;
+
+		const groupElement = e.target.closest('[data-group-id]');
+		if (!groupElement) return;
+
+		const fieldId = groupElement.dataset.fieldId;
+		this.persistFieldState(fieldId);
+	}
+	clearGroupListeners() {
+		document.removeEventListener('dragstart', this.dragStartHandler);
+		document.removeEventListener('dragend', this.dragEndHandler);
+		document.removeEventListener('dragenter', this.dragEnterHandler);
+		document.removeEventListener('dragover', this.dragOverHandler);
+		document.removeEventListener('dragleave', this.dragLeaveHandler);
+		document.removeEventListener('drop', this.dropHandler);
+
+		document.removeEventListener('touchstart', this.touchStartHandler);
+		document.removeEventListener('touchmove', this.touchMoveHandler);
+		document.removeEventListener('touchend', this.touchEndHandler);
+		document.removeEventListener('touchcancel', this.touchCancelHandler);
+	}
+
+	handleClick(e) {
+		if (!e.target.closest(this.selectors.field.field)) {
+			return;
+		}
+
+		if (window.targetCheck(e, '.restart-uploads')) {
+			e.preventDefault();
+			const fieldId = this.getFieldIdFromElement(e.target);
+			this.restartUploads(fieldId);
+		} else if (window.targetCheck(e, '.dismiss-cache-restore')) {
+			e.preventDefault();
+			const notification = e.target.closest('.upload-recovery-notification');
+			if (notification) notification.remove();
+		} else if (window.targetCheck(e, '.create-from-selection')) {
+			e.preventDefault();
+			let group = this.createGroup(this.getFieldFromElement(e.target));
+			this.addSelectionToGroup(group);
+		} else if (window.targetCheck(e, '.remove-selection')) {
+			e.preventDefault();
+			this.removeSelection(e.target);
+		} else if (window.targetCheck(e, '.add-to-group, .add-selection-to-group')) {
+			e.preventDefault();
+			this.addSelectionToGroup(e.target);
+		} else if (window.targetCheck(e, '.remove-group')) {
+			e.preventDefault();
+			const groupElement = e.target.closest('.upload-group');
+			if (groupElement) {
+				let field = this.getFieldFromElement(groupElement);
+				this.removeGroup(groupElement, true);
+			}
+		} else if (window.targetCheck(e, '.remove')) {
+			e.preventDefault();
+			const uploadId = this.getUploadIdFromElement(e.target);
+			const fieldId = this.getFieldIdFromElement(e.target);
+			if (uploadId && fieldId) {
+				this.removeUpload(fieldId, uploadId);
+			}
+		} else if (window.targetCheck(e, '.submit-uploads')) {
+			e.preventDefault();
+			const fieldId = this.getFieldIdFromElement(e.target);
+			this.submitUploads(fieldId);
+		} else if (window.targetCheck(e, '.retry-upload')) {
+			e.preventDefault();
+			const uploadId = this.getUploadIdFromElement(e.target);
+			this.retryUpload(uploadId);
+		}
+	}
+	handleChange(e) {
+		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
+			return;
+		}
+		e.preventDefault();
+
+		if (window.targetCheck(e, '[type="file"]')) {
+			console.log(this.fields);
+			let field = this.getFieldFromElement(e.target);
+			console.log(field);
+			if (!field) {
+				console.warn('File change on unregistered field: ', field.key)
+				return;
+			}
+
+			const files = Array.from(e.target.files);
+			if (files.length === 0) return;
+
+			this.processFiles(field.key, files);
+			e.target.value = '';
+		} else if (e.target.closest('.upload-meta')) {
+			e.preventDefault();
+			let name = e.target.name;
+			let value = e.target.value;
+			let upload = this.getUploadFromElement(e.target);
+			upload.changes[name] = value;
+			this.uploads.set(upload.id, upload);
+			this.persistFieldState(upload.fieldId);
+
+			//It's meta!
+			//TODO:
+			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
+			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
+			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
+			//Send to server for processing
+		} else if (e.target.closest('.group.fields')) {
+			let group = this.getGroupFromElement(e.target);
+			let name = e.target.name;
+			group.changes[name] = e.target.value;
+
+			this.persistFieldState(group.fieldId);
+			this.groups.set(group.id, group);
+		}
+	}
+
+	handlePaste(e) {
+		window.debouncer.schedule(
+			'imagePaste',
+			() => {
+				const items = Array.from(e.clipboardData.items);
+				const imageItems = items.filter(item => item.type.startsWith('image/'));
+
+				if (imageItems.length === 0) return;
+
+				e.preventDefault();
+
+				const fieldId = this.getFieldIdFromElement(e.target);
+				if (!fieldId) return;
+
+				// Convert clipboard items to files
+				const files = [];
+				imageItems.forEach((item, index) => {
+					const file = item.getAsFile();
+					if (file) {
+						// Rename for clarity
+						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
+							type: file.type,
+							lastModified: Date.now()
+						});
+						files.push(newFile);
+					}
+				});
+
+				if (files.length > 0) {
+					this.processFiles(fieldId, files);
+				}
+			},
+			100
+		);
+	}
+
+	isTouchOnFormElement(target) {
+		// Check if target is a form element or inside one
+		const formElements = [
+			'input', 'button', 'label', 'select', 'textarea',
+		];
+
+		return formElements.some(selector => {
+			return target.matches(selector) || target.closest(selector);
+		});
+	}
+	/**** DRAG AND TOUCH *****/
+	startDragOperation(config) {
+		const {
+			primaryElement,
+			sourceType,
+			startPosition,
+			event
+		} = config;
+
+		const uploadId = this.getUploadIdFromElement(primaryElement);
+		const fieldId = this.getFieldIdFromElement(primaryElement);
+
+		// Determine what items to drag
+		const draggedItems = this.getDraggedItems(primaryElement);
+
+		// Initialize drag state
+		this.dragState = {
+			primaryItem: uploadId,
+			draggedItems: draggedItems,
+			isDragging: true,
+			isMultiDrag: draggedItems.length > 1,
+			fieldId: fieldId,
+			sourceType: sourceType,
+			startTime: Date.now(),
+			startPosition: startPosition,
+			currentPosition: startPosition,
+			currentTarget: null,
+			validTarget: null,
+			dragPreview: null,
+			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
+			touchMoved: false
+		};
+
+		// Create drag preview
+		this.createDragPreview(primaryElement);
+
+		// Apply dragging state
+		this.applyDraggingState(true);
+
+		const announceText = this.dragState.isMultiDrag
+			? `Started dragging ${draggedItems.length} items`
+			: 'Started dragging item';
+
+		this.a11y.announce(announceText);
+		this.provideDragFeedback('start');
+
+		return true;
+	}
+
+	updateDragOperation(position, elementUnderPointer) {
+		if (!this.dragState.isDragging) return;
+
+		const { sourceType, startPosition } = this.dragState;
+
+		// Update position
+		this.dragState.currentPosition = position;
+
+		// Check for significant movement (touch)
+		if (sourceType === 'touch' && !this.dragState.touchMoved) {
+			const deltaX = Math.abs(position.x - startPosition.x);
+			const deltaY = Math.abs(position.y - startPosition.y);
+
+			if (deltaX > 10 || deltaY > 10) {
+				this.dragState.touchMoved = true;
+			}
+		}
+
+		// Update preview and target
+		this.updateDragPreview(position);
+		this.updateDropTarget(elementUnderPointer);
+	}
+
+	endDragOperation(elementUnderPointer = null) {
+		if (!this.dragState.isDragging) return;
+
+		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
+			this.dragState.validTarget;
+
+		// Process drop if valid - but only here, not in handleDrop
+		if (wasSuccessful && this.dragState.validTarget) {
+			this.processItemDrop({
+				itemIds: this.dragState.draggedItems,
+				targetElement: this.dragState.validTarget,
+				fieldId: this.dragState.fieldId,
+				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
+				sourceType: this.dragState.sourceType
+			});
+		}
+
+		// Cleanup
+		this.cleanupDragOperation();
+
+		const announceText = wasSuccessful
+			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
+			: 'Drag cancelled';
+
+		this.a11y.announce(announceText);
+	}
+
+	/**
+	 * Shared method to process any drop operation (drag or touch)
+	 * @param {Object} dropData - Standardized drop data
+	 * @returns {boolean} Success status
+	 */
+	processItemDrop(dropData) {
+		const {
+			itemIds,
+			targetElement,
+			fieldId,
+			dropType,
+			sourceType
+		} = dropData;
+
+		if (!itemIds?.length || !targetElement || !fieldId) {
+			return false;
+		}
+
+		// Determine if it's a preview drop
+		let isPreviewDrop = targetElement.classList.contains('item-grid') && targetElement.classList.contains('preview');
+
+		// Handle empty group drops by creating the group element
+		let actualTarget = targetElement;
+		if (targetElement.classList.contains('empty-group')) {
+			let group = this.createGroup(fieldId);
+			if (!group) {
+				console.error('Failed to create group');
+				return false;
+			}
+			actualTarget = group.querySelector('.item-grid');
+			if (!actualTarget) {
+				console.error('Group element missing .item-grid');
+				return false;
+			}
+			isPreviewDrop = false;
+		}
+
+		// Use existing addImageToGroup method for each item
+		itemIds.forEach(uploadId => {
+			this.addImageToGroup(uploadId, actualTarget, isPreviewDrop);
+		});
+
+
+		const field = this.fields.get(fieldId);
+		if (field) {
+			this.clearAllSelections(field);
+		}
+
+		this.persistFieldState(fieldId);
+
+		// Announce completion
+		const announceText = dropType === 'multiple'
+			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
+			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
+
+		this.a11y.announce(announceText);
+		this.provideFeedback(sourceType, 'success', {
+			count: itemIds.length,
+			isMultiple: dropType === 'multiple'
+		});
+
+		return true;
+	}
+
+
+
+	cleanupDragOperation() {
+		if (this.dragState.dragPreview) {
+			this.dragState.dragPreview.remove();
+		}
+
+		this.applyDraggingState(false);
+		this.clearDropTargetStates();
+
+		// Reset state
+		this.dragState.isDragging = false;
+		this.dragState.dragPreview = null;
+		this.dragState.draggedItems = [];
+	}
+
+	/**
+	 * Determine what items to drag (single or multiple selection)
+	 */
+	getDraggedItems(element) {
+		const selectedUploads = this.getSelectedUploads(element);
+		const primaryUploadId = element.dataset.uploadId;
+
+		// If we have multiple selections and primary is selected, drag all
+		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
+			return selectedUploads;
+		}
+
+		// Otherwise, just drag the primary item
+		return [primaryUploadId];
+	}
+
+	/**
+	 * Apply/remove dragging visual state to items
+	 */
+	applyDraggingState(isDragging) {
+		this.dragState.draggedItems.forEach(uploadId => {
+			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
+			if (element) {
+				element.classList.toggle('dragging', isDragging);
+			}
+		});
+	}
+
+	/**
+	 * Create drag preview element
+	 */
+	createDragPreview(originalElement) {
+		const { isMultiDrag, draggedItems } = this.dragState;
+
+		if (isMultiDrag) {
+			this.dragState.dragPreview = this.createMultiDragPreview(originalElement, draggedItems);
+		} else {
+			this.dragState.dragPreview = this.createSingleDragPreview(originalElement);
+		}
+
+		// Set initial transform to position it at start
+		const offset = this.dragState.sourceType === 'touch'
+			? (this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 })
+			: (this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 });
+
+		this.dragState.dragPreview.style.transform =
+			`translate(${this.dragState.startPosition.x + offset.x}px, ${this.dragState.startPosition.y + offset.y}px) scale(1.05)`;
+
+		document.body.appendChild(this.dragState.dragPreview);
+	}
+
+	/**
+	 * Create single item drag preview
+	 */
+	createSingleDragPreview(originalElement) {
+		const preview = originalElement.cloneNode(true);
+		preview.dataset.uploadId = preview.dataset.uploadId+'-dragging';
+		this.styleDragPreview(preview, false);
+		return preview;
+	}
+
+	styleDragPreview(preview, isMulti = false) {
+		preview.style.cssText = `
+        position: fixed;
+        z-index: 10000;
+        pointer-events: none;
+        opacity: 0.9;
+        transform: scale(1.05);
+        transition: transform 0.2s ease;
+        ${isMulti ? `
+            width: 120px;
+            height: 120px;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
+            padding: 4px;
+        ` : `
+            border-radius: 4px;
+            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
+        `}
+    `;
+
+		// Add dragging class for additional styling
+		preview.classList.add('drag-preview', 'is-dragging');
+		if (isMulti) {
+			preview.classList.add('multi-item');
+		}
+	}
+
+	/**
+	 * Create multiple items drag preview
+	 */
+	createMultiDragPreview(originalElement, draggedItems) {
+		const container = document.createElement('div');
+		container.className = 'drag-preview multi-item';
+
+		container.style.cssText = `
+		position: relative;
+		width: 120px;
+		height: 120px;
+	`;
+
+		// Create stacked effect with up to 3 items
+		const displayCount = Math.min(draggedItems.length, 3);
+
+		for (let i = 0; i < displayCount; i++) {
+			const uploadId = draggedItems[i];
+			const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
+
+			if (uploadElement) {
+				const stackedItem = uploadElement.cloneNode(true);
+				stackedItem.dataset.uploadId = uploadId + '_dragging';
+
+				// **FIX**: Improved stacking with better visual separation
+				stackedItem.style.cssText = `
+				position: absolute;
+				top: ${i * 8}px;
+				left: ${i * 8}px;
+				width: calc(100% - ${i * 8}px);
+				height: calc(100% - ${i * 8}px);
+				opacity: ${1 - (i * 0.15)};
+				transform: rotate(${(i - 1) * 3}deg);
+				z-index: ${10 - i};
+				border-radius: 4px;
+				overflow: hidden;
+				box-shadow: 0 2px 8px rgba(0,0,0,0.${5 - i});
+			`;
+				container.appendChild(stackedItem);
+			}
+		}
+
+		// Add count badge
+		if (draggedItems.length > 1) {
+			const badge = this.createCountBadge(draggedItems.length);
+			container.appendChild(badge);
+		}
+
+		this.styleDragPreview(container, true);
+		return container;
+	}
+	/**
+	 * Update drag preview position
+	 */
+	updateDragPreview(position) {
+		if (!this.dragState.dragPreview) return;
+
+		// Calculate offset based on preview type and source
+		let offset;
+		if (this.dragState.sourceType === 'touch') {
+			offset = this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 };
+		} else {
+			offset = this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 };
+		}
+
+		const deltaX = position.x - this.dragState.startPosition.x;
+		const deltaY = position.y - this.dragState.startPosition.y;
+
+		this.dragState.dragPreview.style.transform = `translate(${deltaX + offset.x}px, ${deltaY + offset.y}px) scale(1.05)`;
+	}
+
+	/**
+	 * Update drop target highlighting
+	 */
+	updateDropTarget(elementUnderPointer) {
+		// Clear previous target
+		if (this.dragState.currentTarget) {
+			this.clearDropTargetState(this.dragState.currentTarget);
+		}
+
+		// Find valid drop target
+		const validTarget = this.findValidDropTarget(elementUnderPointer);
+
+		// Update state
+		this.dragState.currentTarget = elementUnderPointer;
+		this.dragState.validTarget = validTarget;
+
+		// Apply visual feedback
+		if (validTarget) {
+			this.applyDropTargetState(validTarget);
+
+			// Haptic feedback for touch
+			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
+				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
+				navigator.vibrate(pattern);
+			}
+		}
+	}
+
+	/**
+	 * Find valid drop target from element
+	 */
+	findValidDropTarget(element) {
+		const target = element?.closest('.item-grid.group, .empty-group, .item-grid.preview');
+		return target && this.getFieldIdFromElement(target) === this.dragState.fieldId ? target : null;
+	}
+
+	/**
+	 * Apply drop target visual state
+	 */
+	applyDropTargetState(target) {
+		target.classList.add('dragover');
+
+		if (this.dragState.isMultiDrag) {
+			target.classList.add('multi-drop');
+			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
+		}
+	}
+
+	/**
+	 * Clear drop target state from element
+	 */
+	clearDropTargetState(target) {
+		target.classList.remove('dragover', 'multi-drop');
+		target.removeAttribute('data-item-count');
+	}
+
+	/**
+	 * Clear all drop target states
+	 */
+	clearDropTargetStates() {
+		document.querySelectorAll('.dragover').forEach(el => {
+			el.classList.remove('dragover', 'multi-drop');
+			el.removeAttribute('data-item-count');
+		});
+	}
+
+	/**
+	 * Create count badge for multi-item preview
+	 */
+	createCountBadge(count) {
+		const badge = document.createElement('div');
+		badge.className = 'selection-count-badge';
+		badge.textContent = count.toString();
+		return badge;
+	}
+
+	/**
+	 * Provide feedback for drag operations
+	 */
+	provideDragFeedback(type) {
+		const hapticPatterns = {
+			start: [50],
+			success: this.dragState.isMultiDrag ? [30, 20, 30] : [50],
+			error: [100, 50, 100],
+			warning: [50]
+		};
+
+		// Haptic feedback (vibration on supported devices)
+		if (navigator.vibrate && hapticPatterns[type]) {
+			navigator.vibrate(hapticPatterns[type]);
+		}
+
+		// Visual feedback
+		const feedback = document.createElement('div');
+		feedback.className = `drag-feedback ${type}`;
+		feedback.style.cssText = `
+		position: fixed;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		padding: 1rem 2rem;
+		background: var(--${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'});
+		color: white;
+		border-radius: var(--radius);
+		z-index: 10001;
+		animation: feedbackPulse 0.3s ease;
+		pointer-events: none;
+	`;
+
+		const icons = {
+			start: '↕️',
+			success: '✓',
+			error: '✗',
+			warning: '⚠'
+		};
+
+		feedback.textContent = icons[type] || '';
+		document.body.appendChild(feedback);
+
+		setTimeout(() => {
+			feedback.style.animation = 'fadeOut 0.3s ease';
+			setTimeout(() => feedback.remove(), 300);
+		}, 500);
+	}
+
+	/**
+	 * Provide consistent feedback for different input methods
+	 */
+	provideFeedback(sourceType, feedbackType, data = {}) {
+		const hapticPatterns = {
+			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
+			error: [100, 50, 100]
+		};
+
+		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
+			navigator.vibrate(hapticPatterns[feedbackType]);
+		}
+	}
+
+	clearDragoverStates() {
+		document.querySelectorAll('.dragover').forEach(el => {
+			el.classList.remove('dragover', 'multi-drop');
+			el.removeAttribute('data-item-count');
+		});
+	}
+	/*********
+	 *  DRAG HANDLERS
+	 ********/
+	handleDragEnter(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		// Only handle external files
+		if (e.dataTransfer.types.includes('Files')) {
+			e.preventDefault();
+			const uploadContainer = e.target.closest('.file-upload-container');
+			if (uploadContainer) {
+				uploadContainer.classList.add('dragover');
+			}
+		}
+	}
+	handleDragLeave(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		const uploadContainer = e.target.closest('.file-upload-container');
+		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
+			uploadContainer.classList.remove('dragover');
+		}
+	}
+	handleDragStart(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		const uploadItem = e.target.closest('[data-upload-id]');
+		if (!uploadItem) return;
+
+		const result = this.startDragOperation({
+			primaryElement: uploadItem,
+			sourceType: 'drag',
+			startPosition: { x: e.clientX, y: e.clientY },
+			event: e
+		});
+
+		if (result) {
+			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
+			e.dataTransfer.effectAllowed = 'move';
+		} else {
+			e.preventDefault();
+		}
+	}
+
+	handleDragOver(e) {
+		if (!this.dragState || !this.dragState.isDragging) return;
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		e.preventDefault();
+
+		e.dataTransfer.dropEffect = 'move';
+
+		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
+		this.updateDragOperation(
+			{ x: e.clientX, y: e.clientY },
+			elementUnderPointer
+		);
+	}
+
+	handleDrop(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+
+		e.preventDefault();
+		this.clearDragoverStates();
+
+		// Handle external files (new uploads)
+		const uploadContainer = e.target.closest('.file-upload-container');
+		if (uploadContainer) {
+			const files = Array.from(e.dataTransfer.files);
+			if (files.length > 0) {
+				const fieldId = this.getFieldIdFromElement(uploadContainer);
+				if (fieldId) {
+					this.processFiles(fieldId, files);
+					this.a11y.announce(`${files.length} file(s) dropped for upload`);
+				}
+			}
+		}
+	}
+
+	handleDragEnd(e) {
+		if (!this.dragState.isDragging) return;
+
+		// Find the element under the final drop position
+		const elementUnderDrop = document.elementFromPoint(
+			this.dragState.currentPosition?.x || e.clientX,
+			this.dragState.currentPosition?.y || e.clientY
+		);
+
+		this.endDragOperation(elementUnderDrop);
+	}
+	/*********
+	 * TOUCH HANDLERS
+	 ********/
+	handleTouchStart(e) {
+		if (!window.targetCheck(e, '.field.upload')) return;
+		if (this.isTouchOnFormElement(e.target)) {
+			return;
+		}
+
+		const uploadItem = e.target.closest('[data-upload-id]');
+		if (!uploadItem) return;
+
+		const touch = e.touches[0];
+
+		const result = this.startDragOperation({
+			primaryElement: uploadItem,
+			sourceType: 'touch',
+			startPosition: { x: touch.clientX, y: touch.clientY },
+			event: e
+		});
+
+		if (result) {
+			e.preventDefault(); // Prevent scrolling
+		}
+	}
+
+	handleTouchMove(e) {
+		if (!this.dragState.isDragging) return;
+
+		e.preventDefault();
+		const touch = e.touches[0];
+		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
+
+		this.updateDragOperation({ x: touch.clientX, y: touch.clientY }, elementUnderTouch);
+	}
+
+	handleTouchEnd(e) {
+		if (!this.dragState.isDragging) return;
+
+		e.preventDefault();
+		const touch = e.changedTouches[0];
+		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
+
+		this.endDragOperation(elementUnderTouch);
+	}
+
+	handleTouchCancel(e) {
+		if (this.dragState.isDragging) {
+			this.cleanupDragOperation();
+			this.a11y.announce('Drag cancelled');
+		}
+	}
+	/*******************************************************************************
+	 QUEUE INTEGRATION
+	 *******************************************************************************/
+	async submitUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Check if there are uploads to submit
+		const pendingUploads = Array.from(field.uploads || [])
+			.map(id => this.uploads.get(id))
+			.filter(upload => upload &&
+				(upload.status === 'processed' ||
+					upload.status === 'processed-original'));
+
+		if (pendingUploads.length === 0) {
+			// this.notifications.add('No uploads ready to submit', 'warning');
+			return;
+		}
+
+		// Queue the uploads
+		try {
+			await this.queueUpload(fieldId);
+			// this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitUploads',
+				fieldId
+			});
+			// this.notifications.add('Failed to submit uploads', 'error');
+		}
+	}
+	async retryUpload(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		try {
+			// Reset status
+			this.updateUploadStatus(uploadId, 'received');
+
+			// If we have the processed file, skip to queuing
+			if (upload.processedFile) {
+				this.updateUploadStatus(uploadId, 'processed');
+				await this.queueUpload(upload.fieldId);
+			} else if (upload.originalFile) {
+				// Reprocess the file
+				const reprocessed = await this.processFile(upload.fieldId, upload.originalFile);
+				if (reprocessed) {
+					await this.queueUpload(upload.fieldId);
+				}
+			} else {
+				throw new Error('No file data available for retry');
+			}
+
+			// this.notifications.add('Retrying upload...', 'info');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'retryUpload',
+				uploadId
+			});
+			// this.notifications.add('Failed to retry upload', 'error');
+		}
+	}
+	async restartUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads) return;
+
+		const failedUploads = Array.from(field.uploads)
+			.map(id => this.uploads.get(id))
+			.filter(upload => upload && upload.status === 'failed');
+
+		if (failedUploads.length === 0) {
+			// this.notifications.add('No failed uploads to restart', 'info');
+			return;
+		}
+
+		for (const upload of failedUploads) {
+			await this.retryUpload(upload.id);
+		}
+
+		// this.notifications.add(`Restarting ${failedUploads.length} upload(s)`, 'info');
+	}
+	async queueUpload(fieldId) {
+		//Further cache it, or is it already cached at this point?
+		const field = this.fields.get(fieldId);
+		if (!field?.uploads) return;
+
+		const uploads = Array.from(field.uploads);
+		if (uploads.length === 0) {
+			return;
+		}
+
+		const data = this.prepareUploadData(field, uploads);
+		this.a11y.announce('Queuing for upload');
+		let img = (uploads.length === 1) ? 'image' : 'images';
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} ${img} to server...`,
+			popup: `Uploading ${uploads.length} ${img}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			},
+			append: '_upload'
+		}
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (!upload) {
+					return;
+				}
+				upload.operationId = operationId;
+				this.updateUploadStatus(uploadId, 'queued');
+			});
+			field.operationId = operationId;
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		} finally {
+			this.persistFieldState(field.key);
+		}
+	}
+
+	prepareUploadData(field, uploads) {
+		console.log('Preparing Upload:', field);
+		const formData = new FormData();
+		formData.append('content', field.content);
+		formData.append('mode', field.mode);
+		formData.append('field_name', field.name);
+		formData.append('field_key', field.key);
+		formData.append('field_type', field.type);
+		formData.append('subtype', field.subtype);
+		formData.append('item_id', field.itemID);		//post, term, or user id
+		formData.append('context', field.context);	//post, term, or user
+		formData.append('destination', field.destination || 'meta'); //meta, post, post_group
+		let uploadMap = [];
+
+		const fieldGroups = this.getFieldGroups(field.key);
+		if (field.destination === 'post_group' && fieldGroups.length > 0) {
+			// User has created groups
+			let groups = [];
+			let titles = [];
+			let featuredImages = [];
+
+			fieldGroups.forEach(group => {
+				let groupUploadIndices = [];
+				let featuredIndex = null;
+
+				group.uploads.forEach(uploadId => {
+					let upload = this.uploads.get(uploadId);
+					if (upload) {
+						const fileToUpload = upload.processedFile || upload.originalFile;
+						if (fileToUpload) {
+							formData.append('files[]', fileToUpload);
+							const fileIndex = uploadMap.length;
+							uploadMap.push(upload.id);
+							groupUploadIndices.push(upload.id);
+
+							// Check if this is the featured image
+							const radioInput = upload.element?.querySelector('[name="featured"]');
+							if (radioInput?.checked) {
+								featuredIndex = upload.id;
+							}
+						}
+					}
+				});
+
+				groups.push(groupUploadIndices);
+				titles.push(group.title || '');
+				featuredImages.push(featuredIndex);
+			});
+
+			formData.append('groups', JSON.stringify(groups));
+			formData.append('group_titles', JSON.stringify(titles));
+			formData.append('featured_images', JSON.stringify(featuredImages));
+		} else {
+			// No groups - just append all files
+			uploads.forEach(uploadId => {
+				let upload = this.uploads.get(uploadId);
+				if (upload) {
+					const fileToUpload = upload.processedFile || upload.originalFile;
+					if (fileToUpload) {
+						formData.append('files[]', fileToUpload);
+						uploadMap.push(upload.id);
+					}
+				}
+			});
+		}
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		console.log('Final FormData:');
+		for (let pair of formData.entries()) {
+			console.log(pair[0], pair[1]);
+		}
+
+		return formData;
+	}
+
+	getFieldGroups(fieldId) {
+		const groups = [];
+
+		this.groups.forEach((groupData, groupId) => {
+			if (groupData.fieldId === fieldId) {
+				groups.push({
+					id: groupId,
+					uploads: Array.from(groupData.uploads || new Set()),
+					meta: this.groupsMeta.get(groupId) || {},
+					element: this.fields.get(fieldId)?.ui.groups.groups.get(groupId)
+				});
+			}
+		});
+
+		return groups;
+	}
+
+	/**
+	 * Build groups data from field state
+	 */
+	buildGroupsData(field, uploads) {
+		const groups = [];
+		const titles = [];
+		const uploadMap = [];
+
+		if (field.groups && field.groups.length > 0) {
+			// User has explicitly created groups
+			field.groups.forEach(group => {
+				const groupUploads = [];
+				group.uploads.forEach(uploadId => {
+					groupUploads.push(uploadId);
+					uploadMap.push(uploadId);
+				});
+				groups.push(groupUploads);
+				titles.push(group.title || '');
+			});
+		} else {
+			// No explicit groups - treat all as one group
+			const allUploads = [];
+			uploads.forEach(uploadId => {
+				allUploads.push(uploadId);
+				uploadMap.push(uploadId);
+			});
+			groups.push(allUploads);
+			titles.push('');
+		}
+
+		return { groups, titles, uploadMap };
+	}
+
+	async queueImageMeta(e) {
+		const upload = this.getUploadFromElement(element);
+		if (!upload) return;
+
+		const field = this.fields.get(upload.fieldId);
+		if (!field) return;
+
+		// Collect meta data from the form
+		const metaContainer = element.closest('.upload-meta');
+		if (!metaContainer) return;
+
+		const metaData = {
+			title: metaContainer.querySelector('[name="title"]')?.value || '',
+			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
+			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
+			description: metaContainer.querySelector('[name="description"]')?.value || ''
+		};
+
+		// Update upload meta
+		upload.meta = { ...upload.meta, ...metaData };
+		this.uploads.set(upload.id, upload);
+
+		// Mark that we have meta changes
+		this.hasMetaChanges = true;
+
+		// Determine if upload has been sent to server
+		const isOnServer = upload.status === 'completed' && upload.attachmentId;
+
+		if (isOnServer) {
+			// Queue immediate update
+			await this.sendMetaUpdate(upload);
+		} else if (upload.operationId) {
+			// Wait for upload to complete, then send meta
+			this.queueDependentMetaUpdate(upload);
+		} else {
+			// Upload hasn't been queued yet, meta will be sent with initial upload
+			this.persistFieldState(field.key);
+		}
+	}
+
+	/**
+	 * Send meta update to server
+	 */
+	async sendMetaUpdate(upload) {
+		const formData = new FormData();
+		formData.append('attachment_id', upload.attachmentId);
+		formData.append('title', upload.meta.title);
+		formData.append('alt_text', upload.meta.alt_text);
+		formData.append('caption', upload.meta.caption);
+		formData.append('description', upload.meta.description);
+		//TODO:
+		// Send an array of attachment IDs with the changes, similar to the post editing logic
+		/**
+		 *  let data = {
+		 *  	items: {
+		 *      	uploadID: {
+		 *          	title: '',
+		 *          	alt: '',
+		 *          	caption: '',
+		 *         		depends_on: ''  <-- only necessary if uploadID is the generated upload_id
+		 *      	}
+		 *  	},
+		 *  	user: userID
+		 *  }
+		 *
+		 *  WHERE uploadID = attachment_id (if already uploaded) or our generated upload_id if the file hasn't been processed yet
+		 *
+		 */
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: formData,
+			title: `Updating metadata for ${upload.meta.originalName}`,
+			canMerge: true,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			}
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+			// this.notifications.add('Metadata updated', 'success');
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
+			});
+		}
+	}
+
+	/**
+	 * Queue meta update that depends on upload completion
+	 */
+	queueDependentMetaUpdate(upload) {
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			dependencies: [upload.operationId],
+			data: () => {
+				// This function will be called when dependencies are resolved
+				const formData = new FormData();
+				formData.append('operation_id', upload.operationId);
+				formData.append('upload_id', upload.id);
+				formData.append('title', upload.meta.title);
+				formData.append('alt_text', upload.meta.alt_text);
+				formData.append('caption', upload.meta.caption);
+				formData.append('description', upload.meta.description);
+				return formData;
+			},
+			title: `Updating metadata after upload`,
+			canMerge: true,
+			headers: {
+				'action_nonce': jvbSettings.dash
+			}
+		};
+
+		this.queue.addToQueue(operation);
+	}
+	/*******************************************************************************
+	 IMAGE PROCESSING
+	 *******************************************************************************/
+	async processFiles(fieldId, files) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Hide upload container, show group display
+		if (field.ui.field.dropZone) {
+			field.ui.field.dropZone.hidden = true;
+		}
+		if (field.ui.groups.display) {
+			field.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+		// Process files
+		const processPromises = Array.from(files).map(async (file, index) => {
+			try {
+				// Create upload ID
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create upload data
+				const uploadData = {
+					id: uploadId,
+					fieldId: fieldId,
+					originalFile: file,
+					processedFile: null,
+					preview: null,
+					status: 'local_processing',
+					element: null,
+					location: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Create preview URL
+				uploadData.preview = URL.createObjectURL(file);
+
+				// Process the file (resize if image)
+				if (file.type.startsWith('image/')) {
+					uploadData.processedFile = await this.processImage(file, field.subtype);
+				} else {
+					uploadData.processedFile = file;
+				}
+
+				// Store blob data separately in IndexedDB
+				if (this.db) {
+					try {
+						await this.storeBlobData(uploadId, uploadData.processedFile || file);
+					} catch (error) {
+						console.warn('Failed to store blob data:', error);
+					}
+				}
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				uploadData.element = this.createImageElement({
+					...uploadData,
+					subtype: subtype
+				}, field.destination === 'post_group');
+
+				// Show progress on the item
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Add to preview grid
+				if (field.ui.field.preview) {
+					field.ui.field.preview.appendChild(uploadData.element);
+					uploadData.location = field.ui.field.preview;
+				}
+
+				// Store upload
+				this.uploads.set(uploadId, uploadData);
+				field.uploads.add(uploadId);
+
+				// Update progress
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+				uploadData.status = 'processed';
+
+				// Fade out item progress after a moment
+				setTimeout(() => {
+					this.showUploadProgress(uploadId, false);
+				}, 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		// Wait for all files to process
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		// Cache the state (now without DOM references)
+		await this.persistFieldState(fieldId);
+
+		// Queue for upload if in direct mode
+		if (field.mode === 'direct' && field.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+		}
+
+		// Lock uploads if max reached
+		this.maybeLockUploads(fieldId);
+	}
+
+	updateFieldState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.ui.field.field) return;
+
+		const container = field.ui.field.field;
+		const uploadCount = field.uploads?.size || 0;
+		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		// Set data attributes for CSS targeting
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		// Update ARIA labels for accessibility
+		if (field.ui.field.preview) {
+			field.ui.field.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	/**
+	 * Store file blob data in IndexedDB
+	 */
+	async storeBlobData(uploadId, file) {
+		if (!this.db) return;
+
+		const blobData = {
+			uploadId: uploadId,
+			data: file,
+			name: file.name,
+			type: file.type,
+			lastModified: file.lastModified,
+			timestamp: Date.now()
+		};
+
+		try {
+			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
+			await tx.objectStore('uploadBlobs').put(blobData);
+		} catch (error) {
+			console.error('Failed to store blob data:', error);
+			throw error;
+		}
+	}
+
+	/**
+	 * Show/hide progress indicator on individual upload items
+	 */
+	showUploadProgress(uploadId, show = true) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
+
+		const progressEl = upload.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => {
+					progressEl.hidden = true;
+				}, 300);
+			}
+		}
+	}
+
+	/**
+	 * Update individual upload progress bar
+	 */
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload || !upload.element) return;
+
+		const progressEl = upload.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) {
+			fill.style.width = `${percent}%`;
+		}
+
+		if (status && details) {
+			details.textContent = this.getStatusText(status);
+		}
+
+		if (status && icon) {
+			icon.innerHTML = this.getStatusIcon(status).outerHTML;
+		}
+	}
+	checkFieldLimits(fieldId, additionalFiles) {
+		const field = this.fields.get(fieldId);
+		if (!field) return false;
+
+		const currentCount = field.uploads?.size || 0;
+		const totalCount = currentCount + additionalFiles;
+
+		if (totalCount > field.maxFiles) {
+			// this.notifications.add(
+			// 	`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
+			// 	'warning'
+			// );
+			return false;
+		}
+
+		return true;
+	}
+	generateUploadId() {
+		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+	}
+	validateFile(file, field) {
+		// Type validation
+		if (!this.settings.allowedTypes.includes(file.type)) {
+			this.notify(`Invalid file type: ${file.type}`, 'error');
+			return false;
+		}
+
+		// Size validation
+		if (file.size > this.settings.maxFileSize) {
+			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
+			return false;
+		}
+
+		return true;
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+
+		const k = 1024;
+		const dm = decimals < 0 ? 0 : decimals;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+	}
+
+	shouldProcessClientSide(file, subtype) {
+		// Only process images client-side
+		if (subtype === 'image' && file.type.startsWith('image/')) {
+			return true;
+		}
+
+		// Videos and documents go straight to server
+		return false;
+	}
+
+	async processBatch(fieldId, files) {
+		const results = [];
+		const processingQueue = [];
+		const maxConcurrent = this.worker.settings.maxConcurrent;
+
+		let total = files.length;
+		let processedCount = 0;
+
+		// Show initial progress
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+		let field = this.fields.get(fieldId);
+		// Initialize field uploads set if needed
+		if (!field.uploads) {
+			field.uploads = new Set();
+		}
+
+
+		for (let i = 0; i < files.length; i++) {
+			this.showUploadProgress(uploadId, true);
+			this.updateUploadProgress(fieldId, i, total);
+			// Wait if we've reached max concurrent processing
+			if (processingQueue.length >= maxConcurrent) {
+				await Promise.race(processingQueue);
+			}
+
+			const processPromise = this.processFile(fieldId, files[i])
+				.then(upload => {
+					// Remove from processing queue
+					const index = processingQueue.indexOf(processPromise);
+					if (index > -1) processingQueue.splice(index, 1);
+
+					if (upload) results.push(upload);
+					return upload;
+				})
+				.catch(error => {
+					console.error(`Failed to process ${files[i].name}:`, error);
+					// Remove from processing queue
+					const index = processingQueue.indexOf(processPromise);
+					if (index > -1) processingQueue.splice(index, 1);
+					return null;
+				});
+
+			processingQueue.push(processPromise);
+		}
+
+		// Wait for remaining files
+		await Promise.all(processingQueue);
+		return results;
+	}
+
+	async processFile(fieldId, file) {
+		const field = this.fields.get(fieldId);
+
+		const upload = await this.setUpload(fieldId, file);
+
+		if (!upload) {
+			return null;
+		}
+		if (!this.shouldProcessClientSide(file, field.subtype)) {
+			return upload;
+		}
+		const uploadId = upload.id;
+		try {
+			// Update UI immediately
+			this.addImageToGroup(uploadId);
+			this.updateUploadStatus(uploadId, 'local_processing');
+
+			// Attempt to process the image
+			let processedFile = null;
+			let processingFailed = false;
+
+			try {
+				processedFile = await this.processImage(file, uploadId);
+			} catch (error) {
+				console.warn(`Processing failed for ${file.name}, using original:`, error);
+				processingFailed = true;
+				processedFile = file; // Use original
+			}
+
+			// Update upload with processed file
+			upload.processedFile = processedFile;
+			upload.processingFailed = processingFailed;
+
+			// Update status
+			this.updateUploadStatus(uploadId, 'processed');
+
+			// Save to uploads map
+			this.uploads.set(uploadId, upload);
+
+			// Persist state
+			if (field && field.key) {
+				await this.persistFieldState(field.key);
+			}
+
+			const message = processingFailed
+				? `${file.name} added (original format)`
+				: `${file.name} processed and ready`;
+			this.a11y.announce(message);
+
+			return upload;
+
+		} catch (error) {
+			// Clean up failed upload
+			this.cleanupFailedUpload(uploadId, field.key);
+
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'processFile',
+				uploadId,
+				fileName: file.name
+			});
+
+			return null;
+		}
+	}
+
+	async processImage(file, uploadId) {
+		const timeout = this.worker.settings.timeout;
+
+		return new Promise((resolve, reject) => {
+			let timeoutId;
+			let taskCompleted = false;
+
+			// Set timeout
+			timeoutId = setTimeout(() => {
+				if (!taskCompleted) {
+					taskCompleted = true;
+
+					// Remove from active tasks
+					this.worker.tasks.delete(uploadId);
+
+					// Maybe restart worker if configured
+					if (this.worker.settings.restartAfterTimeout) {
+						this.restartCompressionWorker();
+					}
+
+					reject(new Error(`Processing timeout for ${file.name}`));
+				}
+			}, timeout);
+
+			// Track this task
+			this.worker.tasks.set(uploadId, { file, timeoutId });
+
+			// Process image
+			this.handleProcess(file, uploadId)
+				.then(result => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						resolve(result);
+					}
+				})
+				.catch(error => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						reject(error);
+					}
+				});
+		});
+	}
+
+	async handleProcess(file, uploadId) {
+		// Skip non-images
+		if (!file.type.startsWith('image/')) {
+			return file;
+		}
+
+		const maxDimension = this.getMaxDimension();
+		const quality = 0.85;
+
+		// Try worker first if available
+		if (this.shouldUseWorker(file)) {
+			try {
+				// Ensure worker is initialized
+				if (!this.worker.worker) {
+					this.initCompressionWorker();
+				}
+
+				if (this.worker.worker) {
+					return await this.processWithWorker(file, uploadId, maxDimension, quality);
+				}
+			} catch (error) {
+				console.warn('Worker processing failed, falling back to main thread:', error);
+			}
+		}
+
+		// Fallback to main thread
+		return await this.processOnMainThread(file, maxDimension, quality);
+	}
+
+	/**
+	 * Process image on main thread with better error handling
+	 */
+	async processOnMainThread(file, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			const img = new Image();
+			const canvas = document.createElement('canvas');
+			const ctx = canvas.getContext('2d');
+			let objectUrl = null;
+
+			const cleanup = () => {
+				img.onload = null;
+				img.onerror = null;
+				if (objectUrl) {
+					URL.revokeObjectURL(objectUrl);
+					objectUrl = null;
+				}
+				// Explicitly clean up canvas
+				canvas.width = 1;
+				canvas.height = 1;
+				ctx.clearRect(0, 0, 1, 1);
+			};
+
+			img.onload = () => {
+				try {
+					const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
+					canvas.width = width;
+					canvas.height = height;
+
+					// Enhanced image smoothing
+					ctx.imageSmoothingEnabled = true;
+					ctx.imageSmoothingQuality = 'high';
+					ctx.drawImage(img, 0, 0, width, height);
+
+					const outputFormat = this.getOptimalFormat(file);
+					const outputQuality = this.getOptimalQuality(file, quality);
+
+					canvas.toBlob(
+						(blob) => {
+							cleanup();
+							if (blob) {
+								const processedFile = new File(
+									[blob],
+									this.getProcessedFileName(file, outputFormat),
+									{ type: outputFormat, lastModified: Date.now() }
+								);
+								resolve(processedFile);
+							} else {
+								reject(new Error('Canvas toBlob failed'));
+							}
+						},
+						outputFormat,
+						outputQuality
+					);
+
+				} catch (error) {
+					cleanup();
+					reject(new Error(`Canvas processing failed: ${error.message}`));
+				}
+			};
+
+			img.onerror = () => {
+				cleanup();
+				reject(new Error(`Failed to load image: ${file.name}`));
+			};
+
+			try {
+				objectUrl = URL.createObjectURL(file);
+				img.src = objectUrl;
+			} catch (error) {
+				cleanup();
+				reject(new Error(`Failed to create object URL: ${error.message}`));
+			}
+		});
+	}
+
+	/**
+	 * Get optimal output format
+	 */
+	getOptimalFormat(file) {
+		// Keep original format for certain types
+		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
+			return file.type;
+		}
+
+		// Use WebP if supported, otherwise JPEG
+		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+	}
+
+	/**
+	 * Get optimal quality setting
+	 */
+	getOptimalQuality(file, requestedQuality) {
+		// Higher quality for smaller files
+		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+		if (file.size < 2 * 1024 * 1024) return requestedQuality;
+
+		// Lower quality for very large files
+		return Math.min(requestedQuality, 0.8);
+	}
+
+	/**
+	 * Generate processed file name
+	 */
+	getProcessedFileName(originalFile, outputFormat) {
+		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
+
+		const extensions = {
+			'image/webp': '.webp',
+			'image/jpeg': '.jpg',
+			'image/png': '.png',
+			'image/gif': '.gif'
+		};
+
+		return baseName + (extensions[outputFormat] || '.jpg');
+	}
+
+	/**
+	 * Get maximum dimension based on device capabilities
+	 */
+	getMaxDimension() {
+		const screenWidth = window.screen.width;
+		const devicePixelRatio = window.devicePixelRatio || 1;
+
+		// Scale based on device capabilities
+		if (screenWidth * devicePixelRatio > 2560) return 2400;
+		if (screenWidth * devicePixelRatio > 1920) return 1920;
+		return 1200;
+	}
+
+	/**
+	 * Determine if we should use Web Worker
+	 */
+	shouldUseWorker(file) {
+		// Use worker for large files or when available
+		return this.worker.worker &&
+			file.size > 1024 * 1024 && // > 1MB
+			typeof OffscreenCanvas !== 'undefined';
+	}
+
+	async processWithWorker(file, uploadId, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			if (!this.worker.worker) {
+				reject(new Error('Worker not available'));
+				return;
+			}
+
+			// Create unique message ID for this task
+			const messageId = `${uploadId}_${Date.now()}`;
+
+			// Handler for this specific message
+			const messageHandler = (e) => {
+				if (e.data.messageId !== messageId) return;
+
+				// Remove handler
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+
+				if (e.data.success) {
+					const processedFile = new File(
+						[e.data.blob],
+						this.getProcessedFileName(file, e.data.format || 'image/webp'),
+						{ type: e.data.format || 'image/webp', lastModified: Date.now() }
+					);
+					resolve(processedFile);
+				} else {
+					reject(new Error(e.data.error || 'Worker processing failed'));
+				}
+			};
+
+			const errorHandler = (error) => {
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+				reject(new Error(`Worker error: ${error.message}`));
+			};
+
+			// Add handlers
+			this.worker.worker.addEventListener('message', messageHandler);
+			this.worker.worker.addEventListener('error', errorHandler);
+
+			// Send message to worker
+			this.worker.worker.postMessage({
+				messageId,
+				file,
+				maxDimension,
+				quality,
+				outputFormat: this.getOptimalFormat(file)
+			});
+		});
+	}
+
+	/**
+	 * Restart compression worker
+	 */
+	restartCompressionWorker() {
+		console.log('Restarting compression worker...');
+
+		// Terminate existing worker
+		if (this.worker.worker) {
+			this.worker.worker.terminate();
+			this.worker.worker = null;
+		}
+
+		// Clear active tasks
+		this.worker.tasks.clear();
+
+		// Check restart limit
+		if (this.worker.restart.count >= this.worker.restart.max) {
+			console.error('Max worker restarts reached, disabling worker');
+			return;
+		}
+
+		this.worker.restart.count++;
+
+		// Reinitialize
+		this.initCompressionWorker();
+	}
+
+	/**
+	 * Initialize Web Worker for image compression
+	 */
+	initCompressionWorker() {
+		if (this.worker.worker || typeof Worker === 'undefined') return;
+
+		try {
+			const workerScript = `
+            self.onmessage = async function(e) {
+                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+
+                try {
+                    // Create ImageBitmap from file
+                    const bitmap = await createImageBitmap(file);
+
+                    // Calculate dimensions
+                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+                    const width = Math.round(bitmap.width * scale);
+                    const height = Math.round(bitmap.height * scale);
+
+                    // Create OffscreenCanvas
+                    const canvas = new OffscreenCanvas(width, height);
+                    const ctx = canvas.getContext('2d');
+
+                    // Draw and resize
+                    ctx.imageSmoothingEnabled = true;
+                    ctx.imageSmoothingQuality = 'high';
+                    ctx.drawImage(bitmap, 0, 0, width, height);
+
+                    // Clean up bitmap
+                    bitmap.close();
+
+                    // Convert to blob
+                    const blob = await canvas.convertToBlob({
+                        type: outputFormat,
+                        quality: quality
+                    });
+
+                    self.postMessage({
+                        messageId,
+                        success: true,
+                        blob: blob,
+                        format: outputFormat
+                    });
+
+                } catch (error) {
+                    self.postMessage({
+                        messageId,
+                        success: false,
+                        error: error.message
+                    });
+                }
+            };
+        `;
+
+			const blob = new Blob([workerScript], { type: 'application/javascript' });
+			this.worker.worker = new Worker(URL.createObjectURL(blob));
+
+		} catch (error) {
+			console.warn('Failed to initialize compression worker:', error);
+			this.worker.worker = null;
+		}
+	}
+
+	/**
+	 * Calculate optimal dimensions with aspect ratio preservation
+	 */
+	calculateOptimalDimensions(img, maxDimension) {
+		let { width, height } = img;
+
+		// Don't upscale
+		if (width <= maxDimension && height <= maxDimension) {
+			return { width, height };
+		}
+
+		// Calculate scale factor
+		const scale = Math.min(maxDimension / width, maxDimension / height);
+
+		return {
+			width: Math.round(width * scale),
+			height: Math.round(height * scale)
+		};
+	}
+
+
+	/**
+	 * Check WebP support
+	 */
+	supportsWebP() {
+		const canvas = document.createElement('canvas');
+		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
+	}
+
+	/**
+	 * Clean up failed upload
+	 */
+	cleanupFailedUpload(uploadId, fieldId) {
+		const field = this.fields.get(fieldId);
+		if (field?.uploads) {
+			field.uploads.delete(uploadId);
+		}
+
+		const upload = this.uploads.get(uploadId);
+		if (upload) {
+			// Clean up preview URL
+			if (upload.preview?.startsWith('blob:')) {
+				URL.revokeObjectURL(upload.preview);
+			}
+
+			// Remove element
+			upload.element?.remove();
+
+			// Remove from uploads
+			this.uploads.delete(uploadId);
+		}
+
+		// Remove from active tasks
+		this.worker.tasks.delete(uploadId);
+	}
+	/*******************************************************************************
+	 UI FUNCTIONALITY
+	 *******************************************************************************/
+	/**
+	 * Update upload status correctly
+	 */
+	updateUploadStatus(uploadId, status) {
+		console.log('Updating upload status for: ', uploadId);
+		let upload = this.uploads.get(uploadId);
+		if(!upload) {
+			return;
+		}
+		upload.status = status;
+
+		this.updateImageUI(upload.id);
+		this.persistFieldState(upload.fieldId);
+	}
+	updateImageUI(uploadId) {
+		console.log('Updating image UI: ', uploadId);
+		const upload = this.uploads.get(uploadId);
+		console.log(upload);
+		if (!upload?.element) return;
+
+
+		const progressEl = upload.element.querySelector('.progress');
+		const itemEl = upload.element;
+
+		console.log('Updating Upload UI:', upload);
+		// Update status class on item for CSS styling
+		if (itemEl) {
+			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
+			itemEl.classList.add(`status-${upload.status}`);
+		}
+
+		if (progressEl) {
+			let icon = this.getStatusIcon(upload.status);
+			let message = this.getStatusText(upload.status);
+			let progress = this.getStatusProgress(upload.status);
+
+			const fill = progressEl.querySelector('.fill');
+			const itemIcon = progressEl.querySelector('span.icon');
+			const itemMessage = progressEl.querySelector('span.details');
+
+			if (fill) {
+				fill.style.width = `${progress}%`;
+			}
+			if (itemMessage) itemMessage.textContent = message;
+			if (itemIcon) {
+				window.removeChildren(itemIcon);
+				itemIcon.append(icon);
+			}
+
+			if (upload.status === 'completed') {
+				setTimeout(() => {
+					if (progressEl) {
+						window.fade(progressEl, false);
+					}
+				}, 1000);
+			}
+		}
+	}
+	/**
+	 * Hide the uploader drop zone if we have reached our limit
+	 */
+	maybeLockUploads(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (field.ui.field.dropZone) {
+			const hasUploads = field.uploads && field.uploads.size > 0;
+			const atMaxFiles = field.uploads && field.uploads.size >= field.maxFiles;
+
+			// Hide if we have uploads OR if we're at max files
+			field.ui.field.dropZone.hidden = hasUploads || atMaxFiles;
+		}
+	}
+	createImageElement(upload, draggable = false) {
+		let image = window.getTemplate('uploadItem');
+		if (!image) {
+			console.error('Image template not found');
+			return;
+		}
+		image.dataset.uploadId = upload.id;
+		console.log(upload);
+		if (upload.originalFile) {
+			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
+		}
+
+
+		image.querySelector('[name="featured"]').value = upload.id;
+		let [
+			featured,
+			img,
+			video,
+			preview,
+			details
+		] = [
+			image.querySelector('[name="featured"]'),
+			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
+			image.querySelector('details')
+		];
+		[
+			featured.value,
+			img.src,
+			img.alt
+		] = [
+			upload.id,
+			upload.preview,
+			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
+		];
+
+		switch (image.dataset.subtype) {
+			case 'image':
+				[
+					img.src,
+					img.alt
+				] = [
+					upload.preview,
+					upload.originalFile?.name ?? upload.meta?.originalName?? ''
+				];
+				video.remove();
+				preview.remove();
+				break;
+			case 'video':
+				video.src = upload.preview;
+				img.remove();
+				preview.remove();
+				break;
+			case 'document':
+				let extension = '';
+				let icon;
+				switch (extension) {
+					case 'pdf':
+						icon = window.getIcon('file-pdf');
+						break;
+					case 'csv':
+						icon = window.getIcon('file-csv');
+						break;
+					case 'doc':
+						icon = window.getIcon('file-doc');
+						break;
+					case 'txt':
+						icon = window.getIcon('file-txt');
+						break;
+					case 'xls':
+						icon = window.getIcon('file-xls');
+						break;
+					default:
+						icon = window.getIcon('file');
+						break;
+				}
+
+				preview.innerText = upload.originalFile.name;
+				preview.prepend(icon);
+				img.remove();
+				video.remove();
+				break;
+		}
+		if (details) {
+			let template = window.getTemplate('uploadMeta');
+			if (template){
+				details.append(template);
+			}
+		}
+		image.draggable = draggable;
+
+		// Update input IDs safely
+		image.querySelectorAll('input').forEach(input => {
+			let id = input.id;
+			if (id) {
+				let newId = id + upload.id;
+				let label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) {
+					label.htmlFor = newId;
+				}
+			}
+		});
+
+		return image;
+	}
+
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		let progressBar = field.ui.field.progress.progress;
+
+		// Create progress bar if it doesn't exist
+		if (!progressBar) {
+			progressBar = window.getTemplate('imageProgress');
+
+			if (!progressBar) {
+				console.warn('Progress bar template not found');
+				return;
+			}
+
+			// Insert after drop zone or at top of container
+			const container = field.ui.field.field;
+			const insertAfter = field.ui.field.dropZone;
+
+			if (insertAfter) {
+				insertAfter.insertAdjacentElement('afterend', progressBar);
+			} else if (container) {
+				container.prepend(progressBar);
+			}
+
+			// Update the field UI reference to match actual structure
+			if (!field.ui.field.progress) {
+				field.ui.field.progress = {};
+			}
+			field.ui.field.progress = {
+				progress: progressBar,
+				bar: progressBar.querySelector('.bar'),
+				fill: progressBar.querySelector('.fill'),
+				details: progressBar.querySelector('.details'),
+				text: progressBar.querySelector('.details .text'),
+				count: progressBar.querySelector('.details .count')
+			};
+		}
+
+
+		progressBar.hidden = false;
+		progressBar.style.display = 'flex';
+		progressBar.style.animation = 'none';
+		progressBar.style.opacity = '1';
+
+		// Update progress bar
+		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
+		const progressFill = field.ui.field.progress.fill;
+		const progressText = field.ui.field.progress.text;
+		const progressCount = field.ui.field.progress.count;
+
+		if (progressFill) {
+			progressFill.style.width = `${progressPercent}%`;
+		}
+
+		if (progressText) {
+			progressText.textContent = message;
+		}
+
+		if (progressCount) {
+			progressCount.textContent = `${current}/${total}`;
+		}
+
+		// Hide when complete
+		if (current >= total) {
+			setTimeout(() => {
+				progressBar.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => {
+					progressBar.hidden = true;
+					progressBar.style.display = 'none';
+				}, 300);
+			}, 1000);
+		}
+	}
+
+	hideUploadProgress(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		const progressBar = field.ui.field.progress.progress;
+		if (progressBar) {
+			window.fade(progressBar, false);
+		}
+	}
+	/*******************************************************************************
+	 INDEXEDDB CACHE FUNCTIONALITY
+	 *******************************************************************************/
+	async initDB() {
+		if (!('indexedDB' in window)) return;
+
+		const request = indexedDB.open(`jvb_uploads_db`, 1);
+
+		request.onupgradeneeded = (e) => {
+			const db = e.target.result;
+			if (!db.objectStoreNames.contains('fieldStates')) {
+				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
+				store.createIndex('timestamp', 'timestamp', { unique: false });
+				store.createIndex('content', 'content', { unique: false });
+				store.createIndex('itemId', 'itemId', { unique: false });
+			}
+
+			// Blob storage remains separate for performance
+			if (!db.objectStoreNames.contains('uploadBlobs')) {
+				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
+			}
+		};
+
+		request.onsuccess = (e) => {
+			this.db = e.target.result;
+			this.loadFields();
+			this.checkPendingUploads();
+		};
+
+		request.onerror = (e) => {
+			console.error('IndexedDB error:', e);
+		};
+	}
+
+	async loadFields() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
+			const fieldStates = tx.objectStore('fieldStates');
+			const blobStore = tx.objectStore('uploadBlobs');
+			const request = fieldStates.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(field => {
+					let uploads = field.uploads;
+					let uploadIds = uploads.map(upload => upload.id);
+					field.uploads = new Set(uploadIds);
+					this.fields.set(field.key, field);
+					uploads.forEach(upload => {
+						this.uploads.set(upload.id, upload);
+					});
+				});
+				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
+				resolve();
+			};
+
+			const blobRequest = blobStore.getAll();
+
+			blobRequest.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					this.uploadBlobs.set(item.id, item);
+				});
+				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
+				resolve();
+			};
+		});
+	}
+
+	getUpload(uploadId) {
+		return this.uploads.get(uploadId);
+	}
+
+	clearField(fieldId) {
+		let uploads = Array.from(this.fields.uploads);
+		uploads.forEach(upload => {
+			this.uploads.delete(upload);
+		});
+		this.fields.delete(fieldId);
+		if (this.db) {
+			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
+			tx.objectStore('fieldStates').delete(fieldId);
+			uploads.forEach(upload => {
+				tx.objectStore('uploadBlobs').delete(upload);
+			});
+		}
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		field.uploads.forEach(upload => {
+			console.log('Attempting to set upload to status: ', status);
+			this.updateUploadStatus(upload, status);
+		});
+
+		// Update UI based on status
+		const container = field.ui.field.field;
+		if (container) {
+			container.dataset.uploadStatus = status;
+
+			// Show/hide relevant UI elements
+			const submitBtn = container.querySelector('.submit-uploads');
+			if (submitBtn) {
+				submitBtn.disabled = status === 'uploading' || status === 'processing';
+			}
+		}
+	}
+
+	/**
+	 * Handle successful upload completion
+	 */
+	handleUploadComplete(operation) {
+		const response = operation.response;
+		if (!response?.uploads) return;
+
+		// Map server IDs to uploads
+		response.uploads.forEach(serverUpload => {
+			const upload = this.uploads.get(serverUpload.upload_id);
+			if (upload) {
+				upload.attachmentId = serverUpload.attachment_id;
+				this.updateUploadStatus(serverUpload.upload_id, 'completed');
+				this.uploads.set(upload.id, upload);
+
+				// Clear from cache since it's now on server
+				this.clearUpload(upload.id);
+			}
+		});
+
+		// Persist updated field state
+		const fieldKey = operation.data.get('field_key');
+		if (fieldKey) {
+			this.persistFieldState(fieldKey);
+		}
+	}
+
+	/**
+	 * Clear individual upload from cache after successful server upload
+	 */
+	async clearUpload(uploadId) {
+		const upload = this.uploads.get(uploadId);
+		if (!upload) return;
+
+		// Clean up preview URL
+		if (upload.preview?.startsWith('blob:')) {
+			URL.revokeObjectURL(upload.preview);
+		}
+
+		this.persistFieldState(upload.fieldId);
+		// Remove from memory
+		this.uploads.delete(uploadId);
+		this.uploadBlobs.delete(uploadId);
+
+		// Remove from IndexedDB
+		if (this.db) {
+			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
+			await tx.objectStore('uploadBlobs').delete(uploadId);
+		}
+	}
+
+	/**
+	 * Store upload with DataStore integration
+	 */
+	async setUpload(fieldId, file, uploadId = null) {
+		if (!uploadId) {
+			uploadId = this.generateUploadId();
+		}
+		const upload = {
+			id: uploadId,
+			fieldId: fieldId,
+			groupId: null,
+			originalFile: file,
+			processedFile: null,
+			status: 'received',
+			progress: { percent: 0, message: 'Received...' },
+			preview: URL.createObjectURL(file),
+			createdAt: Date.now(),
+			meta: {
+				title: '',
+				alt_text: '',
+				caption: '',
+				originalName: file.name,
+				originalType: file.type,
+				originalSize: file.size
+			},
+			changes: {}
+		};
+
+		// Add to field
+		const field = this.fields.get(fieldId);
+		if (!field) {
+			console.error(`Field ${fieldId} not found`);
+			return null;
+		}
+		if (!field.uploads) field.uploads = new Set();
+		field.uploads.add(uploadId);
+
+		upload.element = this.createImageElement(upload, field.type==='groupable');
+		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
+
+		// Store in memory
+		this.uploads.set(uploadId, upload);
+		this.updateImageUI(uploadId);
+
+		// Persist to DataStore
+		await this.persistFieldState(fieldId);
+
+		return upload;
+	}
+
+	/**
+	 * Get uploads for a field, optionally cleaned for storage
+	 * @param {string} fieldId
+	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
+	 * @returns {Array}
+	 */
+	getFieldUploads(fieldId, clean = false) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.uploads) return [];
+
+		return Array.from(field.uploads)
+			.map(uploadId => {
+				const upload = this.uploads.get(uploadId);
+				if (!upload) return null;
+
+				if (clean) {
+					// Return cleaned version without DOM references
+					return {
+						id: upload.id,
+						fieldId: upload.fieldId,
+						status: upload.status,
+						preview: upload.preview,
+						attachmentId: upload.attachmentId,
+						operationId: upload.operationId,
+						groupId: upload.groupId || null,
+						meta: {
+							originalName: upload.meta?.originalName || upload.originalFile?.name,
+							size: upload.meta?.size || upload.originalFile?.size,
+							type: upload.meta?.type || upload.originalFile?.type,
+							title: upload.meta?.title,
+							alt: upload.meta?.alt,
+							caption: upload.meta?.caption
+						}
+					};
+				}
+
+				// Return full upload object
+				return upload;
+			})
+			.filter(Boolean);
+	}
+
+	/**
+	 * Persist upload to DataStore
+	 */
+	async persistFieldState(fieldId) {
+		if (!this.db) return;
+
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Create clean field config
+		const { ui, ...cleanConfig } = field;
+
+		const fieldState = {
+			fieldId: fieldId,
+			timestamp: Date.now(),
+
+			config: {
+				...cleanConfig,
+				fieldName: field.name,
+				dataField: field.ui?.field?.field?.dataset?.field
+			},
+
+			// Recovery context with normalized URL
+			context: {
+				url: this.normalizeUrl(window.location.href),
+				fullUrl: window.location.href, // Keep for reference
+				modalType: this.getModalType(field),
+				formId: field.formId,
+				// **FIX**: Store additional identifiers
+				fieldSelector: `.field.upload[data-field="${field.name}"]`
+			},
+
+			// Uploads (cleaned of DOM references and blob URLs)
+			uploads: this.getFieldUploads(fieldId, true).map(upload => {
+				// **FIX**: Don't store blob URLs as they become invalid
+				const { preview, element, location, ...cleanUpload } = upload;
+				return cleanUpload;
+			}),
+
+			// Groups structure
+			groups: Array.from(this.groups.entries())
+				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
+				.map(([id, data]) => ({
+					id: data.id,
+					uploads: Array.from(data.uploads),
+					meta: data.meta || {},
+					changes: data.changes || {}
+				}))
+		};
+
+		try {
+			const tx = this.db.transaction(['fieldStates'], 'readwrite');
+			await tx.objectStore('fieldStates').put(fieldState);
+		} catch (error) {
+			console.error('Failed to persist field state:', error);
+		}
+	}
+
+	normalizeUrl(url) {
+		try {
+			const urlObj = new URL(url);
+			// Return just the origin + pathname (no query string or hash)
+			return urlObj.origin + urlObj.pathname;
+		} catch (e) {
+			return url;
+		}
+	}
+	/*******************************************************************************
+	 RESTORE FUNCTIONALITY
+	 *******************************************************************************/
+	async checkPendingUploads() {
+		console.log('Checking for pending uploads');
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const fieldStore = tx.objectStore('fieldStates');
+
+		const allFieldStates = await new Promise(resolve => {
+			const request = fieldStore.getAll();
+			request.onsuccess = () => resolve(request.result);
+		});
+
+		console.log('All Field States', allFieldStates);
+
+		// ADD DETAILED LOGGING HERE:
+		allFieldStates.forEach(field => {
+			console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
+			field.uploads.forEach((upload, idx) => {
+				console.log(`  Upload ${idx}:`, {
+					id: upload.id,
+					status: upload.status,
+					operationId: upload.operationId,
+					hasOperationId: !!upload.operationId
+				});
+			});
+		});
+
+		// Filter for pending uploads (not yet sent to server)
+		const pendingFields = allFieldStates.filter(field =>
+			field.uploads.some(upload =>
+				// If no operationId, it hasn't been sent to server yet
+				!upload.operationId &&
+				// And it's been processed locally
+				(upload.status === 'completed' ||
+					upload.status === 'processed' ||
+					upload.status === 'local_processing' ||
+					upload.status === 'processed-original')
+			)
+		);
+
+		console.log('Pending Fields: ', pendingFields);
+
+		if (pendingFields.length === 0) return;
+
+		// Show recovery notification
+		this.showRecoveryNotification(pendingFields);
+	}
+
+	async showRecoveryNotification(pendingFields) {
+		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+		const totalGroups = pendingFields.reduce((sum, field) =>
+			sum + (field.groups?.length || 0), 0);
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			console.error('Restore notification template not found');
+			return;
+		}
+
+		// Build appropriate message
+		let message = '';
+		if (totalGroups > 0) {
+			message = `${totalGroups} organized group(s) with ${totalUploads} upload(s) ready to submit.`;
+		} else {
+			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+		}
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) {
+			detailsEl.textContent = message;
+		}
+
+		// Build the restoration preview
+		for (const field of pendingFields) {
+			console.log('Field to restore:', field);
+			let fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			// Set field name/title
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) {
+				titleEl.textContent = field.config.name || 'Unnamed Field';
+			}
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			// Process each upload
+			for (const upload of field.uploads) {
+
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const blobData = await this.getBlobData(upload.id);
+
+
+				if (blobData) {
+					try {
+						// Create new blob URL from stored data
+						const blob = new Blob([blobData.data], { type: blobData.type });
+						const previewUrl = URL.createObjectURL(blob);
+
+						let [
+							featured,
+							img,
+							video,
+							preview,
+							details
+						] = [
+							uploadItem.querySelector('[name="featured"]'),
+							uploadItem.querySelector('img'),
+							uploadItem.querySelector('video'),
+							uploadItem.querySelector('label > span'),
+							uploadItem.querySelector('details')
+						];
+
+						uploadItem.dataset.uploadId = upload.id;
+
+						let subtype = this.getSubtypeFromMime(blobData.type);
+						console.log(subtype);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									upload.originalFile?.name ?? upload.meta?.originalName?? ''
+								];
+								video.remove();
+								preview.remove();
+								break;
+							case 'video':
+								video.src = previewUrl;
+								img.remove();
+								preview.remove();
+								break;
+							case 'document':
+								let extension = '';
+								let icon;
+								switch (extension) {
+									case 'pdf':
+										icon = window.getIcon('file-pdf');
+										break;
+									case 'csv':
+										icon = window.getIcon('file-csv');
+										break;
+									case 'doc':
+										icon = window.getIcon('file-doc');
+										break;
+									case 'txt':
+										icon = window.getIcon('file-txt');
+										break;
+									case 'xls':
+										icon = window.getIcon('file-xls');
+										break;
+									default:
+										icon = window.getIcon('file');
+										break;
+								}
+
+								preview.innerText = upload.originalFile.name;
+								preview.prepend(icon);
+								img.remove();
+								video.remove();
+								break;
+						}
+
+						// Store URL for cleanup later
+						uploadItem.dataset.previewUrl = previewUrl;
+					} catch (error) {
+						console.warn('Failed to create preview for upload:', upload.id, error);
+					}
+				}
+
+				// Set upload metadata
+				const nameEl = uploadItem.querySelector('summary span');
+				if (nameEl) {
+					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+				}
+
+				const metaEl = uploadItem.querySelector('details');
+				if (metaEl && upload.meta) {
+					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+				}
+
+				// Update input IDs safely
+				uploadItem.querySelectorAll('input').forEach(input => {
+					let id = input.id;
+					if (id) {
+						let newId = id + upload.id;
+						let label = input.parentNode.querySelector(`label[for="${id}"]`);
+						input.id = newId;
+						if (label) {
+							label.htmlFor = newId;
+						}
+					}
+				});
+
+				if (itemGrid) {
+					itemGrid.appendChild(uploadItem);
+				}
+			}
+
+			notification.querySelector('.wrap').appendChild(itemGrid);
+		}
+
+		// Event handlers
+		const restoreBtn = notification.querySelector('.restore-selected');
+		if (restoreBtn) {
+			restoreBtn.addEventListener('click', () => {
+				const selectedUploads = this.getSelectedRestorationUploads(notification);
+				if (selectedUploads.length === 0) {
+					// this.notifications.add('No uploads selected for restoration', 'warning');
+					return;
+				}
+				this.restoreSelectedUploads(selectedUploads);
+
+				// Clean up blob URLs before removing notification
+				this.cleanupRestoreNotificationUrls(notification);
+				notification.remove();
+			});
+		}
+
+		const dismissBtn = notification.querySelector('.dismiss-cache-check');
+		if (dismissBtn) {
+			dismissBtn.addEventListener('click', () => {
+				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(pendingFields));
+				// this.notifications.add('Uploads saved for later restoration', 'info');
+
+				// Clean up blob URLs
+				this.cleanupRestoreNotificationUrls(notification);
+				notification.remove();
+			});
+		}
+
+		const clearBtn = notification.querySelector('.restart-uploads');
+		if (clearBtn) {
+			clearBtn.addEventListener('click', async () => {
+				const confirmed = confirm('This will permanently delete all cached uploads. Continue?');
+				if (confirmed) {
+					await this.clearCachedUploads(pendingFields);
+
+					// Clean up blob URLs
+					this.cleanupRestoreNotificationUrls(notification);
+					notification.remove();
+				}
+			});
+		}
+
+		document.querySelector('main').appendChild(notification);
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			ui: {
+				selectAll: notification.querySelector('.select-all-restore'),
+				count: notification.querySelector('.selection-count'),
+			},
+		});
+
+		this.restoreModal.handleOpen();
+		this.restoreModal.subscribe((event, data) => {
+			if (event === 'modal-close') {
+				this.cleanupStoredRestoration();
+			}
+		});
+	}
+
+	cleanupStoredRestoration() {
+		//TODO delete saved uploads from cache, cleanup blobs
+	}
+
+	cleanupRestoreNotificationUrls(notification) {
+		notification.querySelectorAll('[data-preview-url]').forEach(item => {
+			const url = item.dataset.previewUrl;
+			if (url && url.startsWith('blob:')) {
+				URL.revokeObjectURL(url);
+			}
+		});
+	}
+
+	getSelectedRestorationUploads(notificationEl) {
+		const selected = [];
+		const checkboxes = notificationEl.querySelectorAll('.restore-checkbox:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('label');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		return selected;
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		// Group by field
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		// Get full field states from IndexedDB
+		if (!this.db) {
+			// this.notifications.add('Cannot restore: Database not available', 'error');
+			return;
+		}
+
+		const tx = this.db.transaction(['fieldStates'], 'readonly');
+		const store = tx.objectStore('fieldStates');
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const request = store.get(fieldId);
+			const fieldState = await new Promise(resolve => {
+				request.onsuccess = () => resolve(request.result);
+				request.onerror = () => resolve(null);
+			});
+
+			if (fieldState) {
+				// Filter to only selected uploads
+				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
+				await this.restoreField(fieldState);
+			}
+		}
+
+		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
+	}
+
+	async restoreField(fieldState) {
+		const { config, context, uploads, groups } = fieldState;
+
+		// If in a modal, open it first
+		if (context.modalType) {
+			await this.openModalForRestore(context);
+		}
+
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+		if (!fieldElement) {
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
+			return;
+		}
+
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fields.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement, config);
+		}
+
+		const field = this.fields.get(fieldKey);
+		if (!field) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// ADDED: Ensure UI groups structure is initialized
+		if (!field.ui.groups) {
+			field.ui.groups = {};
+		}
+		if (!field.ui.groups.groups) {
+			field.ui.groups.groups = new Map();
+		}
+
+		// Make sure we have the container and empty group references
+		if (!field.ui.groups.container) {
+			field.ui.groups.container = fieldElement.querySelector('.item-grid.groups');
+		}
+		if (!field.ui.groups.empty) {
+			field.ui.groups.empty = fieldElement.querySelector('.empty-group');
+		}
+
+		// Restore uploads
+		for (const uploadData of uploads) {
+			await this.restoreUpload(field, uploadData);
+		}
+
+		// Restore groups
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(field, groups, uploads);
+		}
+
+		// Update UI
+		this.updateFieldState(fieldKey);
+		this.maybeLockUploads(fieldKey);
+
+		// Queue for upload if needed (should not happen for post_group)
+		if (config.mode === 'direct' && config.destination !== 'post_group') {
+			await this.queueUpload(fieldKey);
+		}
+	}
+
+	async restoreUpload(field, uploadData) {
+		// Try to get blob data from IndexedDB
+		const blobData = await this.getBlobData(uploadData.id);
+
+		if (blobData) {
+			// Recreate file from blob data
+			const file = new File(
+				[blobData.data],
+				blobData.name,
+				{ type: blobData.type, lastModified: blobData.lastModified }
+			);
+
+			uploadData.originalFile = file;
+			uploadData.processedFile = file;
+
+			// **FIX**: Create fresh blob URL since old one is invalid
+			uploadData.preview = URL.createObjectURL(file);
+		} else {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return; // Skip this upload if we can't restore the file
+		}
+
+		// Add to field
+		if (!field.uploads) field.uploads = new Set();
+		field.uploads.add(uploadData.id);
+
+		// Recreate DOM element
+		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
+		uploadData.element = this.createImageElement({
+			...uploadData,
+			subtype: subtype
+		}, field.destination === 'post_group');
+
+		// Restore to correct location
+		let location;
+		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
+			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
+		} else {
+			location = field.ui.field.preview;
+		}
+
+		if (location) {
+			location.appendChild(uploadData.element);
+			uploadData.location = location;
+		}
+
+		// Store in memory
+		this.uploads.set(uploadData.id, uploadData);
+	}
+
+	async restoreFieldStates(fieldStates) {
+		// Group by URL
+		const byUrl = new Map();
+		fieldStates.forEach(field => {
+			if (!byUrl.has(field.context.url)) {
+				byUrl.set(field.context.url, []);
+			}
+			byUrl.get(field.context.url).push(field);
+		});
+
+		// If all on current page, restore directly
+		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
+			for (const fieldState of fieldStates) {
+				await this.restoreField(fieldState);
+			}
+			// this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
+		} else {
+			// Store intent to restore and navigate
+			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
+
+			// Navigate to first URL
+			const firstUrl = byUrl.keys().next().value;
+			if (window.location.href !== firstUrl) {
+				window.location.href = firstUrl;
+			}
+		}
+	}
+
+	async restoreGroups(field, groups, uploads) {
+		// Ensure the groups.groups Map exists
+		if (!field.ui.groups.groups) {
+			field.ui.groups.groups = new Map();
+		}
+
+		for (const groupData of groups) {
+			// Create group element
+			const groupElement = this.createGroupElement(groupData.id, field.key);
+
+			// Store in field UI Map
+			field.ui.groups.groups.set(groupData.id, groupElement);
+
+			// Insert into DOM
+			if (field.ui.groups.container && field.ui.groups.empty) {
+				field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
+			} else if (field.ui.groups.container) {
+				field.ui.groups.container.appendChild(groupElement);
+			}
+
+			// FIXED: Create proper group structure matching createGroup()
+			this.groups.set(groupData.id, {
+				id: groupData.id,
+				fieldId: field.key,
+				element: groupElement,
+				uploads: new Set(groupData.uploads), // FIXED: was groupData.uploadIds
+				meta: groupData.meta || {},
+				changes: groupData.changes || {}
+			});
+
+			// Move uploads to group
+			// FIXED: use groupData.uploads instead of groupData.uploadIds
+			groupData.uploads.forEach(uploadId => {
+				const upload = uploads.find(u => u.id === uploadId);
+				if (upload && upload.element) {
+					const groupGrid = groupElement.querySelector('.item-grid');
+					if (groupGrid) {
+						groupGrid.appendChild(upload.element);
+						upload.location = groupGrid;
+						upload.groupId = groupData.id;
+					}
+				}
+			});
+		}
+	}
+
+	async getBlobData(uploadId) {
+		if (!this.db) return null;
+
+		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
+		const request = tx.objectStore('uploadBlobs').get(uploadId);
+
+		return new Promise(resolve => {
+			request.onsuccess = () => resolve(request.result);
+			request.onerror = () => resolve(null);
+		});
+	}
+
+	async openModalForRestore(context) {
+		const { modalType, formId } = context;
+
+		// Find and click the appropriate button to open the modal
+		let trigger = null;
+
+		switch(modalType) {
+			case 'create':
+				trigger = document.querySelector('[data-action="create"]');
+				break;
+			case 'edit':
+				// Need to find the specific edit button
+				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
+				break;
+			case 'bulkEdit':
+				trigger = document.querySelector('[data-action="bulk-edit"]');
+				break;
+		}
+
+		if (trigger) {
+			trigger.click();
+
+			// Wait for modal to open
+			await new Promise(resolve => setTimeout(resolve, 300));
+		}
+	}
+
+	async clearCachedUploads(fieldStates) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
+
+		for (const field of fieldStates) {
+			// Delete field state
+			await tx.objectStore('fieldStates').delete(field.fieldId);
+
+			// Delete all associated blobs
+			for (const upload of field.uploads) {
+				await tx.objectStore('uploadBlobs').delete(upload.id);
+
+				// Clean up preview URLs
+				if (upload.preview?.startsWith('blob:')) {
+					URL.revokeObjectURL(upload.preview);
+				}
+			}
+		}
+
+		// this.notifications.add('Cached uploads cleared', 'info');
+	}
+
+// Check for restoration intent on page load
+	async checkRestorationIntent() {
+		const restoreData = sessionStorage.getItem('jvb_restore_uploads');
+		if (!restoreData) return;
+
+		const fieldStates = JSON.parse(restoreData);
+		const currentUrlFields = fieldStates.filter(f => f.context.url === window.location.href);
+
+		if (currentUrlFields.length > 0) {
+			for (const fieldState of currentUrlFields) {
+				await this.restoreField(fieldState);
+			}
+
+			// Remove restored fields from session storage
+			const remaining = fieldStates.filter(f => f.context.url !== window.location.href);
+			if (remaining.length > 0) {
+				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(remaining));
+			} else {
+				sessionStorage.removeItem('jvb_restore_uploads');
+			}
+
+			// this.notifications.add(`Restored ${currentUrlFields.length} field(s)`, 'success');
+		}
+	}
+	/*******************************************************************************
+	 GROUP FUNCTIONALITY
+	 Includes selection, dragging, and grouping logic
+	 *******************************************************************************/
+	/**
+	 *
+	 * @param {string} uploadId as defined by setUpload
+	 * @param {HTMLElement|null} target The target location
+	 * @param {boolean} persist whethet to cache this change
+	 */
+	addImageToGroup(uploadId, target = null, persist = true) {
+		let upload = this.getUpload(uploadId);
+		if(!upload) {
+			return;
+		}
+		let field = this.fields.get(upload.fieldId);
+		if (!field) {
+			return;
+		}
+		//Already in the Preview Grid, or already in the group we're moving to
+		if ((!target && upload.location === field.ui.field.preview) || target === upload.location) {
+			return;
+		}
+
+		if (upload.location) {
+			let groupId = upload.location.dataset.groupId;
+			if (groupId) {
+				let group = this.groups.get(groupId);
+				if (group && group.uploads) {
+					group.uploads.delete(uploadId);
+
+					// ADDED: Delete empty groups automatically
+					if (group.uploads.size === 0) {
+						this.removeGroup(groupId);
+					}
+				}
+			}
+		}
+
+		const checkbox = upload.element.querySelector('[name*="select-item"]');
+		if (checkbox) {
+			checkbox.checked = false;
+		}
+
+		upload.element.querySelector('[name="featured"]').hidden = !target;
+		//If no target, it's going to the preview grid
+		if (!target) {
+			target = field.ui.field.preview;
+		} else {
+			let groupId = target.dataset.groupId;
+			let group = this.groups.get(groupId);
+			if (!group) {
+				group = this.createGroup(upload.fieldId);
+			}
+			group.uploads.add(uploadId);
+		}
+
+
+		target.append(upload.element);
+		if (persist) {
+			this.persistFieldState(field.key);
+		}
+	}
+
+	addSelectionToGroup(target) {
+		let field = this.getFieldFromElement(target);
+		if (!field) {
+			return;
+		}
+		if (this.selected.get(field.key).size === 0) {
+			return;
+		}
+		let group = this.getGroupFromElement(target);
+		if (!group) {
+			group = this.createGroup(field.key);
+		}
+
+		Array.from(this.selected).forEach(uploadId => {
+			this.addImageToGroup(uploadId, group.grid, false);
+		});
+
+		this.persistFieldState(group.fieldId);
+	}
+
+
+	/**
+	 * Remove an empty group from the field
+	 * @param {string} groupId - The group to remove
+	 * @param {boolean} confirm - ask for confirmation
+	 */
+	removeGroup(groupId, confirm = false) {
+		let group = this.groups.get(groupId);
+		if (!group) {
+			return;
+		}
+
+		if (confirm && group.uploads && group.uploads.size > 0) {
+			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
+				return;
+			}
+		}
+
+		// Move any remaining uploads back to preview
+		if (group.uploads && group.uploads.size > 0) {
+			Array.from(group.uploads).forEach(uploadId => {
+				this.addImageToGroup(uploadId, null, false);
+			});
+		}
+
+		// Remove from groups Map
+		this.groups.delete(groupId);
+
+		// Remove DOM element
+		let groupElement = group.element;
+		if (groupElement) {
+			groupElement.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		this.persistFieldState(group.fieldId);
+	}
+
+	/**
+	 * Create a new group
+	 */
+	createGroup(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (!field.groups) {
+			field.groups = [];
+		}
+
+		const groupId = `group_${Date.now()}`;
+		field.groups.push({
+			id: groupId,
+			title: '',
+			uploads: []
+		});
+
+		// Create the group element
+		const groupElement = this.createGroupElement(groupId, fieldId);
+
+		if (!groupElement) return null;
+
+		// Store in the groups Map with full group data structure
+		this.groups.set(groupId, {
+			id: groupId,
+			fieldId: fieldId,
+			element: groupElement,
+			uploads: new Set(),
+			meta: {},
+			changes: {}
+		});
+
+		// Add to UI
+		const container = field.ui.groups.container;
+		if (container) {
+			const emptyGroup = container.querySelector('.empty-group');
+			if (emptyGroup) {
+				container.insertBefore(groupElement, emptyGroup);
+			} else {
+				container.appendChild(groupElement);
+			}
+		}
+
+		this.persistFieldState(field.key);
+
+		return groupElement;
+	}
+
+
+	/**
+	 * Remove upload from group
+	 */
+	removeFromGroup(fieldId, uploadId, groupId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const group = field.groups.find(g => g.id === groupId);
+		if (!group) return;
+
+		group.uploads = group.uploads.filter(id => id !== uploadId);
+
+		this.renderGroupUI(fieldId);
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Update group title
+	 */
+	updateGroupTitle(fieldId, groupId, title) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const group = field.groups.find(g => g.id === groupId);
+		if (!group) return;
+
+		group.title = title;
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Delete group
+	 */
+	deleteGroup(fieldId, groupId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		field.groups = field.groups.filter(g => g.id !== groupId);
+
+		this.renderGroupUI(fieldId);
+		this.persistFieldState(field.key);
+	}
+
+	/**
+	 * Render group UI
+	 */
+	renderGroupUI(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field || !field.groups) return;
+
+		const container = field.ui.group.container;
+		if (!container) {
+			console.warn('Groups container not found for field:', fieldId);
+			return;
+		}
+
+		// Clear existing
+		window.removeChildren(container);
+
+		// Render each group
+		field.groups.forEach(group => {
+			const groupEl = this.createGroupElement(fieldId, group);
+			container.appendChild(groupEl);
+		});
+	}
+
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			// Set unique IDs and names for form fields
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+			let field = this.fields.get(fieldId);
+			if (field.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				summary.textContent = field.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details').remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	handleSelectAll(element, checked = null) {
+		const field = this.getFieldFromElement(element);
+		if (!field) return;
+
+		const handler = this.selectionHandlers.get(field.key);
+		if (!handler) return;
+
+		// Use element's checked state if not provided
+		if (checked === null) {
+			checked = element.checked;
+		}
+
+		handler.selectAll(checked);
+		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
+	}
+
+	clearAllSelections(field) {
+		const handler = this.selectionHandlers.get(field.key);
+		if (handler) {
+			handler.clearSelection();
+		}
+	}
+
+	getSelectedUploads(element) {
+		const field = this.getFieldFromElement(element);
+		if (!field) return [];
+
+		const handler = this.selectionHandlers.get(field.key);
+		return handler ? handler.getSelected() : [];
+	}
+
+	removeSelection(button) {
+		let fieldId = this.getFieldIdFromElement(button);
+
+		const selectedUploads = this.getSelectedUploads(button);
+		if (selectedUploads.length === 0) {
+			this.notify('No uploads selected', 'warning');
+			return;
+		}
+
+		selectedUploads.forEach(upload => {
+			this.removeUpload(fieldId, upload);
+		});
+	}
+
+	removeUpload(fieldId, uploadId) {
+		const field = this.fields.get(fieldId);
+		const upload = this.uploads.get(uploadId);
+
+		if (!field || !upload) return;
+
+		// Remove from field
+		field.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = this.groups.get(upload.groupId);
+			if (group && group.uploads) {
+				group.uploads.delete(uploadId);
+
+				if (group.uploads.size === 0) {
+					this.removeGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		upload.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state after removal
+		this.updateFieldState(fieldId);
+
+		// Update UI
+		this.maybeLockUploads(fieldId);
+		const handler = this.selectionHandlers.get(field.key);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	/**************************************************************************
+	 META
+	 Handled separately, in case it is edited in the middle of processing images
+	 **************************************************************************/
+
+	/**************************************************************************
+	 SUBSCRIBERS
+	 **************************************************************************/
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	handleBeforeUnload(e) {
+		// Check for any uploads in processing or pending state
+		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
+			upload.status === 'processing' ||
+			upload.status === 'pending' ||
+			upload.status === 'uploading'
+		);
+
+		if (unsavedUploads.length > 0) {
+			const message = 'You have uploads in progress. Are you sure you want to leave?';
+			e.preventDefault();
+			e.returnValue = message;
+			return message;
+		}
+	}
+	/**************************************************************************
+	 CLEANUP
+	 **************************************************************************/
+	cleanup() {
+		this.clearListeners();
+		if (this.hasGroups) {
+			this.clearGroupListeners();
+		}
+		this.compressionWorker = null;
+		this.subscribers.clear();
+	}
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+	window.jvbUploads = new UploadManager();
+});
diff --git a/assets/js/concise/UserSettings.js b/assets/js/concise/UserSettings.js
new file mode 100644
index 0000000..91e950e
--- /dev/null
+++ b/assets/js/concise/UserSettings.js
@@ -0,0 +1,127 @@
+class UserSettings {
+	constructor() {
+		this.cache = new window.jvbCache('settings');
+		this.debouncer = window.debouncer;
+
+		this.isLoggedIn = jvbSettings.currentUser !== null;
+
+		this.initListeners();
+
+		this.subscribers = new Set();
+	}
+
+	initListeners() {
+		this.changeHandler = this.handleChange.bind(this);
+
+		document.addEventListener('change', this.changeHandler);
+	}
+
+	handleChange(e) {
+
+	}
+
+	saveSetting(name, value) {
+		this.cache.setItem(name, value);
+		if (this.isLoggedIn) {
+
+		}
+	}
+
+	loadSetting(name) {
+		let value = this.cache.getItem(name);
+
+		if (this.isLoggedIn) {
+
+		}
+	}
+
+	loadUserSetting(name) {
+
+	}
+
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/***************************************************
+	 CLEANUP
+	***************************************************/
+	destroy() {
+		document.removeEventListener('change', this.changeHandler);
+		this.subscribers.clear();
+	}
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+	window.jvbUserSettings = new UserSettings();
+});
+
+// Theme switching functionality
+document.addEventListener('DOMContentLoaded', function() {
+	console.log('Theme switch initiated');
+	const themeSwitch = document.getElementById('theme-switch');
+
+	if (!themeSwitch) return;
+
+	// Initialize theme from localStorage or system preference
+	const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
+	const storedTheme = localStorage.getItem('theme');
+
+	if (storedTheme) {
+		document.documentElement.classList.toggle('dark', storedTheme === 'dark');
+		themeSwitch.checked = storedTheme === 'dark';
+	} else {
+		document.documentElement.classList.toggle('dark', prefersDark.matches);
+		themeSwitch.checked = prefersDark.matches;
+	}
+
+	// Handle theme switch changes
+	themeSwitch.addEventListener('change', async function () {
+		const isDark = this.checked;
+		document.documentElement.classList.toggle('dark', isDark);
+		localStorage.setItem('theme', isDark ? 'dark' : 'light');
+
+		// If user is logged in, save preference
+		if (jvbSettings.currentUser !== null) {
+			try {
+				await fetch(`${jvbSettings.api}settings`, {
+					method: 'POST',
+					headers: {
+						'Content-Type': 'application/json',
+						'X-WP-Nonce': jvbSettings.nonce,
+						'action_nonce': jvbSettings.dash,
+					},
+					body: JSON.stringify({
+						dark_mode: isDark,
+						user: jvbSettings.currentUser
+					})
+				});
+			} catch (error) {
+				console.error('Failed to save theme preference:', error);
+			}
+		}
+
+		// Update label
+		const label = document.getElementById('theme-switch');
+		if (label) {
+			label.title = isDark ? 'Toggle Light Mode' : 'Toggle Dark Mode';
+		}
+	});
+
+	// Handle system theme changes
+	prefersDark.addEventListener('change', (e) => {
+		if (!localStorage.getItem('theme')) {
+			const isDark = e.matches;
+			document.documentElement.classList.toggle('dark', isDark);
+			themeSwitch.checked = isDark;
+		}
+	});
+});
diff --git a/assets/js/concise/View.js b/assets/js/concise/View.js
index b31b782..70c8e2f 100644
--- a/assets/js/concise/View.js
+++ b/assets/js/concise/View.js
@@ -3,7 +3,6 @@
  */
 class ViewController {
 	constructor(container, store) {
-		console.log(container);
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
 
@@ -40,20 +39,25 @@
 		}
 
 		this.ui = window.uiFromSelectors(this.selectors, this.container);
-		console.log(this.ui);
 	}
 
 	init() {
 		// Subscribe to store updates
 		this.store.subscribe((event, data) => {
 			switch(event) {
-				case 'data-fetched':
-				case 'data-cached':
+				case 'data-loaded':
+				case 'items-saved':
 					this.handleDataUpdate(data);
 					break;
 				case 'items-updated':
 					this.handleItemsUpdate(data.items);
 					break;
+				case 'item-saved':
+					// this.updateItem(data.item);
+					break;
+				case 'item-deleted':
+					// this.deleteItem(data.item);
+					break;
 			}
 		});
 
@@ -128,10 +132,10 @@
 
 	toggleColumns(column, show) {
 		let theColumn = this.ui.table.columns.filter(col => col.className === column);
-		console.log(theColumn);
-		console.log('Toggle Columns');
-		console.log(column, show);
-		console.log(this.ui.table.columns);
+		// console.log(theColumn);
+		// console.log('Toggle Columns');
+		// console.log(column, show);
+		// console.log(this.ui.table.columns);
 	}
 
 	setupViewSwitcher() {
@@ -201,11 +205,16 @@
 
 		items.forEach(item => {
 			let card;
-			if (this.items.grid.has(item.id)) {
-				card = this.items.grid.get(item.id);
-			} else {
+			if (this.store.renderOrRetrieve) {
 				card = this.store.renderOrRetrieve(item, 'grid', this.renderGridItem.bind(this));
-				this.items.grid.set(item.id, card);
+			} else {
+				// Fallback to local cache
+				if (this.items.grid.has(item.id)) {
+					card = this.items.grid.get(item.id);
+				} else {
+					card = this.renderGridItem(item);
+					this.items.grid.set(item.id, card);
+				}
 			}
 
 			fragment.appendChild(card);
@@ -368,7 +377,7 @@
 					if (!isEmpty) {
 						field.querySelector('input').checked = parseInt(value) === 1;
 					}
-					field.querySelector('.toggle-label').hidden = true;
+					field.querySelector('.toggle-label')?.remove();
 					break;
 				case 'select':
 					label.remove();
diff --git a/assets/js/dash/CRUD.js b/assets/js/dash/CRUD.js
index 7bd4d50..e9f39d7 100644
--- a/assets/js/dash/CRUD.js
+++ b/assets/js/dash/CRUD.js
@@ -18,16 +18,23 @@
 		// Initialize components
 		this.store = new window.jvbStore({
 			name: this.content,
+			storeName: this.content,
 			endpoint: 'content',
 			headers: {
 				'action_nonce': jvbSettings.dash,
 			},
+			indexes: [
+				{ name: 'status', keyPath: 'post_status'},
+				{ name: 'modified', keyPath: 'modified'},
+			],
 			filters: {
 				content: this.content,
 				user: jvbSettings.currentUser,
 				page: 1,
 				status: 'all'
-			}
+			},
+			TTL: 3600000,
+			cacheDOM: true
 		});
 
 		this.status = 'all';
@@ -209,6 +216,14 @@
 		// Load initial data
 		this.store.fetch();
 
+		this.queue.subscribe((event, data) => {
+			switch (event) {
+				case 'operation-status':
+					//update items?
+					break;
+			}
+		});
+
 		this.initialized = true;
 	}
 
@@ -238,7 +253,7 @@
 							};
 							window.fade(actionBtn.closest('.item'), false);
 							this.savePosts(changes, `Sending ${this.singular} to trash...`);
-							this.store.deleteItem(id);
+							this.store.delete(id);
 						}
 						break;
 					case 'trash':
@@ -268,17 +283,17 @@
 					case 'bulk-delete':
 						const toDelete = Array.from(this.viewController.selectedItems);
 						if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
-							toDelete.forEach(id => this.store.deleteItem(id));
+							toDelete.forEach(id => this.store.delete(id));
 							this.viewController.clearSelection();
 						}
 						break;
 
 					case 'sync':
-						this.store.syncQueue();
+						// this.store.syncQueue();
 						break;
 
 					case 'refresh':
-						this.store.fetchFromServer(this.store.filters);
+						this.store.fetch();
 						break;
 				}
 			}
@@ -469,7 +484,7 @@
 		window.removeChildren(container);
 		for (let selected of this.viewController.selectedItems) {
 			console.log(selected);
-			let item = this.store.getItem(selected);
+			let item = this.store.get(selected);
 			console.log(item);
 			const img = window.getTemplate('bulkItem');
 			if (!img) return;
@@ -502,7 +517,7 @@
 	}
 
 	populateEditForm(itemID) {
-		let item = this.store.getItem(itemID);
+		let item = this.store.get(itemID);
 		console.log(item);
 		if (item) {
 			this.ui.modals.edit.dataset.itemID = itemID;
diff --git a/assets/js/dash/SquareCheckout.js b/assets/js/dash/SquareCheckout.js
index 1a1dda3..3fc9a3c 100644
--- a/assets/js/dash/SquareCheckout.js
+++ b/assets/js/dash/SquareCheckout.js
@@ -15,7 +15,6 @@
 		}, config);
 
 
-
 		this.stepMultiplier = 1;
 
 		this.cache = new window.jvbCache('cart', {TTL: 8.64e+7});
@@ -36,6 +35,13 @@
 
 		this.initElements();
 		this.bindEvents();
+
+		this.popup = new window.jvbPopup({
+			popup: this.checkout,
+			toggle: this.toggle,
+			name: 'Cart',
+			onOpen: this.maybeAddEmptyState.bind(this),
+		});
 		this.init();
 
 		this.toggle.hidden = false;
@@ -49,11 +55,7 @@
 		}
 	}
 	handleClick(e) {
-		if (window.targetCheck(e, '.toggle-cart')) {
-			let toggle = window.targetCheck(e, '.toggle-cart');
-			console.log('Toggle found. Toggling cart');
-			this.toggleCart();
-		} else if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
+		if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
 			let quantity = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, quantity);
 		}else if (window.targetCheck(e, '[data-add-to-cart]')) {
@@ -64,10 +66,6 @@
 			this.handleRemoveFromCart(remove);
 		} else if (window.targetCheck(e, '[data-clear-cart]')) {
 			this.clearCart();
-		} else if (this.checkout.classList.contains('expanded') &&
-			!this.checkout.contains(e.target) &&
-			e.target !== this.toggle) {
-			this.closeCart();
 		}
 	}
 
@@ -150,22 +148,6 @@
 		}
 	}
 
-	toggleCart() {
-		if (!this.checkout.classList.contains('expanded')) {
-			this.openCart();
-		} else {
-			this.closeCart();
-		}
-	}
-	openCart(message = 'Opened Cart') {
-		this.checkout.classList.add('expanded');
-		this.toggle.title = 'Hide cart';
-		this.toggle.ariaExpanded = true;
-		this.toggle.querySelector('span').textContent = 'Close Cart';
-		this.a11y.announce(message);
-		this.maybeAddEmptyState();
-		document.addEventListener('keydown', this.keyHandler);
-	}
 	maybeAddEmptyState() {
 		let empty = this.itemsList.querySelector('.empty');
 		if(empty) {
@@ -187,18 +169,8 @@
 			this.checkoutPanel.title = 'Checkout';
 		}
 	}
-	closeCart(message = 'Closed Cart') {
-		this.checkout.classList.remove('expanded');
-		this.toggle.title = 'Show Cart';
-		this.toggle.ariaExpanded = false;
-
-		this.toggle.querySelector('span').textContent = '';
-		this.a11y.announce(message);
-		document.removeEventListener('keydown', this.keyHandler);
-	}
 	handleEscape(e) {
 		if (e.key === 'Escape') {
-			this.closeCart('Closed Cart with escape key');
 			this.stepMultiplier = 1;
 		} else if (e.ctrlKey && e.shiftKey) {
 			this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000);
diff --git a/assets/js/dash/Tabs.js b/assets/js/dash/Tabs.js
index 228da86..7d86023 100644
--- a/assets/js/dash/Tabs.js
+++ b/assets/js/dash/Tabs.js
@@ -156,7 +156,7 @@
      * @param {string} tab - Tab to switch to ('items' or 'lists')
      * @param {boolean} updateHistory - Whether to push the state to the url
      */
-    switchTab(tab, updateHistory = true) {
+    switchTab(tab, updateHistory = false) {
 
 		document.activeElement?.blur();
 		// if (typeof this.callbacks['onSwitch'] === 'function') {
@@ -213,8 +213,8 @@
      * Update URL when a child tab changes
      */
     updateUrlFromChild() {
+		console.log('Updating URL');
 		if (('updateURL' in this.callbacks) ? this.callbacks.updateURL : true) {
-			console.log('UpdateUrlFromChild');
 			if (!this.parent) {
 				// Only the root container should update the URL
 				const fullPath = this.getFullTabPath(this.activeTab);
diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/dash/UtilityFunctions.js
index 1d3197d..ec64aac 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/dash/UtilityFunctions.js
@@ -1,63 +1,3 @@
-// Theme switching functionality
-document.addEventListener('DOMContentLoaded', function() {
-	console.log('Theme switch initiated');
-	const themeSwitch = document.getElementById('theme-switch');
-
-	if (!themeSwitch) return;
-
-	// Initialize theme from localStorage or system preference
-	const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
-	const storedTheme = localStorage.getItem('theme');
-
-	if (storedTheme) {
-		document.documentElement.classList.toggle('dark', storedTheme === 'dark');
-		themeSwitch.checked = storedTheme === 'dark';
-	} else {
-		document.documentElement.classList.toggle('dark', prefersDark.matches);
-		themeSwitch.checked = prefersDark.matches;
-	}
-
-	// Handle theme switch changes
-	themeSwitch.addEventListener('change', async function () {
-		const isDark = this.checked;
-		document.documentElement.classList.toggle('dark', isDark);
-		localStorage.setItem('theme', isDark ? 'dark' : 'light');
-
-		// If user is logged in, save preference
-		if (jvbSettings.currentUser !== null) {
-			try {
-				await fetch(`${jvbSettings.api}settings`, {
-					method: 'POST',
-					headers: {
-						'Content-Type': 'application/json',
-						'X-WP-Nonce': jvbSettings.nonce,
-						'action_nonce': jvbSettings.dash,
-					},
-					body: JSON.stringify({
-						dark_mode: isDark
-					})
-				});
-			} catch (error) {
-				console.error('Failed to save theme preference:', error);
-			}
-		}
-
-		// Update label
-		const label = document.getElementById('theme-switch');
-		if (label) {
-			label.title = isDark ? 'Toggle Light Mode' : 'Toggle Dark Mode';
-		}
-	});
-
-	// Handle system theme changes
-	prefersDark.addEventListener('change', (e) => {
-		if (!localStorage.getItem('theme')) {
-			const isDark = e.matches;
-			document.documentElement.classList.toggle('dark', isDark);
-			themeSwitch.checked = isDark;
-		}
-	});
-});
 
 /**
  *
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index c2217fb..599b61c 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.content&&(this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"}}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm(this.store),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} 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)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}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"}},this.ui=window.uiFromSelectors(this.elements)}init(){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.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.store.fetch(),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.deleteItem(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.deleteItem(e))),this.viewController.clearSelection());break;case"sync":this.store.syncQueue();break;case"refresh":this.store.fetchFromServer(this.store.filters)}}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.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")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);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(console.log(this.status),"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(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.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.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}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){console.log(t);let s=this.store.getItem(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.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),console.log("Bulk Edit form registered")}populateEditForm(e){let t=this.store.getItem(e);if(console.log(t),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}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('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)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.content&&(this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,storeName:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"status",keyPath:"post_status"},{name:"modified",keyPath:"modified"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"},TTL:36e5,cacheDOM:!0}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm(this.store),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} 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)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}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"}},this.ui=window.uiFromSelectors(this.elements)}init(){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.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.store.fetch(),this.queue.subscribe(((e,t)=>{e})),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.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")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);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(console.log(this.status),"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(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.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.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}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){console.log(t);let s=this.store.get(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.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),console.log("Bulk Edit form registered")}populateEditForm(e){let t=this.store.get(e);if(console.log(t),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}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('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)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(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 e4bc7b4..00c15b9 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-window.jvbStore=class{constructor(e={}){this.config={name:"default",endpoint:!1,apiBase:jvbSettings.api,TTL:36e5,showLoading:!0,headers:{},filters:{},...e},this.config.endpoint||console.warn("No endpoint set. Only saving locally"),this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.headers={"X-WP-Nonce":jvbSettings.nonce,...this.config.headers},this.items=new Map,this.cache=new Map,this.httpHeaders=new Map,this.domCache=new Map,this.forms=new Map,this.filters=e.filters??{},this.subscribers=new Set,this.db=null,this.currentRequest=null,this.cachedContent=JSON.parse(cacheJVB.cache)||{},this.lastTimestampUpdate=Date.now(),this.initDB(),document.addEventListener("beforeUnload",(()=>this.destroy()))}async initDB(){if(!("indexedDB"in window))return;const e=indexedDB.open(`jvb_${this.config.name}_db`,1);e.onupgradeneeded=e=>{const t=e.target.result;if(t.objectStoreNames.contains("items")||t.createObjectStore("items",{keyPath:"id"}),t.objectStoreNames.contains("dom")||t.createObjectStore("dom",{keyPath:"id"}),!t.objectStoreNames.contains("forms")){let e=t.createObjectStore("forms",{keyPath:"formId"});e.createIndex("status","status",{unique:!1}),e.createIndex("operationId","operationId",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}if(!t.objectStoreNames.contains("cache")){const e=t.createObjectStore("cache",{keyPath:"key"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("endpoint","endpoint",{unique:!1}),e.createIndex("filters","filters",{unique:!1})}t.objectStoreNames.contains("headers")||t.createObjectStore("headers",{keyPath:"key"})},e.onsuccess=e=>{this.db=e.target.result,this.loadFromDB()},e.onerror=e=>{console.error("IndexedDB error:",e)}}async loadFromDB(){if(this.db)try{await Promise.all([this.loadItems(),this.loadCache(),this.loadHeaders(),this.loadDOMCache(),this.loadForms()])}catch(e){console.error("Error loading from DB:",e)}}async loadItems(){if(this.db)return new Promise((e=>{this.db.transaction(["items"],"readonly").objectStore("items").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.items.set(e.id,e)})),this.notify("items-loaded",{items:Array.from(this.items.values())}),e()}}))}async loadCache(){if(this.db)return new Promise((e=>{this.db.transaction(["cache"],"readonly").objectStore("cache").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.isCacheValid(e)&&this.cache.set(e.key,e)})),e()}}))}async loadHeaders(){if(this.db)return new Promise((e=>{this.db.transaction(["headers"],"readonly").objectStore("headers").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.httpHeaders.set(e.key,e)})),e()}}))}async loadDOMCache(){if(this.db)return new Promise((e=>{this.db.transaction(["dom"],"readonly").objectStore("dom").getAll().onsuccess=t=>{t.target.result.forEach((e=>{const t={};Object.entries(e.views).forEach((([e,s])=>{const i=document.createElement("div");i.innerHTML=s,t[e]=i.firstElementChild})),this.domCache.set(e.id,t)})),e()}}))}async loadForms(){if(this.db)return new Promise((e=>{this.db.transaction(["forms"],"readonly").objectStore("forms").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.forms.set(e.key,e)})),e()}}))}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading.showModal():this.loading.close()}async fetch(e=null,t={}){const{filters:s=this.filters,headers:i={}}=t;this.config.showLoading&&this.setLoading(!0);const r=e||this.config.endpoint;if(!r)throw new Error("No endpoint specified");const a=this.generateCacheKey(r,s),o=this.cleanFilters(s),n=new URLSearchParams(o),c=`${this.config.apiBase}${r}${n.toString()?"?"+n:""}`,h={...this.headers,...i},d=this.generateHeaderKey(c),l=this.httpHeaders.get(d),f=this.cache.get(a);l&&f&&(l.etag&&(h["If-None-Match"]=l.etag),l.lastModified&&(h["If-Modified-Since"]=l.lastModified));try{const e=await fetch(c,{method:"GET",headers:h});if(console.log("DataStore response status: ",e.status),304===e.status&&(console.debug(`304 Not Modified for ${c}`),f))return f.timestamp=Date.now(),this.cache.set(a,f),await this.saveCacheToDB(a,f),this.currentRequest={filters:o,data:f.data,cached:!0},this.notify("data-cached",{data:f.data,filters:o,cached:!0}),f.data;if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);this.storeResponseHeaders(d,e);const t=await e.json();console.log("Fetched data: ",t);const s={key:a,endpoint:r,data:t,timestamp:Date.now(),filters:o};return this.cache.set(a,s),await this.saveCacheToDB(a,s),t.items&&this.config.endpoint===r&&this.updateItems(t.items),this.currentRequest={filters:o,data:t,cached:!1},this.notify("data-fetched",{endpoint:r,data:t,filters:o}),t}catch(e){if(console.error("Fetch error:",e),f)return console.warn("Returning stale cache due to fetch error"),this.currentRequest={filters:o,data:f.data,cached:!0,stale:!0},this.notify("stale-cache-used",{data:f.data,filters:o}),f.data;throw this.notify("fetch-error",{error:e,filters:o}),e}finally{this.config.showLoading&&this.setLoading(!1)}}updateItems(e){this.items.clear(),e.forEach((e=>{this.items.set(e.id,e)})),this.saveItemsToDB(),this.notify("items-updated",{items:e})}getCurrentRequest(){return this.currentRequest}getItem(e){let t=parseInt(e);e=isNaN(t)?e:t;const s=this.items.get(e);return s?this.unserializeData(s):null}setItem(e,t,s=!0){if(s&&this.items.has(e)){let s=this.getItem(e);t=window.deepMerge(s,t)}const i=this.serializeData(t);return this.items.set(e,i),this.saveItemsToDB(),this.notify("item-stored",t),t}hasUnrecoverableFiles(e){if(!e||"object"!=typeof e)return!1;if(e._wasFile||e._wasBlob)return!0;if(Array.isArray(e))return e.some((e=>this.hasUnrecoverableFiles(e)));if(e instanceof FormData){for(const[t,s]of e.entries())if(s instanceof File||s instanceof Blob)return!0;return!1}return Object.values(e).some((e=>this.hasUnrecoverableFiles(e)))}serializeFormData(e){const t={};for(const[s,i]of e.entries())i instanceof File||(s in t?(Array.isArray(t[s])||(t[s]=[t[s]]),t[s].push(i)):t[s]=i);return t}serializeData(e){if(!e)return null;if(e instanceof HTMLElement)return null;if("object"!=typeof e)return e;if(null===e)return null;if(e instanceof FormData)return{_type:"FormData",...this.serializeFormData(e)};if(Array.isArray(e))return e.map((e=>this.serializeData(e)));if(e instanceof Date)return{_type:"Date",value:e.toISOString()};const t={};for(const[s,i]of Object.entries(e))t[s]=this.serializeData(i);return t}unserializeData(e){if(!e||"object"!=typeof e)return e;if(null===e)return null;if(e._type)switch(e._type){case"FormData":return this.unserializeFormData(e);case"File":return{_wasFile:!0,_fileMetadata:e,name:e.name,type:e.type,size:e.size};case"Blob":return{_wasBlob:!0,_blobMetadata:e,type:e.type,size:e.size};case"Date":return new Date(e.value)}if(Array.isArray(e))return e.map((e=>this.unserializeData(e)));const t={};for(const[s,i]of Object.entries(e))t[s]=this.unserializeData(i);return t}unserializeFormData(e){const t=new FormData;for(const[s,i]of Object.entries(e))Array.isArray(i)?i.forEach((e=>{e?._isFile?(console.warn(`Cannot restore file "${e.name}" from stored data`),t.append(s+"_was_file",JSON.stringify(e))):t.append(s,e)})):i?._isFile?(console.warn(`Cannot restore file "${i.name}" from stored data`),t.append(s+"_was_file",JSON.stringify(i))):null!=i&&t.append(s,i);return t}clearItem(e){this.items.delete(e),this.db&&this.db.transaction(["items"],"readwrite").objectStore("items").delete(e)}cleanFilters(e){const t={};return Object.entries(e).forEach((([e,s])=>{null!=s&&""!==s&&("taxonomies"===e&&"object"==typeof s?Object.entries(s).forEach((([e,s])=>{Array.isArray(s)&&s.length>0?t[`tax_${e}`]=s.join(","):s&&(t[`tax_${e}`]=s)})):"date"===e&&"object"==typeof s?(s.after&&(t.after=s.after),s.before&&(t.before=s.before)):t[e]=s)})),t}setFilter(e,t){const s=this.filters[e];""===t||null==t?delete this.filters[e]:this.filters[e]=t,this.notify("filters-changed",{filters:this.filters,changed:{key:e,oldValue:s,newValue:t}}),this.config.endpoint&&this.fetch()}removeFilter(e){const t=this.filters[e];void 0!==t&&(delete this.filters[e],this.notify("filters-changed",{filters:this.filters,removed:{key:e,oldValue:t}}),this.config.endpoint&&this.fetch())}clearFilters(){const e={...this.filters};this.filters=this.config.filters,this.notify("filters-cleared",{oldFilters:e,filters:this.filters}),this.config.endpoint&&this.fetch()}generateCacheKey(e,t){const s=Object.keys(t).sort().reduce(((e,s)=>(e[s]=t[s],e)),{});return`${e}_${JSON.stringify(s)}`}generateHeaderKey(e){return`headers_${e}`}isCacheValid(e,t=this.config.TTL){return!(!e||!e.timestamp)&&Date.now()-e.timestamp<t}storeResponseHeaders(e,t){const s={key:e,etag:t.headers.get("ETag"),lastModified:t.headers.get("Last-Modified"),timestamp:Date.now()};this.httpHeaders.set(e,s),this.saveHeadersToDB(e,s)}clearCache(){this.cache.clear(),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").clear(),this.notify("cache-cleared")}invalidateCache(e){const t=[];this.cache.forEach(((s,i)=>{("string"==typeof e&&i.includes(e)||e instanceof RegExp&&e.test(i))&&t.push(i)})),t.forEach((e=>{this.cache.delete(e),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").delete(e)})),this.notify("cache-invalidated",{count:t.length})}storeDOMElement(e,t,s){this.domCache.has(e)||this.domCache.set(e,{});const i=this.domCache.get(e);i[t]=s.cloneNode(!0),this.domCache.set(e,i),this.saveDOMCacheToDB(e,i)}getDOMElement(e,t){const s=this.domCache.get(e);return s&&s[t]?s[t].cloneNode(!0):null}hasDOMElement(e,t){const s=this.domCache.get(e);return s&&s[t]}clearDOMCache(e){this.domCache.delete(e),this.db&&this.db.transaction(["dom"],"readwrite").objectStore("dom").delete(e)}clearAllDOMCache(){this.domCache.clear(),this.db&&this.db.transaction(["dom"],"readwrite").objectStore("dom").clear()}renderOrRetrieve(e,t,s){const i=this.getDOMElement(e.id,t);if(i)return i;const r=s(e);return this.storeDOMElement(e.id,t,r),r}async saveItemsToDB(){if(!this.db)return;const e=this.db.transaction(["items"],"readwrite").objectStore("items");e.clear(),this.items.forEach((t=>{t._deleted||e.put(t)}))}async saveCacheToDB(e,t){this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").put(t)}async saveHeadersToDB(e,t){this.db&&this.db.transaction(["headers"],"readwrite").objectStore("headers").put(t)}async saveDOMCacheToDB(e,t){if(!this.db)return;const s={id:e,views:{}};Object.entries(t).forEach((([e,t])=>{t&&t.outerHTML&&(s.views[e]=t.outerHTML)})),this.db.transaction(["dom"],"readwrite").objectStore("dom").put(s)}async saveFormsToDB(e,t){this.db&&this.db.transaction(["forms"],"readwrite").objectStore("forms").put(t)}storeForm(e,t){this.forms.set(e,t),this.saveFormsToDB(e,t)}getForm(e){return this.forms.has(e)?this.forms.get(e):null}getAllForms(){return this.forms}clearForm(e){this.forms.delete(e),this.db&&this.db.transaction(["forms"],"readwrite").objectStore("forms").delete(e)}clearAllForms(){this.forms.clear(),this.db&&this.db.transaction(["forms"],"readwrite").objectStore("dom").clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.db&&this.db.close(),this.subscribers.clear(),this.items.clear(),this.cache.clear(),this.domCache.clear(),this.httpHeaders.clear()}};
\ No newline at end of file
+window.jvbStore=class{constructor(e={}){this.config={name:"default",version:1,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,headers:{},filters:{},TTL:36e5,useHttpCaching:!0,cacheKeyStrategy:"filters",showLoading:!0,stripDOMReferences:!0,storeBlobs:!1,...e},this.db=null,this.data=new Map,this.cache=new Map,this.httpHeaders=new Map,this.subscribers=new Set,this.currentRequest=null,this.filters=this.config.filters??{},this.headers={"X-WP-Nonce":jvbSettings?.nonce,...this.config.headers},this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.initDB(),window.addEventListener("beforeunload",(()=>this.destroy()))}async initDB(){if(!("indexedDB"in window))return void console.warn("IndexedDB not supported");const e=`jvb_${this.config.name}_db`,t=indexedDB.open(e,this.config.version);t.onupgradeneeded=e=>{const t=e.target.result;if(!t.objectStoreNames.contains(this.config.storeName)){const e=t.createObjectStore(this.config.storeName,{keyPath:this.config.keyPath});this.config.indexes.forEach((t=>{e.createIndex(t.name,t.keyPath||t.name,{unique:t.unique||!1})}))}if(this.config.endpoint&&!t.objectStoreNames.contains("cache")){const e=t.createObjectStore("cache",{keyPath:"key"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("endpoint","endpoint",{unique:!1}),e.createIndex("filters","filters",{unique:!1})}this.config.useHttpCaching&&!t.objectStoreNames.contains("headers")&&t.createObjectStore("headers",{keyPath:"key"}),this.config.storeBlobs&&!t.objectStoreNames.contains("blobs")&&t.createObjectStore("blobs",{keyPath:"uploadId"}),this.config.onUpgrade&&this.config.onUpgrade(t,e.oldVersion,e.newVersion)},t.onsuccess=e=>{this.db=e.target.result,this.loadFromDB()},t.onerror=t=>{console.error(`IndexedDB error for ${e}:`,t),this.config.onError&&this.config.onError(t)}}async loadFromDB(){if(!this.db)return;const e=[this.loadData()];this.config.endpoint&&e.push(this.loadCache()),this.config.useHttpCaching&&e.push(this.loadHeaders());try{await Promise.all(e),this.notify("data-loaded",{count:this.data.size,store:this.config.storeName})}catch(e){console.error("Error loading from DB:",e)}}async loadData(){if(this.db)return new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).getAll();s.onsuccess=t=>{t.target.result.forEach((e=>{const t=this.config.stripDOMReferences?this.stripDOMReferences(e):e,s=this.getItemKey(t);this.data.set(s,t)})),e()},s.onerror=e=>t(e)}))}stripDOMReferences(e){if(!e||"object"!=typeof e)return e;if(Array.isArray(e))return e.map((e=>this.stripDOMReferences(e)));const t={};for(const[s,i]of Object.entries(e))this.isDOMReference(s,i)||(i instanceof Set?t[s]=Array.from(i):i instanceof Map?t[s]=Object.fromEntries(i):t[s]="object"==typeof i&&null!==i?this.stripDOMReferences(i):i);return t}isDOMReference(e,t){return!!(t instanceof HTMLElement||t instanceof NodeList||t instanceof HTMLCollection||t&&void 0!==t.nodeType)||!!["element","el","dom","node","ui","container","wrapper"].some((t=>e.toLowerCase().includes(t)))}getItemKey(e){if("function"==typeof this.config.keyPath)return this.config.keyPath(e);const t=this.config.keyPath.split(".");let s=e;for(const e of t)s=s?.[e];return s}async save(e){const t=this.getItemKey(e),s=this.config.stripDOMReferences?this.stripDOMReferences(e):e;return this.data.set(t,s),await this.saveToDB(s),this.notify("item-saved",{item:s,key:t}),s}async saveToDB(e){if(this.db)return new Promise(((t,s)=>{const i=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName).put(e);i.onsuccess=()=>t(),i.onerror=e=>s(e)}))}async saveMany(e){if(!this.db)return;const t=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName),s=e.map((e=>{const s=this.config.stripDOMReferences?this.stripDOMReferences(e):e,i=this.getItemKey(s);return this.data.set(i,s),t.put(s)}));await Promise.all(s),this.notify("items-saved",{count:e.length})}get(e){return this.data.get(e)}getAll(){return Array.from(this.data.values())}async delete(e,t=null){if(this.data.delete(e),t||(t=this.config.storeName),this.db){const s=this.db.transaction([t],"readwrite").objectStore(t);await s.delete(e)}this.notify("item-deleted",{key:e})}async saveBlob(e,t){if(!this.db)return;const s=this.db.transaction(["blobs"],"readwrite").objectStore("blobs");await s.put({key:e,data:t,type:t.type,name:t.name})}async getBlob(e){return this.db?new Promise((t=>{const s=this.db.transaction(["blobs"],"readonly").objectStore("blobs").get(e);s.onsuccess=()=>t(s.result),s.onerror=()=>t(null)})):null}async clear(){if(this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.domCache&&this.domCache.clear(),this.db){const e=[this.config.storeName];this.config.endpoint&&e.push("cache"),this.config.useHttpCaching&&e.push("headers");const t=this.db.transaction(e,"readwrite");e.forEach((e=>{this.db.objectStoreNames.contains(e)&&t.objectStore(e).clear()}))}this.notify("data-cleared")}async fetch(e={}){if(!this.config.endpoint)throw new Error("No endpoint configured for fetch");const{filters:t=this.filters,headers:s={}}=e;this.config.showLoading&&this.setLoading(!0);const i=this.generateCacheKey(t),r=this.cache.get(i);if(r&&this.isCacheValid(r))return r.data;const o={...this.headers,...s};if(this.config.useHttpCaching){const e=this.httpHeaders.get(i);e&&(e.etag&&(o["If-None-Match"]=e.etag),e.lastModified&&(o["If-Modified-Since"]=e.lastModified))}const n=this.cleanFilters(t),a=new URLSearchParams(n),c=`${this.config.apiBase}${this.config.endpoint}${a.toString()?"?"+a:""}`;try{const e=await fetch(c,{method:"GET",headers:o});if(304===e.status&&r)return r.timestamp=Date.now(),this.saveCache(i,r),r.data;if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);const s=await e.json();this.config.useHttpCaching&&this.storeResponseHeaders(i,e);const n={key:i,data:s,timestamp:Date.now(),endpoint:this.config.endpoint,filters:t};return this.cache.set(i,n),this.saveCache(i,n),Array.isArray(s)?await this.saveMany(s):s.items&&await this.saveMany(s.items),s}catch(e){if(console.error("Fetch error:",e),r)return console.warn("Using stale cache due to fetch error"),r.data;throw e}finally{this.config.showLoading&&this.setLoading(!1)}}cleanFilters(e){const t={};return Object.entries(e).forEach((([e,s])=>{null!=s&&""!==s&&("taxonomies"===e&&"object"==typeof s?Object.entries(s).forEach((([e,s])=>{Array.isArray(s)&&s.length>0?t[`tax_${e}`]=s.join(","):s&&(t[`tax_${e}`]=s)})):"date"===e&&"object"==typeof s?(s.after&&(t.after=s.after),s.before&&(t.before=s.before)):t[e]=s)})),t}generateCacheKey(e){if("custom"===this.config.cacheKeyStrategy&&this.config.generateCacheKey)return this.config.generateCacheKey(e);const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}setFilter(e,t){this.filters||(this.filters={});const s=this.filters[e];""===t||null==t?delete this.filters[e]:this.filters[e]=t,this.notify("filters-changed",{filters:this.filters,changed:{key:e,oldValue:s,newValue:t}}),this.config.endpoint&&this.fetch()}removeFilter(e){const t=this.filters[e];void 0!==t&&(delete this.filters[e],this.notify("filters-changed",{filters:this.filters,removed:{key:e,oldValue:t}}),this.config.endpoint&&this.fetch())}clearFilters(){const e={...this.filters};this.filters=this.config.filters,this.notify("filters-cleared",{oldFilters:e,filters:this.filters}),this.config.endpoint&&this.fetch()}setFilters(e){if(this.filters={...this.filters,...e},!1!==this.config.autoFetch)return this.fetch(this.filters)}isCacheValid(e){return!(!e||!e.timestamp)&&Date.now()-e.timestamp<this.config.TTL}storeResponseHeaders(e,t){const s={key:e,etag:t.headers.get("ETag"),lastModified:t.headers.get("Last-Modified"),timestamp:Date.now()};this.httpHeaders.set(e,s),this.db&&this.db.transaction(["headers"],"readwrite").objectStore("headers").put(s)}async saveCache(e,t){if(!this.db)return;const s=this.db.transaction(["cache"],"readwrite").objectStore("cache");await s.put(t)}async loadCache(){if(this.db)return new Promise((e=>{this.db.transaction(["cache"],"readonly").objectStore("cache").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.isCacheValid(e)&&this.cache.set(e.key,e)})),e()}}))}async loadHeaders(){if(this.db)return new Promise((e=>{this.db.transaction(["headers"],"readonly").objectStore("headers").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.httpHeaders.set(e.key,e)})),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("Subscriber error:",e)}}))}async query(e,t){return this.db?new Promise(((s,i)=>{const r=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName);if(!r.indexNames.contains(e))return void i(new Error(`Index ${e} does not exist`));const o=r.index(e),n=void 0!==t?o.getAll(t):o.getAll();n.onsuccess=e=>{const t=e.target.result.map((e=>this.config.stripDOMReferences?this.stripDOMReferences(e):e));s(t)},n.onerror=e=>i(e)})):[]}async count(){return this.db?new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).count();s.onsuccess=t=>e(t.target.result),s.onerror=e=>t(e)})):this.data.size}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading.showModal():this.loading.close()}destroy(){this.currentRequest&&this.currentRequest.abort(),this.subscribers.clear(),this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.db&&(this.db.close(),this.db=null)}clearCache(){this.cache.clear(),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").clear(),this.notify("cache-cleared")}};
\ No newline at end of file
diff --git a/assets/js/min/dragHandler.min.js b/assets/js/min/dragHandler.min.js
new file mode 100644
index 0000000..505555b
--- /dev/null
+++ b/assets/js/min/dragHandler.min.js
@@ -0,0 +1 @@
+window.jvbDragHandler=class{constructor(t){this.draggableSelector=t.draggableSelector,this.dropTargetSelector=t.dropTargetSelector,this.getItemId=t.getItemId,this.getSelectedItems=t.getSelectedItems,this.validateDrop=t.validateDrop,this.onDrop=t.onDrop,this.handleSelector=t.handleSelector||null,this.ignoreSelector=t.ignoreSelector||"input, button, label, select, textarea, a",this.onDragStart=t.onDragStart||null,this.onDragEnd=t.onDragEnd||null,this.previewElement=t.previewElement||"img, video, .icon",this.previewOptions={offset:{x:-30,y:-40},showCount:!0,...t.previewOptions},this.dragThreshold={time:90,distance:5},this.state=this.getInitialState(),this.onPointerDown=this.handlePointerDown.bind(this),this.onPointerMove=this.handlePointerMove.bind(this),this.onPointerUp=this.handlePointerUp.bind(this),this.onPointerCancel=this.handlePointerCancel.bind(this),this.init()}getInitialState(){return{active:!1,itemIds:[],startPos:null,currentPos:null,targetElement:null,previewElement:null,startTime:null,holdTimer:null,pointerId:null,pointerTarget:null}}init(){document.addEventListener("pointerdown",this.onPointerDown)}handlePointerDown(t){if(this.shouldIgnoreElement(t.target))return;const e=t.target.closest(this.draggableSelector);if(!e)return;if(this.handleSelector){const i=t.target.closest(this.handleSelector);if(!i||!e.contains(i))return}const i=this.getItemId(e);if(!i)return;const n=this.getSelectedItems(e),r=n.includes(i)?n:[i];t.preventDefault(),this.state={active:!1,itemIds:r,startPos:{x:t.clientX,y:t.clientY},currentPos:{x:t.clientX,y:t.clientY},targetElement:null,previewElement:null,startTime:Date.now(),holdTimer:null,draggableElement:e,pointerId:t.pointerId,pointerTarget:e},document.addEventListener("pointermove",this.onPointerMove),document.addEventListener("pointerup",this.onPointerUp),document.addEventListener("pointercancel",this.onPointerCancel),this.state.holdTimer=setTimeout((()=>{this.state&&!this.state.active&&this.startDrag()}),this.dragThreshold.time)}startDrag(){if(this.state&&!this.state.active){if(this.state.holdTimer=null,this.state.active=!0,this.state.pointerTarget&&null!==this.state.pointerId)try{this.state.pointerTarget.setPointerCapture(this.state.pointerId)}catch(t){console.warn("Could not capture pointer:",t)}if(this.state.previewElement=this.createPreview(this.state.draggableElement,this.state.itemIds),this.applyDraggingState(!0),this.updatePreview(),this.onDragStart&&this.onDragStart(this.state.itemIds,this.state.draggableElement),window.jvbA11y){const t=this.state.itemIds.length>1?`Started dragging ${this.state.itemIds.length} items`:"Started dragging item";window.jvbA11y.announce(t)}}}handlePointerMove(t){if(!this.state)return;if(this.state.currentPos={x:t.clientX,y:t.clientY},!this.state.active&&this.state.holdTimer){const e=t.clientX-this.state.startPos.x,i=t.clientY-this.state.startPos.y;Math.sqrt(e*e+i*i)>2*this.dragThreshold.distance&&(clearTimeout(this.state.holdTimer),this.startDrag())}if(!this.state.active)return;t.preventDefault(),this.updatePreview();const e=document.elementFromPoint(t.clientX,t.clientY),i=this.findDropTarget(e);i!==this.state.targetElement&&(this.clearTargetHighlight(),this.state.targetElement=i,i&&this.validateDrop(this.state.itemIds,i)&&this.highlightTarget(i))}handlePointerUp(t){if(!this.state)return;if(this.state.holdTimer&&!this.state.active)return void this.cancelDragStart();if(!this.state.active)return void this.cleanup();document.removeEventListener("pointermove",this.onPointerMove),document.removeEventListener("pointerup",this.onPointerUp),document.removeEventListener("pointercancel",this.onPointerCancel);const{itemIds:e,targetElement:i}=this.state;let n=!1;if(i&&this.validateDrop(e,i)&&(this.onDrop(e,i),n=!0),this.cleanup(),this.onDragEnd&&this.onDragEnd(e,n),window.jvbA11y){const t=n?e.length>1?`Moved ${e.length} items`:"Item moved":"Drag cancelled";window.jvbA11y.announce(t)}}handlePointerCancel(t){this.state&&(document.removeEventListener("pointermove",this.onPointerMove),document.removeEventListener("pointerup",this.onPointerUp),document.removeEventListener("pointercancel",this.onPointerCancel),this.state.holdTimer&&(clearTimeout(this.state.holdTimer),this.state.holdTimer=null),this.cleanup(),window.jvbA11y&&window.jvbA11y.announce("Drag cancelled"))}cancelDragStart(){this.state&&this.state.holdTimer&&(clearTimeout(this.state.holdTimer),this.state.holdTimer=null),document.removeEventListener("pointermove",this.onPointerMove),document.removeEventListener("pointerup",this.onPointerUp),document.removeEventListener("pointercancel",this.onPointerCancel),this.cleanup()}createPreview(t,e){let i=window.getTemplate("dragPreview");i||(i=this.createPreviewElement());let n=i.querySelector(".drag-items"),r=n.querySelector(".drag-item");e.forEach(((t,e)=>{let i=this.findElementByItemId(t);if(i){let t=i.querySelector(this.previewElement)?.cloneNode(!0);if(t){let e=r.cloneNode(!0);e.appendChild(t),n.append(e)}}})),r.remove();let s=i.querySelector(".drag-count");return s&&(s.textContent="{"+e.length+"}",s.hidden=e.length<=1),document.body.appendChild(i),i}createPreviewElement(){const t=document.createElement("div");t.className="drag-preview";let e=t.cloneNode(!0),i=t.cloneNode(!0),n=t.cloneNode(!0);return n.className="drag-item",e.className="drag-items",e.append(n),i.className="drag-count",t.append(e),t.append(i),t}updatePreview(){this.state.previewElement&&this.state.currentPos&&(this.state.previewElement.style.left=`${this.state.currentPos.x+this.previewOptions.offset.x}px`,this.state.previewElement.style.top=`${this.state.currentPos.y+this.previewOptions.offset.y}px`)}findDropTarget(t){return t?t.closest(this.dropTargetSelector):null}highlightTarget(t){if(t&&(t.classList.add("dragover"),this.state.isMultiDrag&&(t.classList.add("multi-drop"),t.setAttribute("data-item-count",this.state.itemIds.length)),navigator.vibrate)){const t=this.state.isMultiDrag?[25,10,25]:[25];navigator.vibrate(t)}}clearTargetHighlight(){document.querySelectorAll(".dragover").forEach((t=>{t.classList.remove("dragover","multi-drop"),t.removeAttribute("data-item-count")}))}applyDraggingState(t){this.state.itemIds.forEach((e=>{const i=this.findElementByItemId(e);i&&i.classList.toggle("dragging",t)}))}findElementByItemId(t){const e=document.querySelectorAll(this.draggableSelector);for(const i of e)if(this.getItemId(i)===t)return i;return null}shouldIgnoreElement(t){return t.matches(this.ignoreSelector)}cleanup(){if(this.state&&this.state.holdTimer&&clearTimeout(this.state.holdTimer),this.state&&this.state.pointerTarget&&null!==this.state.pointerId)try{this.state.pointerTarget.releasePointerCapture(this.state.pointerId)}catch(t){}this.clearTargetHighlight(),this.applyDraggingState(!1),this.state&&this.state.previewElement&&this.state.previewElement.remove(),this.state=this.getInitialState()}destroy(){this.cleanup(),document.removeEventListener("pointerdown",this.onPointerDown),document.removeEventListener("pointermove",this.onPointerMove),document.removeEventListener("pointerup",this.onPointerUp),document.removeEventListener("pointercancel",this.onPointerCancel)}};
\ No newline at end of file
diff --git a/assets/js/min/favourites.min.js b/assets/js/min/favourites.min.js
index 06406b3..c166151 100644
--- a/assets/js/min/favourites.min.js
+++ b/assets/js/min/favourites.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(){this.store=new window.jvbStore({name:"favourites",endpoint:"favourites",useIndexedDB:!0,TTL:1/0,showLoading:!1,filters:{user:jvbSettings.currentUser,content:"all",order:"desc",orderby:"date",page:1,all:!0}}),this.store.subscribe(((t,e)=>{t})),this.store.fetch()}toggleFavourite(t){if(!jvbSettings.currentUser)return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){if(!jvbSettings.currentUser)return!1;let o=this.store.getItem(e);return!!o&&"add"===o.action}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,""!==jvbSettings.currentUser&&(window.jvbFavourites=new t)})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
+(()=>{class t{constructor(){this.store=new window.jvbStore({name:"favourites",storeName:"favourites",endpoint:"favourites",indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:864e5,showLoading:!1,filters:{user:jvbSettings.currentUser,content:"all",order:"desc",orderby:"date",page:1,all:!0}}),this.listStore=new window.jvbStore({name:"favourites_lists",storeName:"lists",keyPath:"listId",endpoint:"favourites/lists",TTL:864e5}),this.store.subscribe(((t,e)=>{t})),this.store.fetch()}toggleFavourite(t){if(!jvbSettings.currentUser)return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,""!==jvbSettings.currentUser&&(window.jvbFavourites=new t)})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index f74d815..59ffea1 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e=null){this.store=e,e||(this.store=new window.jvbStore({name:"forms",TTL:604800})),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.initListeners()}async checkPendingOperations(){if(this.store)try{this.store.getAllForms()}catch(e){console.error("Failed to load pending forms:",e)}}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(s,t.firstChild),s.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),s.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.clearForm(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("submit",this.submitHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),this.globalHandlersAdded=!0)}registerForm(e,t={}){const s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s;const a={element:e,id:s,options:{autoSave:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,a),this.forms.set(s,a),this.store&&a.options.cache){const e=this.store.getForm(s);e&&e.formData&&this.showPendingNotification(e)}return a}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&new window.jvbTabs(e),window.jvbSelector&&window.jvbSelector.scanExistingFields()}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),a=e.querySelector(".repeater-items"),i=e.querySelector("template");s&&i&&a&&(window.Sortable&&new Sortable(a,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),a.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const s=e.querySelector(".repeater-items"),a=e.querySelector("template"),i=s.children.length,r=e.dataset.field,n=a.content.cloneNode(!0).firstElementChild;n.dataset.index=i,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${r}:${i}:${t}`,e.id=`${r}-${i}-${t}`;const s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:r,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const s=e.closest(".repeater"),a=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:a,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),a=e.dataset.field;Array.from(s.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const s=e.name.split(":");if(3===s.length){const i=s[2];e.name=`${a}:${t}:${i}`,e.id=`${a}-${t}-${i}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:a,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const a=s.dataset.dependsOn,i=s.dataset.dependsValue,r=s.dataset.dependsOperator||"==";t.dependencies.has(a)||t.dependencies.set(a,[]),t.dependencies.get(a).push({field:s,requiredValue:i,operator:r}),this.checkFieldDependency(e,s,a,i,r)}))}checkFieldDependency(e,t,s,a,i){const r=e.querySelector(`[name="${s}"]`);if(!r)return;const n=this.getFieldValue(r),o=this.evaluateCondition(n,a,i);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const a=String(e||""),i=String(t||"");switch(s){case"==":default:return a==i;case"!=":return a!=i;case">":return parseFloat(a)>parseFloat(i);case"<":return parseFloat(a)<parseFloat(i);case">=":return parseFloat(a)>=parseFloat(i);case"<=":return parseFloat(a)<=parseFloat(i);case"contains":return a.includes(i);case"empty":return""===a;case"not_empty":return""!==a}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),s=e.closest(".field");let a=s?.querySelector(".char-count");!a&&s&&(a=document.createElement("div"),a.className="char-count",a.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(a));const i=()=>{const s=e.value.length;a&&(a.querySelector(".current").textContent=s,a.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),a&&(a.querySelector(".current").textContent=t))};e.addEventListener("input",i),i()}))}initImageUploadFields(){window.jvbUploads.scanFields()}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;e.preventDefault();const s=this.forms.get(t.dataset.formId);if(!s)return;const a=this.collectFormData(t);e.preventDefault(),this.notify("form-submit",{formId:s.id,data:a,config:s})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}}handleNumberClick(e,t){let s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let a=parseFloat(t.step);a=Math.max(a,1),e.ctrlKey&&e.shiftKey?a*=50:e.ctrlKey?a*=5:e.shiftKey&&(a*=10);let i=""===t.value?0:parseFloat(t.value);t.value=i+a*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,a,i]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],r=parseFloat(e.value);r<t?(e.value=t,i.disabled=!0):r>s?(e.value=s,a.disabled=!1):a.disabled?a.disabled=!1:i.disabled&&(i.disabled=!1)}handleChange(e){if(this.subscribers.size>0){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);if(!a)return;const i=a.dependencies.get(t.name);if(i&&i.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)})),a.options.autoSave&&!s.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(a,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);a&&a.options.autoSave&&!s.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}getDelayForField(e){return console.log("Get Delay for Field",e),"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.handleInput,{passive:!0});const s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>this.autosave(e)),t)}handleInput(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.cacheFormData(e,t);const s=this.getChangedFields(e.data,t);if(0!==Object.keys(s).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,a]of Object.entries(t))"object"==typeof a&&(s[e]=a);this.notify("form-autosave",{formId:e.id,changes:s,fullData:t,config:e})}}cacheFormData(e,t){try{this.store.storeForm(e.id,{formId:e.id,formData:t,timestamp:Date.now(),status:"pending",operationId:null})}catch(e){console.error("Failed to cache form data:",e)}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const s=this.collectFormData(t.element),a=this.getChangedFields(t.lastSnapshot,s);return Object.keys(a).length>0}showFormStatus(e,t){const s=e.querySelector(".form-status");s&&s.remove();const a=document.createElement("div");a.className=`form-status status-${t}`;a.textContent={saving:"Saving changes...",saved:"Changes saved",error:"Failed to save changes",offline:"Changes will be saved when online"}[t]||t,e.insertBefore(a,e.firstChild),"saved"===t&&setTimeout((()=>a.remove()),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let s={};const a={},i={};for(let[r,n]of t.entries()){if(this.ignore.includes(r)||r.endsWith("_temp"))continue;this.getFieldProcessor(r)(r,n,s,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(s,a):(s=this.mergeRepeaterData(s,a),this.mergePostData(s,i))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:e.includes("[")?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const a={};Object.keys(t[s]).forEach((e=>{const i=t[s][e];Object.keys(i).length>0&&(a[e]=i)})),e[s]=Object.values(a)})),e}mergePostData(e,t){for(let[t,s]in Object.entries(s))e[t]=s;return e}processTableField(e,t,s,a,i,r){let[n,o]=e.split("|");!n in i&&(i[n]={});this.getFieldProcessor(o)(o,t,i,a,i,r)}processRepeaterField(e,t,s,a,i,r){let[n,o,d]=e.split(":");const l=d.endsWith("[]");d=d.replace("[]",""),a[n]||(a[n]={}),a[n][o]||(a[n][o]={}),l||a[n][o][d]?(a[n][o][d]?Array.isArray(a[n][o][d])||(a[n][o][d]=[a[n][o][d]]):a[n][o][d]=[],a[n][o][d].push(t)):a[n][o][d]=t}processGroupField(e,t,s,a,i,r){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let d=s[o];for(let e=1;e<n.length-1;e++){const t=n[e];d[t]||(d[t]={}),d=d[t]}const l=n[n.length-1];void 0!==d[l]?(Array.isArray(d[l])||(d[l]=[d[l]]),d[l].push(t)):d[l]=t}processLocationField(e,t,s,a,i,r){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(s,n)||(s[n]={},Object.hasOwn(s,"sendAll")?s.sendAll.includes(n)||s.sendAll.push(n):s.sendAll=[n]),s[n][o]=t}processRegularField(e,t,s,a,i,r){s[e]?(Array.isArray(s[e])||(s[e]=[s[e]]),s[e].push(t)):s[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(console.log("Cleaning up form",t),this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("submit",this.submitHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0)),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){if("item-saved"===e)"autosave"===t.item.status&&this.showFormStatus(t.item.formId,"autosave")}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(s,t.firstChild),s.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),s.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("submit",this.submitHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),this.globalHandlersAdded=!0)}registerForm(e,t={}){const s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s;const a={element:e,id:s,options:{autoSave:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,a),this.forms.set(s,a),this.store&&a.options.cache){const e=this.store.getForm(s);e&&e.formData&&this.showPendingNotification(e)}return a}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&new window.jvbTabs(e),window.jvbSelector&&window.jvbSelector.scanExistingFields()}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),a=e.querySelector(".repeater-items"),i=e.querySelector("template");s&&i&&a&&(window.Sortable&&new Sortable(a,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),a.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const s=e.querySelector(".repeater-items"),a=e.querySelector("template"),i=s.children.length,r=e.dataset.field,n=a.content.cloneNode(!0).firstElementChild;n.dataset.index=i,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${r}:${i}:${t}`,e.id=`${r}-${i}-${t}`;const s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:r,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const s=e.closest(".repeater"),a=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:a,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),a=e.dataset.field;Array.from(s.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const s=e.name.split(":");if(3===s.length){const i=s[2];e.name=`${a}:${t}:${i}`,e.id=`${a}-${t}-${i}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:a,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const a=s.dataset.dependsOn,i=s.dataset.dependsValue,r=s.dataset.dependsOperator||"==";t.dependencies.has(a)||t.dependencies.set(a,[]),t.dependencies.get(a).push({field:s,requiredValue:i,operator:r}),this.checkFieldDependency(e,s,a,i,r)}))}checkFieldDependency(e,t,s,a,i){const r=e.querySelector(`[name="${s}"]`);if(!r)return;const n=this.getFieldValue(r),o=this.evaluateCondition(n,a,i);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const a=String(e||""),i=String(t||"");switch(s){case"==":default:return a==i;case"!=":return a!=i;case">":return parseFloat(a)>parseFloat(i);case"<":return parseFloat(a)<parseFloat(i);case">=":return parseFloat(a)>=parseFloat(i);case"<=":return parseFloat(a)<=parseFloat(i);case"contains":return a.includes(i);case"empty":return""===a;case"not_empty":return""!==a}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),s=e.closest(".field");let a=s?.querySelector(".char-count");!a&&s&&(a=document.createElement("div"),a.className="char-count",a.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(a));const i=()=>{const s=e.value.length;a&&(a.querySelector(".current").textContent=s,a.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),a&&(a.querySelector(".current").textContent=t))};e.addEventListener("input",i),i()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;this.store.delete(t.dataset.formId),e.preventDefault();const s=this.forms.get(t.dataset.formId);if(!s)return;const a=this.collectFormData(t);e.preventDefault(),this.notify("form-submit",{formId:s.id,data:a,config:s})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}}handleNumberClick(e,t){let s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let a=parseFloat(t.step);a=Math.max(a,1),e.ctrlKey&&e.shiftKey?a*=50:e.ctrlKey?a*=5:e.shiftKey&&(a*=10);let i=""===t.value?0:parseFloat(t.value);t.value=i+a*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,a,i]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],r=parseFloat(e.value);r<t?(e.value=t,i.disabled=!0):r>s?(e.value=s,a.disabled=!1):a.disabled?a.disabled=!1:i.disabled&&(i.disabled=!1)}handleChange(e){if(this.subscribers.size>0){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);if(!a)return;const i=a.dependencies.get(t.name);if(i&&i.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)})),a.options.autoSave&&!s.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(a,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);a&&a.options.autoSave&&!s.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.handleInput,{passive:!0});const s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>this.autosave(e)),t)}handleInput(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}),this.showFormStatus(e.id,"saved");const s=this.getChangedFields(e.data,t);if(0!==Object.keys(s).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,a]of Object.entries(t))"object"==typeof a&&(s[e]=a);this.notify("form-autosave",{formId:e.id,changes:s,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const s=this.collectFormData(t.element),a=this.getChangedFields(t.lastSnapshot,s);return Object.keys(a).length>0}showFormStatus(e,t){const s=e.querySelector(".form-status");s&&s.remove();const a=document.createElement("div");a.className=`form-status status-${t}`;a.textContent={saving:"Saving changes...",saved:"Changes saved",error:"Failed to save changes",offline:"Changes will be saved when online"}[t]||t,e.insertBefore(a,e.firstChild),"saved"===t&&setTimeout((()=>a.remove()),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let s={};const a={},i={};for(let[r,n]of t.entries()){if(this.ignore.includes(r)||r.endsWith("_temp"))continue;this.getFieldProcessor(r)(r,n,s,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(s,a):(s=this.mergeRepeaterData(s,a),this.mergePostData(s,i))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:e.includes("[")?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const a={};Object.keys(t[s]).forEach((e=>{const i=t[s][e];Object.keys(i).length>0&&(a[e]=i)})),e[s]=Object.values(a)})),e}mergePostData(e,t){for(let[t,s]in Object.entries(s))e[t]=s;return e}processTableField(e,t,s,a,i,r){let[n,o]=e.split("|");!n in i&&(i[n]={});this.getFieldProcessor(o)(o,t,i,a,i,r)}processRepeaterField(e,t,s,a,i,r){let[n,o,d]=e.split(":");const l=d.endsWith("[]");d=d.replace("[]",""),a[n]||(a[n]={}),a[n][o]||(a[n][o]={}),l||a[n][o][d]?(a[n][o][d]?Array.isArray(a[n][o][d])||(a[n][o][d]=[a[n][o][d]]):a[n][o][d]=[],a[n][o][d].push(t)):a[n][o][d]=t}processGroupField(e,t,s,a,i,r){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let d=s[o];for(let e=1;e<n.length-1;e++){const t=n[e];d[t]||(d[t]={}),d=d[t]}const l=n[n.length-1];void 0!==d[l]?(Array.isArray(d[l])||(d[l]=[d[l]]),d[l].push(t)):d[l]=t}processLocationField(e,t,s,a,i,r){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(s,n)||(s[n]={},Object.hasOwn(s,"sendAll")?s.sendAll.includes(n)||s.sendAll.push(n):s.sendAll=[n]),s[n][o]=t}processRegularField(e,t,s,a,i,r){s[e]?(Array.isArray(s[e])||(s[e]=[s[e]]),s[e].push(t)):s[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("submit",this.submitHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0)),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
new file mode 100644
index 0000000..f57924b
--- /dev/null
+++ b/assets/js/min/handleSelection.min.js
@@ -0,0 +1 @@
+window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.ui=e.ui||{},this.itemSelector=e.itemSelector||".item",this.checkboxSelector=e.checkboxSelector||'[name*="select-item"]',this.selectedItems=new Set,this.lastSelected=null,this.subscribers=new Set,this.init()}init(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("keydown",this.keyHandler)}handleKeys(e){(e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.ui.selectAll&&(this.ui.selectAll.checked=!0,this.selectAll(!0),window.jvbA11y&&window.jvbA11y.announce("All items selected"))),"Escape"===e.key&&this.selectedItems.size>0&&(this.selectAll(!1),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")),("Delete"===e.key||"Backspace"===e.key)&&!e.target.matches("input, textarea")&&this.selectedItems>0&&(e.preventDefault(),confirm(`Remove ${this.selectedItems.size} selected item${1!==this.selectedItems.size?"s":""}?`)&&this.deselect(this.selectedItems))}handleClick(e){const t=e.target.closest(`${this.checkboxSelector}, label[for]`);if(!t)return;const s="LABEL"===t.tagName?document.getElementById(t.getAttribute("for")):t;if(s)if(e.shiftKey&&this.lastSelected)e.preventDefault(),this.handleRangeSelection(s);else{const e=s.closest(this.itemSelector);e&&(this.lastSelected=e)}}handleChange(e){if(this.ui.selectAll&&e.target===this.ui.selectAll)this.selectAll(e.target.checked);else{const t=e.target.closest(this.checkboxSelector);if(!t)return;this.toggleSelection(this.getItemId(t))}}toggleSelection(e){if(!e)return;let t=!0;this.selectedItems.has(e)?(t=!1,this.selectedItems.delete(e)):this.selectedItems.add(e),t?this.notify("item-selected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}):this.notify("item-deselected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(this.itemSelector);e||(this.selectedItems.clear(),this.ui.selectAll&&(this.ui.selectAll.checked=!1)),t.forEach((t=>{const s=this.getItemId(t),i=t.querySelector(this.checkboxSelector);i&&(i.checked=e),e&&s&&this.selectedItems.add(s)})),this.notify("select-all",{container:this.container,selected:e,items:t}),this.updateSelectionUI()}clearSelection(){this.selectAll(!1)}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(this.itemSelector));const t=e.closest(this.itemSelector);if(!t)return;const s=Array.from(this.container.querySelectorAll(this.itemSelector)),i=s.indexOf(this.lastSelected),c=s.indexOf(t);if(-1===i||-1===c)return;const l=Math.min(i,c),n=Math.max(i,c);let h=!e.checked;for(let e=l;e<=n;e++){const t=s[e],i=t.querySelector(this.checkboxSelector),c=this.getItemId(t);i&&c&&(i.checked=h,this.selectedItems.add(c))}this.lastSelected=t,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:this.selectedItems,container:this.container});const r=n-l+1;window.jvbA11y&&window.jvbA11y.announce(`Selected ${r} items in range`)}updateSelectionUI(){const e=this.selectedItems.size,t=this.container.querySelectorAll(this.itemSelector).length;if(this.ui.bulkControls&&(this.ui.bulkControls.hidden=0===e),this.ui.count){const t=1===e?"item":"items";this.ui.count.textContent=0===e?"":`{ ${e} ${t} selected }`,this.ui.count.hidden=0===e}if(this.ui.selectAll){this.ui.selectAll.checked=t>0&&e===t,this.ui.selectAll.indeterminate=e>0&&e<t;const s=this.ui.selectAll.nextElementSibling||this.ui.selectAll.previousElementSibling;s&&"LABEL"===s.tagName&&(s.textContent=t>0&&e===t?"Clear Selection":"Select All")}}getItemId(e){const t=e.closest(this.itemSelector);return t?t.dataset.id||t.dataset.itemId||t.dataset.uploadId||t.id:null}getSelected(){return Array.from(this.selectedItems)}isSelected(e){return this.selectedItems.has(e)}select(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.add(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!0)}})),this.updateSelectionUI(),this.notify("item-selected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}deselect(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.delete(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!1)}})),this.updateSelectionUI(),this.notify("item-deselected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.container&&(this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("keydown",this.keyHandler)),this.clearSelection(),this.subscribers.clear(),this.container=null,this.ui=null,this.lastSelected=null}};
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index 2fe936e..8a2b288 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-window.jvbPopulate=class{constructor(e,t={},a={},l={}){for(let[r,i]of Object.entries(t)){let t=e.querySelector(`[data-field="${r}"]`);t&&this.populateField(t,r,i,a,l)}}populateField(e,t,a,l={},r={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,l);break;case"gallery":this.populateGalleryField(e,t,a,l);break;case"repeater":this.populateRepeaterField(e,t,a,r);break;case"taxonomy":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)}}getFieldType(e){const t=["image","gallery","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;if(e.dataset.type)return e.dataset.type;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"}populateTextField(e,t,a){const l=e.querySelector(`[name="${t}"], input, textarea`);if(l&&(l.value=String(a||""),l.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=l.value.length)}}populateTextareaField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(l){if(l.value,l.value=String(a||""),l.dispatchEvent(new Event("change",{bubbles:!0})),l.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=l.value.length;const a=parseInt(l.dataset.limit,10);l.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="number"]`);l&&(l.value=Number(a)||0)}populateBooleanField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="checkbox"]`);l&&(l.checked=Boolean(a))}populateSelectField(e,t,a){const l=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=l);const i=e.querySelector(`input[type="radio"][name="${t}"][value="${l}"]`);i&&(i.checked=!0)}populateSetField(e,t,a){let l=a;if("string"==typeof a)try{l=JSON.parse(a)}catch(e){l=a.split(",").map((e=>e.trim()))}Array.isArray(l)||(l=[String(l)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=l.includes(e.value)}))}populateDateField(e,t,a){const l=e.querySelector(`[name="${t}"], input`);if(l&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(l.type){case"date":l.value=t.toISOString().split("T")[0];break;case"time":l.value=t.toTimeString().slice(0,5);break;case"datetime-local":l.value=t.toISOString().slice(0,16);break;default:l.value=e}}catch(t){l.value=e}}}populateEditorField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!l)return void console.warn(`Editor field ${t}: textarea not found`);const r=String(a||"");l.value=r;const i=e.querySelector(".editor");if(i){let e=null;if(i.__quill)e=i.__quill;else if(i.quill)e=i.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(i);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===i){e=t;break}e?(e.root.innerHTML=r,i.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),i.innerHTML=r)}else console.warn(`Editor container not found for ${t}`);l.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((l=>{if(void 0!==a[l]){const r=e.querySelector(`[name="${t}_${l}"], [name="${l}"]`);r&&(r.value=String(a[l]||""))}}))}populateTaxonomyField(e,t,a){let l=[];if(Array.isArray(a))l=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);l=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){l=a.split(",").map((e=>e.trim()))}else a&&(l=[String(a)]);if(0===l.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=l.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,l={}){if(!a)return;const r=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===r.length)return;const i=e.querySelector(`input[type="hidden"][name="${t}"]`);i&&(i.value=r.join(","));const o=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");o&&(window.removeChildren(o),r.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),r=t.querySelector("details"),i=window.getTemplate("uploadMeta");r.append(i),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[l[e].medium,l[e].alt,l[e].title,l[e].alt,l[e].caption],r.querySelector(".upload-meta > .hint")?.remove(),o.append(t)})),r.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,l={}){this.populateImageField(e,t,a,l)}populateRepeaterField(e,t,a,l={}){if(!a||!Array.isArray(a))return;const r=e.querySelector(".repeater-items"),i=e.querySelector("template");r&&i?(window.removeChildren(r),a.forEach(((a,l)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(i.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 r=e.name,i=`${t}:${l}:${r}`,o=`${t}-${l}-${r}-${e.value}`;e.name=i,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[r]&&this.populateRepeaterFieldValue(e,r,a[r])})),r.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={},r={}){for(let[l,i]of Object.entries(t)){let t=e.querySelector(`[data-field="${l}"]`);t&&this.populateField(t,l,i,a,r)}}populateField(e,t,a,r={},l={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,r);break;case"gallery":this.populateGalleryField(e,t,a,r);break;case"repeater":this.populateRepeaterField(e,t,a,l);break;case"taxonomy":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)}}getFieldType(e){const t=["image","gallery","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;if(e.dataset.type)return e.dataset.type;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"}populateTextField(e,t,a){const r=e.querySelector(`[name="${t}"], input, textarea`);if(r&&(r.value=String(a||""),r.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=r.value.length)}}populateTextareaField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(r){if(r.value,r.value=String(a||""),r.dispatchEvent(new Event("change",{bubbles:!0})),r.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=r.value.length;const a=parseInt(r.dataset.limit,10);r.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="number"]`);r&&(r.value=Number(a)||0)}populateBooleanField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="checkbox"]`);r&&(r.checked=Boolean(a))}populateSelectField(e,t,a){const r=String(a||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=r);const i=e.querySelector(`input[type="radio"][name="${t}"][value="${r}"]`);i&&(i.checked=!0)}populateSetField(e,t,a){let r=a;if("string"==typeof a)try{r=JSON.parse(a)}catch(e){r=a.split(",").map((e=>e.trim()))}Array.isArray(r)||(r=[String(r)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=r.includes(e.value)}))}populateDateField(e,t,a){const r=e.querySelector(`[name="${t}"], input`);if(r&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(r.type){case"date":r.value=t.toISOString().split("T")[0];break;case"time":r.value=t.toTimeString().slice(0,5);break;case"datetime-local":r.value=t.toISOString().slice(0,16);break;default:r.value=e}}catch(t){r.value=e}}}populateEditorField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!r)return void console.warn(`Editor field ${t}: textarea not found`);const l=String(a||"");r.value=l;const i=e.querySelector(".editor");if(i){let e=null;if(i.__quill)e=i.__quill;else if(i.quill)e=i.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(i);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===i){e=t;break}e?(e.root.innerHTML=l,i.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),i.innerHTML=l)}else console.warn(`Editor container not found for ${t}`);r.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((r=>{if(void 0!==a[r]){const l=e.querySelector(`[name="${t}_${r}"], [name="${r}"]`);l&&(l.value=String(a[r]||""))}}))}populateTaxonomyField(e,t,a){let r=[];if(Array.isArray(a))r=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);r=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){r=a.split(",").map((e=>e.trim()))}else a&&(r=[String(a)]);if(0===r.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=r.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,r={}){if(!a)return;const l=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===l.length)return;const i=e.querySelector(`input[type="hidden"][name="${t}"]`);i&&(i.value=l.join(","));const o=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),o&&(window.removeChildren(o),l.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),l=t.querySelector("details"),i=window.getTemplate("uploadMeta");l.append(i),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[r[e].medium,r[e].alt,r[e].title,r[e].alt,r[e].caption],l.querySelector(".upload-meta > .hint")?.remove(),o.append(t)})),l.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,r={}){this.populateImageField(e,t,a,r)}populateRepeaterField(e,t,a,r={}){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),i=e.querySelector("template");l&&i?(window.removeChildren(l),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(i.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 l=e.name,i=`${t}:${r}:${l}`,o=`${t}-${r}-${l}-${e.value}`;e.name=i,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.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/popup.min.js b/assets/js/min/popup.min.js
new file mode 100644
index 0000000..8008ba1
--- /dev/null
+++ b/assets/js/min/popup.min.js
@@ -0,0 +1 @@
+window.jvbPopup=class{constructor(e={}){this.config={toggle:document.querySelector("[data-toggles]"),popup:document.querySelector(".jvb-pop"),name:"Popup",onOpen:()=>{},onClose:()=>{},onEscape:()=>{},...e},this.a11y=window.jvbA11y,this.toggleID=""===this.config.toggle.id?"."+this.config.toggle.className.split(" ").join("."):this.config.toggle.id,this.initListeners()}initListeners(){this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),document.addEventListener("click",this.handleToggle.bind(this))}handleToggle(e){(e.target===this.config.toggle||e.target.closest(this.toggleID))&&this.togglePopup()}handleClick(e){this.config.popup.contains(e.target)||e.target===this.config.toggle?window.targetCheck(e,".close")&&this.closePopup():this.closePopup()}togglePopup(){this.config.popup.classList.contains("expanded")?this.closePopup():this.openPopup()}openPopup(e=`Opened ${this.config.name}`){this.config.popup.classList.add("expanded"),this.config.toggle.classList.add("expanded"),this.config.toggle.title=`Hide ${this.config.name}`,this.config.toggle.ariaExpanded=!0,this.config.toggle.querySelector("span").textContent=`Close ${this.config.name}`,this.a11y.announce(e),this.config.onOpen(),document.addEventListener("keydown",this.keyHandler),document.addEventListener("click",this.clickHandler)}closePopup(e=`Closed ${this.config.name}`){this.config.popup.classList.remove("expanded"),this.config.toggle.classList.remove("expanded"),this.config.toggle.title=`Show ${this.config.name}`,this.config.toggle.ariaExpanded=!1,this.config.toggle.querySelector("span").textContent="",this.a11y.announce(e),this.config.onClose(),document.removeEventListener("keydown",this.keyHandler),document.removeEventListener("click",this.clickHandler)}handleEscape(e){"Escape"===e.key&&(this.closePopup(`Closed ${this.config.name} with escape key`),this.config.onEscape())}};
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index f6e3412..84975eb 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.user=jvbSettings.currentUser,this.headers={"X-WP-Nonce":jvbSettings.nonce,...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",endpoint:this.config.endpoint,useIndexedDB:!0,TTL:1/0,showLoading:!1}),this.queue=new Map,this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-fetched":case"data-cached":this.updateOperationsFromServer(t.data.items);break;case"items-updated":this.updateOperationsFromServer(t.items);break;case"item-stored":this.updateOperationsFromServer([t])}})),this.store.fetch(),this.notify("queue-initialized",{operations:e})}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},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 console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.queue.values()).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.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.queue.set(e.id,e),this.store.setItem(e.id,e)}updateOperationStatus(e,t){let s=this.queue.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.getItem(e)}clearQueue(e){this.queue.has(e)&&this.queue.delete(e),this.store.clearItem(e)}startActivityTracking(){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.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}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)}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading");const t=`${this.config.apiBase}${e.endpoint}`;let s;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),n=await i.json();if(!i.ok||!1===n.success)throw new Error(n.message||`HTTP ${i.status}`);if(n.id&&e.id!==n.id){const t=this.getQueue(n.id);t?(t.data=window.deepMerge(t.data,e.data),t.status="pending",t.serverData=n,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=n.id,e.status="pending",e.serverData=n,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status="pending",e.serverData=n,this.updateOperationStatus(e.id,"pending"),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.pollServer(),this.pollTimer=setInterval((()=>{this.pollServer()}),this.config.pollInterval),this.updateCountdown())}pollServer(e=!1){if(0!==this.getOperationsByStatus(["pending","processing","uploading"]).length||e){this.updateStatusPanel("pending");try{this.store.fetch()}catch(e){console.error("Polling error:",e)}finally{this.updateStatusPanel()}}else this.stopPolling()}async updateOperationsFromServer(e){let t=!1;const s=new Set;for(const t of e){let e=this.queue.has(t.id)?this.queue.get(t.id):{};s.add(t.id),t.status!==e.status&&(e={...e,...t},this.queue.set(e.id,e),this.updateOperationStatus(e.id,e.status))}const i=this.getOperationsByStatus(["pending","processing","uploading"]);for(const e of i)s.has(e.id)||(e.status="completed",e.completedAt=Date.now(),this.setQueue(e),t=!0,this.updateOperationStatus(e.id,e.status));0===this.getOperationsByStatus(["pending","processing","uploading"]).length&&this.stopPolling(),this.updateUI()}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(e,t){if(0!==(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length){["cancel","dismiss"].includes(t)&&e.forEach((e=>{this.removeOperationFromUI(e)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t})});if(!i.ok){const e=await i.json().catch((()=>{}));throw new Error(e.message||`${t} failed: ${i.status}`)}const n=await i.json();if(!n.success)throw new Error(n.message||`${t} operation failed`);return["cancel","dismiss"].includes(t)?e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),this.clearQueue(e)})):(e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),n}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t)));if(i.retried)return i;throw s}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleEscape.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel)||e.target.closest(this.selectors.toggle)){if(e.target.closest(this.selectors.toggle))this.togglePanel(!this.panelIsOpen());else if(e.target.closest(this.selectors.refreshButton))this.pollServer(!0);else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}else this.panelIsOpen()&&this.togglePanel(!1)}handleChange(e){}handleEscape(e){"Escape"===e.key&&this.togglePanel(!1)}panelIsOpen(){return this.ui.panel?.classList.contains("expanded")}togglePanel(e){this.ui.panel&&(e?document.addEventListener("keydown",this.keyHandler):document.removeEventListener("keydown",this.keyHandler),this.ui.toggle.title=e?"Hide Queue":"Show Queue",this.a11y.announce(e?"Opened Queue Panel":"Closed Queue Panel"),this.ui.panel.ariaExpanded=e,this.ui.panel.classList.toggle("expanded",e))}initUI(){if(this.icons={queued:"refresh",localProcessing:"refresh",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"synced",failed:"error",failed_permanent:"error"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[e,t]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(e))if("object"==typeof t){this.ui[e]={};for(let[s,i]of Object.entries(t))this.ui[e][s]=this.ui.panel.querySelector(i)}else this.ui[e]=this.ui.panel.querySelector(t)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const e=this.getQueueStats();if(this.ui.count){const t=e.total-e.completed;this.ui.count.textContent=t>0?t:"",this.ui.count.style.display=t>0?"":"none"}if(this.ui.indicator){const t=e.queued>0||e.uploading>0||e.pending>0||e.processing>0;this.ui.indicator.classList.toggle("active",t)}let t=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===t.length,Object.entries(this.ui.filters).forEach((([t,s])=>{const i="all"===t?e.total:e[t]||0,n=s.querySelector(".count");n&&(n.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(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""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}getQueueStats(){const e={};return this.statuses.forEach((t=>{e[t]=0})),Array.from(this.store.items.values()).forEach((t=>{e.hasOwnProperty(t.status)&&e[t.status]++})),e.total=Array.from(this.store.items.values()).length,e}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.getActiveFilter(),t=this.getFilteredOperations(e);if(window.removeChildren(this.ui.itemsContainer),0===t.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else{let e=this.ui.itemsContainer.querySelector(".emptyQueue");e&&e.remove(),t.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.appendChild(t)}))}}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),n=t.querySelector(".type"),a=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),l=t.querySelector(".progress .fill");if(n&&(n.textContent=e.title),a){a.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);a.title=t,a.prepend(window.getIcon(this.icons[e.status])),a.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const u=t.querySelector(".actions");u&&this.updateActionButtons(e,u)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),n=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",n.classList.add("dismiss"),n.textContent="Dismiss",n.dataset.action="dismiss",t.appendChild(i),t.appendChild(n);break;case"completed":const a=window.getTemplate("button");a.dataset.action="dismiss",a.classList.add("dismiss"),a.textContent="Dismiss",t.appendChild(a)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let e=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{e--,this.ui.countdown.textContent=e,e<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),this.activeFilter=e,this.renderOperations()}getActiveFilter(){const e=this.ui.panel?.querySelector(".filter.active");return e?.dataset.filter||"all"}getFilteredOperations(e){const t=Array.from(this.store.items.values());return"all"===e?t:t.filter((t=>t.status===e))}showPopup(e,t="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=e),this.ui.popup.className=`popup ${t} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(e,t=!0){return e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e],t?Array.from(this.queue.values()).filter((t=>e.includes(t.status))):Array.from(this.queue.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.queue.some((e=>"queued"===e.status))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.user=jvbSettings.currentUser,this.headers={"X-WP-Nonce":jvbSettings.nonce,...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",storeName:"operations",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1}),this.queue=new Map,this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),console.log(this.ui),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-fetched":case"data-cached":this.updateOperationsFromServer(t.data.items);break;case"items-updated":this.updateOperationsFromServer(t.items);break;case"item-stored":this.updateOperationsFromServer([t])}})),this.store.fetch(),this.notify("queue-initialized",{operations:e})}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},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 console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.queue.values()).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.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.queue.set(e.id,e),this.store.save(e.id,e)}updateOperationStatus(e,t){let s=this.queue.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.queue.has(e)?this.queue.get(e):this.store.getItem(e)}clearQueue(e){this.queue.has(e)&&this.queue.delete(e),this.store.clearItem(e)}startActivityTracking(){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.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}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)}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading");const t=`${this.config.apiBase}${e.endpoint}`;let s;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);if(a.id&&e.id!==a.id){const t=this.getQueue(a.id);t?(t.data=window.deepMerge(t.data,e.data),t.status="pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=a.id,e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,"pending"),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.pollServer(),this.pollTimer=setInterval((()=>{this.pollServer()}),this.config.pollInterval),this.updateCountdown())}pollServer(e=!1){if(0!==this.getOperationsByStatus(["pending","processing","uploading"]).length||e){this.updateStatusPanel("pending");try{this.store.fetch()}catch(e){console.error("Polling error:",e)}finally{this.updateStatusPanel()}}else this.stopPolling()}async updateOperationsFromServer(e){let t=!1;const s=new Set;for(const t of e){let e=this.queue.has(t.id)?this.queue.get(t.id):{};s.add(t.id),t.status!==e.status&&(e={...e,...t},this.queue.set(e.id,e),this.updateOperationStatus(e.id,e.status))}const i=this.getOperationsByStatus(["pending","processing","uploading"]);for(const e of i)s.has(e.id)||(e.status="completed",e.completedAt=Date.now(),this.setQueue(e),t=!0,this.updateOperationStatus(e.id,e.status));0===this.getOperationsByStatus(["pending","processing","uploading"]).length&&this.stopPolling(),this.updateUI()}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(e,t){if(0!==(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length){["cancel","dismiss"].includes(t)&&e.forEach((e=>{this.removeOperationFromUI(e)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t})});if(!i.ok){const e=await i.json().catch((()=>{}));throw new Error(e.message||`${t} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return["cancel","dismiss"].includes(t)?e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),this.clearQueue(e)})):(e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t)));if(i.retried)return i;throw s}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.refreshButton))this.pollServer(!0);else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}handleChange(e){}initUI(){if(this.icons={queued:"refresh",localProcessing:"refresh",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"synced",failed:"error",failed_permanent:"error"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[e,t]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(e))if("object"==typeof t){this.ui[e]={};for(let[s,i]of Object.entries(t))this.ui[e][s]=this.ui.panel.querySelector(i)}else this.ui[e]=this.ui.panel.querySelector(t)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const e=this.getQueueStats();if(this.ui.count){const t=e.total-e.completed;this.ui.count.textContent=t>0?t:"",this.ui.count.style.display=t>0?"":"none"}if(this.ui.indicator){const t=e.queued>0||e.uploading>0||e.pending>0||e.processing>0;this.ui.indicator.classList.toggle("active",t)}let t=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===t.length,Object.entries(this.ui.filters).forEach((([t,s])=>{const i="all"===t?e.total:e[t]||0,a=s.querySelector(".count");a&&(a.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(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""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}getQueueStats(){const e={};return this.statuses.forEach((t=>{e[t]=0})),Array.from(this.store.items.values()).forEach((t=>{e.hasOwnProperty(t.status)&&e[t.status]++})),e.total=Array.from(this.store.items.values()).length,e}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.getActiveFilter(),t=this.getFilteredOperations(e);if(window.removeChildren(this.ui.itemsContainer),0===t.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else{let e=this.ui.itemsContainer.querySelector(".emptyQueue");e&&e.remove(),t.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.appendChild(t)}))}}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),u=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),u&&(u.style.width=`${i}%`);const l=t.querySelector(".actions");l&&this.updateActionButtons(e,l)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let e=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{e--,this.ui.countdown.textContent=e,e<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),this.activeFilter=e,this.renderOperations()}getActiveFilter(){const e=this.ui.panel?.querySelector(".filter.active");return e?.dataset.filter||"all"}getFilteredOperations(e){const t=Array.from(this.store.items.values());return"all"===e?t:t.filter((t=>t.status===e))}showPopup(e,t="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=e),this.ui.popup.className=`popup ${t} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(e,t=!0){return e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e],t?Array.from(this.queue.values()).filter((t=>e.includes(t.status))):Array.from(this.queue.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.queue.some((e=>"queued"===e.status))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
new file mode 100644
index 0000000..68cd4e7
--- /dev/null
+++ b/assets/js/min/referral.min.js
@@ -0,0 +1 @@
+(()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),console.log(this.forms),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n            <div class="banner-icon">🎉</div>\n            <div class="banner-content">\n                <strong>${window.escapeHtml(e)}</strong> referred you!\n                <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n            </div>\n        `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const o=r.querySelector("p");o&&(o.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){console.log("Form Submission!"),window.debouncer.cancel("check-referral"),e.preventDefault(),console.log("Still working?");const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};console.log(t),console.log(t.id),"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic-link",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic-link","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=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 8d3f875..991ae3a 100644
--- a/assets/js/min/selector.min.js
+++ b/assets/js/min/selector.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.stores=new Map,this.storeSubscriptions=new Map,this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners()}getOrCreateStore(e){if(!this.stores.has(e)){const t=new window.jvbStore({name:`tax_${e}`,endpoint:"terms",TTL:36e5,filters:{taxonomy:e,page:1,search:"",parent:0}}),i=t.subscribe(((t,i)=>{this.handleStoreEvent(e,t,i)}));this.stores.set(e,t),this.storeSubscriptions.set(e,i)}return this.stores.get(e)}handleStoreEvent(e,t,i){if(this.activeStore&&this.activeStore.config.name===`tax_${e}`)switch(t){case"items-loaded":case"data-fetched":case"data-cached":case"stale-cache-used":this.handleTermsLoaded(i);break;case"fetch-error":this.handleFetchError(i.error)}"items-updated"!==t&&"items-loaded"!==t||this.updateFieldsForTaxonomy(e,i.items)}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,r=e.filters?.page>1;0===t.length?(r||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,r,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,r)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(){document.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,r=e.querySelector("button.taxonomy-toggle"),a={id:s,input:i,container:e,taxonomy:r.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(r.dataset.max)||0,canSearch:"search"in r.dataset,canCreate:"creatable"in r.dataset,isRequired:"required"in r.dataset,selectedTerms:new Set,toggle:r,selectedContainer:e.querySelector(".selected-items"),...t};const n=i.value.trim();if(""!==n){n.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>a.selectedTerms.add(e)))}return this.fields.set(s,a),this.getOrCreateStore(a.taxonomy),a.selectedTerms.size>0&&this.initFieldDisplay(s),s}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=this.getOrCreateStore(t.taxonomy),s=Array.from(t.selectedTerms),r=[],a=[];if(s.forEach((e=>{const t=i.getItem(e);t?r.push(t):a.push(e)})),r.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)})),a.length>0)try{const s=await i.fetch("terms",{filters:{taxonomy:t.taxonomy,termIDs:a.join(",")}});s.terms&&s.terms.forEach((t=>{i.setItem(t.id,t),this.addTermToDisplay(e,t.id,t.name,t.path)}))}catch(e){console.error("Failed to fetch missing terms:",e)}}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){this.activeField=e,this.currentConfig=this.fields.get(e),console.log("Current Taxonomy:",this.currentConfig.taxonomy),console.log("Labels: ",jvbSettings.labels[this.currentConfig.taxonomy]),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.activeStore=this.getOrCreateStore(this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms&&this.currentConfig.selectedTerms.forEach((e=>{const t=this.activeStore.getItem(e);t?this.selectedTerms.set(e,{id:e,name:t.name,path:t.path}):this.selectedTerms.set(e,{id:e,name:`Term ${e}`,path:`Term ${e}`})}))}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(){this.activeField&&this.currentConfig?(this.resetModalState(),this.updateModalForTaxonomy(),this.activeStore.clearFilters(),this.currentConfig.canSearch&&(this.ui.search.input.focus(),this.searchHandler=window.debounce((()=>this.handleSearch()),300),this.ui.search.input.addEventListener("input",this.searchHandler)),this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalSelections(),this.observer.observe(this.ui.sentinel),this.fetchCurrentTerms()):console.error("No active field set for modal")}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeStore=null,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const r=i.selectedContainer.querySelector(`[data-id="${s}"]`);r&&r.remove();const a=Array.from(i.selectedTerms);i.input.value=a.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const r=this.fields.get(e);if(!r||r.selectedContainer.querySelector(`[data-id="${t}"]`))return;const a=window.getTemplate("selectedTerm").cloneNode(!0);a.dataset.id=t,a.dataset.path=s,a.dataset.name=i,a.dataset.taxonomy=r.taxonomy,a.querySelector("span").textContent=s,a.querySelector("button").title=`Remove ${i}`,r.selectedContainer.appendChild(a)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(){const e=this.ui.searchInput.value.trim();e.length>=2||0===e.length?(this.activeStore.setFilter("page",1),this.activeStore.setFilter("search",e),window.removeChildren(this.ui.termsList),(e.length>=2||0===e.length)&&(this.showLoading(),this.fetchCurrentTerms())):(this.hideLoading(),this.showEmptyState("Enter at least 2 characters to search."))}navigateToParent(){this.activeStore.filters.parent;this.activeStore.setFilter("parent",0),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.activeStore.setFilter("parent",e),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.activeStore.setFilter("parent",t),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.ui.breadcrumbs.back.hidden=0===t}fetchCurrentTerms(){this.activeStore&&(this.showLoading(),this.activeStore.fetch())}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.activeStore.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.activeStore.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.activeStore.getDOMElement(e.id,"list-item");if(t){const i=t.querySelector('input[type="checkbox"]');i&&(i.checked=this.selectedTerms.has(e.id),i.disabled=!i.checked&&this.disabled),this.ui.termsList.appendChild(t)}else{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&(this.activeStore.storeDOMElement(e.id,"list-item",t),this.ui.termsList.appendChild(t))}}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),r=t.querySelector("label"),a=t.querySelector("span, .term-name");if(s&&r&&a&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,r.htmlFor=s.id,r.title=e.path||e.name,r.dataset.path=e.path,a.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.activeStore?.filters?.search||"",t=this.activeStore?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found."){const t=window.getTemplate("noResults").cloneNode(!0);e&&t.querySelector("span")&&(t.querySelector("span").textContent=e),this.ui.termsList.appendChild(t)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.storeSubscriptions.forEach((e=>e())),this.stores.forEach((e=>e.destroy())),this.fields.clear(),this.stores.clear(),this.storeSubscriptions.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector||(window.jvbSelector=new e)}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.store=new window.jvbStore({name:"taxonomies",storeName:"terms",keyPath:"id",indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:72e5,filters:{taxonomy:"",page:1,search:"",parent:0}}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.store.subscribe(this.handleStoreEvent.bind(this))}handleStoreEvent(e,t,i){if(this.activeStore&&this.activeStore.config.name===`tax_${e}`)switch(t){case"items-loaded":case"data-fetched":case"data-cached":case"stale-cache-used":this.handleTermsLoaded(i);break;case"fetch-error":this.handleFetchError(i.error)}"items-updated"!==t&&"items-loaded"!==t||this.updateFieldsForTaxonomy(e,i.items)}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,r=e.filters?.page>1;0===t.length?(r||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,r,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,r)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(){document.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,r=e.querySelector("button.taxonomy-toggle"),a={id:s,input:i,container:e,taxonomy:r.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(r.dataset.max)||0,canSearch:"search"in r.dataset,canCreate:"creatable"in r.dataset,isRequired:"required"in r.dataset,selectedTerms:new Set,toggle:r,selectedContainer:e.querySelector(".selected-items"),...t};const n=i.value.trim();if(""!==n){n.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>a.selectedTerms.add(e)))}return this.fields.set(s,a),this.store.setFilter("taxonomy",a.taxonomy),a.selectedTerms.size>0&&this.initFieldDisplay(s),s}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=Array.from(t.selectedTerms),s=[],r=[];if(i.forEach((e=>{const t=this.store.getItem(e);t?s.push(t):r.push(e)})),s.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)})),r.length>0)try{const i=await this.store.fetch({filters:{taxonomy:t.taxonomy,termIDs:r.join(",")}});i.terms&&i.terms.forEach((t=>{this.store.setItem(t.id,t),this.addTermToDisplay(e,t.id,t.name,t.path)}))}catch(e){console.error("Failed to fetch missing terms:",e)}}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){if(this.activeField=e,this.currentConfig=this.fields.get(e),console.log("Current Taxonomy:",this.currentConfig.taxonomy),console.log("Labels: ",jvbSettings.labels[this.currentConfig.taxonomy]),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms){let e=[];if(this.currentConfig.selectedTerms.forEach((t=>{const i=this.store.getItem(t);i?this.selectedTerms.set(t,{id:t,name:i.name,path:i.path}):e.push(t)})),e.length>0){this.fetchSpecificTerms(e).forEach((e=>{this.selectedTerms.set(e.id,{id:e.id,name:e.name,path:e.path})}))}}}fetchSpecificTerms(e){return[]}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(){this.activeField&&this.currentConfig?(this.resetModalState(),this.updateModalForTaxonomy(),this.activeStore.clearFilters(),this.currentConfig.canSearch&&(this.ui.search.input.focus(),this.searchHandler=window.debounce((()=>this.handleSearch()),300),this.ui.search.input.addEventListener("input",this.searchHandler)),this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalSelections(),this.observer.observe(this.ui.sentinel),this.fetchCurrentTerms()):console.error("No active field set for modal")}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeStore=null,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const r=i.selectedContainer.querySelector(`[data-id="${s}"]`);r&&r.remove();const a=Array.from(i.selectedTerms);i.input.value=a.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const r=this.fields.get(e);if(!r||r.selectedContainer.querySelector(`[data-id="${t}"]`))return;const a=window.getTemplate("selectedTerm").cloneNode(!0);a.dataset.id=t,a.dataset.path=s,a.dataset.name=i,a.dataset.taxonomy=r.taxonomy,a.querySelector("span").textContent=s,a.querySelector("button").title=`Remove ${i}`,r.selectedContainer.appendChild(a)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(){const e=this.ui.searchInput.value.trim();e.length>=2||0===e.length?(this.activeStore.setFilter("page",1),this.activeStore.setFilter("search",e),window.removeChildren(this.ui.termsList),(e.length>=2||0===e.length)&&(this.showLoading(),this.fetchCurrentTerms())):(this.hideLoading(),this.showEmptyState("Enter at least 2 characters to search."))}navigateToParent(){this.activeStore.filters.parent;this.activeStore.setFilter("parent",0),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.activeStore.setFilter("parent",e),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.activeStore.setFilter("parent",t),this.activeStore.setFilter("page",1),window.removeChildren(this.ui.termsList),this.showLoading(),this.fetchCurrentTerms(),this.ui.breadcrumbs.back.hidden=0===t}fetchCurrentTerms(){this.activeStore&&(this.showLoading(),this.activeStore.fetch())}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.activeStore.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.activeStore.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.activeStore.getDOMElement(e.id,"list-item");if(t){const i=t.querySelector('input[type="checkbox"]');i&&(i.checked=this.selectedTerms.has(e.id),i.disabled=!i.checked&&this.disabled),this.ui.termsList.appendChild(t)}else{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&(this.activeStore.storeDOMElement(e.id,"list-item",t),this.ui.termsList.appendChild(t))}}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),r=t.querySelector("label"),a=t.querySelector("span, .term-name");if(s&&r&&a&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,r.htmlFor=s.id,r.title=e.path||e.name,r.dataset.path=e.path,a.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.activeStore?.filters?.search||"",t=this.activeStore?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found."){const t=window.getTemplate("noResults").cloneNode(!0);e&&t.querySelector("span")&&(t.querySelector("span").textContent=e),this.ui.termsList.appendChild(t)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector||(window.jvbSelector=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/square.min.js b/assets/js/min/square.min.js
index 550b94e..8ea9e23 100644
--- a/assets/js/min/square.min.js
+++ b/assets/js/min/square.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(t={}){this.checkout=document.querySelector("aside#cart"),this.checkout&&(this.config=Object.assign({application_id:squareConfig.application_id,location_id:squareConfig.location_id,api_url:squareConfig.api_url,nonce:squareConfig.nonce,currency:squareConfig.currency||"CAD"},t),this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.payments=null,this.card=null,this.isInitialized=!1,this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.initElements(),this.bindEvents(),this.init(),this.toggle.hidden=!1)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(t){if(window.targetCheck(t,".toggle-cart")){window.targetCheck(t,".toggle-cart");console.log("Toggle found. Toggling cart"),this.toggleCart()}else if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")?this.clearCart():this.checkout.classList.contains("expanded")&&!this.checkout.contains(t.target)&&t.target!==this.toggle&&this.closeCart()}handleChange(t,e){console.log("Checkout change");let a=window.targetCheck(t,".quantity-input");if(a){let e=t.target.closest(".quantity"),i=a.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${e.dataset.id}"] input`);t&&(t.value=a.value)}i>0?this.handleAddToCart(e):this.handleRemoveFromCart(e)}}handleNumberClick(t,e){console.log(e),t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let[t,i]=[parseInt(e.dataset.step),e.querySelector("input")],r=""===i.value?0:parseInt(i.value);i.value=r+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let[e,a,i,r,s]=[t.dataset.min,t.dataset.max,t.querySelector("input"),t.querySelector(".increase"),t.querySelector(".decrease")],o=parseInt(i.value);o<e?(i.value=e,s.disabled=!0):o>a?(i.value=a,r.disabled=!1):r.disabled?r.disabled=!1:s.disabled&&(s.disabled=!1)}toggleCart(){this.checkout.classList.contains("expanded")?this.closeCart():this.openCart()}openCart(t="Opened Cart"){this.checkout.classList.add("expanded"),this.toggle.title="Hide cart",this.toggle.ariaExpanded=!0,this.toggle.querySelector("span").textContent="Close Cart",this.a11y.announce(t),this.maybeAddEmptyState(),document.addEventListener("keydown",this.keyHandler)}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}closeCart(t="Closed Cart"){this.checkout.classList.remove("expanded"),this.toggle.title="Show Cart",this.toggle.ariaExpanded=!1,this.toggle.querySelector("span").textContent="",this.a11y.announce(t),document.removeEventListener("keydown",this.keyHandler)}handleEscape(t){"Escape"===t.key?(this.closeCart("Closed Cart with escape key"),this.stepMultiplier=1):t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id;this.createItemElement(t);let a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,r=parseFloat(a*i);this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:r,square_catalog_id:t.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{console.log(t);let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity"),i=t.price,r=t.quantity;[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(i),i,t.square_catalog_id,r,window.formatPrice(r*i)],this.table.append(e)})),this.updateTotal()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let t=0;this.cartItems.forEach((e=>{console.log(e),t+=e.total}));let e=.05*t;t=window.formatPrice(t+e),e=window.formatPrice(e),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,e),window.typeText(this.grandTotal,t),this.totalTax.classList.remove("typeText")}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,r=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let r=e.querySelector(".quantity");[r.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,r.dataset.price,r.dataset.squareCatalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.squareCatalogId]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[r,window.formatPrice(r*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>{e.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(t){console.error("Failed to initialize Square payments:",t),this.handleError(t)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),"1"!==squareConfig.isOpen&&(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}bindEvents(){this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container")}catch(t){throw console.error("Failed to initialize card:",t),t}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#007cba"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(t){if("1"!==squareConfig.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading.showLoading("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading.hideLoading()}}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({post_id:t.post_id,quantity:t.quantity,price:t.price,name:t.name})));return{total:100*e.reduce(((t,e)=>t+e.price*e.quantity),0),items:e,customer:{email:t.querySelector('[name="email"]')?.value||"",name:t.querySelector('[name="name"]')?.value||"",phone:t.querySelector('[name="phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(t){try{const e=await this.card.tokenize();if("OK"===e.status)return await this.submitToServer(e.token,t);throw new Error("Card tokenization failed: "+(e.errors?.join(", ")||"Unknown error"))}catch(t){throw console.error("Payment processing failed:",t),t}}async submitToServer(t,e){if("1"!==squareConfig.isOpen)return;const a=await fetch(this.config.api_url+"save-order",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({order_id:t.orderId,payment_id:t.paymentId,customer:e.customer,items:e.items,action:"jvb_integration_action",service:"square",integration_action:"save_order"})}),i=await a.json();if(!a.ok)throw new Error(i.message||"Failed to save order");return i}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const t=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}async loadCustomerProfile(t){const e=await fetch("/wp-json/jvb/v1/square/customer",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({email:t})}),a=await e.json();a&&(this.displaySavedCards(a.cards),this.fillCustomerInfo(a.customer))}displaySavedCards(t){const e=document.getElementById("saved-cards");t.length&&(e.innerHTML=`\n            <h3>Saved Payment Methods</h3>\n            ${t.map((t=>`\n                <label>\n                    <input type="radio" name="payment_method" value="${t.id}">\n                    •••• ${t.last_4} (${t.card_brand})\n                </label>\n            `)).join("")}\n            <label>\n                <input type="radio" name="payment_method" value="new" checked>\n                Use new card\n            </label>\n        `)}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:t,form:e}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.wp_order_id}`;window.location.href=a}handleError(t){console.error("Square checkout error:",t),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:t}})),window.jvbNotifications?.show?.(t.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new t}))})();
\ No newline at end of file
+(()=>{class t{constructor(t={}){this.checkout=document.querySelector("aside#cart"),this.checkout&&(this.config=Object.assign({application_id:squareConfig.application_id,location_id:squareConfig.location_id,api_url:squareConfig.api_url,nonce:squareConfig.nonce,currency:squareConfig.currency||"CAD"},t),this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.payments=null,this.card=null,this.isInitialized=!1,this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.initElements(),this.bindEvents(),this.popup=new window.jvbPopup({popup:this.checkout,toggle:this.toggle,name:"Cart",onOpen:this.maybeAddEmptyState.bind(this)}),this.init(),this.toggle.hidden=!1)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(t){if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")&&this.clearCart()}handleChange(t,e){console.log("Checkout change");let a=window.targetCheck(t,".quantity-input");if(a){let e=t.target.closest(".quantity"),i=a.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${e.dataset.id}"] input`);t&&(t.value=a.value)}i>0?this.handleAddToCart(e):this.handleRemoveFromCart(e)}}handleNumberClick(t,e){console.log(e),t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let[t,i]=[parseInt(e.dataset.step),e.querySelector("input")],r=""===i.value?0:parseInt(i.value);i.value=r+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let[e,a,i,r,s]=[t.dataset.min,t.dataset.max,t.querySelector("input"),t.querySelector(".increase"),t.querySelector(".decrease")],o=parseInt(i.value);o<e?(i.value=e,s.disabled=!0):o>a?(i.value=a,r.disabled=!1):r.disabled?r.disabled=!1:s.disabled&&(s.disabled=!1)}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}handleEscape(t){"Escape"===t.key?this.stepMultiplier=1:t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id;this.createItemElement(t);let a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,r=parseFloat(a*i);this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:r,square_catalog_id:t.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{console.log(t);let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity"),i=t.price,r=t.quantity;[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(i),i,t.square_catalog_id,r,window.formatPrice(r*i)],this.table.append(e)})),this.updateTotal()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let t=0;this.cartItems.forEach((e=>{console.log(e),t+=e.total}));let e=.05*t;t=window.formatPrice(t+e),e=window.formatPrice(e),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,e),window.typeText(this.grandTotal,t),this.totalTax.classList.remove("typeText")}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,r=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let r=e.querySelector(".quantity");[r.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,r.dataset.price,r.dataset.squareCatalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.squareCatalogId]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[r,window.formatPrice(r*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>{e.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(t){console.error("Failed to initialize Square payments:",t),this.handleError(t)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),"1"!==squareConfig.isOpen&&(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}bindEvents(){this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container")}catch(t){throw console.error("Failed to initialize card:",t),t}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#007cba"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(t){if("1"!==squareConfig.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading.showLoading("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading.hideLoading()}}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({post_id:t.post_id,quantity:t.quantity,price:t.price,name:t.name})));return{total:100*e.reduce(((t,e)=>t+e.price*e.quantity),0),items:e,customer:{email:t.querySelector('[name="email"]')?.value||"",name:t.querySelector('[name="name"]')?.value||"",phone:t.querySelector('[name="phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(t){try{const e=await this.card.tokenize();if("OK"===e.status)return await this.submitToServer(e.token,t);throw new Error("Card tokenization failed: "+(e.errors?.join(", ")||"Unknown error"))}catch(t){throw console.error("Payment processing failed:",t),t}}async submitToServer(t,e){if("1"!==squareConfig.isOpen)return;const a=await fetch(this.config.api_url+"save-order",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({order_id:t.orderId,payment_id:t.paymentId,customer:e.customer,items:e.items,action:"jvb_integration_action",service:"square",integration_action:"save_order"})}),i=await a.json();if(!a.ok)throw new Error(i.message||"Failed to save order");return i}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const t=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}async loadCustomerProfile(t){const e=await fetch("/wp-json/jvb/v1/square/customer",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({email:t})}),a=await e.json();a&&(this.displaySavedCards(a.cards),this.fillCustomerInfo(a.customer))}displaySavedCards(t){const e=document.getElementById("saved-cards");t.length&&(e.innerHTML=`\n            <h3>Saved Payment Methods</h3>\n            ${t.map((t=>`\n                <label>\n                    <input type="radio" name="payment_method" value="${t.id}">\n                    •••• ${t.last_4} (${t.card_brand})\n                </label>\n            `)).join("")}\n            <label>\n                <input type="radio" name="payment_method" value="new" checked>\n                Use new card\n            </label>\n        `)}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:t,form:e}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.wp_order_id}`;window.location.href=a}handleError(t){console.error("Square checkout error:",t),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:t}})),window.jvbNotifications?.show?.(t.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new t}))})();
\ No newline at end of file
diff --git a/assets/js/min/tabs.min.js b/assets/js/min/tabs.min.js
index 1e24ce5..cf10a6e 100644
--- a/assets/js/min/tabs.min.js
+++ b/assets/js/min/tabs.min.js
@@ -1 +1 @@
-window.jvbTabs=class{constructor(t,a={},e=null){this.tabs=t.querySelector(".tabs"),this.a11y=window.jvbA11y,this.updateURL=!0,this.parent=e,this.childTabs=new Map,"updateURL"in a&&!1===a.updateURL&&(this.updateURL=!1),this.callbacks=a,this.activeTab=this.updateURL?this.getInitialTabFromHash():t.querySelector("button.tab.active")?.dataset.tab,this.container=t,this.tabs&&this.tabs.addEventListener("click",(t=>{const a=t.target.closest("[data-tab]");if(a){let t=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(a.dataset.tab,t)}})),this.initializeChildTabs(),this.selectDropdown=document.querySelector("select.tab-list"),this.selectDropdown&&this.selectDropdown.addEventListener("change",(t=>{let a=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(t.target.value,a)}));let s=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.activeTab||(this.activeTab=document.querySelector("button.tab")?.dataset.tab),this.switchTab(this.activeTab,s)}initializeChildTabs(){this.tabs.querySelectorAll("button").forEach((t=>{let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`);if(a&&a.querySelector(".tabs")){let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`),e=new window.jvbTabs(a,{},this);this.childTabs.set(t.dataset.tab,e)}}))}getInitialTabFromHash(){if(!window.location.hash)return!1;const t=window.location.hash.substring(1).split("/");if(this.parent){if(this.parent&&t.length>1){const a=this.getParentDepth();if(a<t.length){const e=t[a];if(this.tabs.querySelector(`[data-tab="${e}"]`))return e}}}else{const a=t[0];if(this.tabs.querySelector(`[data-tab="${a}"]`))return a}return null}getParentDepth(){let t=0,a=this.parent;for(;a;)t++,a=a.parent;return t}getFullTabPath(t){return this.parent?`${this.parent.getFullTabPath(this.parent.activeTab)}/${t}`:t}switchTab(t,a=!0){if(document.activeElement?.blur(),this.tabs.querySelectorAll("[data-tab]").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-selected",a.dataset.tab===t)})),this.container.querySelectorAll(".tab-content").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-hidden",a.dataset.tab!==t),a.hidden=a.dataset.tab!==t})),this.activeTab=t,this.callbacks[t]&&this.callbacks[t](),a)if(this.parent)this.parent.updateUrlFromChild();else{let a=t;const e=this.childTabs.get(t);e&&e.activeTab&&(a=e.getFullTabPath(e.activeTab)),window.history.pushState({tab:a},"",`#${a}`)}this.selectDropdown&&this.selectDropdown.querySelector(`option[value="${t}"]`)&&(this.selectDropdown.value=t),this.a11y.announce(`Switched to ${t} tab`)}updateUrlFromChild(){if(!("updateURL"in this.callbacks)||this.callbacks.updateURL)if(console.log("UpdateUrlFromChild"),this.parent)this.parent.updateUrlFromChild();else{const t=this.getFullTabPath(this.activeTab);window.history.pushState({tab:t},"",`#${t}`)}}};
\ No newline at end of file
+window.jvbTabs=class{constructor(t,a={},e=null){this.tabs=t.querySelector(".tabs"),this.a11y=window.jvbA11y,this.updateURL=!0,this.parent=e,this.childTabs=new Map,"updateURL"in a&&!1===a.updateURL&&(this.updateURL=!1),this.callbacks=a,this.activeTab=this.updateURL?this.getInitialTabFromHash():t.querySelector("button.tab.active")?.dataset.tab,this.container=t,this.tabs&&this.tabs.addEventListener("click",(t=>{const a=t.target.closest("[data-tab]");if(a){let t=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(a.dataset.tab,t)}})),this.initializeChildTabs(),this.selectDropdown=document.querySelector("select.tab-list"),this.selectDropdown&&this.selectDropdown.addEventListener("change",(t=>{let a=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(t.target.value,a)}));let s=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.activeTab||(this.activeTab=document.querySelector("button.tab")?.dataset.tab),this.switchTab(this.activeTab,s)}initializeChildTabs(){this.tabs.querySelectorAll("button").forEach((t=>{let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`);if(a&&a.querySelector(".tabs")){let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`),e=new window.jvbTabs(a,{},this);this.childTabs.set(t.dataset.tab,e)}}))}getInitialTabFromHash(){if(!window.location.hash)return!1;const t=window.location.hash.substring(1).split("/");if(this.parent){if(this.parent&&t.length>1){const a=this.getParentDepth();if(a<t.length){const e=t[a];if(this.tabs.querySelector(`[data-tab="${e}"]`))return e}}}else{const a=t[0];if(this.tabs.querySelector(`[data-tab="${a}"]`))return a}return null}getParentDepth(){let t=0,a=this.parent;for(;a;)t++,a=a.parent;return t}getFullTabPath(t){return this.parent?`${this.parent.getFullTabPath(this.parent.activeTab)}/${t}`:t}switchTab(t,a=!1){if(document.activeElement?.blur(),this.tabs.querySelectorAll("[data-tab]").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-selected",a.dataset.tab===t)})),this.container.querySelectorAll(".tab-content").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-hidden",a.dataset.tab!==t),a.hidden=a.dataset.tab!==t})),this.activeTab=t,this.callbacks[t]&&this.callbacks[t](),a)if(this.parent)this.parent.updateUrlFromChild();else{let a=t;const e=this.childTabs.get(t);e&&e.activeTab&&(a=e.getFullTabPath(e.activeTab)),window.history.pushState({tab:a},"",`#${a}`)}this.selectDropdown&&this.selectDropdown.querySelector(`option[value="${t}"]`)&&(this.selectDropdown.value=t),this.a11y.announce(`Switched to ${t} tab`)}updateUrlFromChild(){if(console.log("Updating URL"),!("updateURL"in this.callbacks)||this.callbacks.updateURL)if(this.parent)this.parent.updateUrlFromChild();else{const t=this.getFullTabPath(this.activeTab);window.history.pushState({tab:t},"",`#${t}`)}}};
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 94f817d..2f76fac 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.notifications=window.jvbNotifications,this.initDB(),this.fields=new Map,this.uploads=new Map,this.uploadBlobs=new Map,this.timeouts=new Map,this.selected=new Map,this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}},this.touch={x:null,y:null},this.hasBulkContext=null!==document.querySelector("details.uploader"),this.isTouching=!1,this.groups=new Map,this.subscribers=new Set,this.settings={allowedTypes:["image/jpeg","image/png","image/gif","image/webp","image/avif"],maxFileSize:5242880,maxProcessingTime:12e4,processingCheckInterval:5e3,smartCompression:!0,fieldTypes:{single:{maxFiles:1,allowMultiple:!1},gallery:{maxFiles:20,allowMultiple:!0},groupable:{maxFiles:20,allowMultiple:!0}}},this.statusMapping={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"},this.init()}async init(){this.initElements(),this.initListeners(),this.initCompressionWorker(),this.queue.subscribe(((e,t)=>{if(console.log("Operation Endpoint: ",t.endpoint),"uploads"===t.endpoint)switch(e){case"cancel-operation":this.clearField(t.data.get("field_key"));break;case"operation-status":console.log("Operation Data: ",t.data);const e=t.data?.field_key||(t.data instanceof FormData?t.data.get("field_key"):null);e&&(console.log("Updating field status:",e,t.status),this.updateFieldStatus(e,t.status))}})),await this.checkPendingUploads(),this.scanFields()}initElements(){this.selectors={field:{field:".field.image",dropZone:".file-upload-container",preview:".item-grid.preview",hiddenValue:'input[type="hidden"]',progress:{progress:".progress",details:".progress .details",fill:".progress .fill",count:".progress .count"}},item:{img:"img",progress:{progress:".progress",details:".progress .details",fill:".progress .fill",count:".progress .count"},status:".status",select:'[name*="select-item"]',actions:".item-actions",featured:'[name="featured"]',meta:".upload-meta"},groups:{container:".item-grid.groups",display:".group-display",selectAll:"#select-all-uploads",actions:".selection-actions",info:".selection-controls .info",count:".selection-count",group:".upload-group",empty:".empty-group"}},this.ui={}}scanFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e,t={}){let a=e.dataset.uploader??this.determineKey(e);if(e.dataset.uploader=a,!this.fields.has(a)){let s=e.dataset.type,i=this.settings.fieldTypes[s]??this.settings.fieldTypes.single,r={key:a,name:e.dataset.field,ui:{},type:s,maxFiles:i.maxFiles,multiple:i.allowMultiple,content:e.dataset.content??e.closest("dialog")?.dataset.content??e.closest("form").dataset.save??!1,itemID:e.dataset.itemID??e.closest("dialog")?.dataset.itemID??!1,context:e.dataset.context??e.closest("dialog")?.dataset.context??!1,mode:e.dataset.mode??"direct",...t};r.ui=window.uiFromSelectors(this.selectors,e),r.ui.groups.groups=new Map,this.selected.set(a,new Set),this.fields.set(a,r),"groupable"!==r.type||this.hasGroups||this.initGroupListeners()}return a}determineKey(e){return`${e.dataset.content??e.closest("dialog")?.dataset.content??e.closest("form").dataset.save??""}_${e.dataset.itemID??e.closest("dialog")?.dataset.itemID??""}_${e.dataset.field}`}getFieldIdFromElement(e){let t=e.closest(".field.image");if(t)return t.dataset.uploader??this.determineKey(t)}getFieldFromElement(e){let t=this.getFieldIdFromElement(e);return!!this.fields.has(t)&&this.fields.get(t)}getUploadFromElement(e){let t=this.getUploadIdFromElement(e);return!!this.uploads.has(t)&&this.uploads.get(t)}getUploadIdFromElement(e){let t=e.closest("[data-upload-id]");return t?.dataset.uploadId||null}getGroupFromElement(e){let t=this.getGroupIdFromElement(e);return!!this.groups.has(t)&&this.groups.get(t)}getGroupIdFromElement(e){return e.dataset.groupId??e.closest("[data-group-id]")?.dataset.groupId??e.closest(":has([data-group-id])")?.querySelector("[data-group-id]")?.dataset.groupId??null}getModalType(e){if(!(e&&e.ui&&e.ui.field&&e.ui.field.field))return null;const t=e.ui.field.field.closest("dialog");return t?t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className:null}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(console.log("Getting status progress for: ",e),e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.hasBulkContext&&(this.pasteHandler=this.handlePaste.bind(this),document.addEventListener("paste",this.pasteHandler)),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),window.addEventListener("beforeunload",this.handleBeforeUnload.bind(this))}clearListeners(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),this.hasBulkContext&&document.removeEventListener("paste",this.pasteHandler)}initGroupListeners(){this.hasGroups=!0,this.dragStartHandler=this.handleDragStart.bind(this),this.dragEndHandler=this.handleDragEnd.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dropHandler=this.handleDrop.bind(this),this.touchStartHandler=this.handleTouchStart.bind(this),this.touchMoveHandler=this.handleTouchMove.bind(this),this.touchEndHandler=this.handleTouchEnd.bind(this),this.touchCancelHandler=this.handleTouchCancel.bind(this),document.addEventListener("dragstart",this.dragStartHandler),document.addEventListener("dragend",this.dragEndHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("drop",this.dropHandler),document.addEventListener("touchstart",this.touchStartHandler),document.addEventListener("touchmove",this.touchMoveHandler),document.addEventListener("touchend",this.touchEndHandler),document.addEventListener("touchcancel",this.touchCancelHandler)}clearGroupListeners(){document.removeEventListener("dragstart",this.dragStartHandler),document.removeEventListener("dragend",this.dragEndHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("drop",this.dropHandler),document.removeEventListener("touchstart",this.touchStartHandler),document.removeEventListener("touchmove",this.touchMoveHandler),document.removeEventListener("touchend",this.touchEndHandler),document.removeEventListener("touchcancel",this.touchCancelHandler)}handleClick(e){if(e.target.closest(this.selectors.field.field))if(window.targetCheck(e,".restart-uploads")){e.preventDefault();const t=this.getFieldIdFromElement(e.target);this.restartUploads(t)}else if(window.targetCheck(e,".dismiss-cache-restore")){e.preventDefault();const t=e.target.closest(".upload-recovery-notification");t&&t.remove()}else if(window.targetCheck(e,"#select-all-uploads"))e.preventDefault(),this.handleSelectAll(e.target);else if(window.targetCheck(e,".upload-select")){e.shiftKey&&this.lastClickedUpload?(e.preventDefault(),this.handleRangeSelection(e.target,e)):this.updateSelection(e)}else if(window.targetCheck(e,".create-from-selection")){e.preventDefault();let t=this.createGroup(this.getFieldFromElement(e.target));this.addSelectionToGroup(t)}else if(window.targetCheck(e,".remove-selection"))e.preventDefault(),this.removeSelection(e.target);else if(window.targetCheck(e,".add-to-group, .add-selection-to-group"))e.preventDefault(),this.addSelectionToGroup(e.target);else if(window.targetCheck(e,".remove-group")){e.preventDefault();const t=e.target.closest(".upload-group");if(t){this.getFieldFromElement(t);this.removeGroup(t,!0)}}else if(window.targetCheck(e,".remove")){e.preventDefault();const t=this.getUploadIdFromElement(e.target),a=this.getFieldIdFromElement(e.target);t&&a&&this.removeUpload(a,t)}else if(window.targetCheck(e,".submit-uploads")){e.preventDefault();const t=this.getFieldIdFromElement(e.target);this.submitUploads(t)}else if(window.targetCheck(e,".retry-upload")){e.preventDefault();const t=this.getUploadIdFromElement(e.target);this.retryUpload(t)}}handleChange(e){if(e.target.closest(this.selectors.field.field)&&!e.target.classList.contains(this.selectors.field.hiddenValue))if(e.preventDefault(),window.targetCheck(e,'[type="file"]')){console.log(this.fields);let t=this.getFieldFromElement(e.target);if(console.log(t),!t)return void console.warn("File change on unregistered field: ",t.key);const a=Array.from(e.target.files);if(0===a.length)return;this.processFiles(t.key,a),e.target.value=""}else if(e.target.name.includes("select-"))this.updateSelection(e);else if(e.target.closest(".upload-meta")){e.preventDefault();let t=e.target.name,a=e.target.value,s=this.getUploadFromElement(e.target);s.changes[t]=a,this.uploads.set(s.id,s),this.persistFieldState(s.fieldId)}else if(e.target.closest(".group.fields")){let t=this.getGroupFromElement(e.target),a=e.target.name;t.changes[a]=e.target.value,this.persistFieldState(t.fieldId),this.groups.set(t.id,t)}}handlePaste(e){window.debouncer.schedule("imagePaste",(()=>{const t=Array.from(e.clipboardData.items).filter((e=>e.type.startsWith("image/")));if(0===t.length)return;e.preventDefault();const a=this.getFieldIdFromElement(e.target);if(!a)return;const s=[];t.forEach(((e,t)=>{const a=e.getAsFile();if(a){const e=new File([a],`pasted_image_${t+1}.png`,{type:a.type,lastModified:Date.now()});s.push(e)}})),s.length>0&&this.processFiles(a,s)}),100)}isTouchOnFormElement(e){return["input","button","label","select","textarea"].some((t=>e.matches(t)||e.closest(t)))}startDragOperation(e){const{primaryElement:t,sourceType:a,startPosition:s,event:i}=e,r=this.getUploadIdFromElement(t),o=this.getFieldIdFromElement(t),n=this.getDraggedItems(t);this.dragState={primaryItem:r,draggedItems:n,isDragging:!0,isMultiDrag:n.length>1,fieldId:o,sourceType:a,startTime:Date.now(),startPosition:s,currentPosition:s,currentTarget:null,validTarget:null,dragPreview:null,touchId:"touch"===a?i.touches[0]?.identifier:null,touchMoved:!1},this.createDragPreview(t),this.applyDraggingState(!0);const l=this.dragState.isMultiDrag?`Started dragging ${n.length} items`:"Started dragging item";return this.a11y.announce(l),this.provideDragFeedback("start"),!0}updateDragOperation(e,t){if(!this.dragState.isDragging)return;const{sourceType:a,startPosition:s}=this.dragState;if(this.dragState.currentPosition=e,"touch"===a&&!this.dragState.touchMoved){const t=Math.abs(e.x-s.x),a=Math.abs(e.y-s.y);(t>10||a>10)&&(this.dragState.touchMoved=!0)}this.updateDragPreview(e),this.updateDropTarget(t)}endDragOperation(e=null){if(!this.dragState.isDragging)return;const t=("drag"===this.dragState.sourceType||this.dragState.touchMoved)&&this.dragState.validTarget;t&&this.dragState.validTarget&&this.processItemDrop({itemIds:this.dragState.draggedItems,targetElement:this.dragState.validTarget,fieldId:this.dragState.fieldId,dropType:this.dragState.isMultiDrag?"multiple":"single",sourceType:this.dragState.sourceType}),this.cleanupDragOperation();const a=t?this.dragState.isMultiDrag?`Moved ${this.dragState.draggedItems.length} items`:"Item moved":"Drag cancelled";this.a11y.announce(a)}processItemDrop(e){const{itemIds:t,targetElement:a,fieldId:s,dropType:i,sourceType:r}=e;if(!t?.length||!a||!s)return!1;let o=a.classList.contains("item-grid")&&a.classList.contains("preview"),n=a;if(a.classList.contains("empty-group")){let e=this.createGroup(s);n=e.querySelector(".item-grid"),o=!1}if(t.forEach((e=>{this.addImageToGroup(e,n,o)})),"multiple"===i){const e=this.fields.get(s);this.clearAllSelections(e)}const l="multiple"===i?`Moved ${t.length} images to ${o?"main area":"group"}`:"Image moved to "+(o?"main area":"group");return this.a11y.announce(l),this.provideFeedback(r,"success",{count:t.length,isMultiple:"multiple"===i}),!0}clearAllSelections(e){if(e.container.querySelectorAll('[name*="select-item"]').forEach((e=>{e.checked=!1})),e.selectAll){e.selectAll.checked=!1;const t=e.selectAll.nextElementSibling;t&&(t.textContent="Select All")}e.selectActions&&(e.selectActions.hidden=!0),e.selectInfo&&(e.selectInfo.hidden=!0)}cleanupDragOperation(){this.dragState.dragPreview&&this.dragState.dragPreview.remove(),this.applyDraggingState(!1),this.clearDropTargetStates(),this.dragState.isDragging=!1,this.dragState.dragPreview=null,this.dragState.draggedItems=[]}getDraggedItems(e){const t=this.getSelectedUploads(e),a=e.dataset.uploadId;return t.length>1&&t.includes(a)?t:[a]}applyDraggingState(e){this.dragState.draggedItems.forEach((t=>{const a=document.querySelector(`[data-upload-id="${t}"]`);a&&a.classList.toggle("dragging",e)}))}createDragPreview(e){const{isMultiDrag:t,draggedItems:a}=this.dragState;this.dragState.dragPreview=t?this.createMultiDragPreview(e,a):this.createSingleDragPreview(e),this.updateDragPreview(this.dragState.startPosition),document.body.appendChild(this.dragState.dragPreview)}createSingleDragPreview(e){const t=e.cloneNode(!0);return t.dataset.uploadId=t.dataset.uploadId+"-dragging",this.styleDragPreview(t,!1),t}styleDragPreview(e,t=!1){e.style.cssText=`\n        position: fixed;\n        z-index: 10000;\n        pointer-events: none;\n        opacity: 0.9;\n        transform: scale(1.05);\n        transition: transform 0.2s ease;\n        ${t?"\n            width: 120px;\n            height: 120px;\n            background: white;\n            border-radius: 8px;\n            box-shadow: 0 8px 32px rgba(0,0,0,0.3);\n            padding: 4px;\n        ":"\n            border-radius: 4px;\n            box-shadow: 0 4px 16px rgba(0,0,0,0.2);\n        "}\n    `,e.classList.add("drag-preview","is-dragging"),t&&e.classList.add("multi-item")}createMultiDragPreview(e,t){const a=document.createElement("div");a.className="drag-preview multi-item";const s=Math.min(t.length,3);for(let e=0;e<s;e++){const s=t[e],i=document.querySelector(`[data-upload-id="${s}"]`);if(i){const t=i.cloneNode(!0);t.dataset.uploadId=s+"_dragging",t.style.cssText=`\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: ${4*e}px;\n\t\t\t\tleft: ${4*e}px;\n\t\t\t\twidth: calc(100% - ${4*e}px);\n\t\t\t\theight: calc(100% - ${4*e}px);\n\t\t\t\topacity: ${1-.15*e};\n\t\t\t\ttransform: rotate(${2*(e-1)}deg);\n\t\t\t\tz-index: ${10-e};\n\t\t\t\tborder-radius: 4px;\n\t\t\t\toverflow: hidden;\n\t\t\t`,a.appendChild(t)}}if(t.length>1){const e=this.createCountBadge(t.length);a.appendChild(e)}return this.styleDragPreview(a,!0),a}updateDragPreview(e){if(!this.dragState.dragPreview)return;let t;t="touch"===this.dragState.sourceType?this.dragState.isMultiDrag?{x:-60,y:-80}:{x:-50,y:-60}:this.dragState.isMultiDrag?{x:15,y:15}:{x:10,y:10};const a=e.x-this.dragState.startPosition.x,s=e.y-this.dragState.startPosition.y;this.dragState.dragPreview.style.transform=`translate(${a+t.x}px, ${s+t.y}px) scale(1.05)`}updateDropTarget(e){this.dragState.currentTarget&&this.clearDropTargetState(this.dragState.currentTarget);const t=this.findValidDropTarget(e);if(this.dragState.currentTarget=e,this.dragState.validTarget=t,t&&(this.applyDropTargetState(t),"touch"===this.dragState.sourceType&&navigator.vibrate)){const e=this.dragState.isMultiDrag?[25,10,25]:[25];navigator.vibrate(e)}}findValidDropTarget(e){if(!e)return null;const t=e.closest(".item-grid.group, .empty-group, .item-grid.preview");if(t){if(this.getFieldIdFromElement(t)===this.dragState.fieldId)return t}return null}applyDropTargetState(e){e.classList.add("dragover"),this.dragState.isMultiDrag&&(e.classList.add("multi-drop"),e.setAttribute("data-item-count",this.dragState.draggedItems.length))}clearDropTargetState(e){e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}clearDropTargetStates(){document.querySelectorAll(".dragover").forEach((e=>{e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}))}createCountBadge(e){const t=document.createElement("div");return t.className="selection-count-badge",t.textContent=e.toString(),t.style.cssText="\n\t\t\tposition: absolute;\n\t\t\ttop: -8px;\n\t\t\tright: -8px;\n\t\t\tbackground: var(--accent-primary);\n\t\t\tcolor: white;\n\t\t\tborder-radius: 50%;\n\t\t\twidth: 24px;\n\t\t\theight: 24px;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 12px;\n\t\t\tfont-weight: bold;\n\t\t\tbox-shadow: 0 2px 8px rgba(0,0,0,0.3);\n\t\t\tz-index: 20;\n\t\t",t}provideDragFeedback(e){const t={start:[50],success:this.dragState.isMultiDrag?[50,25,50,25,50]:[50,25,50],cancel:[100]};"touch"===this.dragState.sourceType&&navigator.vibrate&&t[e]&&navigator.vibrate(t[e])}provideFeedback(e,t,a={}){const s={success:a.isMultiple?[50,25,50,25,50]:[50,25,50],error:[100,50,100]};"touch"===e&&navigator.vibrate&&s[t]&&navigator.vibrate(s[t])}clearDragoverStates(){document.querySelectorAll(".dragover").forEach((e=>{e.classList.remove("dragover","multi-drop"),e.removeAttribute("data-item-count")}))}handleDragEnter(e){if(window.targetCheck(e,".image.field")&&e.dataTransfer.types.includes("Files")){e.preventDefault();const t=e.target.closest(".file-upload-container");t&&t.classList.add("dragover")}}handleDragLeave(e){if(!window.targetCheck(e,".image.field"))return;const t=e.target.closest(".file-upload-container");t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragStart(e){if(!window.targetCheck(e,".image.field"))return;const t=e.target.closest("[data-upload-id]");if(!t)return;this.startDragOperation({primaryElement:t,sourceType:"drag",startPosition:{x:e.clientX,y:e.clientY},event:e})?(e.dataTransfer.setData("text/plain",this.dragState.primaryItem),e.dataTransfer.effectAllowed="move"):e.preventDefault()}handleDragOver(e){this.dragState.isDragging&&window.targetCheck(e,".image.field")&&(e.preventDefault(),this.updateDragOperation({x:e.clientX,y:e.clientY},e.target))}handleDrop(e){if(!window.targetCheck(e,".image.field"))return;e.preventDefault(),this.clearDragoverStates();const t=e.target.closest(".file-upload-container");if(t){const a=Array.from(e.dataTransfer.files);if(a.length>0){const e=this.getFieldIdFromElement(t);e&&(this.processFiles(e,a),this.a11y.announce(`${a.length} file(s) dropped for upload`))}}}handleDragEnd(e){if(!this.dragState.isDragging)return;const t=document.elementFromPoint(this.dragState.currentPosition?.x||e.clientX,this.dragState.currentPosition?.y||e.clientY);this.endDragOperation(t)}handleTouchStart(e){if(!window.targetCheck(e,".image.field"))return;if(this.isTouchOnFormElement(e.target))return;const t=e.target.closest("[data-upload-id]");if(!t)return;const a=e.touches[0];this.startDragOperation({primaryElement:t,sourceType:"touch",startPosition:{x:a.clientX,y:a.clientY},event:e})&&e.preventDefault()}handleTouchMove(e){if(!this.dragState.isDragging)return;e.preventDefault();const t=e.touches[0],a=document.elementFromPoint(t.clientX,t.clientY);this.updateDragOperation({x:t.clientX,y:t.clientY},a)}handleTouchEnd(e){if(!this.dragState.isDragging)return;e.preventDefault();const t=e.changedTouches[0],a=document.elementFromPoint(t.clientX,t.clientY);this.endDragOperation(a)}handleTouchCancel(e){this.dragState.isDragging&&(this.cleanupDragOperation(),this.a11y.announce("Drag cancelled"))}async submitUploads(e){const t=this.fields.get(e);if(!t)return;const a=Array.from(t.uploads||[]).map((e=>this.uploads.get(e))).filter((e=>e&&("processed"===e.status||"processed-original"===e.status)));if(0!==a.length)try{await this.queueUpload(e),this.notifications.add(`Submitting ${a.length} upload(s)`,"info")}catch(t){this.error.log(t,{component:"UploadManager",action:"submitUploads",fieldId:e}),this.notifications.add("Failed to submit uploads","error")}else this.notifications.add("No uploads ready to submit","warning")}async retryUpload(e){const t=this.uploads.get(e);if(!t)return;if(this.fields.get(t.fieldId))try{if(this.updateUploadStatus(e,"received"),t.processedFile)this.updateUploadStatus(e,"processed"),await this.queueUpload(t.fieldId);else{if(!t.originalFile)throw new Error("No file data available for retry");await this.processFile(t.fieldId,t.originalFile)&&await this.queueUpload(t.fieldId)}this.notifications.add("Retrying upload...","info")}catch(t){this.error.log(t,{component:"UploadManager",action:"retryUpload",uploadId:e}),this.notifications.add("Failed to retry upload","error")}}async restartUploads(e){const t=this.fields.get(e);if(!t?.uploads)return;const a=Array.from(t.uploads).map((e=>this.uploads.get(e))).filter((e=>e&&"failed"===e.status));if(0!==a.length){for(const e of a)await this.retryUpload(e.id);this.notifications.add(`Restarting ${a.length} upload(s)`,"info")}else this.notifications.add("No failed uploads to restart","info")}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const a=Array.from(t.uploads);if(0===a.length)return;const s=this.prepareUploadData(t,a);this.a11y.announce("Queuing for upload");let i=1===a.length?"image":"images";const r={endpoint:"uploads",method:"POST",data:s,title:`Uploading ${a.length} ${i} to server...`,popup:`Uploading ${a.length} ${i}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(r);return a.forEach((t=>{let a=this.uploads.get(t);a&&(a.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.persistFieldState(t.key)}}prepareUploadData(e,t){console.log("Preparing Upload:",e);const a=new FormData;a.append("content",e.content),a.append("mode",e.mode),a.append("field_name",e.name),a.append("field_key",e.key),a.append("field_type",e.type),a.append("item_id",e.itemID),a.append("context",e.context);let s=[];t.forEach((e=>{let t=this.uploads.get(e);if(t){const i=t.processedFile||t.originalFile;i?(a.append("files[]",i),s.push(t.id)):console.warn(`No file for upload ${e}`)}else console.warn(`Upload ${e} not found in uploads map`)})),a.append("upload_map",s),console.log("Final FormData:");for(let e of a.entries())console.log(e[0],e[1]);return a}async queueImageMeta(e){const t=this.getUploadFromElement(element);if(!t)return;const a=this.fields.get(t.fieldId);if(!a)return;const s=element.closest(".upload-meta");if(!s)return;const i={title:s.querySelector('[name="title"]')?.value||"",alt_text:s.querySelector('[name="alt_text"]')?.value||"",caption:s.querySelector('[name="caption"]')?.value||"",description:s.querySelector('[name="description"]')?.value||""};t.meta={...t.meta,...i},this.uploads.set(t.id,t),this.hasMetaChanges=!0;"completed"===t.status&&t.attachmentId?await this.sendMetaUpdate(t):t.operationId?this.queueDependentMetaUpdate(t):this.persistFieldState(a.key)}async sendMetaUpdate(e){const t=new FormData;t.append("attachment_id",e.attachmentId),t.append("title",e.meta.title),t.append("alt_text",e.meta.alt_text),t.append("caption",e.meta.caption),t.append("description",e.meta.description);const a={endpoint:"uploads/meta",method:"POST",data:t,title:`Updating metadata for ${e.meta.originalName}`,canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(a),this.notifications.add("Metadata updated","success")}catch(t){this.error.log(t,{component:"UploadManager",action:"sendMetaUpdate",uploadId:e.id})}}queueDependentMetaUpdate(e){const t={endpoint:"uploads/meta",method:"POST",dependencies:[e.operationId],data:()=>{const t=new FormData;return t.append("operation_id",e.operationId),t.append("upload_id",e.id),t.append("title",e.meta.title),t.append("alt_text",e.meta.alt_text),t.append("caption",e.meta.caption),t.append("description",e.meta.description),t},title:"Updating metadata after upload",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};this.queue.addToQueue(t)}async processFiles(e,t){const a=this.fields.get(e);if(!a)return;const s=t.filter((e=>this.validateFile(e,a)));if(0===s.length)return;if(!this.checkFieldLimits(e,s.length))return;const i=await this.processBatch(e,s);this.maybeLockUploads(e),a.groupDisplay&&(a.groupDisplay.hidden=!1),i.length>0&&await this.queueUpload(e),this.hideUploadProgress(e),this.a11y.announce(`Processed ${i.length} of ${s.length} files`)}checkFieldLimits(e,t){const a=this.fields.get(e);if(!a)return!1;const s=a.uploads?.size||0;return!(s+t>a.maxFiles)||(this.notifications.add(`Cannot add ${t} files. Max ${a.maxFiles} allowed, currently have ${s}.`,"warning"),!1)}generateUploadId(){return`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const a=t<0?0:t,s=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,s)).toFixed(a))+" "+["Bytes","KB","MB","GB"][s]}async processBatch(e,t){const a=[],s=[],i=this.worker.settings.maxConcurrent;let r=t.length;for(let o=0;o<t.length;o++){this.updateUploadProgress(e,o,r),s.length>=i&&await Promise.race(s);const n=this.processFile(e,t[o]).then((e=>{const t=s.indexOf(n);return t>-1&&s.splice(t,1),e&&a.push(e),e})).catch((e=>{console.error(`Failed to process ${t[o].name}:`,e);const a=s.indexOf(n);return a>-1&&s.splice(a,1),null}));s.push(n)}return await Promise.all(s),a}async processFile(e,t){const a=this.fields.get(e),s=await this.setUpload(e,t),i=s.id;try{this.addImageToGroup(i),this.updateUploadStatus(i,"local_processing");let e=null,r=!1;try{e=await this.processImage(t,i)}catch(a){console.warn(`Processing failed for ${t.name}, using original:`,a),r=!0,e=t}s.processedFile=e,s.processingFailed=r,this.updateUploadStatus(i,"processed"),this.uploads.set(i,s),a&&a.key&&await this.persistFieldState(a.key);const o=r?`${t.name} added (original format)`:`${t.name} processed and ready`;return this.a11y.announce(o),s}catch(e){return this.cleanupFailedUpload(i,a.key),this.error.log(e,{component:"UploadManager",action:"processFile",uploadId:i,fileName:t.name}),null}}async processImage(e,t){const a=this.worker.settings.timeout;return new Promise(((s,i)=>{let r,o=!1;r=setTimeout((()=>{o||(o=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),i(new Error(`Processing timeout for ${e.name}`)))}),a),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{o||(o=!0,clearTimeout(r),this.worker.tasks.delete(t),s(e))})).catch((e=>{o||(o=!0,clearTimeout(r),this.worker.tasks.delete(t),i(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const a=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,a,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,a,.85)}async processOnMainThread(e,t,a){return new Promise(((s,i)=>{const r=new Image,o=document.createElement("canvas"),n=o.getContext("2d");let l=null;const d=()=>{r.onload=null,r.onerror=null,l&&(URL.revokeObjectURL(l),l=null),o.width=1,o.height=1,n.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(r,t);o.width=l,o.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(r,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,a);o.toBlob((t=>{if(d(),t){const a=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});s(a)}else i(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),i(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),i(new Error(`Failed to load image: ${e.name}`))};try{l=URL.createObjectURL(e),r.src=l}catch(e){d(),i(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,a,s){return new Promise(((i,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const o=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===o)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),t.data.success){const a=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});i(a)}else r(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:o,file:e,maxDimension:a,quality:s,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){console.log("Restarting compression worker..."),this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n            self.onmessage = async function(e) {\n                const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n                try {\n                    // Create ImageBitmap from file\n                    const bitmap = await createImageBitmap(file);\n\n                    // Calculate dimensions\n                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n                    const width = Math.round(bitmap.width * scale);\n                    const height = Math.round(bitmap.height * scale);\n\n                    // Create OffscreenCanvas\n                    const canvas = new OffscreenCanvas(width, height);\n                    const ctx = canvas.getContext('2d');\n\n                    // Draw and resize\n                    ctx.imageSmoothingEnabled = true;\n                    ctx.imageSmoothingQuality = 'high';\n                    ctx.drawImage(bitmap, 0, 0, width, height);\n\n                    // Clean up bitmap\n                    bitmap.close();\n\n                    // Convert to blob\n                    const blob = await canvas.convertToBlob({\n                        type: outputFormat,\n                        quality: quality\n                    });\n\n                    self.postMessage({\n                        messageId,\n                        success: true,\n                        blob: blob,\n                        format: outputFormat\n                    });\n\n                } catch (error) {\n                    self.postMessage({\n                        messageId,\n                        success: false,\n                        error: error.message\n                    });\n                }\n            };\n        "],{type:"application/javascript"});this.worker.worker=new Worker(URL.createObjectURL(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:a,height:s}=e;if(a<=t&&s<=t)return{width:a,height:s};const i=Math.min(t/a,t/s);return{width:Math.round(a*i),height:Math.round(s*i)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}cleanupFailedUpload(e,t){const a=this.fields.get(t);a?.uploads&&a.uploads.delete(e);const s=this.uploads.get(e);s&&(s.preview?.startsWith("blob:")&&URL.revokeObjectURL(s.preview),s.element?.remove(),this.uploads.delete(e)),this.worker.tasks.delete(e)}updateUploadStatus(e,t){console.log("Updating upload status for: ",e);let a=this.uploads.get(e);a&&(a.status=t,this.updateImageUI(a.id),this.persistFieldState(a.fieldId))}updateImageUI(e){console.log("Updating image UI: ",e);const t=this.uploads.get(e);if(console.log(t),!t?.element)return;const a=t.element.querySelector(".progress"),s=t.element;if(console.log("Updating Upload UI:",t),s&&(s.className=s.className.replace(/status-[\w-]+/g,""),s.classList.add(`status-${t.status}`)),a){let e=this.getStatusIcon(t.status),s=this.getStatusText(t.status),i=this.getStatusProgress(t.status);const r=a.querySelector(".fill"),o=a.querySelector("span.icon"),n=a.querySelector("span.details");r&&(r.style.width=`${i}%`),n&&(n.textContent=s),o&&(window.removeChildren(o),o.append(e)),"completed"===t.status&&setTimeout((()=>{a&&window.fade(a,!1)}),1e3)}}maybeLockUploads(e){const t=this.fields.get(e);t&&t.ui.field.dropZone&&(t.ui.field.dropZone.hidden=t.uploads&&t.uploads.size>=t.maxFiles)}createImageElement(e,t=!1){let a=window.getTemplate("uploadItem");if(!a)return void console.error("Image template not found");a.dataset.uploadId=e.id,a.querySelector('[name="featured"]').value=e.id;let[s,i,r]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("details")];if([s.value,i.src,i.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],r){let e=window.getTemplate("uploadMeta");e&&r.append(e)}return a.draggable=t,a.querySelectorAll("input").forEach((t=>{let a=t.id;if(a){let s=a+e.id,i=t.parentNode.querySelector(`label[for="${a}"]`);t.id=s,i&&(i.htmlFor=s)}})),a}updateUploadProgress(e,t,a,s){const i=this.fields.get(e);if(!i)return;let r=i.ui.field.progress.progress;if(!r){r=window.getTemplate("imageProgress");const e=i.dropZone||i.container.firstElementChild;e?e.insertAdjacentElement("afterend",r):i.container.prepend(r)}const o=a>0?Math.round(t/a*100):0,n=i.ui.field.progress.fill,l=i.ui.field.progress.details,d=i.ui.field.progress.count;n&&(n.style.width=`${o}%`),l&&(l.textContent=s),d&&(d.textContent=`${t}/${a}`),t===a&&r.classList.add("completed")}hideUploadProgress(e){const t=this.fields.get(e);if(!t)return;const a=t.ui.field.progress.progress;a&&window.fade(a,!1)}async initDB(){if(!("indexedDB"in window))return;const e=indexedDB.open("jvb_uploads_db",1);e.onupgradeneeded=e=>{const t=e.target.result;if(!t.objectStoreNames.contains("fieldStates")){const e=t.createObjectStore("fieldStates",{keyPath:"fieldId"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("content","content",{unique:!1}),e.createIndex("itemId","itemId",{unique:!1})}t.objectStoreNames.contains("uploadBlobs")||t.createObjectStore("uploadBlobs",{keyPath:"uploadId"})},e.onsuccess=e=>{this.db=e.target.result,this.loadFields()},e.onerror=e=>{console.error("IndexedDB error:",e)}}async loadFields(){if(this.db)return new Promise((e=>{const t=this.db.transaction(["fieldStates","uploadBlobs"],"readonly"),a=t.objectStore("fieldStates"),s=t.objectStore("uploadBlobs");a.getAll().onsuccess=t=>{t.target.result.forEach((e=>{let t=e.uploads,a=t.map((e=>e.id));e.uploads=new Set(a),this.fields.set(e.key,e),t.forEach((e=>{this.uploads.set(e.id,e)}))})),this.notify("uploads-loaded",{items:Array.from(this.uploads.values())}),e()};s.getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.uploadBlobs.set(e.id,e)})),this.notify("blobs-loaded",{items:Array.from(this.uploadBlobs.values())}),e()}}))}getUpload(e){return this.uploads.get(e)}clearField(e){let t=Array.from(this.fields.uploads);if(t.forEach((e=>{this.uploads.delete(e)})),this.fields.delete(e),this.db){const a=this.db.transaction(["fieldStates","uploadBlobs"],"readwrite");a.objectStore("fieldStates").delete(e),t.forEach((e=>{a.objectStore("uploadBlobs").delete(e)}))}}updateFieldStatus(e,t){const a=this.fields.get(e);if(!a)return;a.uploads.forEach((e=>{console.log("Attempting to set upload to status: ",t),this.updateUploadStatus(e,t)}));const s=a.ui.field.field;if(s){s.dataset.uploadStatus=t;const e=s.querySelector(".submit-uploads");e&&(e.disabled="uploading"===t||"processing"===t)}}handleUploadComplete(e){const t=e.response;if(!t?.uploads)return;t.uploads.forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,this.updateUploadStatus(e.upload_id,"completed"),this.uploads.set(t.id,t),this.clearUpload(t.id))}));const a=e.data.get("field_key");a&&this.persistFieldState(a)}async clearUpload(e){const t=this.uploads.get(e);if(t&&(t.preview?.startsWith("blob:")&&URL.revokeObjectURL(t.preview),this.persistFieldState(t.fieldId),this.uploads.delete(e),this.uploadBlobs.delete(e),this.db)){const t=this.db.transaction(["uploadBlobs"],"readwrite");await t.objectStore("uploadBlobs").delete(e)}}async setUpload(e,t,a=null){a||(a=this.generateUploadId());const s={id:a,fieldId:e,groupId:null,originalFile:t,processedFile:null,status:"received",progress:{percent:0,message:"Received..."},preview:URL.createObjectURL(t),createdAt:Date.now(),meta:{title:"",alt_text:"",caption:"",originalName:t.name,originalType:t.type,originalSize:t.size},changes:{}},i=this.fields.get(e);return i?(i.uploads||(i.uploads=new Set),i.uploads.add(a),s.element=this.createImageElement(s,"groupable"===i.type),s.ui=window.uiFromSelectors(this.selectors.item,s.element),this.uploads.set(a,s),this.updateImageUI(a),await this.persistFieldState(e),s):(console.error(`Field ${e} not found`),null)}getFieldUploads(e,t){const a=this.fields.get(e);return console.log("Got field uploads: ",a),a?.uploads?Array.from(a.uploads).map((e=>{let a=this.uploads.get(e);if(!a)return null;if(t){const{element:e,ui:t,...s}=a;a=s}return a})).filter(Boolean):[]}async persistFieldState(e){if(!this.db)return;const t=this.fields.get(e);if(!t)return;const{ui:a,container:s,dropZone:i,previewGrid:r,selectAll:o,selectActions:n,selectInfo:l,selectCount:d,groupDisplay:c,...u}=t,p={fieldId:e,timestamp:Date.now(),config:{key:u.key,id:u.id,name:u.name,type:u.type,content:u.content,itemID:u.itemID,context:u.context,mode:u.mode,maxFiles:u.maxFiles,multiple:u.multiple},context:{url:window.location.href,modalType:this.getModalType(t),formId:t.formId},uploads:this.getFieldUploads(e,!0),groups:Array.from(this.groups.entries()).filter((([t,a])=>a.fieldId===e&&a.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),meta:t.meta,changes:t.changes})))},h=this.db.transaction(["fieldStates"],"readwrite");await h.objectStore("fieldStates").put(p)}async checkPendingUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const a=e.getAll();a.onsuccess=()=>t(a.result)}))).filter((e=>e.uploads.some((e=>"processing"===e.status||"processed"===e.status||"pending"===e.status))));0!==t.length&&this.showRecoveryNotification(t)}showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0);let a=window.getTemplate("restoreNotification");[a.querySelector(".restore-details").textContent]=[`${t} upload(s) from ${e.length} field(s) can be recovered.`],e.forEach((e=>{console.log(e);let t=window.getTemplate("restoreField");e.uploads.forEach((e=>{let a=window.getTemplate("restoreItem");[a.querySelector("img").src]=[e.preview],t.append(a)})),a.append(t)})),a.querySelector('[data-action="restore"]').addEventListener("click",(()=>{this.restoreFieldStates(e),a.remove()})),a.querySelector('[data-action="dismiss"]').addEventListener("click",(()=>{this.notifications.add("Uploads saved for later restoration","info"),a.remove()})),a.querySelector('[data-action="clear"]').addEventListener("click",(()=>{this.clearCachedUploads(e),a.remove()})),document.body.appendChild(a)}async restoreFieldStates(e){const t=new Map;if(e.forEach((e=>{t.has(e.context.url)||t.set(e.context.url,[]),t.get(e.context.url).push(e)})),1===t.size&&t.has(window.location.href)){for(const t of e)await this.restoreField(t);this.notifications.add(`Restored ${e.length} field(s)`,"success")}else{sessionStorage.setItem("jvb_restore_uploads",JSON.stringify(e));const a=t.keys().next().value;window.location.href!==a&&(window.location.href=a)}}async restoreField(e){const{config:t,context:a,uploads:s,groups:i}=e;a.modalType&&await this.openModalForRestore(a);const r=document.querySelector(`.field.image[data-field-id="${t.id}"]`);if(!r)return void console.warn(`Field ${t.id} not found for restoration`);const o=this.registerUploader(r,t),n=this.fields.get(o);for(const e of s)await this.restoreUpload(n,e);i&&i.length>0&&await this.restoreGroups(n,i,s),this.maybeLockUploads(o),"direct"===t.mode&&await this.queueUpload(o)}async restoreUpload(e,t){const a=await this.getBlobData(t.id);let s=null;a&&(s=new File([a.data],a.name,{type:a.type,lastModified:a.lastModified}),t.processedFile=s),e.uploads||(e.uploads=new Set),e.uploads.add(t.id),t.element=this.createImageElement(t,"groupable"===e.type);const i=t.groupId?e.ui.groups.groups.get(t.groupId):e.ui.field.preview;i&&(i.append(t.element),t.location=i),this.uploads.set(t.id,t)}async restoreGroups(e,t,a){for(const s of t){const t=this.createGroupElement(s.id,e.key);e.ui.groups.groups.set(s.id,t),e.ui.groups.container.insertBefore(t,e.ui.groups.empty);const i=new Set(s.uploadIds);this.groups.set(s.id,i),s.meta&&this.groupsMeta.set(s.id,s.meta),s.uploadIds.forEach((e=>{const i=a.find((t=>t.id===e));i&&i.element&&(t.querySelector(".item-grid").append(i.element),i.location=t.querySelector(".item-grid"),i.groupId=s.id)}))}}async getBlobData(e){if(!this.db)return null;const t=this.db.transaction(["uploadBlobs"],"readonly").objectStore("uploadBlobs").get(e);return new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}))}async openModalForRestore(e){const{modalType:t,formId:a}=e;let s=null;switch(t){case"create":s=document.querySelector('[data-action="create"]');break;case"edit":s=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":s=document.querySelector('[data-action="bulk-edit"]')}s&&(s.click(),await new Promise((e=>setTimeout(e,300))))}async clearCachedUploads(e){if(!this.db)return;const t=this.db.transaction(["fieldStates","uploadBlobs"],"readwrite");for(const a of e){await t.objectStore("fieldStates").delete(a.fieldId);for(const e of a.uploads)await t.objectStore("uploadBlobs").delete(e.id),e.preview?.startsWith("blob:")&&URL.revokeObjectURL(e.preview)}this.notifications.add("Cached uploads cleared","info")}async checkRestorationIntent(){const e=sessionStorage.getItem("jvb_restore_uploads");if(!e)return;const t=JSON.parse(e),a=t.filter((e=>e.context.url===window.location.href));if(a.length>0){for(const e of a)await this.restoreField(e);const e=t.filter((e=>e.context.url!==window.location.href));e.length>0?sessionStorage.setItem("jvb_restore_uploads",JSON.stringify(e)):sessionStorage.removeItem("jvb_restore_uploads"),this.notifications.add(`Restored ${a.length} field(s)`,"success")}}addImageToGroup(e,t=null,a=!0){let s=this.getUpload(e);if(!s)return;let i=this.fields.get(s.fieldId);if(i&&(t||s.location!==i.ui.field.preview)&&t!==s.location){if(s.location){let t=s.location.dataset.groupId;if(t){let a=this.groups.get(t);a&&(a.delete(e),0===a.size&&this.removeGroup(t))}}if(s.element.querySelector('[name="featured"]').hidden=!t,t){let a=t.dataset.groupId,i=this.groups.get(a);i||(i=this.createGroup(s.fieldId)),i.uploads.add(e)}else t=i.ui.field.preview;t.append(s.element),a&&this.persistFieldState(i.key)}}addSelectionToGroup(e){let t=this.getFieldFromElement(e);if(!t)return;if(0===this.selected.get(t.key).size)return;let a=this.getGroupFromElement(e);a||(a=this.createGroup(t.key)),Array.from(this.selected).forEach((e=>{this.addImageToGroup(e,a.grid,!1)})),this.persistFieldState(a.fieldId)}removeGroup(e,t=!1){let a=this.groups.get(e);if(!a)return;if(t&&!window.confirm("This will delete this group. Any uploads in this group will return to the main grid. Are you sure?"))return;a.uploads.size>0&&Array.from(a.uploads).forEach((e=>{this.addImageToGroup(e)}));let s=a.element;s&&(window.fade(s,!1),this.a11y.announce("Empty group removed")),this.persistFieldState(a.fieldId)}createGroup(e){let t=this.fields.get(e);if(!t)return;let a=t.ui.groups.size;t.ui.groups.groups.set(`group-${a}`,this.createGroupElement(`group-${a}`,e));let s=t.ui.groups.groups.get(`group-${a}`);t.ui.groups.container.insertAfter(s,t.ui.groups.empty);let i={fieldId:t.key,id:`group-${a}`,element:s,grid:s.querySelector(".item-grid"),uploads:new Set,meta:{post_title:"",post_excerpt:""},changes:{}};return this.groups.set(`group-${a}`,i),i}createGroupElement(e,t){let a=window.getTemplate("imageGroup");if(!a)return;a.dataset.groupId=e,a.dataset.fieldId=t;let s=window.getTemplate("groupMetaData");return a.querySelector(".fields")?.append(s),a}handleSelectAll(e,t=null){const a=this.getFieldFromElement(e);if(!a)return;null===t&&(t=e.checked);(a.previewGrid.querySelectorAll("[data-upload-id]")||[]).forEach((e=>{const a=e.querySelector('[name*="select-item"]');a&&(a.checked=t)})),this.updateSelectAll(e),this.a11y.announce(t?"All uploads selected":"All uploads deselected"),this.lastClickedUpload=null}updateSelection(e){let t=this.getFieldFromElement(e.target),a=this.getUploadFromElement(e.target);t&&a?(this.lastClickedUpload=a.id,e.target.checked?this.selected.get(t.key).add(a.id):this.selected.get(t.key).delete(a.id)):console.log("No field or upload found...")}updateSelectAll(e){const t=this.getFieldFromElement(e);if(!t)return;const a=this.getSelectedUploads(e);a.length>0?(t.selectActions.hidden=!1,t.selectInfo.hidden=!1,t.selectCount.textContent=`${a.length}`):(t.selectActions.hidden=!0,t.selectInfo.hidden=!0);let s=a.length===t.container.querySelectorAll(".item-grid.preview .upload-item").length;t.selectAll.checked=s,t.selectAll.nextElementSibling.textContent=s?"Clear Selection":"Select All"}getSelectedUploads(e){let t=this.getFieldFromElement(e);if(t)return Array.from(this.selected.get(t.key)??[])}handleRangeSelection(e,t){if(!this.getFieldFromElement(e))return;const a=this.getUploadIdFromElement(e);if(!a||!this.lastClickedUpload)return;const s=e.closest(".item-grid"),i=Array.from(s.querySelectorAll("[data-upload-id]")),r=i.findIndex((e=>e.dataset.uploadId===this.lastClickedUpload)),o=i.findIndex((e=>e.dataset.uploadId===a));if(-1===r||-1===o)return;const n=Math.min(r,o),l=Math.max(r,o);for(let e=n;e<=l;e++){const t=i[e].querySelector('[name*="select-item"]');t&&(t.checked=!0)}e.checked=!0,this.updateSelectAll(e);const d=l-n+1;this.a11y.announce(`Selected ${d} items in range`),this.lastClickedUpload=a}removeSelection(e){let t=this.getFieldIdFromElement(e);const a=this.getSelectedUploads(e);0!==a.length?a.forEach((e=>{this.removeUpload(t,e)})):this.notify("No uploads selected","warning")}removeUpload(e,t){const a=this.fields.get(e),s=this.uploads.get(t);if(a&&s){if(a.uploads?.delete(t),s.groupId){const e=this.groups.get(s.groupId);e?.delete(t)}s.element?.remove(),this.clearUpload(t),this.maybeLockUploads(e),this.updateSelectAll(a.ui.field.field),this.a11y.announce("Upload removed")}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}handleBeforeUnload(e){if(Array.from(this.uploads.values()).filter((e=>"processing"===e.status||"pending"===e.status||"uploading"===e.status)).length>0){const t="You have uploads in progress. Are you sure you want to leave?";return e.preventDefault(),e.returnValue=t,t}}cleanup(){this.clearListeners(),this.hasGroups&&this.clearGroupListeners(),this.compressionWorker=null,this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=new window.jvbStore({name:"upload_fields",storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],stripDOMReferences:!0,TTL:6048e5}),this.uploadStore=new window.jvbStore({name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}]}),this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.uploadBlobs=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',hiddenValue:'input[type="hidden"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={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"},this.init()}async init(){await this.loadFields(),await this.loadUploads(),this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if("uploads"!==t.endpoint&&"uploads/meta"!==t.endpoint)return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data.fieldId;switch(e){case"cancel-operation":s&&this.clearField(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":(t.result?.data||[]).forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploads.set(t.id,t))})),s&&this.cleanField(s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),r={id:t,config:s,element:e,ui:this.buildFieldUI(e),uploads:new Set,groups:new Set,state:"ready"};return this.fields.set(t,r),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,r=this.getCurrentSelection(t);return r&&r.includes(s)?r:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),r=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(r)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragStart:e=>{},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),r=this.selectionHandlers.get(s);r?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r?(this.processFiles(r,s),this.a11y.announce(`${s.length} file(s) dropped for upload`)):console.error("No field ID found for drop zone")}handleItemDrop(e,t){const s=t.classList.contains("preview");let r=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return void console.error("Failed to create group");r=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,r)}));const o=this.getFieldIdFromElement(t);this.schedulePersistance(o);const i=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(i)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const t=this.getFieldIdFromElement(e.target),s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}t&&("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e))}getCurrentSelection(e){let t=[];for(let[s,r]of this.selectionHandlers)(e===s||s.includes(e))&&r.selectedItems.size>0&&(t=t.concat([...r.selectedItems]));return t}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}getModalType(e){if(void 0!==e._cachedModalType)return e._cachedModalType;if(!e||!e.element)return e._cachedModalType=null,null;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":this.fields.get(s).element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s);break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const r=this.selected.get(s);if(r&&0!==r.size){const e=this.createGroup(s);if(!e)return;r.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${r.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,r=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(r)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,r=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(r,s),this.a11y.announce("Item removed"),this.schedulePersistance(r))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fields.get(e);if(!t)return;const s=t.ui.field;if(!s)return;const r=new window.jvbHandleSelection({container:s,ui:{selectAll:s.querySelector('[name="select-all-uploads"]'),bulkControls:s.querySelector(".selection-actions"),count:s.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,r),r}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const r=this.groups.get(t);if(!r)return;const o=new window.jvbHandleSelection({container:r.element,ui:{selectAll:r.element.querySelector(this.selectors.groups.selectAll),bulkControls:r.element.querySelector(this.selectors.groups.actions),count:r.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,o),o}handleSelectAll(e,t){}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",store:this.fields},upload:{selector:this.selectors.items.item,key:"uploadId",store:this.uploads},group:{selector:this.selectors.groups.container,key:"groupId",store:this.groups}}[t];if(!s)return null;const r=e.closest(s.selector);if(!r)return null;const o=r.dataset[s.key];return s.store.get(o)}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){return this.getFromElement(e,"field")?.id??null}getUploadIdFromElement(e){return this.getFromElement(e,"upload")?.id??null}getGroupIdFromElement(e){return this.getFromElement(e,"group")?.id??null}async processFiles(e,t){const s=this.fields.get(e);if(!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups.display&&(s.ui.groups.display.hidden=!1);const r=t.length;let o=0;this.updateUploadProgress(e,0,r,"Processing files..."),s.uploads||(s.uploads=new Set);const i=Array.from(t).map((async(t,i)=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,a={id:i,attachment_id:null,fieldId:e,originalFile:t,processedFile:null,preview:null,status:"local_processing",element:null,location:null,meta:{originalName:t.name,size:t.size,type:t.type}};a.preview=this.createPreviewUrl(t),t.type.startsWith("image/")?a.processedFile=await this.processImage(t,s.subtype):a.processedFile=t,await this.uploadStore.saveBlob(i,a.processedFile||t);const l=this.getSubtypeFromMime(t.type);return a.element=this.createUploadElement({...a,subtype:l},"post_group"===s.config.destination),this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),s.ui.preview&&(s.ui.preview.appendChild(a.element),a.location=s.ui.preview),this.uploads.set(i,a),s.uploads.add(i),o++,this.updateUploadProgress(e,o,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),a.status="processed",setTimeout((()=>{this.showUploadProgress(i,!1)}),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),o++,this.updateUploadProgress(e,o,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),await this.schedulePersistance(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}updateFieldState(e){const t=this.fields.get(e);if(!t||!t.ui.field)return;const s=t.ui.field,r=t.uploads?.size||0,o=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=r>0?"true":"false",s.dataset.uploadCount=r.toString(),s.dataset.hasGroups=o?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,r){const o=this.fields.get(e);if(!o?.ui?.progress?.progress)return;const i=o.ui.progress,a=s>0?t/s*100:0;i.fill&&(i.fill.style.width=`${a}%`),i.text&&(i.text.textContent=r),i.count&&(i.count.textContent=`${t}/${s}`),i.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.fields.get(e);s&&(s.state=t)}updateUploadStatus(e,t){const s=this.uploads.get(e);s&&(s.status=t,this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploads.get(e);if(!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${t.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(t.status),t.status)}showUploadProgress(e,t=!0){const s=this.uploads.get(e);if(!s||!s.element)return;const r=s.element.querySelector(".progress");r&&(t?(r.style.removeProperty("animation"),r.hidden=!1):(r.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{r.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const r=this.uploads.get(e);if(!r||!r.element)return;const o=r.element.querySelector(".progress");if(!o)return;const i=o.querySelector(".fill"),a=o.querySelector(".details"),l=o.querySelector(".icon");i&&(i.style.width=`${t}%`),s&&a&&(a.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}checkFieldLimits(e,t){const s=this.fields.get(e);if(!s)return!1;return(s.uploads?.size||0)+t<=s.maxFiles}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,r=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,r)).toFixed(s))+" "+["Bytes","KB","MB","GB"][r]}shouldProcessClientSide(e,t){return!("image"!==t||!e.type.startsWith("image/"))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((r,o)=>{let i,a=!1;i=setTimeout((()=>{a||(a=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),o(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:i}),this.handleProcess(e,t).then((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),r(e))})).catch((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),o(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((r,o)=>{const i=new Image,a=document.createElement("canvas"),l=a.getContext("2d");let n=null;const d=()=>{i.onload=null,i.onerror=null,n&&(URL.revokeObjectURL(n),n=null),a.width=1,a.height=1,l.clearRect(0,0,1,1)};i.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(i,t);a.width=n,a.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(i,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);a.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});r(s)}else o(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),o(new Error(`Canvas processing failed: ${e.message}`))}},i.onerror=()=>{d(),o(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),i.src=n}catch(e){d(),o(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,r){return new Promise(((o,i)=>{if(!this.worker.worker)return void i(new Error("Worker not available"));const a=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===a)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});o(s)}else i(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),i(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:a,file:e,maxDimension:s,quality:r,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n            self.onmessage = async function(e) {\n                const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n                try {\n                    // Create ImageBitmap from file\n                    const bitmap = await createImageBitmap(file);\n\n                    // Calculate dimensions\n                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n                    const width = Math.round(bitmap.width * scale);\n                    const height = Math.round(bitmap.height * scale);\n\n                    // Create OffscreenCanvas\n                    const canvas = new OffscreenCanvas(width, height);\n                    const ctx = canvas.getContext('2d');\n\n                    // Draw and resize\n                    ctx.imageSmoothingEnabled = true;\n                    ctx.imageSmoothingQuality = 'high';\n                    ctx.drawImage(bitmap, 0, 0, width, height);\n\n                    // Clean up bitmap\n                    bitmap.close();\n\n                    // Convert to blob\n                    const blob = await canvas.convertToBlob({\n                        type: outputFormat,\n                        quality: quality\n                    });\n\n                    self.postMessage({\n                        messageId,\n                        success: true,\n                        blob: blob,\n                        format: outputFormat\n                    });\n\n                } catch (error) {\n                    self.postMessage({\n                        messageId,\n                        success: false,\n                        error: error.message\n                    });\n                }\n            };\n        "],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:r}=e;if(s<=t&&r<=t)return{width:s,height:r};const o=Math.min(t/s,t/r);return{width:Math.round(s*o),height:Math.round(r*o)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}maybeLockUploads(e){const t=this.fields.get(e);if(!t?.ui?.dropZone)return;if("post_group"===t.config.destination)return;const s=t.uploads?.size||0,r=t.config?.maxFiles||999;t.ui.dropZone.hidden=s>=r,t.element.classList.toggle("at-max-uploads",s>=r)}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return void console.error("Image template not found");s.dataset.uploadId=e.id,e.originalFile&&(s.dataset.subtype=this.getSubtypeFromMime(e.originalFile.type)),s.querySelector('[name="featured"]').value=e.id;let[r,o,i,a,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch([r.value,o.src,o.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],s.dataset.subtype){case"image":[o.src,o.alt]=[e.preview,e.originalFile?.name??e.meta?.originalName??""],i.remove(),a.remove();break;case"video":i.src=e.preview,o.remove(),a.remove();break;case"document":const t=e.originalFile?.name??e.meta?.originalName??"",s=t.split(".").pop()?.toLowerCase()??"",r={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(r[s]||"file");a.innerText=e.originalFile.name,a.prepend(l),o.remove(),i.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),s}async submitUploads(e){const t=this.fields.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=this.getFieldGroups(e);if(0===r.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=[],i=new FormData;let a=[];s=s.map((e=>this.uploads.get(e))),r.forEach(((e,t)=>{const r={images:[],fields:{}};for(let[t,s]of Object.entries(e.changes))r.fields[t]=s;s.filter((t=>t.groupId===e.id)).forEach((e=>{if(e){const t=e.processedFile||e.originalFile;if(t){i.append("files[]",t);const s={upload_id:e.id,index:a.length};r.images.push(s),a.push(e.id)}}})),o.push(r)})),s.filter((e=>!Object.hasOwn(e,"groupId"))).forEach((e=>{if(e){const t={images:[],fields:{}},s=e.processedFile||e.originalFile;if(s){i.append("files[]",s);const r={upload_id:e.id,index:a.length};t.images.push(r),a.push(e.id)}o.push(t)}})),i.append("content",t.config.content),i.append("user",t.config.itemID),i.append("posts",JSON.stringify(o)),i.append("upload_ids",JSON.stringify(a));const l={endpoint:"uploads/groups",method:"POST",data:i,title:`Creating ${o.length} ${t.config.content}${o.length>1?"s":""} from uploads...`,popup:`Creating ${o.length} post${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,this.a11y.announce(`Creating ${o.length} post${o.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}finally{this.schedulePersistance(t.id)}}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const s=Array.from(t.uploads);if(0===s.length)return;const r=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");let o=1===s.length?"file":"files";const i={endpoint:"uploads",method:"POST",data:r,title:`Uploading ${s.length} ${o} to server...`,popup:`Uploading ${s.length} ${o}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(i);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.schedulePersistance(t.id)}}prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let r=[];const o=this.getFieldGroups(e.id);if("post_group"===e.config.destination&&o.length>0){let e=[],t=[],i=[];o.forEach((o=>{let a=[],l=null;o.uploads.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;if(e){s.append("files[]",e);r.length;r.push(t.id),a.push(t.id);const o=t.element?.querySelector('[name="featured"]');o?.checked&&(l=t.id)}}})),e.push(a),t.push(o.title||""),i.push(l)})),s.append("groups",JSON.stringify(e)),s.append("group_titles",JSON.stringify(t)),s.append("featured_images",JSON.stringify(i))}else t.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;e&&(s.append("files[]",e),r.push(t.id))}}));return s.append("upload_ids",JSON.stringify(r)),s}getFieldGroups(e){const t=[];return this.groups.forEach(((s,r)=>{if(s.fieldId===e){const o=this.fields.get(e),i=o?.ui?.groups?.groups?.get(r);t.push({id:r,uploads:Array.from(s.uploads||new Set),changes:s.changes||{},element:i||null})}})),t}async queueUploadMeta(e){const t=this.getUploadFromElement(e.target);if(!t)return;if(!this.fields.get(t.fieldId))return;if(!e.target.closest(".upload-meta"))return;let s={};s[e.target.name]=e.target.value,t.meta={...t.meta,...s};let r={};r[t.attachmentId??t.id]=t.meta;const o={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(o)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:t.id})}}createGroup(e,t=null){const s=this.fields.get(e);if(!s)return console.error("Field not found:",e),null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return console.error("Failed to create group element"),null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,r),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(r,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(r);const o={id:t,fieldId:e,element:r,grid:r.querySelector(".item-grid.group"),uploads:new Set,changes:{}};return this.groups.set(t,o),this.addGroupSelectionHandler(e,t),this.schedulePersistance(e),o}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let r=window.getTemplate("groupMetadata");const o=s.querySelector(".fields");if(o&&r){o.append(r);const i=o.querySelector('[name="post_title"]'),a=o.querySelector('[name="post_excerpt"]');i&&(i.id=`${e}_title`,i.name=`${e}[post_title]`),a&&(a.id=`${e}_excerpt`,a.name=`${e}[post_excerpt]`);let l=this.fields.get(t);if(""!==l.config.content){s.querySelector("summary").textContent=l.config.content+" Fields"}}else s.querySelector("details").remove();const i=s.querySelector(".item-grid.group");return i&&(i.dataset.groupId=e),s}deleteGroup(e,t=!0){let s=this.groups.get(e);if(!s)return;let r=!0;t&&s.uploads&&s.uploads.size>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&s.uploads&&s.uploads.size>0&&Array.from(s.uploads).forEach((e=>{this.addImageToGroup(e,null,!1)})),this.groups.delete(e);let o=s.element;o&&(o.remove(),this.a11y.announce("Group removed")),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){let r=this.uploads.get(e);if(!r)return;let o=this.fields.get(r.fieldId);if(!o)return;if(!t&&r.location===o.ui.preview||t===r.location)return;if(r.location){let t=r.location.dataset.groupId;if(t){let s=this.groups.get(t);s&&s.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t))}}const i=r.element.querySelector('[name*="select-item"]');i&&(i.checked=!1);let a=r.element.querySelector('[name="featured"]');if(a.hidden=!t,t){if(!t.classList.contains("item-grid")||!t.classList.contains("preview")){let s=t.dataset.groupId;a.name=s+"_"+a.name;let o=this.groups.get(s);o||(o=this.createGroup(r.fieldId),t=o.grid,s=o.id),o&&(o.uploads.add(e),r.groupId=s)}}else t=o.ui.preview,r.groupId=null;r.location=t,t.append(r.element),s&&this.schedulePersistance(o.id)}removeFromGroup(e){const t=this.uploads.get(e);if(!t)return;const s=this.fields.get(t.fieldId);if(!s)return;if(t.groupId){const s=this.groups.get(t.groupId);s?.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t.groupId,!1)),t.groupId=null}s.ui?.preview&&(s.ui.preview.appendChild(t.element),t.location=s.ui.preview);const r=t.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1)}removeUpload(e,t){const s=this.fields.get(e),r=this.uploads.get(t);if(!s||!r)return;if(s.uploads?.delete(t),r.groupId){const e=this.groups.get(r.groupId);e&&e.uploads&&(e.uploads.delete(t),0===e.uploads.size&&this.removeGroup(r.groupId))}r.element?.remove(),this.clearUpload(t),this.updateFieldState(e),this.maybeLockUploads(e);const o=this.selectionHandlers.get(s.id);o&&o.deselect(t),this.a11y.announce("Upload removed")}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.fields.get(e);if(!t)return;const s={...t,id:e,fieldId:e,uploads:Array.from(t.uploads||[]).map((e=>this.uploads.get(e))),groups:Array.from(this.groups.entries()).filter((([t,s])=>s.fieldId===e&&s.uploads&&s.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),changes:t.changes||{}}))),context:{url:this.normalizeUrl(window.location.href),fullUrl:window.location.href,modalType:this.getModalType(t),formId:t.formId,fieldSelector:`.field.upload[data-field="${t.config.name}"]`},timestamp:Date.now()};await this.fieldStore.save(s)}normalizeUrl(e){try{const t=new URL(e);return t.origin+t.pathname}catch(t){return e}}getFieldUploads(e,t=!1){const s=this.fields.get(e);return s&&s.uploads?Array.from(s.uploads).map((e=>{const s=this.uploads.get(e);return s?t?{id:s.id,fieldId:s.fieldId,status:s.status,attachmentId:s.attachmentId,operationId:s.operationId,groupId:s.groupId||null,changes:s.changes||{},meta:{originalName:s.meta?.originalName||s.originalFile?.name,size:s.meta?.size||s.originalFile?.size,type:s.meta?.type||s.originalFile?.type,title:s.meta?.title,alt:s.meta?.alt,caption:s.meta?.caption}}:s:null})).filter(Boolean):[]}async checkForStoredUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const s=e.getAll();s.onsuccess=()=>t(s.result)}))).filter((e=>e.uploads.some((e=>!e.operationId&&("completed"===e.status||"processed"===e.status||"local_processing"===e.status||"processed-original"===e.status)))));0!==t.length&&this.showRecoveryNotification(t)}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}handleGroupMetaChange(e){let t=this.getGroupFromElement(e);if(!t)return;Object.hasOwn(t,"changes")||(t.changes={});let s=e.name;if(s.includes("group")){let e=t.id+"_",r=t.id+"[";s=s.replace(e,"").replace(r,"").replace("]","")}t.changes[`${s}`]=e.value,this.groups.set(t.id,t),this.schedulePersistance(t.fieldId)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let r,o=window.getTemplate("restoreNotification");if(!o)return void console.error("Restore notification template not found");if(s>0){r=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else r=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const i=o.querySelector(".restore-details");i&&(i.textContent=r);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const r=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const o=await this.uploadStore.getBlob(e.id);if(o)try{const r=new Blob([o.data],{type:o.type}),i=this.createPreviewUrl(r);let[a,l,n,d,c]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let u=this.getSubtypeFromMime(o.type);switch(s.dataset.subtype=u,u){case"image":[l.src,l.alt]=[i,e.originalFile?.name??e.meta?.originalName??""],n.remove(),d.remove();break;case"video":n.src=i,l.remove(),d.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}d.innerText=e.originalFile.name,d.prepend(t),l.remove(),n.remove()}s.dataset.previewUrl=i}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const i=s.querySelector("summary span");i&&(i.textContent=e.meta?.originalName||"Unknown file");const a=s.querySelector("details");a&&e.meta&&(a.textContent=`${this.formatBytes(e.meta.size)} • ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),r&&r.appendChild(s)}o.querySelector(".wrap").appendChild(r)}document.querySelector(".field.upload").appendChild(o),o=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(o),this.restoreSelection=new window.jvbHandleSelection({container:o,ui:{selectAll:o.querySelector("#select-all-restore"),count:o.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async restoreSelectedUploads(e){const t=new Map;if(e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)})),!this.db)return;const s=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates");for(const[e,r]of t.entries()){const t=s.get(e),o=await new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}));o&&(o.uploads=o.uploads.filter((e=>r.includes(e.id))),await this.restoreField(o))}}async restoreField(e){const{config:t,context:s,uploads:r,groups:o,id:i}=e;s.modalType&&await this.openModalForRestore(s);let a=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!a){const e=`${t.content}_${t.itemID}_${t.name}`;a=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!a)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=a.dataset.uploader;l&&this.fields.has(l)||(l=this.registerUploader(a,t));const n=this.fields.get(l);if(n){n.state=e.state||"ready",n.ui=this.buildFieldUI(a),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),o&&o.length>0&&await this.restoreGroups(l,o);for(const e of r)await this.restoreUpload(n,e);this.updateFieldState(l),this.maybeLockUploads(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}else console.error("Failed to register field for restoration")}async restoreUpload(e,t){const s=await this.uploadStore.getBlob(t.id);if(!s)return void console.warn("Blob data not found for upload:",t.id);{const e=s.data instanceof File?s.data:new File([s.data],s.name,{type:s.type,lastModified:s.lastModified});t.originalFile=e,t.processedFile=e,t.preview=this.createPreviewUrl(e)}e.uploads||(e.uploads=new Set),e.uploads.add(t.id);const r=this.getSubtypeFromMime(t.originalFile.type);let o;if(t.element=this.createUploadElement({...t,subtype:r},"post_group"===e.config.destination),o=t.groupId&&e.ui.groups.groups.has(t.groupId)?e.ui.groups.groups.get(t.groupId).querySelector(".item-grid"):e.ui.preview,o&&(o.appendChild(t.element),t.location=o),this.uploads.set(t.id,t),t.groupId){const e=this.groups.get(t.groupId);e&&e.uploads&&e.uploads.add(t.id)}}async restoreGroups(e,t){for(const s of t){const t=this.createGroup(e,s.id);if(t&&(s.meta&&(t.meta={...s.meta}),s.changes&&(t.changes={...s.changes}),s.title)){const e=t.element.querySelector('[name*="post_title"]');e&&(e.value=s.title)}}}async openModalForRestore(e){const{modalType:t,formId:s}=e;let r=null;switch(t){case"create":r=document.querySelector('[data-action="create"]');break;case"edit":r=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":r=document.querySelector('[data-action="bulk-edit"]')}r&&(r.click(),await new Promise((e=>setTimeout(e,300))))}handleFieldStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":console.log(`Field state saved: ${t.key}`)}}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.checkForStoredUploads();break;case"item-saved":this.showSaveIndicator(t.key)}}async saveUpload(e){if(e.file instanceof File||e.file instanceof Blob){await this.uploadStore.saveBlob(e.id,e.file);const{file:t,originalFile:s,...r}=e;await this.uploadStore.save(r)}else await this.uploadStore.save(e)}async loadFields(){(await this.fieldStore.getAll()).forEach((e=>{e.uploads&&Array.isArray(e.uploads)&&(e.uploads=new Set(e.uploads.map((e=>e.id)))),this.fields.set(e.fieldId,e)}))}async loadUploads(){(await this.uploadStore.getAll()).forEach((e=>{this.uploads.set(e.id,e)}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.fields.clear(),this.uploads.clear(),this.groups.clear(),this.selected.clear(),this.subscribers.clear()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){this.fieldStore.clear(),this.uploadStore.clear()}async clearField(e){await this.fieldStore.delete(e);const t=this.fields.get(e);if(t?.uploads)for(const e of t.uploads)await this.uploadStore.delete(e);this.fields.delete(e)}async clearUpload(e,t=!0){const s=this.uploads.get(e);if(s){if(this.revokePreviewUrl(s.preview),s.element){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}t&&await this.schedulePersistance(s.fieldId),this.uploads.delete(e),this.uploadStore.delete(e),this.uploadStore.delete(e,"blobs")}}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index 6e810c5..978b9cc 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{document.addEventListener("DOMContentLoaded",(function(){console.log("Theme switch initiated");const t=document.getElementById("theme-switch");if(!t)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),n=localStorage.getItem("theme");n?(document.documentElement.classList.toggle("dark","dark"===n),t.checked="dark"===n):(document.documentElement.classList.toggle("dark",e.matches),t.checked=e.matches),t.addEventListener("change",(async function(){const t=this.checked;if(document.documentElement.classList.toggle("dark",t),localStorage.setItem("theme",t?"dark":"light"),null!==jvbSettings.currentUser)try{await fetch(`${jvbSettings.api}settings`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash},body:JSON.stringify({dark_mode:t})})}catch(t){console.error("Failed to save theme preference:",t)}const e=document.getElementById("theme-switch");e&&(e.title=t?"Toggle Light Mode":"Toggle Dark Mode")})),e.addEventListener("change",(e=>{if(!localStorage.getItem("theme")){const n=e.matches;document.documentElement.classList.toggle("dark",n),t.checked=n}}))})),window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,o=Math.floor((n-e)/1e3),i=Math.floor(o/60),r=Math.floor(i/60),s=Math.floor(r/24);return r<24?0===r?0===i?"Just now":`${i} ${1===i?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:s<7?`${s} ${1===s?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const o=Math.floor((e-n)/1e3),i=Math.floor(o/60);return o<60?"In a moment":i<5?"In a few minutes":i<20?"Coming up soon":i<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let o=n.querySelector("button.up"),i=n.querySelector("button.down");return"up"===e&&o.classList.add("voted"),"down"===e&&i.classList.add("voted"),t.upvotes>0&&(o.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(i.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),o=new Date(e);return n.toDateString()===o.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===o.getMonth()&&n.getFullYear()===o.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${o.getDate()}, ${o.getFullYear()}`:n.getFullYear()===o.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${o.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${o.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${o.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...o){clearTimeout(n),n=setTimeout((()=>t.apply(this,o)),e)}},window.throttle=function(t,e){let n;return function(){const o=arguments,i=this;n||(t.apply(i,o),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...o){n||(t.apply(this,o),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,o=Math.floor((n-e)/864e5);return o<1?"Today":o<2?"Yesterday":o<7?`${o} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((o=>{let i=0;t.textContent="";const r=setInterval((()=>{i<e.length?(t.textContent+=e.charAt(i),i++):(clearInterval(r),o())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let o=t.textContent,i=o.length;const r=setInterval((()=>{i>0?(i--,t.textContent=o.substring(0,i)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,o=10,i=1e3,r=250){let s=!0;return async function(){for(;s;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,i))),await window.eraseText(t,o),await new Promise((t=>setTimeout(t,r)))}(),function(){s=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let o;switch(n){case this.VALUE_CREATED:o=e;break;case this.VALUE_DELETED:o=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:o=e}return{type:n,data:o}}let n={},o=!1;for(let i in t)if(!this.isFunction(t[i])){let r;e&&void 0!==e[i]&&(r=e[i]);const s=this.map(t[i],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[i]=s.data:n[i]=s,o=!0)}if(e)for(let i in e)if(!this.isFunction(e[i])&&(void 0===t||void 0===t[i])){const t=this.map(void 0,e[i]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[i]=t.data:n[i]=t,o=!0)}return o?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let o in e)this.isFunction(e[o])||(void 0!==t[o]?n[o]=this.merge(t[o],e[o]):n[o]=e[o]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let o=n.cloneNode(!0);o.textContent=e,t.append(o)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let o=n.cloneNode(!0);window.handleImageField(o,e),t.append(o)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[o,i]of Object.entries(t))n[o]="object"==typeof i?window.uiFromSelectors(i,e):e?e.querySelector(i):document.querySelector(i);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),console.log("Scheduling action: ",t),console.log("With callback",e),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(console.log("Cancelling ",t),clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())console.log("clearing timeout: ",t),clearTimeout(t);this.timeouts.clear()}}})();
\ No newline at end of file
+(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),s=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:s<7?`${s} ${1===s?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let s=!0;return async function(){for(;s;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){s=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),console.log("Scheduling action: ",t),console.log("With callback",e),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(console.log("Cancelling ",t),clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())console.log("clearing timeout: ",t),clearTimeout(t);this.timeouts.clear()}}})();
\ No newline at end of file
diff --git a/assets/js/min/view.min.js b/assets/js/min/view.min.js
index 49f2324..afc634f 100644
--- a/assets/js/min/view.min.js
+++ b/assets/js/min/view.min.js
@@ -1 +1 @@
-window.jvbViews=class{constructor(e,t){console.log(e),this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container),console.log(this.ui)}init(){this.store.subscribe(((e,t)=>{switch(e){case"data-fetched":case"data-cached":this.handleDataUpdate(t);break;case"items-updated":this.handleItemsUpdate(t.items)}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){let i=this.ui.table.columns.filter((t=>t.className===e));console.log(i),console.log("Toggle Columns"),console.log(e,t),console.log(this.ui.table.columns)}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){e.data&&e.data.items&&this.render(e.data.items)}handleItemsUpdate(e){this.render(e)}render(e=null){if(this.store){if(!e){const t=this.store.getCurrentRequest();if(!(t&&t.data&&t.data.items))return;e=t.data.items}switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else console.error("No store connected to renderer")}renderGrid(e){this.toggleGrid(),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.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,l.src,l.alt,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??"",e.id,e.id],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label").hidden=!0;break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}};
\ No newline at end of file
+window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.handleDataUpdate(t);break;case"items-updated":this.handleItemsUpdate(t.items)}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){e.data&&e.data.items&&this.render(e.data.items)}handleItemsUpdate(e){this.render(e)}render(e=null){if(this.store){if(!e){const t=this.store.getCurrentRequest();if(!(t&&t.data&&t.data.items))return;e=t.data.items}switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else console.error("No store connected to renderer")}renderGrid(e){this.toggleGrid(),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.store.renderOrRetrieve?i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)):this.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.renderGridItem(e),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,l.src,l.alt,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??"",e.id,e.id],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label")?.remove();break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}};
\ No newline at end of file
diff --git a/build/video/block.json b/build/video/block.json
index 7a88d90..0c88755 100644
--- a/build/video/block.json
+++ b/build/video/block.json
@@ -1,7 +1,7 @@
 {
   "$schema": "https://schemas.wp.org/trunk/block.json",
   "apiVersion": 3,
-  "name": "jvb/video-cover",
+  "name": "jvb/video",
   "version": "1.0.0",
   "title": "Video Cover",
   "category": "jvb",
@@ -16,9 +16,21 @@
     "spacing": {
       "margin": true,
       "padding": true
+    },
+    "color": {
+      "background": true,
+      "text": true
     }
   },
   "attributes": {
+    "title": {
+      "type": "string",
+      "default": ""
+    },
+    "description": {
+      "type": "rich-text",
+      "default": ""
+    },
     "posterId": {
       "type": "number",
       "default": 0
@@ -66,6 +78,18 @@
     "fadeEffect": {
       "type": "boolean",
       "default": false
+    },
+    "overlayOpacity": {
+      "type": "number",
+      "default": 0
+    },
+    "contentAlignment": {
+      "type": "string",
+      "default": "center"
+    },
+    "minHeight": {
+      "type": "number",
+      "default": 0
     }
   },
   "textdomain": "jvb",
diff --git a/build/video/index-rtl.css b/build/video/index-rtl.css
index 76b8ed9..a6ed157 100644
--- a/build/video/index-rtl.css
+++ b/build/video/index-rtl.css
@@ -1 +1 @@
-.video-cover-editor{background:#f0f0f0;border:2px dashed #ccc;border-radius:4px;min-height:200px}.video-cover-editor .video-cover-preview{position:relative;width:100%}.video-cover-editor .video-cover-preview img{display:block;height:auto;width:100%}.video-cover-editor .video-cover-preview .video-overlay{background:rgba(0,0,0,.7);bottom:0;color:#fff;font-size:14px;right:0;padding:10px;position:absolute;left:0}.video-cover-editor .video-cover-preview .video-overlay p{margin:0}.video-cover-editor .video-cover-placeholder{align-items:center;color:#666;display:flex;font-size:16px;justify-content:center;min-height:200px}.video-source-list{list-style:none;margin:10px 0;padding:0}.video-source-list .video-source-item{align-items:center;background:#f5f5f5;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:5px;padding:8px 12px}.video-source-list .video-source-item .video-source-mime{font-family:monospace;font-size:13px}
+.video-cover-editor{background:#f0f0f0;border:2px dashed #ccc;border-radius:4px;min-height:200px;position:relative}.video-cover-editor .video-cover-preview{min-height:300px;position:relative;width:100%}.video-cover-editor .video-cover-preview img{display:block;height:auto;width:100%}.video-cover-editor .video-cover-preview .video-overlay-preview{background:#000;bottom:0;right:0;pointer-events:none;position:absolute;left:0;top:0}.video-cover-editor .video-cover-preview .video-cover-content-preview{bottom:0;display:flex;right:0;padding:2rem;position:absolute;left:0;top:0;z-index:2}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-center{align-items:flex-start;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center-left{align-items:center;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center{align-items:center;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center-right{align-items:center;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content{color:#fff;max-width:1200px;text-shadow:0 2px 4px rgba(0,0,0,.5);width:100%}.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h1,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h2,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h3,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h4,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h5,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h6,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content p{color:#fff}.video-cover-editor .video-cover-preview .video-info{background:rgba(0,0,0,.7);bottom:0;color:#fff;font-size:14px;right:0;padding:10px;position:absolute;left:0;z-index:3}.video-cover-editor .video-cover-preview .video-info p{margin:0}.video-cover-editor .video-cover-placeholder{align-items:center;color:#666;display:flex;font-size:16px;justify-content:center;min-height:200px}.video-source-list{list-style:none;margin:10px 0;padding:0}.video-source-list .video-source-item{align-items:center;background:#f5f5f5;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:5px;padding:8px 12px}.video-source-list .video-source-item .video-source-mime{font-family:monospace;font-size:13px}
diff --git a/build/video/index.asset.php b/build/video/index.asset.php
index f806b91..1eb3395 100644
--- a/build/video/index.asset.php
+++ b/build/video/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n'), 'version' => '58cb97ca090cbaa7a151');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n'), 'version' => '0bc754bf0d806d2540bd');
diff --git a/build/video/index.css b/build/video/index.css
index b9e9e4d..5616a94 100644
--- a/build/video/index.css
+++ b/build/video/index.css
@@ -1 +1 @@
-.video-cover-editor{background:#f0f0f0;border:2px dashed #ccc;border-radius:4px;min-height:200px}.video-cover-editor .video-cover-preview{position:relative;width:100%}.video-cover-editor .video-cover-preview img{display:block;height:auto;width:100%}.video-cover-editor .video-cover-preview .video-overlay{background:rgba(0,0,0,.7);bottom:0;color:#fff;font-size:14px;left:0;padding:10px;position:absolute;right:0}.video-cover-editor .video-cover-preview .video-overlay p{margin:0}.video-cover-editor .video-cover-placeholder{align-items:center;color:#666;display:flex;font-size:16px;justify-content:center;min-height:200px}.video-source-list{list-style:none;margin:10px 0;padding:0}.video-source-list .video-source-item{align-items:center;background:#f5f5f5;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:5px;padding:8px 12px}.video-source-list .video-source-item .video-source-mime{font-family:monospace;font-size:13px}
+.video-cover-editor{background:#f0f0f0;border:2px dashed #ccc;border-radius:4px;min-height:200px;position:relative}.video-cover-editor .video-cover-preview{min-height:300px;position:relative;width:100%}.video-cover-editor .video-cover-preview img{display:block;height:auto;width:100%}.video-cover-editor .video-cover-preview .video-overlay-preview{background:#000;bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0}.video-cover-editor .video-cover-preview .video-cover-content-preview{bottom:0;display:flex;left:0;padding:2rem;position:absolute;right:0;top:0;z-index:2}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-center{align-items:flex-start;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center-left{align-items:center;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center{align-items:center;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-center-right{align-items:center;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover-editor .video-cover-preview .video-cover-content-preview.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content{color:#fff;max-width:1200px;text-shadow:0 2px 4px rgba(0,0,0,.5);width:100%}.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h1,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h2,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h3,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h4,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h5,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content h6,.video-cover-editor .video-cover-preview .video-cover-content-preview .video-cover-content p{color:#fff}.video-cover-editor .video-cover-preview .video-info{background:rgba(0,0,0,.7);bottom:0;color:#fff;font-size:14px;left:0;padding:10px;position:absolute;right:0;z-index:3}.video-cover-editor .video-cover-preview .video-info p{margin:0}.video-cover-editor .video-cover-placeholder{align-items:center;color:#666;display:flex;font-size:16px;justify-content:center;min-height:200px}.video-source-list{list-style:none;margin:10px 0;padding:0}.video-source-list .video-source-item{align-items:center;background:#f5f5f5;border-radius:4px;display:flex;justify-content:space-between;margin-bottom:5px;padding:8px 12px}.video-source-list .video-source-item .video-source-mime{font-family:monospace;font-size:13px}
diff --git a/build/video/index.js b/build/video/index.js
index 3b085c1..5ccd292 100644
--- a/build/video/index.js
+++ b/build/video/index.js
@@ -1 +1 @@
-(()=>{"use strict";var e,o={128:()=>{const e=window.wp.blocks,o=window.wp.i18n,s=window.wp.blockEditor,i=window.wp.components,r=window.ReactJSXRuntime,l=["video/mp4","video/webm","video/ogg","video/ogv"],n=JSON.parse('{"UU":"jvb/video-cover"}');(0,e.registerBlockType)(n.UU,{edit:function({attributes:e,setAttributes:n}){const{posterId:d,posterUrl:t,videoSources:c,mobileSources:a,fadeEffect:v}=e,p=(0,s.useBlockProps)({className:"video-cover-editor"}),m=(e,o=!1)=>{const s={id:e.id,url:e.url,mime:e.mime};o?a.some((o=>o.mime===e.mime))||n({mobileSources:[...a,s]}):c.some((o=>o.mime===e.mime))||n({videoSources:[...c,s]})},j=(e,s=!1)=>0===e.length?null:(0,r.jsx)("ul",{className:"video-source-list",children:e.map(((e,l)=>(0,r.jsxs)("li",{className:"video-source-item",children:[(0,r.jsx)("span",{className:"video-source-mime",children:e.mime}),(0,r.jsx)(i.Button,{isDestructive:!0,isSmall:!0,onClick:()=>((e,o=!1)=>{if(o){const o=[...a];o.splice(e,1),n({mobileSources:o})}else{const o=[...c];o.splice(e,1),n({videoSources:o})}})(l,s),children:(0,o.__)("Remove","jvb")})]},l)))});return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.InspectorControls,{children:(0,r.jsxs)(i.PanelBody,{title:(0,o.__)("Video Settings","jvb"),initialOpen:!0,children:[(0,r.jsx)(i.BaseControl,{label:(0,o.__)("Poster Image","jvb"),help:(0,o.__)("Image shown while video loads","jvb"),children:(0,r.jsx)(s.MediaUploadCheck,{children:(0,r.jsx)(s.MediaUpload,{onSelect:e=>{n({posterId:e.id,posterUrl:e.url})},allowedTypes:["image"],value:d,render:({open:e})=>(0,r.jsxs)(r.Fragment,{children:[t&&(0,r.jsx)("img",{src:t,alt:(0,o.__)("Poster preview","jvb"),style:{maxWidth:"100%",marginBottom:"10px"}}),(0,r.jsx)(i.Button,{onClick:e,variant:t?"secondary":"primary",children:t?(0,o.__)("Change Poster","jvb"):(0,o.__)("Select Poster","jvb")}),t&&(0,r.jsx)(i.Button,{isDestructive:!0,onClick:()=>n({posterId:0,posterUrl:""}),style:{marginLeft:"10px"},children:(0,o.__)("Remove","jvb")})]})})})}),(0,r.jsxs)(i.BaseControl,{label:(0,o.__)("Desktop Video Sources","jvb"),help:(0,o.__)("Add multiple formats for better browser support","jvb"),children:[j(c,!1),(0,r.jsx)(s.MediaUploadCheck,{children:(0,r.jsx)(s.MediaUpload,{onSelect:e=>m(e,!1),allowedTypes:l,render:({open:e})=>(0,r.jsx)(i.Button,{onClick:e,variant:"secondary",children:(0,o.__)("Add Desktop Video","jvb")})})})]}),(0,r.jsxs)(i.BaseControl,{label:(0,o.__)("Mobile Video Sources (Optional)","jvb"),help:(0,o.__)("Smaller videos for mobile devices","jvb"),children:[j(a,!0),(0,r.jsx)(s.MediaUploadCheck,{children:(0,r.jsx)(s.MediaUpload,{onSelect:e=>m(e,!0),allowedTypes:l,render:({open:e})=>(0,r.jsx)(i.Button,{onClick:e,variant:"secondary",children:(0,o.__)("Add Mobile Video","jvb")})})})]}),(0,r.jsx)(i.ToggleControl,{label:(0,o.__)("Fade Effect","jvb"),help:(0,o.__)("Add fade class to video element","jvb"),checked:v,onChange:e=>n({fadeEffect:e})})]})}),(0,r.jsx)("div",{...p,children:t?(0,r.jsxs)("div",{className:"video-cover-preview",children:[(0,r.jsx)("img",{src:t,alt:(0,o.__)("Video poster","jvb")}),(0,r.jsx)("div",{className:"video-overlay",children:(0,r.jsxs)("p",{children:[c.length," ",(0,o.__)("desktop source(s)","jvb"),a.length>0&&`, ${a.length} ${(0,o.__)("mobile source(s)","jvb")}`]})})]}):(0,r.jsx)("div",{className:"video-cover-placeholder",children:(0,r.jsx)("p",{children:(0,o.__)("Configure video sources in the sidebar →","jvb")})})}),(0,r.jsx)("div",{className:"inner-blocks",children:(0,r.jsx)(s.InnerBlocks,{})})]})},save:()=>null})}},s={};function i(e){var r=s[e];if(void 0!==r)return r.exports;var l=s[e]={exports:{}};return o[e](l,l.exports,i),l.exports}i.m=o,e=[],i.O=(o,s,r,l)=>{if(!s){var n=1/0;for(a=0;a<e.length;a++){for(var[s,r,l]=e[a],d=!0,t=0;t<s.length;t++)(!1&l||n>=l)&&Object.keys(i.O).every((e=>i.O[e](s[t])))?s.splice(t--,1):(d=!1,l<n&&(n=l));if(d){e.splice(a--,1);var c=r();void 0!==c&&(o=c)}}return o}l=l||0;for(var a=e.length;a>0&&e[a-1][2]>l;a--)e[a]=e[a-1];e[a]=[s,r,l]},i.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={205:0,601:0};i.O.j=o=>0===e[o];var o=(o,s)=>{var r,l,[n,d,t]=s,c=0;if(n.some((o=>0!==e[o]))){for(r in d)i.o(d,r)&&(i.m[r]=d[r]);if(t)var a=t(i)}for(o&&o(s);c<n.length;c++)l=n[c],i.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return i.O(a)},s=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];s.forEach(o.bind(null,0)),s.push=o.bind(null,s.push.bind(s))})();var r=i.O(void 0,[601],(()=>i(128)));r=i.O(r)})();
\ No newline at end of file
+(()=>{"use strict";var e,o={128:()=>{const e=window.wp.blocks,o=window.wp.i18n,l=window.wp.blockEditor,t=window.wp.components,i=window.ReactJSXRuntime,r=["video/mp4","video/webm","video/ogg","video/ogv"],n=[["core/heading",{level:1,placeholder:"Add heading...",textAlign:"center"}],["core/paragraph",{placeholder:"Add description...",align:"center"}],["core/buttons",{layout:{type:"flex",justifyContent:"center"}}]];(0,e.registerBlockType)("jvb/video",{edit:function({attributes:e,setAttributes:s}){const{posterId:a,posterUrl:d,videoSources:c,mobileSources:v,fadeEffect:p,overlayOpacity:b,contentAlignment:m,minHeight:h}=e,j=(0,l.useBlockProps)({className:"video-cover-editor",style:{minHeight:h?`${h}px`:void 0}}),_=(0,l.useInnerBlocksProps)({className:"video-cover-content"},{template:n,templateLock:!1}),u=(e,o=!1)=>{const l={id:e.id,url:e.url,mime:e.mime};o?v.some((o=>o.mime===e.mime))||s({mobileSources:[...v,l]}):c.some((o=>o.mime===e.mime))||s({videoSources:[...c,l]})},g=(e,l=!1)=>0===e.length?null:(0,i.jsx)("ul",{className:"video-source-list",children:e.map(((e,r)=>(0,i.jsxs)("li",{className:"video-source-item",children:[(0,i.jsx)("span",{className:"video-source-mime",children:e.mime}),(0,i.jsx)(t.Button,{isDestructive:!0,isSmall:!0,onClick:()=>((e,o=!1)=>{if(o){const o=[...v];o.splice(e,1),s({mobileSources:o})}else{const o=[...c];o.splice(e,1),s({videoSources:o})}})(r,l),children:(0,o.__)("Remove","jvb")})]},r)))});return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(l.InspectorControls,{children:[(0,i.jsxs)(t.PanelBody,{title:(0,o.__)("Video Settings","jvb"),initialOpen:!0,children:[(0,i.jsx)(t.BaseControl,{label:(0,o.__)("Poster Image","jvb"),help:(0,o.__)("Image shown while video loads","jvb"),children:(0,i.jsx)(l.MediaUploadCheck,{children:(0,i.jsx)(l.MediaUpload,{onSelect:e=>{s({posterId:e.id,posterUrl:e.url})},allowedTypes:["image"],value:a,render:({open:e})=>(0,i.jsxs)(i.Fragment,{children:[d&&(0,i.jsx)("img",{src:d,alt:(0,o.__)("Poster preview","jvb"),style:{maxWidth:"100%",marginBottom:"10px"}}),(0,i.jsx)(t.Button,{onClick:e,variant:d?"secondary":"primary",children:d?(0,o.__)("Change Poster","jvb"):(0,o.__)("Select Poster","jvb")}),d&&(0,i.jsx)(t.Button,{isDestructive:!0,onClick:()=>s({posterId:0,posterUrl:""}),style:{marginLeft:"10px"},children:(0,o.__)("Remove","jvb")})]})})})}),(0,i.jsxs)(t.BaseControl,{label:(0,o.__)("Desktop Video Sources","jvb"),help:(0,o.__)("Add multiple formats for better browser support","jvb"),children:[g(c,!1),(0,i.jsx)(l.MediaUploadCheck,{children:(0,i.jsx)(l.MediaUpload,{onSelect:e=>u(e,!1),allowedTypes:r,render:({open:e})=>(0,i.jsx)(t.Button,{onClick:e,variant:"secondary",children:(0,o.__)("Add Desktop Video","jvb")})})})]}),(0,i.jsxs)(t.BaseControl,{label:(0,o.__)("Mobile Video Sources (Optional)","jvb"),help:(0,o.__)("Smaller videos for mobile devices","jvb"),children:[g(v,!0),(0,i.jsx)(l.MediaUploadCheck,{children:(0,i.jsx)(l.MediaUpload,{onSelect:e=>u(e,!0),allowedTypes:r,render:({open:e})=>(0,i.jsx)(t.Button,{onClick:e,variant:"secondary",children:(0,o.__)("Add Mobile Video","jvb")})})})]}),(0,i.jsx)(t.ToggleControl,{label:(0,o.__)("Fade Effect","jvb"),help:(0,o.__)("Add fade class to video element","jvb"),checked:p,onChange:e=>s({fadeEffect:e})})]}),(0,i.jsxs)(t.PanelBody,{title:(0,o.__)("Overlay Settings","jvb"),initialOpen:!0,children:[(0,i.jsx)(t.RangeControl,{label:(0,o.__)("Overlay Opacity","jvb"),help:(0,o.__)("Darken video for better text readability","jvb"),value:b,onChange:e=>s({overlayOpacity:e}),min:0,max:100,step:5}),(0,i.jsx)(t.SelectControl,{label:(0,o.__)("Content Alignment","jvb"),value:m,options:[{label:(0,o.__)("Top Left","jvb"),value:"top-left"},{label:(0,o.__)("Top Center","jvb"),value:"top-center"},{label:(0,o.__)("Top Right","jvb"),value:"top-right"},{label:(0,o.__)("Center Left","jvb"),value:"center-left"},{label:(0,o.__)("Center","jvb"),value:"center"},{label:(0,o.__)("Center Right","jvb"),value:"center-right"},{label:(0,o.__)("Bottom Left","jvb"),value:"bottom-left"},{label:(0,o.__)("Bottom Center","jvb"),value:"bottom-center"},{label:(0,o.__)("Bottom Right","jvb"),value:"bottom-right"}],onChange:e=>s({contentAlignment:e})}),(0,i.jsx)(t.RangeControl,{label:(0,o.__)("Minimum Height","jvb"),help:(0,o.__)("Minimum height in pixels (leave 0 for auto)","jvb"),value:h,onChange:e=>s({minHeight:e}),min:0,max:1e3,step:50})]})]}),(0,i.jsx)("div",{...j,children:d||c.length>0?(0,i.jsxs)("div",{className:"video-cover-preview",children:[d&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:d,alt:(0,o.__)("Video poster","jvb")}),b>0&&(0,i.jsx)("div",{className:"video-overlay-preview",style:{opacity:b/100}})]}),(0,i.jsx)("div",{className:`video-cover-content-preview align-${m}`,children:(0,i.jsx)("div",{..._})}),(0,i.jsx)("div",{className:"video-info",children:(0,i.jsxs)("p",{children:[c.length," ",(0,o.__)("desktop source(s)","jvb"),v.length>0&&`, ${v.length} ${(0,o.__)("mobile source(s)","jvb")}`]})})]}):(0,i.jsx)("div",{className:"video-cover-placeholder",children:(0,i.jsx)("p",{children:(0,o.__)("Configure video sources in the sidebar →","jvb")})})})]})},save:()=>null})}},l={};function t(e){var i=l[e];if(void 0!==i)return i.exports;var r=l[e]={exports:{}};return o[e](r,r.exports,t),r.exports}t.m=o,e=[],t.O=(o,l,i,r)=>{if(!l){var n=1/0;for(c=0;c<e.length;c++){for(var[l,i,r]=e[c],s=!0,a=0;a<l.length;a++)(!1&r||n>=r)&&Object.keys(t.O).every((e=>t.O[e](l[a])))?l.splice(a--,1):(s=!1,r<n&&(n=r));if(s){e.splice(c--,1);var d=i();void 0!==d&&(o=d)}}return o}r=r||0;for(var c=e.length;c>0&&e[c-1][2]>r;c--)e[c]=e[c-1];e[c]=[l,i,r]},t.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={205:0,601:0};t.O.j=o=>0===e[o];var o=(o,l)=>{var i,r,[n,s,a]=l,d=0;if(n.some((o=>0!==e[o]))){for(i in s)t.o(s,i)&&(t.m[i]=s[i]);if(a)var c=a(t)}for(o&&o(l);d<n.length;d++)r=n[d],t.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return t.O(c)},l=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];l.forEach(o.bind(null,0)),l.push=o.bind(null,l.push.bind(l))})();var i=t.O(void 0,[601],(()=>t(128)));i=t.O(i)})();
\ No newline at end of file
diff --git a/build/video/style-index-rtl.css b/build/video/style-index-rtl.css
index 668a8e1..1508f45 100644
--- a/build/video/style-index-rtl.css
+++ b/build/video/style-index-rtl.css
@@ -1 +1 @@
-.video-cover{display:block;height:auto;-o-object-fit:cover;object-fit:cover;width:100%}.video-cover.fade{animation:fadeIn 1s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.video-cover video{filter:grayscale(100%) contrast(1);flex:1 0 100%;height:105vh;right:50%;max-width:none;mix-blend-mode:multiply;opacity:.85;pointer-events:none;position:absolute;top:0;transform:translate(40%,-10%);width:300vw!important}
+.video-cover-wrapper{display:flex;min-height:400px;overflow:hidden;position:relative;width:100%}.video-cover-wrapper .video-cover-bg{height:auto;right:50%;min-height:100%;min-width:100%;-o-object-fit:cover;object-fit:cover;position:absolute;top:50%;transform:translate(50%,-50%);width:auto;z-index:0}.video-cover-wrapper .video-cover-bg.fade{animation:fadeIn 1s ease-in}.video-cover-wrapper .video-cover-overlay{background:#000;bottom:0;right:0;position:absolute;left:0;top:0;z-index:1}.video-cover-wrapper .video-cover-content{color:#fff;padding:2rem;position:relative;width:100%;z-index:2}.video-cover-wrapper .video-cover-content h1,.video-cover-wrapper .video-cover-content h2,.video-cover-wrapper .video-cover-content h3,.video-cover-wrapper .video-cover-content h4,.video-cover-wrapper .video-cover-content h5,.video-cover-wrapper .video-cover-content h6{color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.5)}.video-cover-wrapper .video-cover-content p{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}.video-cover-wrapper .video-cover-content .wp-block-button__link{text-shadow:none}.video-cover-wrapper.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover-wrapper.align-top-center{align-items:flex-start;justify-content:center}.video-cover-wrapper.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover-wrapper.align-center-left{align-items:center;justify-content:flex-start}.video-cover-wrapper.align-center{align-items:center;justify-content:center}.video-cover-wrapper.align-center-right{align-items:center;justify-content:flex-end}.video-cover-wrapper.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover-wrapper.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover-wrapper.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover-wrapper.alignfull{margin-right:calc(50% - 50vw);margin-left:calc(50% - 50vw);max-width:none;width:100vw}.video-cover-wrapper.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@media(max-width:768px){.video-cover-wrapper{min-height:300px}.video-cover-wrapper .video-cover-content{padding:1.5rem}}@media(max-width:480px){.video-cover-wrapper{min-height:250px}.video-cover-wrapper .video-cover-content{padding:1rem}}
diff --git a/build/video/style-index.css b/build/video/style-index.css
index 01411b5..5a65ae4 100644
--- a/build/video/style-index.css
+++ b/build/video/style-index.css
@@ -1 +1 @@
-.video-cover{display:block;height:auto;-o-object-fit:cover;object-fit:cover;width:100%}.video-cover.fade{animation:fadeIn 1s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.video-cover video{filter:grayscale(100%) contrast(1);flex:1 0 100%;height:105vh;left:50%;max-width:none;mix-blend-mode:multiply;opacity:.85;pointer-events:none;position:absolute;top:0;transform:translate(-40%,-10%);width:300vw!important}
+.video-cover-wrapper{display:flex;min-height:400px;overflow:hidden;position:relative;width:100%}.video-cover-wrapper .video-cover-bg{height:auto;left:50%;min-height:100%;min-width:100%;-o-object-fit:cover;object-fit:cover;position:absolute;top:50%;transform:translate(-50%,-50%);width:auto;z-index:0}.video-cover-wrapper .video-cover-bg.fade{animation:fadeIn 1s ease-in}.video-cover-wrapper .video-cover-overlay{background:#000;bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.video-cover-wrapper .video-cover-content{color:#fff;padding:2rem;position:relative;width:100%;z-index:2}.video-cover-wrapper .video-cover-content h1,.video-cover-wrapper .video-cover-content h2,.video-cover-wrapper .video-cover-content h3,.video-cover-wrapper .video-cover-content h4,.video-cover-wrapper .video-cover-content h5,.video-cover-wrapper .video-cover-content h6{color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.5)}.video-cover-wrapper .video-cover-content p{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5)}.video-cover-wrapper .video-cover-content .wp-block-button__link{text-shadow:none}.video-cover-wrapper.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover-wrapper.align-top-center{align-items:flex-start;justify-content:center}.video-cover-wrapper.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover-wrapper.align-center-left{align-items:center;justify-content:flex-start}.video-cover-wrapper.align-center{align-items:center;justify-content:center}.video-cover-wrapper.align-center-right{align-items:center;justify-content:flex-end}.video-cover-wrapper.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover-wrapper.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover-wrapper.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover-wrapper.alignfull{margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);max-width:none;width:100vw}.video-cover-wrapper.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@media(max-width:768px){.video-cover-wrapper{min-height:300px}.video-cover-wrapper .video-cover-content{padding:1.5rem}}@media(max-width:480px){.video-cover-wrapper{min-height:250px}.video-cover-wrapper .video-cover-content{padding:1rem}}
diff --git a/globals.php b/globals.php
index 49af2a9..32762da 100644
--- a/globals.php
+++ b/globals.php
@@ -3,6 +3,8 @@
 	exit;
 }
 
+use JVBase\utility\Features;
+
 function jvbGlobalDirectories():array
 {
     $directories = get_option(BASE.'_global_directories');
@@ -210,13 +212,16 @@
         $manageableContent = false;
     }
     if ($manageableContent === false) {
+
         $manageableContent = [];
         $bios = [];
         foreach (JVB_USER as $role => $config) {
             $manageableContent = array_merge($manageableContent, jvbRolePages($role));
         }
 
-
+		if (Features::forSite()->has('referrals')) {
+			$manageableContent[] = 'referrals';
+		}
         foreach (JVB_TAXONOMY as $tax => $config) {
             if (jvbCheck('is_content', $config)) {
                 $manageableContent[] = strtolower($config['plural']);
diff --git a/inc/blocks/VideoCoverBlock.php b/inc/blocks/VideoCoverBlock.php
index e6292cd..c0b18a6 100644
--- a/inc/blocks/VideoCoverBlock.php
+++ b/inc/blocks/VideoCoverBlock.php
@@ -14,7 +14,7 @@
 class VideoCoverBlock
 {
 	protected static ?VideoCoverBlock $instance = null;
-	protected string $path = JVB_DIR . '/build/video-cover';
+	protected string $path = JVB_DIR . '/build/video';
 
 	public static function getInstance(): VideoCoverBlock
 	{
@@ -47,6 +47,9 @@
 		$mobile_sources = $attributes['mobileSources'] ?? [];
 		$css_class = $attributes['className'] ?? '';
 		$fade_class = $attributes['fadeEffect'] ?? false ? 'fade' : '';
+		$overlay_opacity = $attributes['overlayOpacity'] ?? 0;
+		$content_alignment = $attributes['contentAlignment'] ?? 'center';
+		$min_height = $attributes['minHeight'] ?? 0;
 		//Get date of current post
 		global $post;
 		$date = date('c',strtotime($post->post_date));
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 44cf5cd..5c806d0 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -327,7 +327,7 @@
 	if (wp_script_is('jvb-uploader')) {
 		?>
 		<template class="emptyGroup">
-			<div class="empty-group">
+			<div class="empty-group row">
 				<p>Drag here to create new group!</p>
 			</div>
 		</template>
@@ -335,27 +335,41 @@
 			<?= jvbImageMeta() ?>
 		</template>
 		<template class="imageGroup">
-			<details class="upload-group">
-				<summary class="row btw">
-					<div class="group-header">
-						<div class="group-actions">
-							<button type="button" class="add-selection-to-group" title="Add selected images to this group">
-								<?= jvbIcon('add') ?>
-								Add to Group
-							</button>
-							<button type="button" class="remove-group" title="Delete group">
-								<?= jvbIcon('delete') ?>
-								Delete Group
-							</button>
+			<div class="upload-group">
+				<div class="group-header">
+					<div class="selected">
+						<div class="field">
+							<input type="checkbox" id="select-all-group" name="select-all-group">
+							<label for="select-all-group">
+								Select All
+							</label>
+						</div>
+						<div class="info" hidden>
 						</div>
 					</div>
-					<div class="group-content col">
-						<div class="item-grid group"></div>
-						<p class="hint group-count"></p>
+					<div class="group-actions">
+						<button type="button" data-action="add-to-group" title="Add selected uploads to this group">
+							<?= jvbIcon('add') ?>
+							Add Here
+						</button>
+						<button type="button" data-action="delete-group" title="Delete group">
+							<?= jvbIcon('delete') ?>
+							Delete Group
+						</button>
 					</div>
-				</summary>
-				<div class="fields"></div>
-			</details>
+				</div>
+				<details>
+					<summary class="row btw">
+						Extra Fields
+					</summary>
+					<div class="fields"></div>
+				</details>
+				<div class="group-content col">
+					<div class="item-grid group"></div>
+					<p class="hint group-count"></p>
+				</div>
+			</div>
+
 		</template>
 		<template class="groupActions">
 			<div class="item-actions">
@@ -368,7 +382,7 @@
 					</label>
 				</div>
 
-				<button type="button" class="remove" title="Remove from Group">
+				<button type="button" data-action="remove-from-group" title="Remove from Group">
 					<?=jvbIcon('delete')?>
 				</button>
 			</div>
@@ -379,14 +393,10 @@
 					<span class="selection-count">0</span> items selected
 				</div>
 				<div class="selection-controls">
-					<button type="button" class="create-from-selection">
+					<button type="button" data-action="add-to-group">
 						<?= jvbIcon('add') ?>
 						New Group
 					</button>
-					<button type="button" class="deselect-all">
-						<?= jvbIcon('close') ?>
-						Deselect All
-					</button>
 				</div>
 			</div>
 		</template>
@@ -412,28 +422,28 @@
 			</div>
 		</template>
 		<template class="uploadItem">
-			<div class="upload-item">
+			<div class="item upload">
 				<div class="preview">
-					<img>
-					<?php jvbRenderProgressBar('<span class="icon"></span><span class="details"></span>') ?>
-					<div class="actions">
-						<input type="checkbox" class="upload-select" name="select-item" id="select-item">
-						<label for="select-item" aria-label="Select image"></label>
-						<div class="item-actions">
-							<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" class="remove" title="Remove from Group">
-								<?=jvbIcon('delete')?>
-							</button>
+					<?php jvbRenderProgressBar('',true) ?>
+					<input type="checkbox" class="upload-select" name="select-item" id="select-item">
+					<label for="select-item" aria-label="Select image">
+						<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('delete')?>
+						</button>
 					</div>
 				</div>
 				<details>
@@ -442,35 +452,37 @@
 			</div>
 		</template>
 		<template class="restoreNotification">
-			<div class="restore-notification">
-				<div class="restore-content">
+			<dialog class="restore-uploads">
+				<div class="wrap">
 					<div class="restore-message">
 						<h4>Looks like we left things hanging</h4>
 						<p class="restore-details"></p>
 						<p class="hint">If you'd rather start over, you can clear this information.</p>
 					</div>
 					<div class="restore-actions">
-						<div class="selection-controls">
-							<button type="button" class="select-all-restore">Select All</button>
-							<button type="button" class="select-none-restore">Select None</button>
+						<div class="selected">
+							<div class="field">
+								<input type="checkbox" id="select-all-restore" name="select-all-restore">
+								<label for="select-all-restore">
+									Select All
+								</label>
+							</div>
+							<div class="info" hidden>
+							</div>
 						</div>
-						<div class="action-buttons">
-							<button type="button" class="restore-selected">
+						<div class="m-actions row nowrap">
+							<button type="button" data-action="restore">
 								<?= jvbIcon('restore') ?>
 								Restore Selected
 							</button>
-							<button type="button" class="restart-uploads">
-								<?= jvbIcon('delete') ?>
-								Scrap Cache
-							</button>
-							<button type="button" class="dismiss-cache-check" title="Dismiss notification">
+							<button type="button" data-action="clear-cache" title="Clear cache and close window">
 								<?= jvbIcon('close') ?>
-								Dismiss
+								Clear Cache
 							</button>
 						</div>
 					</div>
 				</div>
-			</div>
+			</dialog>
 		</template>
 		<template class="restoreField">
 			<div class="restore-field">
@@ -478,24 +490,6 @@
 				<div class="item-grid restore"></div>
 			</div>
 		</template>
-		<template class="restoreItem">
-			<label class="restore-item">
-				<div class="preview">
-					<img>
-					<div class="image-placeholder"><svg width="40" height="40" fill="currentColor" viewBox="0 0 256 256">
-							<path d="M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40ZM56,56H200V158.75l-26.07-26.06a16,16,0,0,0-22.63,0l-20,20-44-44a16,16,0,0,0-22.62,0L56,117.37V56Zm144,144H56V141.67l36.7-36.7,44,44a8,8,0,0,0,11.31,0l20-20L200,161.67V200ZM148,100a12,12,0,1,1,12,12A12,12,0,0,1,148,100Z"/>
-						</svg></div>
-				</div>
-				<div class="restore-item-info">
-					<div class="name"></div>
-					<div class="restore-item-meta">
-					</div>
-				</div>
-				<div class="restore-item-controls">
-					<input type="checkbox" id="restore-" class="restore-checkbox" checked>
-				</div>
-			</label>
-		</template>
 
 		<template class="startOverConfirmation">
 			<dialog class="start-over-confirmation">
@@ -519,6 +513,12 @@
 				</div>
 			</dialog>
 		</template>
+		<template class="dragPreview">
+			<div class="drag-preview">
+				<div class="drag-items"><div class="drag-item"></div></div>
+				<div class="drag-count" hidden></div>
+			</div>
+		</template>
 		<?php
 	}
 	if (wp_script_is('jvb-selector')) {
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index 7026729..1831a33 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -1,5 +1,7 @@
 <?php
 
+use JVBase\utility\Features;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -9,11 +11,12 @@
 
 function jvbClientQueue():void
 {
-    if (!jvbSiteHasDashboard() || !is_user_logged_in()) {
+    if (!Features::forSite()->has('dashboard') || !is_user_logged_in()) {
         return;
     }
+
     ?>
-    <aside id="queue" class="col start btw" aria-expanded="false" hidden>
+    <aside id="queue" class="left col start btw" aria-expanded="false" hidden>
         <div class="status-actions row start nowrap">
 			<div class="refresh row btw">
                 <span class="countdown row" title="Will refresh again...">5</span>
@@ -60,6 +63,7 @@
     </aside>
 	<button class="qtoggle row" title="Show Queue" aria-controls="queue" hidden>
 		<?= jvbIcon('save') ?>
+		<span class="screen-reader-text"></span>
 		<span class="indicator"></span>
 		<span class="count row"></span>
 	</button>
@@ -106,7 +110,7 @@
         ob_start();
         ?>
         <li>
-            <a href="<?=get_home_url(2, '/dash/')?>" title="Behind the Scenes">
+            <a href="<?=get_home_url(null, '/dash/')?>" title="Behind the Scenes">
                 <?= jvbIcon('dashboard', ['title' => 'Behind the Scenes'])?>
                 <span class="screen-reader-text">Go Behind the Scenes</span>
             </a>
@@ -119,7 +123,7 @@
                 </span>
             </button>
             <ul class="notifications-preview submenu">
-                <li id="view-all"><a href="<?=get_home_url(2, '/dash/notifications/')?>" class="view-all">View All Notifications</a></li>
+                <li id="view-all"><a href="<?=get_home_url(null, '/dash/notifications/')?>" class="view-all">View All Notifications</a></li>
             </ul>
             <template class="notificationItem">
                 <li class="notification-preview">
@@ -168,7 +172,7 @@
                 'fields'    => 'ids'
             ));
             if ($page->have_posts()) {
-                $end = ($t == 'About') ? '<li><a href="'.get_home_url(2, '/directory/partners/').'" title="View our Partners">Partners</a></li>' : '';
+                $end = ($t == 'About') ? '<li><a href="'.get_home_url(null, '/directory/partners/').'" title="View our Partners">Partners</a></li>' : '';
                 $links .= $open.get_the_permalink($page->posts[0]).'" title="'.$t.$mid.$t.$close.$end;
             }
             wp_reset_postdata();
@@ -324,6 +328,7 @@
     return ob_get_clean();
 }
 
+add_action('wp_footer', 'jvbLoadingScreen');
 function jvbLoadingScreen():string
 {
 	return '<dialog class="loading">
@@ -364,6 +369,7 @@
 	$header = '<nav class="tabs row start" role="tablist">';
 	$content = '';
 	$i = 0;
+
 	foreach ($tabs as $slug => $config) {
 		//Header
 		$active = ($i === 0) ? ' active' : '';
@@ -386,7 +392,17 @@
 		$content .= '>
 			<h2>'.$config['title'].'</h2>';
 			if ( $config['description']) {
-				$content .= apply_filters('the_content', $config['description']);
+				if (!is_array($config['description'])) {
+					$content .= apply_filters('the_content', $config['description']);
+				} else {
+//					foreach ($config['description'] as $desc) {
+//						$content .= apply_filters('the_content', $desc);
+//					}
+					$content .= implode('',array_map(function ($paragraph) {
+						return apply_filters('the_content', $paragraph);
+					}, $config['description']));
+				}
+
 			}
 
 		$content .= $config['content'].'
@@ -403,10 +419,11 @@
 	return $out;
 }
 
-function jvbRenderProgressBar(string $inside ='')
+function jvbRenderProgressBar(string $inside ='', $top = false)
 {
+	$top = $top ? ' abs top' : '';
 	?>
-	<div class="progress">
+	<div class="progress<?=$top?>">
 		<div class="bar">
 			<div class="fill"></div>
 		</div>
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index 05cc87c..4288820 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -1137,7 +1137,7 @@
 	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
 	{
 		$site_name = get_bloginfo('name');
-		$reset_url = get_home_url(2, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
+		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
 
 		$message = sprintf(
 			"Welcome to %s!\n\n" .
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index a7b5451..4eb7b1c 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -717,24 +717,21 @@
 		// User login tracking for security
 		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 
-		add_action('wp_footer', [$this, 'outputCheckout']);
+		add_action('jvbAdditionalActions', [$this, 'outputCheckout']);
 
 		// Enqueue checkout scripts
 		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 	}
 
 
-	public function outputCheckout():void {
+	public function outputCheckout(array $actions):array {
 		if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
-			return;
+			return $actions;
 		}
-		?>
-		<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
-			<?= jvbIcon('cart')?><span class="abs"></span><span class="abs count"></span>
-		</button>
-		<aside id="cart">
-			<form id="checkout" data-form-id="checkout" data-save="checkout">
-				<?php
+
+		$form = '<aside id="cart" class="right">
+			<form id="checkout" data-form-id="checkout" data-save="checkout">';
+
 				$tabs = [
 					'cartItems' => [
 						'title'	=> 'Your Order',
@@ -743,10 +740,10 @@
 						'content'	=> $this->cartContent()
 					],
 					'checkout'	=> [
-						'title'	=> 'Checkout',
-						'icon'	=> 'checkout',
-						'description' => 'Securely checkout with your name, email, and payments processed by Square.',
-						'content'	=> '<div class="checkout-section">
+			'title'	=> 'Checkout',
+			'icon'	=> 'checkout',
+			'description' => 'Securely checkout with your name, email, and payments processed by Square.',
+			'content'	=> '<div class="checkout-section">
 								<h3>Customer Information</h3>
 
 								<input type="text" name="name" placeholder="Full Name" required autocomplete="name">
@@ -762,29 +759,28 @@
 								<div id="saved-cards"></div>
 								<div id="square-card-container"></div>
 							</div>'
-					],
+		],
 					'order'	=> [
-						'title'	=> 'Your Order',
-						'icon' => 'truck',
-						'hidden'	=> true,
-						'description' => '',
-						'content'	=> $this->renderOrderStatus()
-					]
+			'title'	=> 'Your Order',
+			'icon' => 'truck',
+			'hidden'	=> true,
+			'description' => '',
+			'content'	=> $this->renderOrderStatus()
+		]
 				];
-				jvbRenderTabs($tabs);
-				?>
+		$form .= jvbRenderTabs($tabs);
 
-				<div class="cart-total row end"><p class="tax">Tax: <span></span></p><p class="total">GRAND TOTAL: <span></span></p></div>
-			</form>
+		$form .= '<div class="cart-total row end"><p class="tax">Tax: <span></span></p><p class="total">GRAND TOTAL: <span></span></p></div>
+		</form>
 		</aside>
 		<template class="restoredCart">
 			<div class="restored">
 				<h3>Looks like we left things hanging</h3>
-				<p>We've restored your cart from your last session below.</p>
-				<p>If you'd rather start over, click the button below.</p>
+				<p>We\'ve restored your cart from your last session below.</p>
+				<p>If you\'d rather start over, click the button below.</p>
 				<div class="row btw">
-					<button type="button" onclick="window.squareCheckout.clearCart();this.closest('.restored').remove()"><?=jvbIcon('trash')?>Clear Cart</button>
-					<button type="button" onclick="this.closest('.restored').remove()"><?= jvbIcon('x')?>Dismiss</button>
+					<button type="button" onclick="window.squareCheckout.clearCart();this.closest(\'.restored\').remove()">'.jvbIcon('trash').'Clear Cart</button>
+					<button type="button" onclick="this.closest(\'.restored\').remove()">'.jvbIcon('x').'Dismiss</button>
 				</div>
 			</div>
 		</template>
@@ -794,13 +790,11 @@
 					<label for="quantity"></label>
 					<div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="17" data-id="">
 
-						<button type="button" class="decrease"aria-label="Decrease Add to Order">
-							<i class="icon minus"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h80A4,4,0,0,1,172,128Z"></path></svg></i>				 </button>
+						<button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus').'</button>
 
 						<input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input">
 
-						<button type="button" class="increase" aria-label="Increase Add to Order">
-							<i class="icon add"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H132v36a4,4,0,0,1-8,0V132H88a4,4,0,0,1,0-8h36V88a4,4,0,0,1,8,0v36h36A4,4,0,0,1,172,128Z"></path></svg></i>				 </button>
+						<button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('add').'</button>
 					</div>
 				</td>
 				<td class="price">
@@ -810,17 +804,25 @@
 					<span class="total"></span>
 				</td>
 				<td>
-					<button type="button" data-remove-from-cart><?= jvbIcon('trash')?></button>
+					<button type="button" data-remove-from-cart>'.jvbIcon('trash').'</button>
 				</td>
 			</tr>
 		</template>
 		<template class="emptyCart">
 			<div class="empty">
 				<p><i><b>No items in cart.</b></i></p>
-				<p>You can <a href="<?= get_post_type_archive_link(BASE.'menu_item')?>" title="Browse our menu">browse our menu</a> to order.</p>
+				<p>You can <a href="'.get_post_type_archive_link(BASE.'menu_item').'" title="Browse our menu">browse our menu</a> to order.</p>
 			</div>
-		</template>
-		<?php
+		</template>';
+
+
+		$actions[] = [
+			'button' => 	'<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
+					'.jvbIcon('cart').'<span class="abs"></span><span class="abs count"></span>
+				</button>',
+			'content' =>	$form
+		];
+		return $actions;
 	}
 
 	private function cartContent():string
@@ -1684,7 +1686,7 @@
 	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
 	{
 		$site_name = get_bloginfo('name');
-		$reset_url = get_home_url(2, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
+		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
 
 		$message = sprintf(
 			"Welcome to %s!\n\n" .
@@ -1939,6 +1941,7 @@
 				'jvb-cache',
 				'jvb-tabs',
 				'jvb-modal',
+				'jvb-popup'
 			],
 			'1.0.0',
 			[
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 9589fdc..6deb11e 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -181,7 +181,8 @@
 	protected function renderHeaderActions():void
 	{
 		$uploadConfig = [
-			'type'			=> 'image',
+			'type'			=> 'upload',
+			'subtype'		=> 'image',
 			'mode'			=> (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection',
 			'create_new'	=> true,
 			'label'			=> (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'],
@@ -189,11 +190,12 @@
 			'singular'		=> $this->singular,
 			'plural'		=> $this->plural,
 			'multiple'		=> true,
+			'destination'	=> 'post'
 		];
 		if (!jvbCheck('single_image', $this->config)) {
-			$uploadConfig['imageType'] = 'groupable';
+			$uploadConfig['destination'] = 'post_group';
 		}
-
+		$uploadConfig['destination'] = 'post_group';
 		if (!jvbCheck('single_image', $this->config)) {
 			$uploadConfig['group_title'] = 'Create '.$this->config['plural'];
 			$uploadConfig['group_description'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index e15be50..0f75df3 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -3,6 +3,7 @@
 
 use JVBase\managers\CRUD;
 use JVBase\meta\MetaManager;
+use JVBase\utility\Features;
 use WP_User;
 
 if (!defined('ABSPATH')) {
@@ -126,6 +127,120 @@
         remove_action('init', [$this, 'buildDashboard']);
     }
 
+	protected function getAllDashboardPages():array
+	{
+		$manageableContent = get_option(BASE.'all_dashboard_pages');
+		if (JVB_TESTING) {
+			$manageableContent = false;
+		}
+		if ($manageableContent === false) {
+
+			$manageableContent = [];
+			$bios = [];
+			foreach (JVB_USER as $role => $config) {
+				$manageableContent = array_merge($manageableContent, jvbRolePages($role));
+			}
+
+			if (Features::forSite()->has('referrals')) {
+				$manageableContent[] = 'referrals';
+			}
+			foreach (JVB_TAXONOMY as $tax => $config) {
+				if (Features::forTaxonomy($tax)->has('is_content')) {
+					$manageableContent[] = strtolower($config['plural']);
+				}
+			}
+			if (Features::forMembership()->has('can_invite')) {
+				$manageableContent[] = 'invites';
+			}
+
+			if (Features::forMembership()->has('term_approval')) {
+				$manageableContent[] = 'approvals';
+			}
+
+			if (Features::forMembership()->has('forum')) {
+				$manageableContent[] = 'news';
+			}
+
+			if (Features::forMembership()->has('member_content')) {
+				$manageableContent[] = 'metrics';
+			}
+
+			if (!empty($bios)) {
+				$manageableContent[] = 'bio';
+			}
+
+			if (Features::forSite()->has('favourites')) {
+				$manageableContent[] = 'favourites';
+			}
+
+			if (Features::anyContentHas('karma') || Features::anyTaxonomyHas('karma') || Features::anyUserHas('karma')){
+				$manageableContent[] = 'karmic-score';
+			}
+
+			if (Features::forSite()->has('notifications')) {
+				$manageableContent[] = 'notifications';
+			}
+
+			if (Features::forSite()->has('support')){
+				$manageableContent[] = 'support';
+			}
+
+			if (Features::hasAnyIntegration()) {
+				$manageableContent[] = 'integrations';
+			}
+
+			$manageableContent[] = 'admin';
+			$manageableContent = apply_filters('jvbDashboardPages', $manageableContent);
+			$manageableContent = array_unique($manageableContent);
+			sort($manageableContent);
+			$manageableContent = array_map(function ($content) {
+				return str_replace('_', '-', $content);
+			}, $manageableContent);
+			update_option(BASE.'all_dashboard_pages', $manageableContent);
+		}
+
+
+		return $manageableContent;
+	}
+
+	protected function getRolePages(string $role):array
+	{
+		if (!array_key_exists(jvbNoBase($role), JVB_USER)) {
+			return [];
+		}
+		$manageableContent = get_option(BASE.$role.'_pages');
+		if (JVB_TESTING) {
+			$manageableContent = false;
+		}
+		if ($manageableContent === false) {
+			$manageableContent = [];
+			$config = JVB_USER[$role];
+			$content = $config['can_create'];
+			$settings = $bio = false;
+			if (array_key_exists('profile', $config)) {
+				$manageableContent[] = $config['profile'];
+			}
+
+			foreach ($content as $c) {
+				if (is_array($c)) {
+					foreach ($c as $type => $contents) {
+						$manageableContent = array_merge($manageableContent, $contents);
+					}
+				} else {
+					$manageableContent = array_merge($manageableContent, [$c]);
+				}
+			}
+
+			if (array_key_exists('has_dashboard', $config)) {
+				$manageableContent[] = 'settings';
+			}
+
+			update_option(BASE.$role.'_pages', $manageableContent);
+		}
+
+		return $manageableContent;
+	}
+
     protected function getTitle(string $page):string
     {
         $content = JVB_CONTENT;
@@ -206,7 +321,14 @@
             return $template;
         }
         if (!isOurPeople() && !current_user_can('manage_options')) {
-            wp_redirect(wp_login_url(get_home_url(2, '/dash')));
+			error_log('Redirecting because:');
+			if (!isOurPeople()) {
+				error_log('Not our people');
+			}
+			if (!current_user_can('manage_options')) {
+				error_log('Cannot manage options');
+			}
+            wp_redirect(wp_login_url(get_home_url(null, '/dash')));
             exit;
         }
 
@@ -214,6 +336,65 @@
 
         $page = $this->getCurrentPage();
 
+		switch ($page) {
+			case 'integrations':
+				if (!Features::hasAnyIntegration('user', $this->role)) {
+					wp_redirect(get_home_url(null, '/dash'));
+					exit;
+				}
+				break;
+			case 'bluesky':
+			case 'cloudflare':
+			case 'facebook':
+			case 'google-maps':
+			case 'google-my-business':
+			case 'helcim':
+			case 'instagram':
+			case 'square':
+			case 'umami':
+				if (!Features::hasIntegration($page,'user', $this->role)) {
+					wp_redirect(get_home_url(null, '/dash'));
+					exit;
+				}
+				break;
+			case 'bio':
+				$permission = JVB_USER[$this->role]['profile']??false;
+				if (!$permission || (!current_user_can('manage_'.$permission) && !current_user_can('manage_options'))) {
+					wp_redirect(get_home_url(null, '/dash'));
+					exit;
+				}
+				break;
+			case 'settings':
+				if (!current_user_can('manage_settings') && !current_user_can('manage_options')) {
+					wp_redirect(get_home_url(null, '/dash'));
+            		exit;
+				}
+				break;
+			case 'approval':
+				if (!current_user_can('skip_moderation')) {
+					wp_redirect(get_home_url(null, '/dash'));
+					exit;
+				}
+				break;
+				case 'dash':
+
+					break;
+			default:
+				$type = match($page) {
+					'menu-item' => 'menu_item',
+					'events'	=> 'event',
+					default => $page
+				};
+
+				$permission = strtolower(str_replace(' ', '_',JVB_CONTENT[$type]['plural']??$type.'s'));
+
+				if (!current_user_can('edit_'.$permission)) {
+					error_log('User cannot edit: '.$permission);
+					wp_redirect(get_home_url(null, '/dash'));
+					exit;
+				}
+				break;
+		}
 		// Enqueue needed styles/scripts
 		jvbInlineStyles('nav');
 		jvbInlineStyles('dash');
@@ -415,7 +596,7 @@
 				$page = str_replace('_', '-', $page);
 				$link = ($page === 'dash') ? '/'.$page : "/dash/$page";
 				?>
-				<link rel="preconnect" href="<?= get_home_url(2, $link)?>"/>
+				<link rel="preconnect" href="<?= get_home_url(null, $link)?>"/>
 				<?php
 			}
 			 ?>
@@ -477,7 +658,7 @@
                     printf(
                         '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
                         $active,
-                        get_home_url(2, $link),
+                        get_home_url(null, $link),
                         $current,
                         $page,
                         $title,
@@ -525,7 +706,7 @@
             $description = $this->getDescription($page);
 
             if ($title !== '') {
-                echo '<li><p><a href="'.get_home_url(2, '/dash/'.$url.'/').'"
+                echo '<li><p><a href="'.get_home_url(null, '/dash/'.$url.'/').'"
                     data-page="'.$url.'" data-dash>'.jvbIcon($page).ucfirst($title).'</a></p>'.$description.'</li>';
             }
 
@@ -543,10 +724,7 @@
      */
     protected function renderForm(string $type):void
     {
-        if (!current_user_can('manage_'.$type)) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
+
         wp_enqueue_script(
             'jvb-bio-manager',
             JVB_URL.'assets/js/min/bioManager.min.js',
@@ -562,10 +740,6 @@
 
     protected function renderSettings():void
     {
-        if (!current_user_can('manage_options') && !current_user_can('manage_settings')) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
 		wp_enqueue_script('jvb-form');
         wp_enqueue_script(
             'jvb-bio-manager',
@@ -593,7 +767,7 @@
 		if (!empty($integrations)) {
 			$out = '<nav class="integrations"><ul>';
 
-			$url = get_home_url(2, '/dash/integrations/');
+			$url = get_home_url(null, '/dash/integrations/');
 			$out .= '<li><a href="'.$url.'">'.jvbIcon('plugs-connected').'Integrations</a></li>';
 			foreach ($integrations as $name=> $integration) {
 				if (!JVB()->userCanConnect($name, $this->user->ID) || !$integration->hasDefaults()) {
@@ -609,12 +783,6 @@
 
 	protected function renderIntegrations(string $page):void
 	{
-
-		//TODO: Make manage_integrations permission
-//		if (!current_user_can('manage_integrations')) {
-//			wp_redirect(get_home_url(2, '/dash'));
-//			exit;
-//		}
 		echo $this->getIntegrationsMenu();
 		$map = [
 			'google-my-business' => 'gmb',
@@ -653,10 +821,6 @@
 
     protected function renderApprovals():void
     {
-        if (!current_user_can('skip_moderation')) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
         ?>
         <div class="approvals container">
             <nav class="tabs row start" role="tablist">
@@ -730,12 +894,6 @@
 			'events'	=> 'event',
 			default => $type
 		};
-
-		$permission = JVB_CONTENT[$type]['plural']??$type.'s';
-        if (!current_user_can('edit_'.$permission)) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
         $crud = new CRUD($type);
         $crud->render();
 
@@ -743,12 +901,6 @@
 
     protected function renderAdmin():void
     {
-        //TODO: This has to be built from the settings from setup.php
-        if (!current_user_can('manage_options')) {
-            wp_redirect(get_home_url(2, '/dash'));
-            exit;
-        }
-
         ?>
         <nav class="tabs row start" role="tablist">
         <?php
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index f2418cc..856eab8 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -97,7 +97,7 @@
                     'slug'  => $slug,
                     'title' => $title,
                     'ID'    => $ID,
-                    'url'   => get_home_url(2, '/directory/'.$slug),
+                    'url'   => get_home_url(null, '/directory/'.$slug),
                     'page'  => $title,
                     'description'    =>$config[$directory]['description']??[],
                     'type'  => $type,
@@ -126,7 +126,7 @@
                         'slug'  => $slug,
                         'title' => $title,
                         'ID'    => $ID,
-                        'url'   => get_home_url(2, '/directory/'.$slug),
+                        'url'   => get_home_url(null, '/directory/'.$slug),
                         'page'  => $title,
                         'description'    =>$config[$directory]['description']??[],
                         'type'  => $type,
@@ -150,7 +150,7 @@
                     'slug'  => 'map',
                     'title' => 'Map',
                     'ID'    => $ID,
-                    'url'   => get_home_url(2, '/directory/map'),
+                    'url'   => get_home_url(null, '/directory/map'),
                     'page'  => 'Map',
                     'type'  => 'term',
                 ];
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 7b04aef..7bce0db 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -195,7 +195,7 @@
     public function handleSuccessfulLogin(string $username, WP_User $user): void
     {
         if (isOurPeople() && !user_can($user, 'manage_options')) {
-            wp_redirect(get_home_url(2, '/dash'));
+            wp_redirect(get_home_url(null, '/dash'));
             exit;
         }
     }
diff --git a/inc/managers/MagicLinkManager.php b/inc/managers/MagicLinkManager.php
new file mode 100644
index 0000000..5b551d2
--- /dev/null
+++ b/inc/managers/MagicLinkManager.php
@@ -0,0 +1,642 @@
+<?php
+namespace JVBase\managers;
+
+use WP_User;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Magic Link Authentication Manager
+ *
+ * Handles passwordless authentication via email magic links.
+ * Can be used for referral signups, password resets, or general login.
+ */
+class MagicLinkManager
+{
+	protected CacheManager $cache;
+	protected EmailManager $email;
+
+	// Token settings
+	protected int $token_expiry = 900; // 15 minutes in seconds
+	protected int $rate_limit_window = 3600; // 1 hour
+	protected int $max_attempts_per_hour = 5;
+
+	// Link types - allows different flows for different purposes
+	const TYPE_LOGIN = 'login';
+	const TYPE_SIGNUP = 'signup';
+	const TYPE_REFERRAL = 'referral';
+	const TYPE_RESET = 'reset';
+
+	public function __construct()
+	{
+		$this->cache = new CacheManager('magic_links', $this->token_expiry);
+		$this->email = new EmailManager();
+
+		// Hook into WordPress auth flow
+		add_action('template_redirect', [$this, 'handleMagicLinkClick']);
+		add_action('wp_login_failed', [$this, 'handleFailedLogin']);
+
+		// Add magic link option to login page
+		add_action('login_form', [$this, 'addMagicLinkOption']);
+		add_filter('authenticate', [$this, 'blockStandardAuth'], 30, 3);
+	}
+
+	/**
+	 * Generate and send a magic link
+	 *
+	 * @param string $email User's email address
+	 * @param string $type Type of magic link (login, signup, referral, reset)
+	 * @param array $context Additional context (referral_code, redirect_url, etc.)
+	 * @return true|WP_Error
+	 */
+	public function sendMagicLink(string $email, string $type = self::TYPE_LOGIN, array $context = [])
+	{
+		// Validate email
+		$email = sanitize_email($email);
+		if (!is_email($email)) {
+			return new WP_Error('invalid_email', 'Invalid email address');
+		}
+
+		// Check rate limiting
+		$rate_check = $this->checkRateLimit($email);
+		if (is_wp_error($rate_check)) {
+			return $rate_check;
+		}
+
+		// Handle different link types
+		switch ($type) {
+			case self::TYPE_LOGIN:
+				return $this->sendLoginLink($email, $context);
+
+			case self::TYPE_SIGNUP:
+				return $this->sendSignupLink($email, $context);
+
+			case self::TYPE_REFERRAL:
+				return $this->sendReferralLink($email, $context);
+
+			case self::TYPE_RESET:
+				return $this->sendResetLink($email, $context);
+
+			default:
+				return new WP_Error('invalid_type', 'Invalid magic link type');
+		}
+	}
+
+	/**
+	 * Send login magic link to existing user
+	 */
+	protected function sendLoginLink(string $email, array $context): bool|WP_Error
+	{
+		// Check if user exists
+		$user = get_user_by('email', $email);
+		if (!$user) {
+			return new WP_Error('user_not_found', 'No account found with this email');
+		}
+
+		// Generate token
+		$token = $this->generateToken($email, self::TYPE_LOGIN, [
+			'user_id' => $user->ID
+		]);
+
+		// Build magic link URL
+		$magic_url = add_query_arg([
+			'magic_token' => $token,
+			'email' => urlencode($email),
+			'action' => 'magic_login'
+		], home_url('/'));
+
+		// Add redirect if specified
+		if (!empty($context['redirect_to'])) {
+			$magic_url = add_query_arg('redirect_to', urlencode($context['redirect_to']), $magic_url);
+		}
+
+		// Send email
+		$subject = 'Sign in to ' . get_bloginfo('name');
+		$message = $this->getLoginEmailTemplate($user->display_name, $magic_url);
+
+		$sent = $this->email->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name'));
+
+		return $sent ? true : new WP_Error('email_failed', 'Failed to send magic link');
+	}
+
+	/**
+	 * Send signup magic link for new user registration
+	 */
+	protected function sendSignupLink(string $email, array $context):bool|WP_Error
+	{
+		// Check if user already exists
+		if (email_exists($email)) {
+			return $this->sendLoginLink($email, $context);
+		}
+
+		// Generate token with signup data
+		$token_data = [
+			'name' => $context['name'] ?? '',
+			'role' => $context['role'] ?? 'subscriber',
+			'meta' => $context['meta'] ?? []
+		];
+
+		$token = $this->generateToken($email, self::TYPE_SIGNUP, $token_data);
+
+		// Build signup completion URL
+		$magic_url = add_query_arg([
+			'magic_token' => $token,
+			'email' => urlencode($email),
+			'action' => 'magic_signup'
+		], home_url('/'));
+
+		// Send welcome email
+		$subject = 'Complete your ' . get_bloginfo('name') . ' registration';
+		$message = $this->getSignupEmailTemplate($context['name'] ?? '', $magic_url);
+
+		$sent = $this->email->sendEmail($email, $subject, $message, 'Confirm Your Account');
+
+		return $sent ? true : new WP_Error('email_failed', 'Failed to send signup link');
+	}
+
+	/**
+	 * Send referral signup link
+	 */
+	protected function sendReferralLink(string $email, array $context):bool|WP_Error
+	{
+		// Check if user already exists
+		if (email_exists($email)) {
+			return new WP_Error('user_exists', 'This person already has an account');
+		}
+
+		// Validate referral code
+		if (empty($context['referral_code'])) {
+			return new WP_Error('missing_referral_code', 'Referral code is required');
+		}
+
+		// Get referrer info for personalized email
+		$referrer_name = $context['referrer_name'] ?? 'A friend';
+
+		// Generate token with referral context
+		$token_data = [
+			'name' => $context['name'] ?? '',
+			'referral_code' => $context['referral_code'],
+			'referrer_id' => $context['referrer_id'] ?? 0
+		];
+
+		$token = $this->generateToken($email, self::TYPE_REFERRAL, $token_data);
+
+		// Build referral signup URL
+		$magic_url = add_query_arg([
+			'magic_token' => $token,
+			'email' => urlencode($email),
+			'action' => 'magic_referral'
+		], home_url('/'));
+
+		// Send personalized referral email
+		$subject = $referrer_name . ' invited you to ' . get_bloginfo('name');
+		$message = $this->getReferralEmailTemplate(
+			$context['name'] ?? '',
+			$referrer_name,
+			$magic_url,
+			$context['reward_text'] ?? ''
+		);
+
+		$sent = $this->email->sendEmail($email, $subject, $message, $referrer_name.' invites you to see the difference at Legacy');
+
+		return $sent ? true : new WP_Error('email_failed', 'Failed to send referral invitation');
+	}
+
+	/**
+	 * Send password reset magic link
+	 */
+	protected function sendResetLink(string $email, array $context):bool|WP_Error
+	{
+		$user = get_user_by('email', $email);
+		if (!$user) {
+			// Return success even if user doesn't exist (security best practice)
+			return true;
+		}
+
+		$token = $this->generateToken($email, self::TYPE_RESET, [
+			'user_id' => $user->ID
+		]);
+
+		$magic_url = add_query_arg([
+			'magic_token' => $token,
+			'email' => urlencode($email),
+			'action' => 'magic_reset'
+		], home_url('/'));
+
+		$subject = 'Reset your password';
+		$message = $this->getResetEmailTemplate($user->display_name, $magic_url);
+
+		$sent = $this->email->sendEmail($email, $subject, $message);
+
+		return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link');
+	}
+
+	/**
+	 * Handle magic link click
+	 */
+	public function handleMagicLinkClick(): void
+	{
+		// Check if this is a magic link request
+		if (!isset($_GET['action']) || !isset($_GET['magic_token']) || !isset($_GET['email'])) {
+			return;
+		}
+
+		$action = sanitize_text_field($_GET['action']);
+		$token = sanitize_text_field($_GET['magic_token']);
+		$email = sanitize_email($_GET['email']);
+
+		// Only handle magic link actions
+		if (!in_array($action, ['magic_login', 'magic_signup', 'magic_referral', 'magic_reset'])) {
+			return;
+		}
+
+		// Verify token
+		$token_data = $this->verifyToken($token, $email);
+
+		if (is_wp_error($token_data)) {
+			$this->handleInvalidToken($token_data);
+			return;
+		}
+
+		// Handle different action types
+		switch ($action) {
+			case 'magic_login':
+				$this->processLogin($token_data);
+				break;
+
+			case 'magic_signup':
+				$this->processSignup($token_data);
+				break;
+
+			case 'magic_referral':
+				$this->processReferralSignup($token_data);
+				break;
+
+			case 'magic_reset':
+				$this->processPasswordReset($token_data);
+				break;
+		}
+	}
+
+	/**
+	 * Process login via magic link
+	 */
+	protected function processLogin(array $token_data): void
+	{
+		$user = get_user_by('ID', $token_data['user_id']);
+
+		if (!$user) {
+			wp_die('Invalid user');
+		}
+
+		// Log the user in
+		wp_clear_auth_cookie();
+		wp_set_current_user($user->ID);
+		wp_set_auth_cookie($user->ID, true);
+
+		// Trigger login action
+		do_action('wp_login', $user->user_login, $user);
+
+		// Determine redirect
+		$redirect = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/dash');
+
+		// Redirect
+		wp_safe_redirect($redirect);
+		exit;
+	}
+
+	/**
+	 * Process signup via magic link
+	 */
+	protected function processSignup(array $token_data): void
+	{
+		// Create the user account
+		$user_id = wp_create_user(
+			$token_data['email'],
+			wp_generate_password(20, true, true), // Random password
+			$token_data['email']
+		);
+
+		if (is_wp_error($user_id)) {
+			wp_die('Failed to create account: ' . $user_id->get_error_message());
+		}
+
+		// Set role
+		$user = get_user_by('ID', $user_id);
+		$user->set_role($token_data['role']);
+
+		// Update display name if provided
+		if (!empty($token_data['name'])) {
+			wp_update_user([
+				'ID' => $user_id,
+				'display_name' => $token_data['name'],
+				'first_name' => $token_data['name']
+			]);
+		}
+
+		// Save any additional meta
+		if (!empty($token_data['meta'])) {
+			foreach ($token_data['meta'] as $key => $value) {
+				update_user_meta($user_id, BASE . $key, $value);
+			}
+		}
+
+		// Log the user in
+		wp_set_current_user($user_id);
+		wp_set_auth_cookie($user_id, true);
+
+		// Trigger registration actions
+		do_action('user_register', $user_id);
+		do_action('wp_login', $user->user_login, $user);
+
+		// Redirect to welcome page or dashboard
+		wp_safe_redirect(home_url('/dash?welcome=1'));
+		exit;
+	}
+
+	/**
+	 * Process referral signup via magic link
+	 */
+	protected function processReferralSignup(array $token_data): void
+	{
+		// Create user account
+		$user_id = wp_create_user(
+			$token_data['email'],
+			wp_generate_password(20, true, true),
+			$token_data['email']
+		);
+
+		if (is_wp_error($user_id)) {
+			wp_die('Failed to create account: ' . $user_id->get_error_message());
+		}
+
+		// Update user info
+		if (!empty($token_data['name'])) {
+			wp_update_user([
+				'ID' => $user_id,
+				'display_name' => $token_data['name'],
+				'first_name' => $token_data['name']
+			]);
+		}
+
+		// Store referral code in session for ReferralManager to pick up
+		if (session_status() === PHP_SESSION_NONE) {
+			session_start();
+		}
+		$_SESSION[BASE . 'referral_code'] = $token_data['referral_code'];
+		setcookie(BASE . 'referral_code', $token_data['referral_code'], time() + (30 * DAY_IN_SECONDS), '/');
+
+		// Process referral (this will be picked up by ReferralManager::processReferral)
+		do_action('user_register', $user_id);
+
+		// Log the user in
+		wp_set_current_user($user_id);
+		wp_set_auth_cookie($user_id, true);
+		do_action('wp_login', get_user_by('ID', $user_id)->user_login, get_user_by('ID', $user_id));
+
+		// Redirect with referral welcome message
+		wp_safe_redirect(home_url('/dash?referral_welcome=1'));
+		exit;
+	}
+
+	/**
+	 * Process password reset
+	 */
+	protected function processPasswordReset(array $token_data): void
+	{
+		// Redirect to password reset form with token
+		wp_safe_redirect(add_query_arg([
+			'action' => 'rp',
+			'key' => $token_data['token'], // Could use magic token or generate WP reset key
+			'login' => $token_data['email']
+		], wp_login_url()));
+		exit;
+	}
+
+	/**
+	 * Generate a secure token
+	 */
+	protected function generateToken(string $email, string $type, array $data): string
+	{
+		// Create unique token
+		$token = wp_generate_password(64, false, false);
+
+		// Store token data in transient
+		$token_data = [
+			'email' => $email,
+			'type' => $type,
+			'created_at' => time(),
+			'expires_at' => time() + $this->token_expiry,
+			'data' => $data
+		];
+
+		$cache_key = 'magic_token_' . $token;
+		set_transient($cache_key, $token_data, $this->token_expiry);
+
+		// Also index by email for rate limiting
+		$this->recordTokenGeneration($email);
+
+		return $token;
+	}
+
+	/**
+	 * Verify a magic link token
+	 */
+	protected function verifyToken(string $token, string $email)
+	{
+		// Retrieve token data
+		$cache_key = 'magic_token_' . $token;
+		$token_data = get_transient($cache_key);
+
+		if (!$token_data) {
+			return new WP_Error('expired_token', 'This link has expired. Please request a new one.');
+		}
+
+		// Verify email matches
+		if ($token_data['email'] !== $email) {
+			return new WP_Error('invalid_token', 'Invalid magic link');
+		}
+
+		// Check expiration
+		if (time() > $token_data['expires_at']) {
+			delete_transient($cache_key);
+			return new WP_Error('expired_token', 'This link has expired. Please request a new one.');
+		}
+
+		// Token is valid - delete it (single use)
+		delete_transient($cache_key);
+
+		// Return merged data
+		return array_merge($token_data['data'], [
+			'email' => $token_data['email'],
+			'type' => $token_data['type']
+		]);
+	}
+
+	/**
+	 * Rate limiting for magic link generation
+	 */
+	protected function checkRateLimit(string $email):bool|WP_Error
+	{
+		$limit_key = 'magic_link_limit_' . md5($email);
+		$attempts = (int) get_transient($limit_key);
+
+		if ($attempts >= $this->max_attempts_per_hour) {
+			return new WP_Error(
+				'rate_limit_exceeded',
+				'Too many login attempts. Please try again in an hour.'
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Record token generation for rate limiting
+	 */
+	protected function recordTokenGeneration(string $email): void
+	{
+		$limit_key = 'magic_link_limit_' . md5($email);
+		$attempts = (int) get_transient($limit_key);
+		set_transient($limit_key, $attempts + 1, $this->rate_limit_window);
+	}
+
+	/**
+	 * Handle invalid/expired tokens
+	 */
+	protected function handleInvalidToken(WP_Error $error): void
+	{
+		wp_die(
+			$error->get_error_message(),
+			'Invalid Link',
+			[
+				'response' => 400,
+				'back_link' => true
+			]
+		);
+	}
+
+	/**
+	 * Add "Send me a magic link" option to login form
+	 */
+	public function addMagicLinkOption(): void
+	{
+		?>
+		<p class="magic-link-option">
+			<a href="#" id="use-magic-link">Send me a login link instead</a>
+		</p>
+		<script>
+			document.getElementById('use-magic-link')?.addEventListener('click', function(e) {
+				e.preventDefault();
+				const email = document.getElementById('user_login')?.value;
+
+				if (!email) {
+					alert('Please enter your email address first');
+					return;
+				}
+
+				// Send magic link request
+				fetch('<?php echo rest_url(BASE . '/v1/magic-link/send'); ?>', {
+					method: 'POST',
+					headers: {
+						'Content-Type': 'application/json',
+					},
+					body: JSON.stringify({
+						email: email,
+						type: 'login'
+					})
+				})
+					.then(r => r.json())
+					.then(data => {
+						if (data.success) {
+							alert('Check your email! We sent you a login link.');
+						} else {
+							alert(data.message || 'Failed to send link');
+						}
+					});
+			});
+		</script>
+		<?php
+	}
+
+	/**
+	 * Optionally block standard password auth for certain users
+	 */
+	public function blockStandardAuth($user, $username, $password)
+	{
+		// Only block if user has magic-link-only flag
+		if ($user instanceof WP_User) {
+			$magic_only = get_user_meta($user->ID, BASE . 'magic_link_only', true);
+			if ($magic_only) {
+				return new WP_Error('magic_link_required', 'Please use the login link sent to your email');
+			}
+		}
+
+		return $user;
+	}
+
+	// ========================================
+	// EMAIL TEMPLATES
+	// ========================================
+
+	protected function getLoginEmailTemplate(string $name, string $magic_url): string
+	{
+		$content = '<h2>Hey ' . esc_html($name) . '!</h2>';
+		$content .= '<p>Click the button below to sign in to your account. This link expires in 15 minutes.</p>';
+		$content .= '<p style="text-align: center; margin: 30px 0;">';
+		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Sign In</a>';
+		$content .= '</p>';
+		$content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email.</p>';
+
+		return $content;
+	}
+
+	protected function getSignupEmailTemplate(string $name, string $magic_url): string
+	{
+		$content = '<h2>Welcome' . ($name ? ', ' . esc_html($name) : '') . '!</h2>';
+		$content .= '<p>You\'re almost there! Click the button below to complete your registration and access your account.</p>';
+		$content .= '<p style="text-align: center; margin: 30px 0;">';
+		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Complete Registration</a>';
+		$content .= '</p>';
+		$content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>';
+
+		return $content;
+	}
+
+	protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text): string
+	{
+		$content = '<h2>Hey' . ($name ? ' ' . esc_html($name) : '') . '!</h2>';
+		$content .= '<p><strong>' . esc_html($referrer_name) . '</strong> thinks you\'d love ' . get_bloginfo('name') . ' and invited you to join!</p>';
+
+		if ($reward_text) {
+			$content .= '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">';
+			$content .= '<h3 style="margin-top: 0;">🎉 Special Offer</h3>';
+			$content .= '<p>' . esc_html($reward_text) . '</p>';
+			$content .= '</div>';
+		}
+
+		$content .= '<p style="text-align: center; margin: 30px 0;">';
+		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>';
+		$content .= '</p>';
+		$content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>';
+
+		return $content;
+	}
+
+	protected function getResetEmailTemplate(string $name, string $magic_url): string
+	{
+		$content = '<h2>Reset Your Password</h2>';
+		$content .= '<p>Hey ' . esc_html($name) . ', we received a request to reset your password.</p>';
+		$content .= '<p style="text-align: center; margin: 30px 0;">';
+		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Reset Password</a>';
+		$content .= '</p>';
+		$content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email.</p>';
+
+		return $content;
+	}
+}
+
+new MagicLinkManager();
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index f10def6..cf6f398 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -604,6 +604,12 @@
 	protected function deepMerge(array $existing, array $new): array
 	{
 		$merged = $existing;
+
+		if (!$this->isAssociativeArray($existing) && !$this->isAssociativeArray($new)) {
+			error_log('It is an associative array!');
+			return array_merge($existing, $new);
+		}
+		error_log('Not an associative array... moving on!');
 		foreach ($new as $key => $newValue) {
 			if (!array_key_exists($key, $existing)) {
 				$merged[$key] = $newValue;
@@ -614,8 +620,15 @@
 						// Recursive merge going deeper, if any of them are associative arrays
 						$merged[$key] = $this->deepMerge($existingValue, $newValue);
 					} else {
-						// Unique merge if indexed arrays
-						$merged[$key] = array_unique(array_merge($existingValue, $newValue), SORT_REGULAR);
+						$containsComplex = $this->containsComplexData($existingValue) || $this->containsComplexData($newValue);
+
+						if ($containsComplex) {
+							// Just merge and re-index - preserves all items from chunks
+							$merged[$key] = array_values(array_merge($existingValue, $newValue));
+						} else {
+							// Simple scalar arrays - use unique merge
+							$merged[$key] = array_unique(array_merge($existingValue, $newValue), SORT_REGULAR);
+						}
 					}
 				} elseif (is_array($existingValue) && !is_array($newValue)) {
 					// The existing value is an array, but the new one isn't
@@ -663,6 +676,21 @@
 		return array_keys($arr) !== range(0, count($arr) - 1);
 	}
 
+	/**
+	 * Check if an array contains complex data (arrays or objects)
+	 * @param array $arr
+	 * @return bool
+	 */
+	protected function containsComplexData(array $arr): bool
+	{
+		foreach ($arr as $item) {
+			if (is_array($item) || is_object($item)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	protected function getEarliestScheduledTime(string $existing, string $new): string
 	{
 		$existing_time = strtotime($existing);
@@ -1264,8 +1292,11 @@
 					$filterResult['result'] = [$filterResult['result']];
 				}
 				// Store the result data
+				error_log('Merging Old Result: '. print_r($oldResult, true));
+				error_log('With Newer Result: '. print_r($filterResult['result'], true));
 				$resultToStore = $this->deepMerge($oldResult, $filterResult['result']);
 
+				error_log('Merged Result: '.print_r($resultToStore, true));
 
 				$resultToStore['processed_at'] = current_time('mysql');
 
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 90671f2..429ea99 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -1,6 +1,9 @@
 <?php
 namespace JVBase\managers;
 
+use JVBase\managers\MagicLinkManager;
+use JVBase\integrations\Cloudflare;
+use JVBase\utility\Features;
 use WP_User;
 use WP_Error;
 
@@ -17,6 +20,7 @@
 class ReferralManager
 {
 	protected $wpdb;
+	protected MagicLinkManager $magic_link;
 	protected CacheManager $cache;
 	protected string $referrals_table;
 	protected string $rewards_table;
@@ -38,6 +42,7 @@
 		$this->cache = new CacheManager('referrals');
 		$this->referrals_table = BASE . 'referrals';
 		$this->rewards_table = BASE . 'referral_rewards';
+		$this->magic_link = new MagicLinkManager();
 
 		// Hook into user registration to track referrals
 		add_action('user_register', [$this, 'processReferral'], 10, 1);
@@ -52,15 +57,35 @@
 
 		add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
 
-		if (is_user_logged_in()) {
-			add_action('wp_footer', [$this, 'outputShareWidget']);
-		}
 
-		add_action('template_redirect', [$this, 'trackReferralCode']);;
+		add_action('jvbAdditionalActions', [$this, 'outputShareWidget']);
+
+		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 		// Schedule cron jobs for reports
 		$this->registerCronJobs();
 	}
 
+	public function enqueueScripts():void
+	{
+		$requirements = [
+			'jvb-utility',
+			'jvb-a11y',
+			'jvb-popup',
+			'jvb-tabs',
+		];
+
+		if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
+			$requirements[] = 'cloudflare-turnstile';
+		}
+		wp_enqueue_script(
+			'jvb-referral',
+			JVB_URL . 'assets/js/min/referral.min.js',
+			$requirements,
+			'1.0.0',
+			true
+		);
+	}
+
 	/**
 	 * Register cron jobs for automated reporting
 	 */
@@ -171,8 +196,8 @@
 	 */
 	public function processReferral(int $user_id): void
 	{
-		// Check if there's a referral code in the session/cookie
-		$referral_code = $this->getReferralCodeFromSession();
+		// Check if user was created via referral magic link
+		$referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
 
 		if (!$referral_code) {
 			return;
@@ -182,20 +207,27 @@
 		$referrer = $this->getUserByReferralCode($referral_code);
 
 		if (!$referrer) {
+			delete_user_meta($user_id, BASE . 'pending_referral_code');
 			return;
 		}
 
-		// Check if this user was already referred (prevent duplicates)
+		// Check for duplicates
 		$existing = $this->getReferralByReferee($user_id);
 		if ($existing) {
+			delete_user_meta($user_id, BASE . 'pending_referral_code');
 			return;
 		}
 
 		// Create referral record
-		$this->createReferral($referrer->ID, $user_id, $referral_code);
+		$result = $this->createReferral($referrer->ID, $user_id, $referral_code);
 
-		// Clear the session
-		$this->clearReferralSession();
+		if ($result) {
+			// Clean up temp meta
+			delete_user_meta($user_id, BASE . 'pending_referral_code');
+
+			// Fire action for tracking
+			do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
+		}
 	}
 
 	/**
@@ -232,7 +264,7 @@
 	 * @param string $code
 	 * @return WP_User|null
 	 */
-	protected function getUserByReferralCode(string $code): ?WP_User
+	public function getUserByReferralCode(string $code): ?WP_User
 	{
 		$users = get_users([
 			'meta_key' => BASE . 'referral_code',
@@ -465,40 +497,62 @@
 	 */
 	public function sendDailyReport(): void
 	{
-		// Get referrals from the last 24 hours
-		$referrals = $this->wpdb->get_results(
-			"SELECT r.*, u.display_name as referrer_name, u.user_email as referrer_email
-             FROM {$this->referrals_table} r
-             LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
-             WHERE r.referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
-             ORDER BY r.referred_at DESC"
-		);
+		$yesterday = date('Y-m-d', strtotime('-1 day'));
 
-		if (empty($referrals)) {
-			return;  // No referrals, no email
+		// Get new referrals from yesterday
+		$new_referrals = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT
+            r.*,
+            u.display_name as referrer_name
+        FROM {$this->referrals_table} r
+        JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
+        WHERE DATE(r.referred_at) = %s
+        ORDER BY r.referred_at DESC",
+			$yesterday
+		));
+
+		// Only send if there's at least 1 new referral
+		if (empty($new_referrals)) {
+			return;
 		}
 
-		// Generate CSV
-		$csv_content = $this->generateCSV($referrals);
-		$csv_filename = 'referrals-' . date('Y-m-d') . '.csv';
+		// Build email content
+		$content = '<h2>Daily Referral Report</h2>';
+		$content .= '<p><strong>' . count($new_referrals) . '</strong> new referral' .
+			(count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')</p>';
 
-		// Save CSV temporarily
-		$upload_dir = wp_upload_dir();
-		$csv_path = $upload_dir['basedir'] . '/' . $csv_filename;
-		file_put_contents($csv_path, $csv_content);
+		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
+		$content .= '<thead><tr style="background: #f5f5f5;">';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referee</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Email</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referrer</th>';
+		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Code</th>';
+		$content .= '</tr></thead><tbody>';
 
-		// Send email with attachment
+		foreach ($new_referrals as $ref) {
+			$content .= '<tr>';
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referee_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referee_email));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referrer_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($ref->referral_code));
+			$content .= '</tr>';
+		}
+
+		$content .= '</tbody></table>';
+
+		// Get admin email
 		$to = get_option('admin_email');
-		$subject = '[' . get_bloginfo('name') . '] Daily Referral Report - ' . date('F j, Y');
+		$subject = sprintf('[%s] %d New Referral%s',
+			get_bloginfo('name'),
+			count($new_referrals),
+			count($new_referrals) !== 1 ? 's' : '');
 
-		$message = $this->generateReportEmail($referrals, 'daily');
 
-		$attachments = [$csv_path];
-
-		wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8'], $attachments);
-
-		// Clean up temporary file
-		unlink($csv_path);
+		jvbMail($to, $subject, $content);
 	}
 
 	/**
@@ -644,35 +698,13 @@
 	 *
 	 * @return array
 	 */
-	protected function getRewardSettings(): array
+	public function getRewardSettings(): array
 	{
 		$saved = get_option(BASE . 'referral_settings', []);
 		return wp_parse_args($saved, $this->default_settings);
 	}
 
 	/**
-	 * Session/Cookie handling for referral codes
-	 */
-	protected function getReferralCodeFromSession(): ?string
-	{
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		return $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? null;
-	}
-
-	protected function clearReferralSession(): void
-	{
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		unset($_SESSION[BASE . 'referral_code']);
-		setcookie(BASE . 'referral_code', '', time() - 3600, '/');
-	}
-
-	/**
 	 * Display referral info in user profile
 	 *
 	 * @param WP_User $user
@@ -834,28 +866,6 @@
 		update_user_meta($user_id, BASE . 'referral_code', strtoupper($code));
 	}
 
-	public function trackReferralCode(): void
-	{
-		if (!isset($_GET['ref'])) {
-			return;
-		}
-
-		$referral_code = strtoupper(sanitize_text_field($_GET['ref']));
-
-		// Start session if not already started
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
-		}
-
-		// Store in both session and cookie (30 day expiry)
-		$_SESSION[BASE . 'referral_code'] = $referral_code;
-		setcookie(BASE . 'referral_code', $referral_code, time() + (30 * DAY_IN_SECONDS), '/');
-
-		// Optional: Redirect to clean URL (removes ?ref= from address bar)
-		$clean_url = remove_query_arg('ref');
-		wp_safe_redirect($clean_url);
-		exit;
-	}
 
 	/**
 	 * Display user's referral code and share options
@@ -863,141 +873,251 @@
 	 *
 	 * @return string HTML output
 	 */
-	public function outputShareWidget(): string
+	public function outputShareWidget(array $actions):array
 	{
-		$user_id = get_current_user_id();
 
+		$user_id = get_current_user_id();
+		jvbDump($user_id);
+		$content = '<aside class="jvb-referral right">';
 		if (!$user_id) {
+			$content .= $this->getUnloggedInReferral();
+		} else {
+			$content .= $this->getLoggedInReferral($user_id);
+		}
+		$content .= '<button type="button" class="close">'.jvbIcon('close').'</button></aside>';
+
+		$actions[] =[
+			'button' => '<button type="button" class="toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false">
+					'.jvbIcon('hand-heart').'<span class="screen-reader-text"></span>
+				</button>',
+			'content'	=> $content
+		];
+
+		return $actions;
+	}
+
+	function getUnloggedInReferral(): string
+	{
+		ob_start();
+		JVB()->connect('cloudflare')->renderTurnstile();
+		$turnstile = ob_get_clean();
+		$codeForm = '<form id="referral-code-form">
+					<div class="status" hidden>
+						<div class="spinner"></div>
+						<p class="message"></p>
+					</div>
+					<div class="field text">
+						<label for="referral-name">Your Name</label>
+						<input type="text"
+							   id="referral-name"
+							   name="name"
+							   placeholder="Mister Meeseeks"
+							   autocomplete="name"
+							   required>
+					</div>
+
+					<div class="field email">
+						<label for="referral-email">Your Email</label>
+						<input type="email"
+							   id="referral-email"
+							   name="email"
+							   placeholder="look@me.com"
+							   autocomplete="email"
+							   required>
+					</div>
+
+					<div class="field text">
+						<label for="referral-code-input">Referral Code</label>
+						<input type="text"
+							   id="referral-code-input"
+							   name="referral_code"
+							   placeholder="e.g., THISISFAKE1234"
+							   required
+							   pattern="[A-Za-z0-9]+"
+							   maxlength="20"
+							   autocomplete="off"
+							   style="text-transform: uppercase;">
+					</div>
+
+					<button type="submit">
+						Get Started
+					</button>
+
+					<p class="helper-text">
+						We\'ll send you a link to complete your registration.
+					</p>
+					'.$turnstile.'
+				</form><div class="success-content" hidden>
+					<h3>Check Your Email!</h3>
+					<p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p>
+					<p class="hint">Can\'t find it? Check your spam folder.</p>
+				</div>';
+
+		$loginForm = '<form id ="login-form">
+		<div class="status" hidden>
+			<div class="spinner"></div>
+			<p class="message"></p>
+		</div>
+		<div class="field email">
+			<label for="login-email">Your Email</label>
+			<input id="login-email" name="login-email" type="email" autocomplete="email">
+		</div>
+		'.$turnstile.'
+		<button type="submit">Login With Magic Link</button>
+</form>
+	<div class="success-content" hidden>
+		<h3>Check Your Email!</h3>
+		<p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p>
+		<p class="hint">Can\'t find it? Check your spam folder.</p>
+	</div>';
+
+		$tabs = [
+			'enterCode' => [
+				'title'	=> 'Have a Code?',
+				'description'	=> [
+					'Enter the code given to you to get 20% off your first treatment!'
+				],
+				'content'	=> $codeForm
+			],
+			'login'	=> [
+				'title'		=> 'Login',
+				'description'	=> [
+					'Login to see your rewards'
+				],
+				'content'	=> $loginForm
+			]
+		];
+
+		return jvbRenderTabs($tabs, true);
+	}
+
+	protected function getReferralSuccessMessage(string $code): string
+	{
+		$referrer = $this->getUserByReferralCode($code);
+
+		if (!$referrer) {
 			return '';
 		}
 
+		$settings = $this->getRewardSettings();
+		$reward_amount = $settings['referee_reward_amount'] ?? 20;
+		$reward_type = $settings['referee_reward_type'] ?? 'percentage';
+
+		$reward_text = $reward_type === 'percentage'
+			? $reward_amount . '% off'
+			: '$' . number_format($reward_amount, 2) . ' off';
+
+		$booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact'));
+
+		ob_start();
+		?>
+		<div class="success-icon">
+			✓
+		</div>
+
+		<div class="success-content">
+			<h3>Success! Your Reward is Ready!</h3>
+
+			<div class="reward-highlight">
+				<p style="margin: 0 0 8px 0;">You'll receive:</p>
+				<p style="margin: 0;"><strong><?php echo esc_html($reward_text); ?> your first treatment!</strong></p>
+			</div>
+
+			<p>Your referral code <strong><?php echo esc_html($code); ?></strong> has been applied. Book your free consultation now to claim your reward!</p>
+
+			<a href="<?php echo esc_url($booking_url); ?>" class="cta-button">
+				Book Your Free Consultation
+			</a>
+
+			<div class="referred-by">
+				Referred by <strong><?php echo esc_html($referrer->display_name); ?></strong>
+			</div>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	function getLoggedInReferral(int $user_id):string
+	{
+		// Logged-in user widget
 		$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
 
 		// Generate code if user doesn't have one
 		if (empty($referral_code)) {
-			$manager = new \JVBase\managers\ReferralManager();
-			$referral_code = $manager->getUserReferralCode($user_id);
+			$referral_code = $this->getUserReferralCode($user_id);
+			if (is_wp_error($referral_code)) {
+				return '';
+			}
 		}
 
 		$share_url = home_url('/?ref=' . $referral_code);
-		$encoded_url = urlencode($share_url);
-		$site_name = get_bloginfo('name');
 
 		ob_start();
 		?>
-		<div class="jvb-referral-widget" style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
-			<h3 style="margin-top: 0;">Share & Earn Rewards</h3>
-			<p>Share your unique referral code with friends and earn rewards when they book!</p>
+			<header>
+				<h3>Share the ♡</h3>
+				<p>Invite your friends.</p>
+				<p>Earn rewards when they book!</p>
+			</header>
 
-			<div class="referral-code-display" style="background: white; padding: 15px; border-radius: 4px; margin: 15px 0; text-align: center;">
-				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Your Referral Code</label>
-				<div style="font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #2271b1;">
-					<?php echo esc_html($referral_code); ?>
-				</div>
-			</div>
-
-			<div class="referral-url" style="margin: 15px 0;">
-				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Share Link</label>
-				<div style="display: flex; gap: 10px;">
-					<input type="text"
-						   readonly
-						   value="<?php echo esc_url($share_url); ?>"
-						   id="referral-url-<?php echo $user_id; ?>"
-						   style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 14px;">
-					<button type="button"
-							onclick="jvbCopyReferralUrl('referral-url-<?php echo $user_id; ?>')"
-							style="padding: 8px 16px; background: #2271b1; color: white; border: none; border-radius: 4px; cursor: pointer;">
-						Copy
-					</button>
-				</div>
-			</div>
-
-			<div class="referral-share-buttons" style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
-				<a href="mailto:?subject=Check out <?php echo esc_attr($site_name); ?>&body=I thought you might like <?php echo esc_url($share_url); ?>"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #666; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					📧 Email
-				</a>
-				<a href="sms:?&body=Check out <?php echo esc_attr($site_name); ?>: <?php echo esc_url($share_url); ?>"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #25D366; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					💬 Text
-				</a>
-				<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $encoded_url; ?>"
-				   target="_blank"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #1877f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					f Facebook
-				</a>
-				<a href="https://twitter.com/intent/tweet?url=<?php echo $encoded_url; ?>&text=Check out <?php echo esc_attr($site_name); ?>"
-				   target="_blank"
-				   class="share-button"
-				   style="padding: 10px 20px; background: #1da1f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
-					𝕏 Twitter
-				</a>
-			</div>
-
-			<div id="referral-stats-<?php echo $user_id; ?>" class="referral-stats" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
-				<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; text-align: center;">
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="total">-</div>
-						<div style="font-size: 12px; color: #666;">Total Referrals</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #00a32a;" data-stat="treated">-</div>
-						<div style="font-size: 12px; color: #666;">Completed</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #dba617;" data-stat="pending">-</div>
-						<div style="font-size: 12px; color: #666;">Pending</div>
-					</div>
-					<div>
-						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="rewards">$0</div>
-						<div style="font-size: 12px; color: #666;">Earned</div>
-					</div>
-				</div>
-			</div>
+		<div class="row even share-buttons">
+			<a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode('I thought you might be interested: ' . $share_url); ?>"
+			   class="share-btn email-share">
+				<?php echo jvbIcon('envelope', ['size' => 20]); ?>
+				Email
+			</a>
+			<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
+			   target="_blank"
+			   rel="noopener noreferrer"
+			   class="share-btn facebook-share">
+				<?php echo jvbIcon('facebook', ['size' => 20]); ?>
+				Facebook
+			</a>
+			<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>"
+			   target="_blank"
+			   rel="noopener noreferrer"
+			   class="share-btn twitter-share">
+				<?php echo jvbIcon('twitter', ['size' => 20]); ?>
+				Twitter
+			</a>
 		</div>
 
-		<script>
-			function jvbCopyReferralUrl(elementId) {
-				const input = document.getElementById(elementId);
-				input.select();
-				document.execCommand('copy');
+			<h4>Your Referral Link</h4>
+			<div class="row btw">
+				<code id="your-referral-link"><?= esc_url($share_url)?></code>
+				<button type="button" class="copy" data-target="your-referral-link">
+					Copy Link
+				</button>
+			</div>
 
-				// Visual feedback
-				const button = input.nextElementSibling;
-				const originalText = button.textContent;
-				button.textContent = 'Copied!';
-				button.style.background = '#00a32a';
 
-				setTimeout(() => {
-					button.textContent = originalText;
-					button.style.background = '#2271b1';
-				}, 2000);
-			}
+			<h4>Your Code</h4>
+			<div class="row btw">
+				<code id="your-referral-code"><?=esc_html($referral_code)?></code>
+				<button type="button" class="copy" data-target="your-referral-code">
+					Copy Code
+				</button>
+			</div>
 
-			// Load stats via AJAX
-			(function() {
-				fetch('<?php echo rest_url(BASE . '/v1/referrals/stats'); ?>', {
-					headers: {
-						'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
-					}
-				})
-					.then(response => response.json())
-					.then(data => {
-						if (data.success && data.stats) {
-							const container = document.getElementById('referral-stats-<?php echo $user_id; ?>');
-							container.querySelector('[data-stat="total"]').textContent = data.stats.total_referrals || 0;
-							container.querySelector('[data-stat="treated"]').textContent = data.stats.treated_count || 0;
-							container.querySelector('[data-stat="pending"]').textContent = data.stats.pending_count || 0;
-							container.querySelector('[data-stat="rewards"]').textContent =
-								'$' + parseFloat(data.stats.available_rewards || 0).toFixed(2);
-						}
-					})
-					.catch(error => console.error('Error loading referral stats:', error));
-			})();
-		</script>
+			<div class="row btw referral-stats">
+				<div class="stat-item">
+					<span class="stat-value" data-stat="total">-</span>
+					<span class="stat-label">Total Referrals</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="treated">-</span>
+					<span class="stat-label">Successful</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="pending">-</span>
+					<span class="stat-label">Pending</span>
+				</div>
+				<div class="stat-item">
+					<span class="stat-value" data-stat="rewards">$0.00</span>
+					<span class="stat-label">Available Rewards</span>
+				</div>
+			</div>
+
 		<?php
 		return ob_get_clean();
 	}
@@ -1032,5 +1152,316 @@
 
 		return $content . $bonus_content;
 	}
+
+	/**
+	 * Send referral invitation via email with magic link
+	 *
+	 * @param int $user_id Referrer's user ID
+	 * @param string $invitee_email Email of person to invite
+	 * @param string $invitee_name Name of person to invite
+	 * @return array|WP_Error Result with success/error
+	 */
+	public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name):array|WP_Error
+	{
+		// Verify user exists
+		if (!$this->checkUser($user_id)) {
+			return new WP_Error('invalid_user', 'Invalid user ID');
+		}
+
+		// Check email rate limit (15/hour)
+		$rate_check = $this->checkEmailRateLimit($user_id);
+		if ($rate_check !== true) {
+			return new WP_Error('rate_limit', 'You can only send 15 invitations per hour. Please try again later.');
+		}
+
+		// Validate email
+		$invitee_email = sanitize_email($invitee_email);
+		if (!is_email($invitee_email)) {
+			return new WP_Error('invalid_email', 'Invalid email address');
+		}
+
+		// Check if this email has already been invited or registered
+		if ($this->isEmailInvited($invitee_email)) {
+			return new WP_Error('already_invited', 'This person has already been invited');
+		}
+
+		if (email_exists($invitee_email)) {
+			return new WP_Error('user_exists', 'This person already has an account');
+		}
+
+		// Get referrer info
+		$referrer = get_user_by('ID', $user_id);
+		$referral_code = $this->getUserReferralCode($user_id);
+
+		if (is_wp_error($referral_code)) {
+			return $referral_code;
+		}
+
+		// Get reward text for email
+		$settings = $this->getRewardSettings();
+		$reward_text = $settings['referee_reward_type'] === 'percentage'
+			? "Get {$settings['referee_reward_amount']}% off your first treatment!"
+			: "Get \${$settings['referee_reward_amount']} off your first treatment!";
+
+		// Record the invitation attempt (for tracking)
+		$this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
+
+		// Send magic link via MagicLinkManager
+		$result = $this->magic_link->sendMagicLink(
+			$invitee_email,
+			MagicLinkManager::TYPE_REFERRAL,
+			[
+				'name' => sanitize_text_field($invitee_name),
+				'referral_code' => $referral_code,
+				'referrer_id' => $user_id,
+				'referrer_name' => $referrer->display_name,
+				'reward_text' => $reward_text
+			]
+		);
+
+		if (is_wp_error($result)) {
+			return $result;
+		}
+
+		return [
+			'success' => true,
+			'message' => 'Invitation sent successfully',
+			'email' => $invitee_email,
+			'name' => $invitee_name
+		];
+	}
+
+	/**
+	 * Send multiple referral invitations
+	 *
+	 * @param int $user_id Referrer's user ID
+	 * @param array $invitations Array of ['email' => '', 'name' => '']
+	 * @return array Results with success/failed arrays
+	 */
+	public function sendBatchReferralInvitations(int $user_id, array $invitations): array
+	{
+		$results = [
+			'success' => [],
+			'failed' => []
+		];
+
+		foreach ($invitations as $invite) {
+			$email = $invite['email'] ?? '';
+			$name = $invite['name'] ?? '';
+
+			if (empty($email) || empty($name)) {
+				$results['failed'][] = [
+					'email' => $email,
+					'name' => $name,
+					'reason' => 'Missing email or name'
+				];
+				continue;
+			}
+
+			$result = $this->sendReferralInvitation($user_id, $email, $name);
+
+			if (is_wp_error($result)) {
+				$results['failed'][] = [
+					'email' => $email,
+					'name' => $name,
+					'reason' => $result->get_error_message()
+				];
+			} else {
+				$results['success'][] = [
+					'email' => $email,
+					'name' => $name
+				];
+			}
+
+			// Small delay between sends to be respectful
+			usleep(100000); // 0.1 seconds
+		}
+
+		return [
+			'success' => !empty($results['success']),
+			'results' => $results,
+			'summary' => sprintf(
+				'Sent %d invitations, %d failed',
+				count($results['success']),
+				count($results['failed'])
+			)
+		];
+	}
+
+	/**
+	 * Check email invitation rate limit (15 per hour)
+	 *
+	 * @param int $user_id
+	 * @return true|string True if allowed, error message if limited
+	 */
+	protected function checkEmailRateLimit(int $user_id):bool|string
+	{
+		$hourly_key = 'referral_invites_hour_' . $user_id;
+		$count = (int) get_transient($hourly_key);
+
+		if ($count >= 15) {
+			return 'hourly_limit_reached';
+		}
+
+		set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS);
+		return true;
+	}
+
+	/**
+	 * Check if an email has already been invited
+	 *
+	 * @param string $email
+	 * @return bool
+	 */
+	protected function isEmailInvited(string $email): bool
+	{
+		// Check invitation tracking table
+		$invitation_key = 'referral_invite_' . md5($email);
+		$invited = get_transient($invitation_key);
+
+		if ($invited) {
+			return true;
+		}
+
+		// Check if there's a pending referral for this email
+		$existing = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT id FROM {$this->referrals_table} WHERE referee_email = %s",
+			$email
+		));
+
+		return !empty($existing);
+	}
+
+	/**
+	 * Record invitation attempt (for tracking and preventing duplicates)
+	 *
+	 * @param int $user_id
+	 * @param string $email
+	 * @param string $name
+	 */
+	protected function recordInvitationAttempt(int $user_id, string $email, string $name): void
+	{
+		// Store for 30 days (same as magic link invitation validity)
+		$invitation_key = 'referral_invite_' . md5($email);
+		$data = [
+			'inviter_id' => $user_id,
+			'email' => $email,
+			'name' => $name,
+			'sent_at' => current_time('mysql')
+		];
+
+		set_transient($invitation_key, $data, 30 * DAY_IN_SECONDS);
+
+		// Also log in user meta for tracking
+		$sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: [];
+		$sent_invites[] = [
+			'email' => $email,
+			'name' => $name,
+			'sent_at' => current_time('mysql')
+		];
+		update_user_meta($user_id, BASE . 'referral_invites_sent', $sent_invites);
+	}
+
+	/**
+	 * Get user's invitation stats
+	 *
+	 * @param int $user_id
+	 * @return array
+	 */
+	public function getUserInvitationStats(int $user_id): array
+	{
+		$sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: [];
+
+		// Count invites sent in last hour
+		$one_hour_ago = strtotime('-1 hour');
+		$recent_count = 0;
+
+		foreach ($sent_invites as $invite) {
+			if (strtotime($invite['sent_at']) > $one_hour_ago) {
+				$recent_count++;
+			}
+		}
+
+		return [
+			'total_sent' => count($sent_invites),
+			'sent_last_hour' => $recent_count,
+			'remaining_this_hour' => max(0, 15 - $recent_count),
+			'can_send_more' => $recent_count < 15
+		];
+	}
+
+	/**
+	 * Export referrals for Jane App cross-reference
+	 *
+	 * @param string $start_date Y-m-d format
+	 * @param string $end_date Y-m-d format
+	 * @return string CSV content
+	 */
+	public function exportReferrals(string $start_date, string $end_date): string
+	{
+		$referrals = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT
+            r.id,
+            r.referee_name,
+            r.referee_email,
+            r.referee_phone,
+            r.referral_code,
+            r.referred_at,
+            r.status,
+            r.treated_at,
+            u.display_name as referrer_name,
+            u.user_email as referrer_email
+        FROM {$this->referrals_table} r
+        JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
+        WHERE DATE(r.referred_at) BETWEEN %s AND %s
+        ORDER BY r.referred_at DESC",
+			$start_date,
+			$end_date
+		));
+
+		// Build CSV
+		$csv_lines = [];
+
+		// Headers
+		$csv_lines[] = [
+			'Referral ID',
+			'Referee Name',
+			'Referee Email',
+			'Referee Phone',
+			'Referral Code',
+			'Referred Date',
+			'Status',
+			'Treated Date',
+			'Referrer Name',
+			'Referrer Email'
+		];
+
+		// Data rows
+		foreach ($referrals as $ref) {
+			$csv_lines[] = [
+				$ref->id,
+				$ref->referee_name,
+				$ref->referee_email,
+				$ref->referee_phone ?: 'N/A',
+				$ref->referral_code,
+				$ref->referred_at,
+				ucfirst($ref->status),
+				$ref->treated_at ?: 'N/A',
+				$ref->referrer_name,
+				$ref->referrer_email
+			];
+		}
+
+		// Convert to CSV string
+		$output = fopen('php://temp', 'r+');
+		foreach ($csv_lines as $line) {
+			fputcsv($output, $line);
+		}
+		rewind($output);
+		$csv_content = stream_get_contents($output);
+		fclose($output);
+
+		return $csv_content;
+	}
 }
 
diff --git a/inc/managers/UploadManager.php b/inc/managers/UploadManager.php
index 6c9655a..d1cd220 100644
--- a/inc/managers/UploadManager.php
+++ b/inc/managers/UploadManager.php
@@ -2,337 +2,294 @@
 namespace JVBase\managers;
 
 use JVBase\JVB;
-use JVBase\meta\MetaManager;
 use Exception;
 use Imagick;
 use WP_Error;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
+
 /**
- * Handles file uploads for edmonton.ink dashboard
- * Includes image processing, validation, optimization, and SEO-friendly naming
+ * Flexible file upload manager for images, videos, and documents
+ * Supports configurable directory structures and processing options
  */
 class UploadManager
 {
-    /**
-     * @var array Default configuration
-     */
-	protected array $config = [
-		'allowed_types' => [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/webp',
-		],
-		'max_size' => 5242880, // 5MB
-		'convert_to_webp' => true,
-		'webp_quality' => 80,
-		'optimize_images' => true,
-		'create_thumbnails' => true,
-		'original_retention' => 2592000, // 30 days in seconds
-		'use_imagick' => null // Will be set in constructor
+	protected array $allowedTypes = [
+		// Images
+		'image/jpeg'    => 'image',
+		'image/png'     => 'image',
+		'image/gif'     => 'image',
+		'image/webp'    => 'image',
+		// Videos
+		'video/mp4'     => 'video',
+		'video/webm'    => 'video',
+		'video/ogg'     => 'video',
+		'video/ogv'     => 'video',
+		'video/quicktime' => 'video',
+		'video/x-msvideo' => 'video',
+		// Documents
+		'application/pdf' => 'document',
+		'application/msword' => 'document',
+		'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'document',
+		'application/vnd.ms-excel' => 'document',
+		'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'document',
+		'text/plain'    => 'document',
+		'text/csv'      => 'document',
+		'application/rtf' => 'document'
 	];
 
-    protected string $post_type;
-    protected int $user_id;
-	protected int $term_id;
-    protected string $upload_dir;
-    protected string $upload_url;
-    protected object $cache;
-    protected object $notifications;
-    protected int $max_retries = 3;
-    protected int $retry_delay = 300; // 5 minutes
 
-    public function __construct($post_type, $user_id, $config = [])
-    {
-        $this->post_type = $post_type;
-        $this->user_id = $user_id;
-        $this->config = wp_parse_args($config, $this->config);
-        // Check for Imagick availability
-        $this->config['use_imagick'] = extension_loaded('imagick');
+	protected array $supportedFormats = [
+		'webp' => 'image/webp',
+		'jpeg' => 'image/jpeg',
+		'jpg'  => 'image/jpeg',
+		'png'  => 'image/png',
+		'gif'  => 'image/gif'
+	];
 
-        $this->setupUploadDirs();
+	protected array $defaultConfig = [
+		'allowed_types' => null, // null = all types allowed
+		'max_size' => [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		],
+		// Image settings
+		'convert' => 'webp',
+		'quality' => 80,
+		'optimize_images' => true,
+		'create_thumbnails' => true,
+		'use_imagick' => null,
+		// Video settings
+		'extract_video_thumbnail' => true,
+		'video_thumbnail_time' => 0,
+		// Document settings
+		'extract_document_preview' => false,
+		// Directory structure
+		'directory_pattern' => '{user_id}/{post_type}',
+		// General
+		'original_retention' => 2592000, // 30 days
+	];
 
-
-        // Schedule cleanup of original files
-//        if (!wp_next_scheduled('jvb_cleanup_original_uploads')) {
-//            wp_schedule_event(time(), 'daily', 'jvb_cleanup_original_uploads');
-//        }
-//        add_action('jvb_cleanup_original_uploads', [$this, 'cleanupOriginalFiles']);
-
-
-        // Track upload statistics
-        add_action('jvb_upload_complete', [$this, 'trackUploadStats']);
-        add_action('jvb_upload_failed', [$this, 'track_failed_upload']);
-    }
-
-    /**
-     * @param array $file_data
-     *
-     * @return array
-     * @throws Exception
-     */
-	public function secureUploadedFile(array $file_data): array
+	protected string $upload_dir;
+	protected string $upload_url;
+	public function __construct()
 	{
-		// Create a persistent temporary directory if it doesn't exist
-		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$this->user_id}";
-		wp_mkdir_p($temp_dir);
+		$upload = wp_upload_dir();
+		$this->upload_dir = $upload['basedir'];
+		$this->upload_url = $upload['baseurl'];
 
-		// Generate a unique filename for temporary storage
-		$temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']);
-		$temp_path = "{$temp_dir}/{$temp_filename}";
-
-		// Move the uploaded file to our temporary storage
-		if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) {
-			// Fallback to copy if needed
-			if (!copy($file_data['tmp_name'], $temp_path)) {
-				throw new Exception('Failed to store uploaded file');
-			}
-		}
-
-		// Return metadata about the stored file
-		return [
-			'temp_path' => $temp_path,
-			'original_name' => $file_data['name'],
-			'mime_type' => $file_data['type'],
-			'file_size' => $file_data['size'],
-			'stored_at' => current_time('mysql')
-		];
-	}
-
-    /**
-     * @param string $temp_path
-     * @param string $post_type
-     * @param int $post_id
-     * @param int $term_id
-     * @param string $original_filename
-     *
-     * @return array
-     * @throws Exception
-     */
-	public function processImageFromStorage(string $temp_path, string $post_type, int $post_id = 0, int $term_id = 0, string $original_filename = ''): array
-	{
-		if (!file_exists($temp_path)) {
-			throw new Exception('Stored file no longer exists: ' . $temp_path);
-		}
-
-		$this->term_id = $term_id;
-		$this->post_type = $post_type;
-
-		// Get base upload directory based on content type
-		$rel_path = $this->getUploadDirectory();
-
-		// Ensure the directory exists
-		$full_upload_path = $this->upload_dir . '/' . $rel_path;
-		if (!wp_mkdir_p($full_upload_path)) {
-			throw new Exception("Failed to create upload directory: {$full_upload_path}");
-		}
-
-		// Generate filename WITHOUT extension (generateFilename should not include extension)
-		$base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id));
-
-		// Process the image directly from our temporary storage
-		return $this->processImage($temp_path, $rel_path, $base_filename, $post_id);
-	}
-
-    /**
-     * @return void
-     * @throws Exception
-     */
-    protected function setupUploadDirs():void
-    {
-        $upload_info = wp_upload_dir();
-        if ($upload_info['error']) {
-            throw new Exception($upload_info['error']);
-        }
-
-        $this->upload_dir = $upload_info['basedir'];
-        $this->upload_url = $upload_info['baseurl'];
-    }
-
-    /**
-     * @return string
-     */
-    public function getUploadDirectory():string
-    {
-		// Default WordPress organization: year/month
-		$default_path = date('Y/m');
-
-		/**
-		 * Filter the upload directory structure
-		 *
-		 * @param string $path The default upload path
-		 * @param string $post_type The post type being uploaded
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_directory', $default_path, $this->post_type, $this->user_id, $this->term_id);
-    }
-
-	/**
-	 * Generate filename with extensible filtering
-	 * Default: generic SEO-friendly format
-	 *
-	 * @param string $original_name
-	 * @param object $user_data
-	 * @return string
-	 */
-	public function generateFilename(string $original_name, object $user_data): string
-	{
-		// Default generic filename: {post_type}-{user_id}-{counter}
-		$filename = sprintf(
-			'%s-%s',
-			sanitize_title($this->post_type),
-			$this->user_id
-		);
-
-		/**
-		 * Filter the generated filename (without extension)
-		 *
-		 * @param string $filename The generated filename
-		 * @param string $original_name The original filename
-		 * @param object $user_data WordPress user data object
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_filename', $filename, $original_name, $user_data, $this->post_type, $this->user_id, $this->term_id).'-'.$this->getNextFileCounter();
-	}
-
-    /**
-     * @return string
-     */
-	protected function getNextFileCounter(): string
-	{
-		// Get counter key for this post type
-		$counter_key = 'upload_counter_' . str_replace(['/', '\\'], '_', $this->post_type);
-
-		// Get current counter value, default to 0
-		$counter = (int)get_user_meta($this->user_id, $counter_key, true) ?: 0;
-
-		// Increment counter
-		$counter++;
-
-		// Update counter in user meta
-		update_user_meta($this->user_id, $counter_key, $counter);
-
-		// Return formatted counter
-		return sprintf('%08d', $counter);
-	}
-
-
-	/**
-	 * Generate alt text with filtering for customization
-	 * Default: basic or empty alt text
-	 *
-	 * @param string $file
-	 * @param object $user_data
-	 * @param int|null $post_id
-	 * @return string
-	 */
-	protected function generateAltText(string $file, object $user_data, int|null $post_id = null): string
-	{
-		// Default: basic alt text or empty
-		$alt_text = '';
-
-		/**
-		 * Filter the generated alt text
-		 *
-		 * @param string $alt_text The generated alt text
-		 * @param string $file The file path
-		 * @param object $user_data WordPress user data object
-		 * @param int|null $post_id The post ID (if applicable)
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_alt_text', $alt_text, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
+		// Check for Imagick
+		$this->defaultConfig['use_imagick'] = extension_loaded('imagick');
 	}
 
 	/**
-	 * Generate image title with filtering for customization
-	 * Default: WordPress default behavior (filename-based)
-	 *
-	 * @param string $file
-	 * @param object $user_data
-	 * @param int|null $post_id
-	 * @return string
+	 * Initial, basic processing of upload data for later processing through OperationQueue.php
+	 * @param array $file_data
+	 * @param array $context
+	 * @return array
 	 */
-	protected function generateImageTitle(string $file, object $user_data, int|null $post_id = null): string
-	{
-		// Default: Use filename without extension (WordPress default behavior)
-		$title = pathinfo($file, PATHINFO_FILENAME);
-
-		/**
-		 * Filter the generated image title
-		 *
-		 * @param string $title The generated title
-		 * @param string $file The file path
-		 * @param object $user_data WordPress user data object
-		 * @param int|null $post_id The post ID (if applicable)
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_image_title', $title, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
-	}
-
-    /**
-     * @param array $file_data
-     * @param array $options
-     *
-     * @return array|WP_Error
-     */
-	public function handleContentUpload(array $file_data, array $options = []): array|WP_Error
+	public function secureUploadedFile(array $file_data, array $context): array
 	{
 		try {
-			if (!isset($file_data['tmp_name']) || !is_uploaded_file($file_data['tmp_name'])) {
-				throw new Exception('No valid file uploaded.');
+			// Validate file upload
+			if (!isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) {
+				throw new Exception('Invalid file upload');
 			}
 
-			$user_data = get_userdata($this->user_id);
-			$post_id = $options['post_id'] ?? 0;
-			$this->term_id = $options['term_id'] ?? 0;
+			$mime_type = mime_content_type($file_data['tmp_name']);
+			$file_type = $this->allowedTypes[$mime_type] ?? 'unknown';
+			if ($file_type === 'unknown') {
+				throw new Exception('Unsupported file type: ' . $mime_type);
+			}
 
-			// Get base upload directory based on content type
-			$base_dir = $this->getUploadDirectory();
-			$original_dir = 'originals';
+			// Check allowed types if specified
+			if ($this->defaultConfig['allowed_types'] && !in_array($mime_type, $this->defaultConfig['allowed_types'])) {
+				throw new Exception('File type not allowed: ' . $mime_type);
+			}
 
-			$rel_path = $base_dir;
-			$this->ensureUploadDirs($rel_path, $original_dir);
+			$temp_dir = $this->generateDirectory($file_type, "{user_id}/temp", $context);
 
-			$filename = $this->generateFilename($file_data['name'], $user_data);
+			// Ensure directories exist
+			$this->ensureDirectories($temp_dir);
 
-			// Store original file
-			$original_file = $this->storeOriginalFile(
-				$file_data['tmp_name'],
-				$base_dir,
-				$original_dir,
-				$filename
-			);
+			// Generate a unique filename for temporary storage
+			$temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']);
+			$temp_path = "{$this->upload_dir}/{$temp_dir}/{$temp_filename}";
 
-			// Process immediately
-			return $this->processImage($original_file, $rel_path, $filename, $post_id);
+			// Move the uploaded file to our temporary storage
+			if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) {
+				// Fallback to copy if needed
+				if (!copy($file_data['tmp_name'], $temp_path)) {
+					throw new Exception('Failed to store uploaded file');
+				}
+			}
+
+			// Return metadata about the stored file
+			return [
+				'temp_path' => $temp_path,
+				'original_name' => $file_data['name'],
+				'mime_type' => $file_data['type'],
+				'file_type' => $file_type,
+				'file_size' => $file_data['size'],
+				'stored_at' => current_time('mysql')
+			];
+		} catch (Exception $e) {
+			JVB()->error()->log('Failed to store uploaded file: ' . print_r($e->getMessage(), true));
+			return [];
+		}
+	}
+
+	/**
+	 * Process an uploaded file with full context
+	 *
+	 * @param string $temp_path
+	 * @param array $context Upload context and configuration
+	 * @return array|WP_Error Result with attachment_id, url, file path
+	 */
+	public function processUpload(string $temp_path, array $context): array|WP_Error
+	{
+		try {
+			// Validate temp file exists
+			if (!file_exists($temp_path)) {
+				throw new Exception('Temporary file not found: ' . $temp_path);
+			}
+
+			// Validate MIME type
+			$mime_type = mime_content_type($temp_path);
+			if (!$this->validateMimeType($temp_path, $mime_type)) {
+				throw new Exception('Invalid or potentially dangerous file type');
+			}
+			$file_type = $this->allowedTypes[$mime_type] ?? 'unknown';
+
+			if ($file_type === 'unknown') {
+				throw new Exception('Unsupported file type: ' . $mime_type);
+			}
+
+			// Merge config
+			$config = array_merge($this->defaultConfig, $context['config'] ?? [], $context);
+
+			// Extract context
+			$user_id = $context['user_id'] ?? get_current_user_id();
+			$post_id = $context['post_id'] ?? 0;
+
+			// Check allowed types if specified
+			if (!empty($config['allowed_types']) && !in_array($mime_type, $config['allowed_types'])) {
+				throw new Exception('File type not allowed: ' . $mime_type);
+			}
+
+			// Validate file size
+			$this->validateFileSize($temp_path, $file_type, $config);
+
+			// Generate directory structure for final storage
+			$directory = $this->generateDirectory($file_type, $config['directory_pattern'] ?? '{user_id}/{content}/{file_type}', $context);
+
+			// Ensure directories exist
+			$this->ensureDirectories($directory);
+
+			// Generate filename
+			$original_name = $context['original_name'] ?? basename($temp_path);
+			$filename = $this->generateFilename($original_name, $context);
+
+			// Store original from temp
+			$original_path = $this->storeOriginalFromTemp($temp_path, $directory, $filename);
+
+			// Process based on file type
+			return match($file_type) {
+				'image' => $this->processImage($original_path, $directory, $filename, $post_id, $config, $context),
+				'video' => $this->processVideo($original_path, $directory, $filename, $post_id, $config, $context),
+				'document' => $this->processDocument($original_path, $directory, $filename, $post_id, $config, $context),
+				default => throw new Exception('Unknown file type')
+			};
 
 		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadManager]:processUpload',
+				$e->getMessage(),
+				['temp_path' => $temp_path, 'context' => $context]
+			);
+
 			return new WP_Error('upload_failed', $e->getMessage());
 		}
 	}
 
-    /**
-     * @param string $rel_path
-     * @param string $original_dir
-     *
-     * @return void
-     * @throws Exception
-     */
-	protected function ensureUploadDirs($rel_path, $original_dir): void
+	/**
+	 * Store original file from temp location (not from PHP upload)
+	 */
+	protected function storeOriginalFromTemp(string $temp_path, string $directory, string $filename): string
+	{
+		$ext = pathinfo($temp_path, PATHINFO_EXTENSION);
+		$original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}";
+
+		// Copy from temp to final location
+		if (!copy($temp_path, $original_path)) {
+			throw new Exception('Failed to store original file');
+		}
+
+		return $original_path;
+	}
+
+	/**
+	 * Clean up empty temporary directories
+	 */
+	public function cleanupEmptyTempDirs(int $user_id): void
+	{
+		$temp_base = "{$this->upload_dir}/{$user_id}/temp";
+
+		if (!is_dir($temp_base)) {
+			return;
+		}
+
+		// Get all subdirectories
+		$dirs = glob($temp_base . '/*', GLOB_ONLYDIR);
+
+		foreach ($dirs as $dir) {
+			// Check if directory is empty
+			$files = scandir($dir);
+			$files = array_diff($files, ['.', '..']);
+
+			if (empty($files)) {
+				@rmdir($dir);
+			}
+		}
+
+		// Try to remove temp base if empty
+		$files = scandir($temp_base);
+		$files = array_diff($files, ['.', '..']);
+		if (empty($files)) {
+			@rmdir($temp_base);
+		}
+	}
+	/**
+	 * Generate directory structure based on pattern
+	 */
+	protected function generateDirectory(string $file_type, string $pattern, array $context): string
+	{
+		$replacements = [
+			'{user_id}' => $context['user_id'] ?? get_current_user_id(),
+			'{post_type}' => $context['content'] ?? '',
+			'{content}' => $context['content'] ?? '',
+			'{file_type}' => $file_type,
+			'{year}' => date('Y'),
+			'{month}' => date('m'),
+		];
+
+		$directory = str_replace(array_keys($replacements), array_values($replacements), $pattern);
+
+		// Allow filtering
+		return apply_filters('jvb_upload_directory', $directory, $file_type, $context);
+	}
+
+	/**
+	 * Ensure directory structure exists
+	 */
+	protected function ensureDirectories(string $rel_path): void
 	{
 		$dirs = [
 			"{$this->upload_dir}/{$rel_path}",
-			"{$this->upload_dir}/{$rel_path}/{$original_dir}"
+			"{$this->upload_dir}/{$rel_path}/originals"
 		];
 
 		foreach ($dirs as $dir) {
@@ -342,19 +299,13 @@
 		}
 	}
 
-
 	/**
-	 * @param string $tmp_file
-	 * @param string $base_dir
-	 * @param string $original_dir
-	 * @param string $filename
-	 * @return string
-	 * @throws Exception
+	 * Store original file
 	 */
-	protected function storeOriginalFile(string $tmp_file, string $base_dir, string $original_dir, string $filename): string
+	protected function storeOriginal(string $tmp_file, string $directory, string $filename): string
 	{
 		$ext = pathinfo($tmp_file, PATHINFO_EXTENSION);
-		$original_path = "{$this->upload_dir}/{$base_dir}/{$original_dir}/{$filename}.{$ext}";
+		$original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}";
 
 		if (!move_uploaded_file($tmp_file, $original_path)) {
 			throw new Exception('Failed to store original file');
@@ -364,275 +315,424 @@
 	}
 
 	/**
-	 * Process image with better error handling
+	 * Validate file size based on type and config
 	 */
-	protected function processImage(string $original_file, string $rel_path, string $filename, int $post_id): array
+	protected function validateFileSize(string $file_path, string $file_type, array $config): void
 	{
-		// Validate the original file still exists
-		if (!file_exists($original_file)) {
-			throw new Exception('Original file no longer exists: ' . $original_file);
+		$size = filesize($file_path);
+		$max_size = $config['max_size'][$file_type] ?? $this->defaultConfig['max_size'][$file_type];
+
+		if ($size > $max_size) {
+			$max_mb = round($max_size / 1048576, 2);
+			$actual_mb = round($size / 1048576, 2);
+			throw new Exception("File too large: {$actual_mb}MB (max: {$max_mb}MB)");
+		}
+	}
+
+	protected function validateMimeType(string $file_path, string $declared_type): bool
+	{
+		// Get actual MIME type
+		$actual_type = mime_content_type($file_path);
+
+		// Check if actual type is allowed
+		if (!array_key_exists($actual_type, $this->allowedTypes)) {
+			return false;
 		}
 
-		// Verify file type before processing
-		$mime_type = mime_content_type($original_file);
-		if (!in_array($mime_type, $this->config['allowed_types'])) {
-			throw new Exception('Invalid file type detected during processing: ' . $mime_type);
+		// For images, verify it's actually an image
+		if (str_starts_with($actual_type, 'image/')) {
+			$image_info = @getimagesize($file_path);
+			if ($image_info === false) {
+				return false;
+			}
 		}
 
-		// Ensure the upload directory exists
-		$full_upload_dir = $this->upload_dir . '/' . $rel_path;
-		if (!wp_mkdir_p($full_upload_dir)) {
-			throw new Exception("Failed to create upload directory: {$full_upload_dir}");
+		return true;
+	}
+
+	/**
+	 * Process image file
+	 * @throws Exception
+	 */
+	protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
+	{
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$original_mime = mime_content_type($original_path);
+
+		// Determine if conversion is needed
+		$should_convert = false;
+		$target_format = false;
+
+		if ($config['convert'] && $config['convert'] !== false) {
+			$target_format = strtolower($config['convert']);
+
+			// Validate format
+			if (!isset($this->supportedFormats[$target_format])) {
+				throw new Exception("Unsupported conversion format: {$target_format}");
+			}
+
+			// Check if conversion is needed
+			$target_mime = $this->supportedFormats[$target_format];
+			$should_convert = ($original_mime !== $target_mime);
 		}
 
-		// Convert to WebP if enabled
-		if ($this->config['convert_to_webp'] && $mime_type !== 'image/webp') {
-			$final_path = "{$full_upload_dir}/{$filename}.webp";
+		// Convert or copy based on configuration
+		if ($should_convert) {
+			$final_path = "{$full_dir}/{$filename}.{$target_format}";
 
-			if ($this->config['use_imagick']) {
-				$this->convertWithImagick($original_file, $final_path);
-			} else {
-				$this->convertWithGd($original_file, $final_path);
+			try {
+				$this->convertImage(
+					$original_path,
+					$final_path,
+					$target_format,
+					$config['quality'],
+					$config['use_imagick']
+				);
+			} catch (Exception $e) {
+				JVB()->error()->log('Image conversion failed: ' . print_r($e->getMessage(), true));
+				// Fallback to original
+				$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+				$final_path = "{$full_dir}/{$filename}.{$ext}";
+				copy($original_path, $final_path);
 			}
 		} else {
-			// Just copy the original with its extension
-			$original_ext = pathinfo($original_file, PATHINFO_EXTENSION);
-			$final_path = "{$full_upload_dir}/{$filename}.{$original_ext}";
-
-			if (!copy($original_file, $final_path)) {
-				throw new Exception("Failed to copy file from {$original_file} to {$final_path}");
-			}
+			// Keep original format
+			$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+			$final_path = "{$full_dir}/{$filename}.{$ext}";
+			copy($original_path, $final_path);
 		}
 
-		// Verify the final file was created
-		if (!file_exists($final_path)) {
-			throw new Exception("Final processed file was not created: {$final_path}");
+		// Create attachment
+		$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
+
+		// Set alt text if provided
+		$alt_text = apply_filters('jvb_upload_alt_text', '', $context);
+		if ($alt_text !== '') {
+			update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
 		}
 
-		// Generate title text
-		$title = $this->generateImageTitle(
-			$final_path,
-			get_userdata($this->user_id),
-			$post_id
-		);
-
-		// Create attachment with title
-		$attachment_id = $this->createAttachment($final_path, $title, $post_id);
-
 		// Generate thumbnails
-		if ($this->config['create_thumbnails']) {
+		if ($config['create_thumbnails']) {
 			$this->generateThumbnails($attachment_id);
 		}
 
-		// Update post attachments with new file info
-		$this->updatePostAttachments($attachment_id, $final_path);
-
 		return [
 			'success' => true,
 			'attachment_id' => $attachment_id,
 			'url' => wp_get_attachment_url($attachment_id),
-			'file' => $final_path
+			'file' => $final_path,
+			'type' => 'image',
+			'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION)
 		];
 	}
 
-    /**
-     * @param string $source
-     * @param string $destination
-     *
-     * @return void
-     * @throws Exception
-     */
-	protected function convertWithImagick(string $source, string $destination, string $toType = 'webp'): void
+	/**
+	 * Process video file
+	 * @throws Exception
+	 */
+	protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
 	{
-		$allowed = ['webp', 'jpeg', 'jpg', 'png'];
-		if (!in_array($toType, $allowed)) {
-			return;
-		}
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+		$final_path = "{$full_dir}/{$filename}.{$ext}";
+
+		// Copy video to final location
+		copy($original_path, $final_path);
+
+		// Create attachment
 		try {
-			$image = new Imagick($source);
-			$image->setImageFormat('webp');
-			$image->setImageCompressionQuality($this->config['webp_quality']);
-			$image->writeImage($destination);
-			$image->clear();
+			$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
 		} catch (Exception $e) {
-			throw new Exception('WebP conversion with Imagick failed: ' . $e->getMessage());
+			JVB()->error()->log('Attachment creation failed: '.print_r($e->getMessage(), true));
+			return ['success' => false];
+		}
+
+
+		// Extract thumbnail if enabled
+		if ($config['extract_video_thumbnail']) {
+			$this->extractVideoThumbnail($attachment_id, $final_path, $config['video_thumbnail_time']);
+		}
+
+		return [
+			'success' => true,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'type' => 'video'
+		];
+	}
+
+	/**
+	 * Process document file
+	 */
+	protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
+	{
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+		$final_path = "{$full_dir}/{$filename}.{$ext}";
+
+		// Copy document to final location
+		copy($original_path, $final_path);
+
+		// Create attachment
+		try {
+			$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
+		} catch (Exception $e) {
+			JVB()->error()->log('Conversion failed: '.print_r($e->getMessage(), true));
+			return ['success' => false];
+		}
+
+
+		return [
+			'success' => true,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'type' => 'document'
+		];
+	}
+
+	/**
+	 * Generate SEO-friendly filename
+	 */
+	protected function generateFilename(string $original_name, array $context): string
+	{
+		$name_parts = pathinfo($original_name);
+		$base_name = sanitize_title($name_parts['filename']);
+		$user_data = array_key_exists('user_id', $context) ? get_userdata((int)$context['user_id']) : get_current_user();
+		$username = $user_data ? sanitize_title($user_data->display_name) : 'user';
+		$timestamp = time();
+
+		return apply_filters(
+			'jvb_upload_filename',
+			"{$username}-{$base_name}-{$timestamp}",
+			$context
+		);
+	}
+
+	protected function sanitizeFileName(string $filename): string
+	{
+		// Remove path info
+		$filename = basename($filename);
+
+		// Remove special characters
+		$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
+
+		// Remove multiple underscores
+		$filename = preg_replace('/_+/', '_', $filename);
+
+		// Ensure it's not too long (255 char limit in most filesystems)
+		if (strlen($filename) > 200) {
+			$ext = pathinfo($filename, PATHINFO_EXTENSION);
+			$name = pathinfo($filename, PATHINFO_FILENAME);
+			$name = substr($name, 0, 200 - strlen($ext) - 1);
+			$filename = $name . '.' . $ext;
+		}
+
+		return $filename;
+	}
+
+	/**
+	 * Create WordPress attachment
+	 */
+	protected function createAttachment(string $file_path, string $title, array $context, int $post_id = 0): int
+	{
+		$file_url = str_replace($this->upload_dir, $this->upload_url, $file_path);
+		$mime_type = mime_content_type($file_path);
+
+		$title = apply_filters('jvb_upload_title', $title, $file_path, $context);
+
+		$attachment_id = wp_insert_attachment([
+			'guid' => $file_url,
+			'post_mime_type' => $mime_type,
+			'post_title' => $title,
+			'post_status' => 'inherit'
+		], $file_path, $post_id);
+
+		if (is_wp_error($attachment_id)) {
+			throw new Exception('Failed to create attachment');
+		}
+
+		require_once(ABSPATH . 'wp-admin/includes/image.php');
+		$metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
+		wp_update_attachment_metadata($attachment_id, $metadata);
+
+		return $attachment_id;
+	}
+
+
+	/**
+	 * Convert an existing attachment to a different format
+	 *
+	 * @param int $attachment_id WordPress attachment ID
+	 * @param string $format Target format: 'webp', 'jpeg', 'png', 'gif'
+	 * @param int $quality Conversion quality (1-100)
+	 * @param bool $replace Replace original file (default: true)
+	 * @return array|WP_Error Result with new attachment details
+	 */
+	public function convertImageTo(int $attachment_id, string $format, int $quality = 80, bool $replace = true): array|WP_Error
+	{
+		try {
+			// Validate format
+			$format = strtolower($format);
+			if (!isset($this->supportedFormats[$format])) {
+				throw new Exception("Unsupported format: {$format}");
+			}
+
+			// Get original file
+			$original_path = get_attached_file($attachment_id);
+			if (!$original_path || !file_exists($original_path)) {
+				throw new Exception('Original file not found');
+			}
+
+			// Check if it's an image
+			$mime_type = get_post_mime_type($attachment_id);
+			if (!str_starts_with($mime_type, 'image/')) {
+				throw new Exception('Attachment is not an image');
+			}
+
+			// Don't convert if already in target format
+			$current_mime = $this->supportedFormats[$format];
+			if ($mime_type === $current_mime) {
+				return [
+					'success' => true,
+					'message' => 'Already in target format',
+					'attachment_id' => $attachment_id,
+					'url' => wp_get_attachment_url($attachment_id)
+				];
+			}
+
+			// Generate new filename
+			$path_info = pathinfo($original_path);
+			$new_filename = $path_info['filename'] . '.' . $format;
+			$new_path = $path_info['dirname'] . '/' . $new_filename;
+
+			// Perform conversion
+			$use_imagick = $this->defaultConfig['use_imagick'];
+			$this->convertImage($original_path, $new_path, $format, $quality, $use_imagick);
+
+			if ($replace) {
+				// Replace the original file
+				$this->replaceAttachmentFile($attachment_id, $new_path, $current_mime);
+				$result_attachment_id = $attachment_id;
+			} else {
+				// Create new attachment
+				$post_id = wp_get_post_parent_id($attachment_id);
+				$result_attachment_id = $this->createAttachment(
+					$new_path,
+					get_the_title($attachment_id),
+					['user_id' => get_post_field('post_author', $attachment_id)],
+					$post_id
+				);
+			}
+
+			// Regenerate thumbnails
+			$this->generateThumbnails($result_attachment_id);
+
+			return [
+				'success' => true,
+				'attachment_id' => $result_attachment_id,
+				'url' => wp_get_attachment_url($result_attachment_id),
+				'format' => $format,
+				'file' => $new_path
+			];
+
+		} catch (Exception $e) {
+			return new WP_Error('conversion_failed', $e->getMessage());
 		}
 	}
 
-    /**
-     * Fixed convertWithGd method with better error handling
-     */
-	protected function convertWithGd(string $source, string $destination, string $toType = 'webp'): void
+	/**
+	 * Generic image conversion method
+	 */
+	protected function convertImage(string $source, string $destination, string $format, int $quality, bool $use_imagick): void
+	{
+		if ($use_imagick) {
+			$this->convertWithImagick($source, $destination, $format, $quality);
+		} else {
+			$this->convertWithGd($source, $destination, $format, $quality);
+		}
+	}
+	/**
+	 * Convert image with Imagick (supports multiple formats)
+	 */
+	protected function convertWithImagick(string $source, string $destination, string $format, int $quality): void
+	{
+		try {
+			$image = new Imagick($source);
+
+			// Set format
+			$image->setImageFormat($format);
+
+			// Set quality
+			$image->setImageCompressionQuality($quality);
+
+			// Format-specific optimizations
+			if ($format === 'webp') {
+				$image->setOption('webp:method', '6'); // Better compression
+			} elseif ($format === 'jpeg' || $format === 'jpg') {
+				$image->setImageCompression(Imagick::COMPRESSION_JPEG);
+			} elseif ($format === 'png') {
+				$image->setImageCompression(Imagick::COMPRESSION_ZIP);
+			}
+
+			$image->writeImage($destination);
+			$image->clear();
+			$image->destroy();
+		} catch (Exception $e) {
+			throw new Exception("Imagick conversion to {$format} failed: " . $e->getMessage());
+		}
+	}
+
+	/**
+	 * Convert image with GD (supports multiple formats)
+	 */
+	protected function convertWithGd(string $source, string $destination, string $format, int $quality): void
 	{
 		$mime_type = mime_content_type($source);
 
-		// Ensure destination directory exists
-		$dest_dir = dirname($destination);
-		if (!wp_mkdir_p($dest_dir)) {
-			throw new Exception("Failed to create destination directory: {$dest_dir}");
+		// Create image resource from source
+		$image = match($mime_type) {
+			'image/jpeg' => imagecreatefromjpeg($source),
+			'image/png' => imagecreatefrompng($source),
+			'image/gif' => imagecreatefromgif($source),
+			'image/webp' => imagecreatefromwebp($source),
+			default => throw new Exception('Unsupported source image type for GD conversion')
+		};
+
+		if (!$image) {
+			throw new Exception('Failed to create image resource');
 		}
 
-		switch ($mime_type) {
-			case 'image/webp':
-				$image = imagecreatefromwebp($source);
-				break;
-			case 'image/jpeg':
-				$image = imagecreatefromjpeg($source);
-				break;
-			case 'image/png':
-				$image = imagecreatefrompng($source);
-				if ($image !== false) {
-					imagepalettetotruecolor($image);
-					imagealphablending($image, true);
-					imagesavealpha($image, true);
-				}
-				break;
-			case 'image/gif':
-				$image = imagecreatefromgif($source);
-				if ($image !== false) {
-					imagepalettetotruecolor($image);
-				}
-				break;
-			default:
-				throw new Exception('Unsupported image type for GD conversion: ' . $mime_type);
-		}
+		// Convert to target format
+		$result = match($format) {
+			'webp' => imagewebp($image, $destination, $quality),
+			'jpeg', 'jpg' => imagejpeg($image, $destination, $quality),
+			'png' => imagepng($image, $destination, $this->qualityToPngCompression($quality)),
+			'gif' => imagegif($image, $destination),
+			default => throw new Exception("Unsupported target format for GD: {$format}")
+		};
 
-		if ($image === false) {
-			throw new Exception('Failed to create image resource from source file: ' . $source);
-		}
-
-
-		// Convert to WebP
-		$result = imagewebp($image, $destination, $this->config['webp_quality']);
-
-		// Clean up memory
 		imagedestroy($image);
 
 		if (!$result) {
-			throw new Exception('WebP conversion with GD failed - imagewebp returned false');
-		}
-
-		// Verify the file was actually created
-		if (!file_exists($destination)) {
-			throw new Exception('WebP file was not created despite imagewebp returning true');
+			throw new Exception("GD conversion to {$format} failed");
 		}
 	}
 
 	/**
-	 * @return void
+	 * Convert quality (1-100) to PNG compression level (0-9)
 	 */
-	public function cleanupOriginalFiles(): void
+	protected function qualityToPngCompression(int $quality): int
 	{
-		$cutoff = time() - $this->config['original_retention'];
-
-		// Get upload directory and find original directories
-		$pattern = "{$this->upload_dir}/**/originals";
-		$original_dirs = glob($pattern, GLOB_ONLYDIR);
-
-		foreach ($original_dirs as $original_dir) {
-			$files = glob("{$original_dir}/*");
-			foreach ($files as $file) {
-				if (filemtime($file) < $cutoff) {
-					unlink($file);
-				}
-			}
-		}
+		// PNG compression is inverted: 0 = no compression, 9 = max compression
+		// Quality: 100 = best quality, 1 = worst quality
+		// So: quality 100 -> compression 0, quality 1 -> compression 9
+		return (int) round((100 - $quality) / 11.11);
 	}
 
 	/**
-	 * @param string|null $temp_dir
-	 * @return void
-	 */
-	protected function cleanupTempFiles(string|null $temp_dir = null): void
-	{
-		if (is_null($temp_dir)) {
-			$temp_dir = $this->upload_dir . '/tmp';
-		}
-
-		if (is_dir($temp_dir)) {
-			$files = glob($temp_dir . '/*');
-			foreach ($files as $file) {
-				if (is_file($file)) {
-					@unlink($file);
-				}
-			}
-			@rmdir($temp_dir);
-		}
-	}
-
-
-	/**
-	 * @param int $retention_days
-	 * @return void
-	 */
-	public function cleanupUserFiles(int $retention_days = 30): void
-	{
-		$cutoff = time() - ($retention_days * DAY_IN_SECONDS);
-		$user_dir = $this->getUploadDirectory();
-
-		if (!is_dir($user_dir)) {
-			return;
-		}
-
-		$this->cleanupDirectory($user_dir, $cutoff);
-	}
-
-    /**
-	 * @param string $dir
-	 * @param int $cutoff
-	 * @return void
-	 */
-	protected function cleanupDirectory(string $dir, int $cutoff): void
-	{
-		$files = glob($dir . '/*');
-		foreach ($files as $file) {
-			if (is_dir($file)) {
-				$this->cleanupDirectory($file, $cutoff);
-			} elseif (filemtime($file) < $cutoff) {
-				@unlink($file);
-			}
-		}
-	}
-
-	/**
-	 * @param string $file
-	 * @param string $title
-	 * @param int $post_id
-	 * @return int|WP_Error
-	 * @throws Exception
-	 */
-	protected function createAttachment(string $file, string $title, int $post_id): int|WP_Error
-	{
-		$file_url = str_replace($this->upload_dir, $this->upload_url, $file);
-
-		$attachment = [
-			'post_mime_type' => mime_content_type($file),
-			'post_title' => $title,
-			'post_name' => sanitize_title($title),
-			'post_content' => '',
-			'post_status' => 'inherit',
-			'guid' => $file_url
-		];
-
-		$attach_id = wp_insert_attachment($attachment, $file, $post_id);
-		if (is_wp_error($attach_id)) {
-			throw new Exception($attach_id->get_error_message());
-		}
-
-		// Generate and set alt text
-		$alt_text = $this->generateAltText($file, get_userdata($this->user_id), $post_id);
-		update_post_meta($attach_id, '_wp_attachment_image_alt', $alt_text);
-
-		return $attach_id;
-	}
-
-	/**
-	 * Generate thumbnails using WordPress's built-in image size system
-	 * This will create all registered image sizes (thumbnail, medium, large, and any custom sizes)
-	 * Sites can register custom image sizes using add_image_size()
-	 *
-	 * @param int $attachment_id
-	 * @return void
+	 * Generate image thumbnails
 	 */
 	protected function generateThumbnails(int $attachment_id): void
 	{
@@ -642,119 +742,13 @@
 	}
 
 	/**
-	 * @param $result
-	 * @return void
+	 * Extract video thumbnail (placeholder - requires ffmpeg)
 	 */
-	protected function trackUploadStats($result)
+	protected function extractVideoThumbnail(int $attachment_id, string $video_path, int $time): void
 	{
-		$stats_key = "upload_stats_{$this->user_id}";
-		$stats = wp_cache_get($stats_key) ?: [
-			'total_uploads' => 0,
-			'successful_uploads' => 0,
-			'failed_uploads' => 0,
-			'total_size' => 0,
-			'last_upload' => null
-		];
-
-		if ($result['success']) {
-			$stats['successful_uploads']++;
-			$stats['total_size'] += filesize($result['file']);
-		} else {
-			$stats['failed_uploads']++;
-		}
-
-		$stats['total_uploads']++;
-		$stats['last_upload'] = current_time('mysql');
-
-		wp_cache_set($stats_key, $stats, '', DAY_IN_SECONDS);
+		// This would require ffmpeg or similar
+		// Placeholder for future implementation
+		apply_filters('jvb_extract_video_thumbnail', null, $attachment_id, $video_path, $time);
 	}
 
-	/**
-	 * @param int $attachment_id
-	 * @param string $new_file_path
-	 * @return void
-	 */
-	protected function updatePostAttachments(int $attachment_id, string $new_file_path): void
-	{
-		// Update attachment post
-		$file_url = str_replace($this->upload_dir, $this->upload_url, $new_file_path);
-		$filename = basename($new_file_path);
-
-		wp_update_post([
-			'ID' => $attachment_id,
-			'guid' => $file_url,
-			'post_mime_type' => mime_content_type($new_file_path),
-			'post_title' => $filename
-		]);
-
-		// Update attachment metadata
-		update_post_meta($attachment_id, '_wp_attached_file', str_replace($this->upload_dir . '/', '', $new_file_path));
-
-		// Update attachment metadata including sizes
-		$metadata = wp_get_attachment_metadata($attachment_id);
-		if ($metadata) {
-			$metadata['file'] = str_replace($this->upload_dir . '/', '', $new_file_path);
-
-			// Update thumbnail paths if they exist
-			if (!empty($metadata['sizes'])) {
-				foreach ($metadata['sizes'] as $size => $info) {
-					$old_file = $info['file'];
-					$new_file = preg_replace(
-						'/\.(jpe?g|png|gif)$/i',
-						'.webp',
-						$old_file
-					);
-					$metadata['sizes'][$size]['file'] = $new_file;
-					$metadata['sizes'][$size]['mime-type'] = 'image/webp';
-				}
-			}
-
-			wp_update_attachment_metadata($attachment_id, $metadata);
-		}
-
-		// If this is a profile/featured image, update those references
-		$post_id = wp_get_post_parent_id($attachment_id);
-		if ($post_id) {
-			$featured_image_id = get_post_thumbnail_id($post_id);
-			if ($featured_image_id === $attachment_id) {
-				// Re-set the featured image to trigger any necessary updates
-				set_post_thumbnail($post_id, $attachment_id);
-			}
-		}
-
-		// Clear any caches
-		clean_attachment_cache($attachment_id);
-		clean_post_cache($post_id);
-	}
-
-	/**
-	 * Clean up empty temporary directories
-	 *
-	 * @param int $user_id
-	 * @return void
-	 */
-	public function cleanupEmptyTempDirs(int $user_id): void
-	{
-		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$user_id}";
-
-		if (is_dir($temp_dir)) {
-			// Check if directory is empty
-			$files = scandir($temp_dir);
-			$is_empty = (count($files) <= 2); // Only . and .. entries
-
-			if ($is_empty) {
-				// Try to remove the empty directory
-				@rmdir($temp_dir);
-
-				// Also check if parent temp directory is empty
-				$parent_temp_dir = "{$this->upload_dir}/jvb_temp_uploads";
-				$parent_files = scandir($parent_temp_dir);
-				$parent_is_empty = (count($parent_files) <= 2); // Only . and .. entries
-
-				if ($parent_is_empty) {
-					@rmdir($parent_temp_dir);
-				}
-			}
-		}
-	}
 }
diff --git a/inc/managers/UploadManager2.php b/inc/managers/UploadManager2.php
new file mode 100644
index 0000000..a322e24
--- /dev/null
+++ b/inc/managers/UploadManager2.php
@@ -0,0 +1,1206 @@
+<?php
+namespace JVBase\inc\managers;
+
+use JVBase\JVB;
+use JVBase\meta\MetaManager;
+use Exception;
+use Imagick;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+    exit; // Exit if accessed directly
+}
+/**
+ * Handles file uploads for edmonton.ink dashboard
+ * Includes image processing, validation, optimization, and SEO-friendly naming
+ */
+class UploadManager2
+{
+	protected array $allowedTypes = [
+		//Images
+		'image/jpeg'	=> 'image',
+		'image/png'		=> 'image',
+		'image/gif'		=> 'image',
+		'image/webp'	=> 'image',
+		//Videos
+		'video/mp4'		=> 'video',
+		'video/webm'	=> 'video',
+		'video/ogg'		=> 'video',
+		'video/ogv'		=> 'video',
+		'video/quicktime'=> 'video', // .mov files
+		'video/x-msvideo'=> 'video',
+		//Documents
+		'application/pdf'		=> 'document',
+		'application/msword'	=> 'document', // .doc
+		'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'document', // .docx
+		'application/vnd.ms-excel'		=> 'document', // .xls
+		'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'document', // .xlsx
+		'text/plain'		=> 'document', // .txt
+		'text/csv'			=> 'document',
+		'application/rtf'	=> 'document'
+	];
+    /**
+     * @var array Default configuration
+     */
+	protected array $config = [
+		'allowed_types' => [
+			//Images
+			'image/jpeg',
+			'image/png',
+			'image/gif',
+			'image/webp',
+			//Videos
+			'video/mp4',
+			'video/webm',
+			'video/ogg',
+			'video/ogv',
+			'video/quicktime', // .mov files
+			'video/x-msvideo',
+			//Documents
+			'application/pdf',
+			'application/msword', // .doc
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
+			'application/vnd.ms-excel', // .xls
+			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
+			'text/plain', // .txt
+			'text/csv',
+			'application/rtf'
+		],
+		'max_size' => [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		],
+		//Image Specific Settings
+		'convert_to_webp' => true,
+		'webp_quality' => 80,
+		'optimize_images' => true,
+		'create_thumbnails' => true,
+		'use_imagick' => null, // Will be set in constructor
+		//Video specific settings
+		'extract_video_thumbnail' => true,
+		'video_thumbnail_time' => 0,
+		// Document-specific settings
+		'extract_document_preview' => false,
+		// General settings
+		'original_retention' => 2592000, // 30 days in seconds
+	];
+
+    protected string $post_type;
+    protected int $user_id;
+	protected int $term_id;
+    protected string $upload_dir;
+    protected string $upload_url;
+    protected object $cache;
+    protected object $notifications;
+    protected int $max_retries = 3;
+    protected int $retry_delay = 300; // 5 minutes
+
+    public function __construct($post_type, $user_id, $config = [])
+    {
+        $this->post_type = $post_type;
+        $this->user_id = $user_id;
+        $this->config = wp_parse_args($config, $this->config);
+        // Check for Imagick availability
+        $this->config['use_imagick'] = extension_loaded('imagick');
+
+        $this->setupUploadDirs();
+
+
+        // Schedule cleanup of original files
+//        if (!wp_next_scheduled('jvb_cleanup_original_uploads')) {
+//            wp_schedule_event(time(), 'daily', 'jvb_cleanup_original_uploads');
+//        }
+//        add_action('jvb_cleanup_original_uploads', [$this, 'cleanupOriginalFiles']);
+
+
+        // Track upload statistics
+        add_action('jvb_upload_complete', [$this, 'trackUploadStats']);
+        add_action('jvb_upload_failed', [$this, 'track_failed_upload']);
+    }
+
+    /**
+     * @param array $file_data
+     *
+     * @return array
+     * @throws Exception
+     */
+	public function secureUploadedFile(array $file_data): array
+	{
+		// Create a persistent temporary directory if it doesn't exist
+		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$this->user_id}";
+		wp_mkdir_p($temp_dir);
+
+		// Generate a unique filename for temporary storage
+		$temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']);
+		$temp_path = "{$temp_dir}/{$temp_filename}";
+
+		// Move the uploaded file to our temporary storage
+		if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) {
+			// Fallback to copy if needed
+			if (!copy($file_data['tmp_name'], $temp_path)) {
+				throw new Exception('Failed to store uploaded file');
+			}
+		}
+
+		// Return metadata about the stored file
+		return [
+			'temp_path' => $temp_path,
+			'original_name' => $file_data['name'],
+			'mime_type' => $file_data['type'],
+			'file_size' => $file_data['size'],
+			'stored_at' => current_time('mysql')
+		];
+	}
+
+	/**
+	 * Process file from secured storage
+	 */
+	public function processFileFromStorage(string $temp_path, string $post_type, int $post_id, int $term_id, string $original_filename): array|WP_Error
+	{
+		if (!file_exists($temp_path)) {
+			throw new Exception('Temporary file no longer exists: ' . $temp_path);
+		}
+
+		$this->term_id = $term_id;
+		$this->post_type = $post_type;
+
+		$rel_path = $this->getUploadDirectory();
+		$base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id));
+
+		return $this->processFile($temp_path, $rel_path, $base_filename, $post_id);
+	}
+
+	/**
+	 * Main entry point - detects file type and routes to appropriate processor
+	 * @throws Exception
+	 */
+	public function processFile(string $file_path, string $rel_path, string $filename, int $post_id): array|WP_Error
+	{
+		if (!file_exists($file_path)) {
+			throw new Exception('File no longer exists: ' . $file_path);
+		}
+
+		$mime_type = mime_content_type($file_path);
+		if (!in_array($mime_type, $this->config['allowed_types'])) {
+			throw new Exception('Invalid file type: ' . $mime_type);
+		}
+
+		$file_type = (array_key_exists($mime_type, $this->allowedTypes)) ? $this->allowedTypes[$mime_type] : 'unknown';
+
+		// Route to appropriate processor
+		switch ($file_type) {
+			case 'image':
+				return $this->processImage($file_path, $rel_path, $filename, $post_id);
+			case 'video':
+				return $this->processVideo($file_path, $rel_path, $filename, $post_id);
+			case 'document':
+				return $this->processDocument($file_path, $rel_path, $filename, $post_id);
+			default:
+				throw new Exception('Unknown file type category');
+		}
+	}
+
+	/**
+	 * Validate file size based on type
+	 */
+	protected function validateFileSize(string $file_path, string $file_type): bool
+	{
+		$file_size = filesize($file_path);
+		$max_size = $this->config['max_size'][$file_type] ?? $this->config['max_size']['image'];
+
+		if ($file_size > $max_size) {
+			throw new Exception(sprintf(
+				'File size (%s) exceeds maximum allowed size (%s) for %s files',
+				size_format($file_size),
+				size_format($max_size),
+				$file_type
+			));
+		}
+
+		return true;
+	}
+
+    /**
+     * @param string $temp_path
+     * @param string $post_type
+     * @param int $post_id
+     * @param int $term_id
+     * @param string $original_filename
+     *
+     * @return array
+     * @throws Exception
+     */
+	public function processImageFromStorage(string $temp_path, string $post_type, int $post_id = 0, int $term_id = 0, string $original_filename = ''): array
+	{
+		if (!file_exists($temp_path)) {
+			throw new Exception('Stored file no longer exists: ' . $temp_path);
+		}
+
+		$this->term_id = $term_id;
+		$this->post_type = $post_type;
+
+		// Get base upload directory based on content type
+		$rel_path = $this->getUploadDirectory();
+
+		// Ensure the directory exists
+		$full_upload_path = $this->upload_dir . '/' . $rel_path;
+		if (!wp_mkdir_p($full_upload_path)) {
+			throw new Exception("Failed to create upload directory: {$full_upload_path}");
+		}
+
+		// Generate filename WITHOUT extension (generateFilename should not include extension)
+		$base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id));
+
+		// Process the image directly from our temporary storage
+		return $this->processImage($temp_path, $rel_path, $base_filename, $post_id);
+	}
+
+    /**
+     * @return void
+     * @throws Exception
+     */
+    protected function setupUploadDirs():void
+    {
+        $upload_info = wp_upload_dir();
+        if ($upload_info['error']) {
+            throw new Exception($upload_info['error']);
+        }
+
+        $this->upload_dir = $upload_info['basedir'];
+        $this->upload_url = $upload_info['baseurl'];
+    }
+
+    /**
+     * @return string
+     */
+    public function getUploadDirectory():string
+    {
+		// Default WordPress organization: year/month
+		$default_path = date('Y/m');
+
+		/**
+		 * Filter the upload directory structure
+		 *
+		 * @param string $path The default upload path
+		 * @param string $post_type The post type being uploaded
+		 * @param int $user_id The user ID
+		 * @param int $term_id The term ID (if applicable)
+		 */
+		return apply_filters('jvb_upload_directory', $default_path, $this->post_type, $this->user_id, $this->term_id);
+    }
+
+	/**
+	 * Generate filename with extensible filtering
+	 * Default: generic SEO-friendly format
+	 *
+	 * @param string $original_name
+	 * @param object $user_data
+	 * @return string
+	 */
+	public function generateFilename(string $original_name, object $user_data): string
+	{
+		// Default generic filename: {post_type}-{user_id}-{counter}
+		$filename = sprintf(
+			'%s-%s',
+			sanitize_title($this->post_type),
+			$this->user_id
+		);
+
+		/**
+		 * Filter the generated filename (without extension)
+		 *
+		 * @param string $filename The generated filename
+		 * @param string $original_name The original filename
+		 * @param object $user_data WordPress user data object
+		 * @param string $post_type The post type
+		 * @param int $user_id The user ID
+		 * @param int $term_id The term ID (if applicable)
+		 */
+		return apply_filters('jvb_upload_filename', $filename, $original_name, $user_data, $this->post_type, $this->user_id, $this->term_id).'-'.$this->getNextFileCounter();
+	}
+
+    /**
+     * @return string
+     */
+	protected function getNextFileCounter(): string
+	{
+		// Get counter key for this post type
+		$counter_key = 'upload_counter_' . str_replace(['/', '\\'], '_', $this->post_type);
+
+		// Get current counter value, default to 0
+		$counter = (int)get_user_meta($this->user_id, $counter_key, true) ?: 0;
+
+		// Increment counter
+		$counter++;
+
+		// Update counter in user meta
+		update_user_meta($this->user_id, $counter_key, $counter);
+
+		// Return formatted counter
+		return sprintf('%08d', $counter);
+	}
+
+
+	/**
+	 * Generate alt text with filtering for customization
+	 * Default: basic or empty alt text
+	 *
+	 * @param string $file
+	 * @param object $user_data
+	 * @param int|null $post_id
+	 * @return string
+	 */
+	protected function generateAltText(string $file, object $user_data, int|null $post_id = null): string
+	{
+		// Default: basic alt text or empty
+		$alt_text = '';
+
+		/**
+		 * Filter the generated alt text
+		 *
+		 * @param string $alt_text The generated alt text
+		 * @param string $file The file path
+		 * @param object $user_data WordPress user data object
+		 * @param int|null $post_id The post ID (if applicable)
+		 * @param string $post_type The post type
+		 * @param int $user_id The user ID
+		 * @param int $term_id The term ID (if applicable)
+		 */
+		return apply_filters('jvb_upload_alt_text', $alt_text, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
+	}
+
+	/**
+	 * Generate image title with filtering for customization
+	 * Default: WordPress default behavior (filename-based)
+	 *
+	 * @param string $file
+	 * @param object $user_data
+	 * @param int|null $post_id
+	 * @return string
+	 */
+	protected function generateImageTitle(string $file, object $user_data, int|null $post_id = null): string
+	{
+		// Default: Use filename without extension (WordPress default behavior)
+		$title = pathinfo($file, PATHINFO_FILENAME);
+
+		/**
+		 * Filter the generated image title
+		 *
+		 * @param string $title The generated title
+		 * @param string $file The file path
+		 * @param object $user_data WordPress user data object
+		 * @param int|null $post_id The post ID (if applicable)
+		 * @param string $post_type The post type
+		 * @param int $user_id The user ID
+		 * @param int $term_id The term ID (if applicable)
+		 */
+		return apply_filters('jvb_upload_image_title', $title, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
+	}
+
+    /**
+     * @param array $file_data
+     * @param array $options
+     *
+     * @return array|WP_Error
+     */
+	public function handleContentUpload(array $file_data, array $options = []): array|WP_Error
+	{
+		try {
+			if (!isset($file_data['tmp_name']) || !is_uploaded_file($file_data['tmp_name'])) {
+				throw new Exception('No valid file uploaded.');
+			}
+
+			$user_data = get_userdata($this->user_id);
+			$post_id = $options['post_id'] ?? 0;
+			$this->term_id = $options['term_id'] ?? 0;
+
+			// Get base upload directory based on content type
+			$base_dir = $this->getUploadDirectory();
+			$original_dir = 'originals';
+
+			$rel_path = $base_dir;
+			$this->ensureUploadDirs($rel_path, $original_dir);
+
+			$filename = $this->generateFilename($file_data['name'], $user_data);
+
+			// Store original file
+			$original_file = $this->storeOriginalFile(
+				$file_data['tmp_name'],
+				$base_dir,
+				$original_dir,
+				$filename
+			);
+
+			// Process immediately
+			return $this->processImage($original_file, $rel_path, $filename, $post_id);
+
+		} catch (Exception $e) {
+			return new WP_Error('upload_failed', $e->getMessage());
+		}
+	}
+
+    /**
+     * @param string $rel_path
+     * @param string $original_dir
+     *
+     * @return void
+     * @throws Exception
+     */
+	protected function ensureUploadDirs($rel_path, $original_dir): void
+	{
+		$dirs = [
+			"{$this->upload_dir}/{$rel_path}",
+			"{$this->upload_dir}/{$rel_path}/{$original_dir}"
+		];
+
+		foreach ($dirs as $dir) {
+			if (!wp_mkdir_p($dir)) {
+				throw new Exception("Failed to create directory: {$dir}");
+			}
+		}
+	}
+
+
+	/**
+	 * @param string $tmp_file
+	 * @param string $base_dir
+	 * @param string $original_dir
+	 * @param string $filename
+	 * @return string
+	 * @throws Exception
+	 */
+	protected function storeOriginalFile(string $tmp_file, string $base_dir, string $original_dir, string $filename): string
+	{
+		$ext = pathinfo($tmp_file, PATHINFO_EXTENSION);
+		$original_path = "{$this->upload_dir}/{$base_dir}/{$original_dir}/{$filename}.{$ext}";
+
+		if (!move_uploaded_file($tmp_file, $original_path)) {
+			throw new Exception('Failed to store original file');
+		}
+
+		return $original_path;
+	}
+
+	/**
+	 * Process image with better error handling
+	 * @throws Exception
+	 */
+	protected function processImage(string $original_file, string $rel_path, string $filename, int $post_id): array
+	{
+		// Validate the original file still exists
+		if (!file_exists($original_file)) {
+			throw new Exception('Original file no longer exists: ' . $original_file);
+		}
+		$this->validateFileSize($original_file, 'image');
+
+		// Verify file type before processing
+		$mime_type = mime_content_type($original_file);
+		if (!in_array($mime_type, $this->config['allowed_types'])) {
+			throw new Exception('Invalid file type detected during processing: ' . $mime_type);
+		}
+
+		// Ensure the upload directory exists
+		$full_upload_dir = $this->upload_dir . '/' . $rel_path;
+		if (!wp_mkdir_p($full_upload_dir)) {
+			throw new Exception("Failed to create upload directory: {$full_upload_dir}");
+		}
+
+		// Convert to WebP if enabled
+		if ($this->config['convert_to_webp'] && $mime_type !== 'image/webp') {
+			$final_path = "{$full_upload_dir}/{$filename}.webp";
+
+			if ($this->config['use_imagick']) {
+				$this->convertWithImagick($original_file, $final_path);
+			} else {
+				$this->convertWithGd($original_file, $final_path);
+			}
+		} else {
+			// Just copy the original with its extension
+			$original_ext = pathinfo($original_file, PATHINFO_EXTENSION);
+			$final_path = "{$full_upload_dir}/{$filename}.{$original_ext}";
+
+			if (!copy($original_file, $final_path)) {
+				throw new Exception("Failed to copy file from {$original_file} to {$final_path}");
+			}
+		}
+
+		// Verify the final file was created
+		if (!file_exists($final_path)) {
+			throw new Exception("Final processed file was not created: {$final_path}");
+		}
+
+		// Generate title text
+		$title = $this->generateImageTitle(
+			$final_path,
+			get_userdata($this->user_id),
+			$post_id
+		);
+
+		// Create attachment with title
+		$attachment_id = $this->createAttachment($final_path, $title, $post_id);
+
+		// Generate thumbnails
+		if ($this->config['create_thumbnails']) {
+			$this->generateThumbnails($attachment_id);
+		}
+
+		// Update post attachments with new file info
+		$this->updatePostAttachments($attachment_id, $final_path);
+
+		return [
+			'success' => true,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path
+		];
+	}
+
+    /**
+     * @param string $source
+     * @param string $destination
+     *
+     * @return void
+     * @throws Exception
+     */
+	protected function convertWithImagick(string $source, string $destination, string $toType = 'webp'): void
+	{
+		$allowed = ['webp', 'jpeg', 'jpg', 'png'];
+		if (!in_array($toType, $allowed)) {
+			return;
+		}
+		try {
+			$image = new Imagick($source);
+			$image->setImageFormat('webp');
+			$image->setImageCompressionQuality($this->config['webp_quality']);
+			$image->writeImage($destination);
+			$image->clear();
+		} catch (Exception $e) {
+			throw new Exception('WebP conversion with Imagick failed: ' . $e->getMessage());
+		}
+	}
+
+    /**
+     * Fixed convertWithGd method with better error handling
+     */
+	protected function convertWithGd(string $source, string $destination, string $toType = 'webp'): void
+	{
+		$mime_type = mime_content_type($source);
+
+		// Ensure destination directory exists
+		$dest_dir = dirname($destination);
+		if (!wp_mkdir_p($dest_dir)) {
+			throw new Exception("Failed to create destination directory: {$dest_dir}");
+		}
+
+		switch ($mime_type) {
+			case 'image/webp':
+				$image = imagecreatefromwebp($source);
+				break;
+			case 'image/jpeg':
+				$image = imagecreatefromjpeg($source);
+				break;
+			case 'image/png':
+				$image = imagecreatefrompng($source);
+				if ($image !== false) {
+					imagepalettetotruecolor($image);
+					imagealphablending($image, true);
+					imagesavealpha($image, true);
+				}
+				break;
+			case 'image/gif':
+				$image = imagecreatefromgif($source);
+				if ($image !== false) {
+					imagepalettetotruecolor($image);
+				}
+				break;
+			default:
+				throw new Exception('Unsupported image type for GD conversion: ' . $mime_type);
+		}
+
+		if ($image === false) {
+			throw new Exception('Failed to create image resource from source file: ' . $source);
+		}
+
+
+		// Convert to WebP
+		$result = imagewebp($image, $destination, $this->config['webp_quality']);
+
+		// Clean up memory
+		imagedestroy($image);
+
+		if (!$result) {
+			throw new Exception('WebP conversion with GD failed - imagewebp returned false');
+		}
+
+		// Verify the file was actually created
+		if (!file_exists($destination)) {
+			throw new Exception('WebP file was not created despite imagewebp returning true');
+		}
+	}
+
+	/**
+	 * @return void
+	 */
+	public function cleanupOriginalFiles(): void
+	{
+		$cutoff = time() - $this->config['original_retention'];
+
+		// Get upload directory and find original directories
+		$pattern = "{$this->upload_dir}/**/originals";
+		$original_dirs = glob($pattern, GLOB_ONLYDIR);
+
+		foreach ($original_dirs as $original_dir) {
+			$files = glob("{$original_dir}/*");
+			foreach ($files as $file) {
+				if (filemtime($file) < $cutoff) {
+					unlink($file);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @param string|null $temp_dir
+	 * @return void
+	 */
+	protected function cleanupTempFiles(string|null $temp_dir = null): void
+	{
+		if (is_null($temp_dir)) {
+			$temp_dir = $this->upload_dir . '/tmp';
+		}
+
+		if (is_dir($temp_dir)) {
+			$files = glob($temp_dir . '/*');
+			foreach ($files as $file) {
+				if (is_file($file)) {
+					@unlink($file);
+				}
+			}
+			@rmdir($temp_dir);
+		}
+	}
+
+
+	/**
+	 * @param int $retention_days
+	 * @return void
+	 */
+	public function cleanupUserFiles(int $retention_days = 30): void
+	{
+		$cutoff = time() - ($retention_days * DAY_IN_SECONDS);
+		$user_dir = $this->getUploadDirectory();
+
+		if (!is_dir($user_dir)) {
+			return;
+		}
+
+		$this->cleanupDirectory($user_dir, $cutoff);
+	}
+
+    /**
+	 * @param string $dir
+	 * @param int $cutoff
+	 * @return void
+	 */
+	protected function cleanupDirectory(string $dir, int $cutoff): void
+	{
+		$files = glob($dir . '/*');
+		foreach ($files as $file) {
+			if (is_dir($file)) {
+				$this->cleanupDirectory($file, $cutoff);
+			} elseif (filemtime($file) < $cutoff) {
+				@unlink($file);
+			}
+		}
+	}
+
+	/**
+	 * @param string $file
+	 * @param string $title
+	 * @param int $post_id
+	 * @return int|WP_Error
+	 * @throws Exception
+	 */
+	protected function createAttachment(string $file, string $title, int $post_id): int|WP_Error
+	{
+		$file_url = str_replace($this->upload_dir, $this->upload_url, $file);
+
+		$attachment = [
+			'post_mime_type' => mime_content_type($file),
+			'post_title' => $title,
+			'post_name' => sanitize_title($title),
+			'post_content' => '',
+			'post_status' => 'inherit',
+			'guid' => $file_url
+		];
+
+		$attach_id = wp_insert_attachment($attachment, $file, $post_id);
+		if (is_wp_error($attach_id)) {
+			throw new Exception($attach_id->get_error_message());
+		}
+
+		// Generate and set alt text for images only
+		if (str_starts_with(mime_content_type($file), 'image/')) {
+			$alt_text = $this->generateAltText($file, get_userdata($this->user_id), $post_id);
+			update_post_meta($attach_id, '_wp_attachment_image_alt', $alt_text);
+		}
+
+		return $attach_id;
+	}
+
+	/**
+	 * Generate thumbnails using WordPress's built-in image size system
+	 * This will create all registered image sizes (thumbnail, medium, large, and any custom sizes)
+	 * Sites can register custom image sizes using add_image_size()
+	 *
+	 * @param int $attachment_id
+	 * @return void
+	 */
+	protected function generateThumbnails(int $attachment_id): void
+	{
+		require_once(ABSPATH . 'wp-admin/includes/image.php');
+		$metadata = wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id));
+		wp_update_attachment_metadata($attachment_id, $metadata);
+	}
+
+	/**
+	 * @param $result
+	 * @return void
+	 */
+	protected function trackUploadStats($result)
+	{
+		$stats_key = "upload_stats_{$this->user_id}";
+		$stats = wp_cache_get($stats_key) ?: [
+			'total_uploads' => 0,
+			'successful_uploads' => 0,
+			'failed_uploads' => 0,
+			'total_size' => 0,
+			'last_upload' => null
+		];
+
+		if ($result['success']) {
+			$stats['successful_uploads']++;
+			$stats['total_size'] += filesize($result['file']);
+		} else {
+			$stats['failed_uploads']++;
+		}
+
+		$stats['total_uploads']++;
+		$stats['last_upload'] = current_time('mysql');
+
+		wp_cache_set($stats_key, $stats, '', DAY_IN_SECONDS);
+	}
+
+	/**
+	 * @param int $attachment_id
+	 * @param string $new_file_path
+	 * @return void
+	 */
+	protected function updatePostAttachments(int $attachment_id, string $new_file_path): void
+	{
+		// Update attachment post
+		$file_url = str_replace($this->upload_dir, $this->upload_url, $new_file_path);
+		$filename = basename($new_file_path);
+
+		wp_update_post([
+			'ID' => $attachment_id,
+			'guid' => $file_url,
+			'post_mime_type' => mime_content_type($new_file_path),
+			'post_title' => $filename
+		]);
+
+		// Update attachment metadata
+		update_post_meta($attachment_id, '_wp_attached_file', str_replace($this->upload_dir . '/', '', $new_file_path));
+
+		// Update attachment metadata including sizes
+		$metadata = wp_get_attachment_metadata($attachment_id);
+		if ($metadata) {
+			$metadata['file'] = str_replace($this->upload_dir . '/', '', $new_file_path);
+
+			// Update thumbnail paths if they exist
+			if (!empty($metadata['sizes'])) {
+				foreach ($metadata['sizes'] as $size => $info) {
+					$old_file = $info['file'];
+					$new_file = preg_replace(
+						'/\.(jpe?g|png|gif)$/i',
+						'.webp',
+						$old_file
+					);
+					$metadata['sizes'][$size]['file'] = $new_file;
+					$metadata['sizes'][$size]['mime-type'] = 'image/webp';
+				}
+			}
+
+			wp_update_attachment_metadata($attachment_id, $metadata);
+		}
+
+		// If this is a profile/featured image, update those references
+		$post_id = wp_get_post_parent_id($attachment_id);
+		if ($post_id) {
+			$featured_image_id = get_post_thumbnail_id($post_id);
+			if ($featured_image_id === $attachment_id) {
+				// Re-set the featured image to trigger any necessary updates
+				set_post_thumbnail($post_id, $attachment_id);
+			}
+		}
+
+		// Clear any caches
+		clean_attachment_cache($attachment_id);
+		clean_post_cache($post_id);
+	}
+
+	/**
+	 * Clean up empty temporary directories
+	 *
+	 * @param int $user_id
+	 * @return void
+	 */
+	public function cleanupEmptyTempDirs(int $user_id): void
+	{
+		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$user_id}";
+
+		if (is_dir($temp_dir)) {
+			// Check if directory is empty
+			$files = scandir($temp_dir);
+			$is_empty = (count($files) <= 2); // Only . and .. entries
+
+			if ($is_empty) {
+				// Try to remove the empty directory
+				@rmdir($temp_dir);
+
+				// Also check if parent temp directory is empty
+				$parent_temp_dir = "{$this->upload_dir}/jvb_temp_uploads";
+				$parent_files = scandir($parent_temp_dir);
+				$parent_is_empty = (count($parent_files) <= 2); // Only . and .. entries
+
+				if ($parent_is_empty) {
+					@rmdir($parent_temp_dir);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Process video files
+	 */
+	protected function processVideo(string $original_file, string $rel_path, string $filename, int $post_id): array
+	{
+		$this->validateFileSize($original_file, self::TYPE_VIDEO);
+
+		$full_upload_dir = $this->upload_dir . '/' . $rel_path;
+		if (!wp_mkdir_p($full_upload_dir)) {
+			throw new Exception("Failed to create upload directory: {$full_upload_dir}");
+		}
+
+		// Keep original extension for videos
+		$original_ext = pathinfo($original_file, PATHINFO_EXTENSION);
+		$final_path = "{$full_upload_dir}/{$filename}.{$original_ext}";
+
+		if (!copy($original_file, $final_path)) {
+			throw new Exception("Failed to copy video file");
+		}
+
+		if (!file_exists($final_path)) {
+			throw new Exception("Final video file was not created: {$final_path}");
+		}
+
+		// Generate title
+		$title = $this->generateMediaTitle($final_path, get_userdata($this->user_id), $post_id, 'video');
+		$attachment_id = $this->createAttachment($final_path, $title, $post_id);
+
+		// Extract video metadata
+		$video_metadata = $this->extractVideoMetadata($final_path);
+		if ($video_metadata) {
+			update_post_meta($attachment_id, '_jvb_video_metadata', $video_metadata);
+		}
+
+		// Generate video thumbnail if enabled
+		if ($this->config['extract_video_thumbnail']) {
+			$thumbnail_id = $this->generateVideoThumbnail($final_path, $attachment_id, $post_id);
+			if ($thumbnail_id) {
+				update_post_meta($attachment_id, '_jvb_video_thumbnail', $thumbnail_id);
+			}
+		}
+
+		return [
+			'success' => true,
+			'type' => self::TYPE_VIDEO,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'metadata' => $video_metadata ?? null
+		];
+	}
+
+	/**
+	 * Process document files
+	 */
+	protected function processDocument(string $original_file, string $rel_path, string $filename, int $post_id): array
+	{
+		$this->validateFileSize($original_file, self::TYPE_DOCUMENT);
+
+		$full_upload_dir = $this->upload_dir . '/' . $rel_path;
+		if (!wp_mkdir_p($full_upload_dir)) {
+			throw new Exception("Failed to create upload directory: {$full_upload_dir}");
+		}
+
+		// Keep original extension for documents
+		$original_ext = pathinfo($original_file, PATHINFO_EXTENSION);
+		$final_path = "{$full_upload_dir}/{$filename}.{$original_ext}";
+
+		if (!copy($original_file, $final_path)) {
+			throw new Exception("Failed to copy document file");
+		}
+
+		if (!file_exists($final_path)) {
+			throw new Exception("Final document file was not created: {$final_path}");
+		}
+
+		// Generate title
+		$title = $this->generateMediaTitle($final_path, get_userdata($this->user_id), $post_id, 'document');
+		$attachment_id = $this->createAttachment($final_path, $title, $post_id);
+
+		// Extract document metadata
+		$doc_metadata = $this->extractDocumentMetadata($final_path);
+		if ($doc_metadata) {
+			update_post_meta($attachment_id, '_jvb_document_metadata', $doc_metadata);
+		}
+
+		return [
+			'success' => true,
+			'type' => self::TYPE_DOCUMENT,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'metadata' => $doc_metadata ?? null
+		];
+	}
+
+	/**
+	 * Extract video metadata using getID3 or FFmpeg if available
+	 */
+	protected function extractVideoMetadata(string $file_path): ?array
+	{
+		$metadata = [
+			'filesize' => filesize($file_path),
+			'mime_type' => mime_content_type($file_path)
+		];
+
+		// Try FFmpeg first (more reliable)
+		if ($this->hasFFmpeg()) {
+			$ffmpeg_data = $this->getVideoMetadataFFmpeg($file_path);
+			if ($ffmpeg_data) {
+				return array_merge($metadata, $ffmpeg_data);
+			}
+		}
+
+		// Fallback to getID3 if available
+		if (class_exists('getID3')) {
+			$getID3 = new \getID3();
+			$file_info = $getID3->analyze($file_path);
+
+			if (isset($file_info['video'])) {
+				$metadata['duration'] = $file_info['playtime_seconds'] ?? null;
+				$metadata['width'] = $file_info['video']['resolution_x'] ?? null;
+				$metadata['height'] = $file_info['video']['resolution_y'] ?? null;
+				$metadata['codec'] = $file_info['video']['codec'] ?? null;
+				$metadata['bitrate'] = $file_info['bitrate'] ?? null;
+			}
+		}
+
+		return $metadata;
+	}
+
+	/**
+	 * Get video metadata using FFmpeg
+	 */
+	protected function getVideoMetadataFFmpeg(string $file_path): ?array
+	{
+		$ffprobe_path = $this->getFFprobePath();
+		if (!$ffprobe_path) {
+			return null;
+		}
+
+		$command = sprintf(
+			'%s -v quiet -print_format json -show_format -show_streams %s',
+			escapeshellarg($ffprobe_path),
+			escapeshellarg($file_path)
+		);
+
+		$output = shell_exec($command);
+		if (!$output) {
+			return null;
+		}
+
+		$data = json_decode($output, true);
+		if (!$data) {
+			return null;
+		}
+
+		$metadata = [];
+
+		// Get duration
+		if (isset($data['format']['duration'])) {
+			$metadata['duration'] = (float) $data['format']['duration'];
+		}
+
+		// Get video stream info
+		foreach ($data['streams'] ?? [] as $stream) {
+			if ($stream['codec_type'] === 'video') {
+				$metadata['width'] = $stream['width'] ?? null;
+				$metadata['height'] = $stream['height'] ?? null;
+				$metadata['codec'] = $stream['codec_name'] ?? null;
+				$metadata['bitrate'] = $stream['bit_rate'] ?? null;
+				break;
+			}
+		}
+
+		return $metadata;
+	}
+
+	/**
+	 * Check if FFmpeg is available
+	 */
+	protected function hasFFmpeg(): bool
+	{
+		return $this->getFFprobePath() !== null;
+	}
+
+	/**
+	 * Get FFprobe path (companion tool to FFmpeg)
+	 */
+	protected function getFFprobePath(): ?string
+	{
+		$paths = ['ffprobe', '/usr/bin/ffprobe', '/usr/local/bin/ffprobe'];
+
+		foreach ($paths as $path) {
+			if (shell_exec("which {$path}")) {
+				return $path;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Generate video thumbnail
+	 */
+	protected function generateVideoThumbnail(string $video_path, int $video_attachment_id, int $post_id): ?int
+	{
+		if (!$this->hasFFmpeg()) {
+			return null;
+		}
+
+		$ffmpeg_path = str_replace('ffprobe', 'ffmpeg', $this->getFFprobePath());
+		if (!$ffmpeg_path) {
+			return null;
+		}
+
+		// Generate thumbnail filename
+		$thumbnail_dir = dirname($video_path) . '/thumbnails';
+		wp_mkdir_p($thumbnail_dir);
+
+		$thumbnail_filename = pathinfo($video_path, PATHINFO_FILENAME) . '-thumb.jpg';
+		$thumbnail_path = $thumbnail_dir . '/' . $thumbnail_filename;
+
+		// Extract frame at specified time
+		$time = $this->config['video_thumbnail_time'];
+		$command = sprintf(
+			'%s -i %s -ss %d -vframes 1 -q:v 2 %s 2>&1',
+			escapeshellarg($ffmpeg_path),
+			escapeshellarg($video_path),
+			$time,
+			escapeshellarg($thumbnail_path)
+		);
+
+		shell_exec($command);
+
+		if (!file_exists($thumbnail_path)) {
+			return null;
+		}
+
+		// Create attachment for thumbnail
+		$title = get_the_title($video_attachment_id) . ' - Thumbnail';
+		$thumbnail_id = $this->createAttachment($thumbnail_path, $title, $post_id);
+
+		return $thumbnail_id;
+	}
+
+	/**
+	 * Extract document metadata
+	 */
+	protected function extractDocumentMetadata(string $file_path): array
+	{
+		$metadata = [
+			'filesize' => filesize($file_path),
+			'mime_type' => mime_content_type($file_path),
+			'extension' => pathinfo($file_path, PATHINFO_EXTENSION)
+		];
+
+		// PDF-specific metadata
+		if ($metadata['mime_type'] === 'application/pdf') {
+			$pdf_metadata = $this->extractPdfMetadata($file_path);
+			if ($pdf_metadata) {
+				$metadata = array_merge($metadata, $pdf_metadata);
+			}
+		}
+
+		return $metadata;
+	}
+
+	/**
+	 * Extract PDF metadata
+	 */
+	protected function extractPdfMetadata(string $file_path): ?array
+	{
+		$metadata = [];
+
+		// Try using pdfinfo if available
+		if (shell_exec('which pdfinfo')) {
+			$output = shell_exec('pdfinfo ' . escapeshellarg($file_path));
+			if ($output) {
+				if (preg_match('/Pages:\s+(\d+)/', $output, $matches)) {
+					$metadata['pages'] = (int) $matches[1];
+				}
+				if (preg_match('/Title:\s+(.+)/', $output, $matches)) {
+					$metadata['title'] = trim($matches[1]);
+				}
+				if (preg_match('/Author:\s+(.+)/', $output, $matches)) {
+					$metadata['author'] = trim($matches[1]);
+				}
+			}
+		}
+
+		// Fallback to basic file parsing if pdfinfo not available
+		if (empty($metadata)) {
+			$content = file_get_contents($file_path, false, null, 0, 1024);
+			if (preg_match('/\/Count\s+(\d+)/', $content, $matches)) {
+				$metadata['pages'] = (int) $matches[1];
+			}
+		}
+
+		return $metadata ?: null;
+	}
+
+	/**
+	 * Generate media title (for videos and documents)
+	 */
+	protected function generateMediaTitle(string $file_path, object $user_data, int $post_id, string $type): string
+	{
+		$filename = pathinfo($file_path, PATHINFO_FILENAME);
+		$post_title = $post_id ? get_the_title($post_id) : '';
+
+		$title = sprintf(
+			'%s %s by %s',
+			ucfirst($type),
+			$post_title ? "for {$post_title}" : $filename,
+			$user_data->display_name
+		);
+
+		/**
+		 * Filter the generated media title
+		 */
+		return apply_filters('jvb_media_title', $title, $file_path, $user_data, $post_id, $type);
+	}
+}
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index 77dd5f4..127238d 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -5,8 +5,14 @@
 require(JVB_DIR . '/icons.php');
 require(JVB_DIR . '/inc/managers/ErrorHandler.php');
 require(JVB_DIR . '/inc/managers/OperationQueue.php');
+require(JVB_DIR . '/inc/managers/EmailManager.php');
 require(JVB_DIR . '/inc/managers/LoginManager.php');
 
+if (Features::forSite()->has('magicLink')) {
+	require(JVB_DIR . '/inc/managers/MagicLinkManager.php');
+}
+
+
 //IF SITE HAS DASHBOARD AND FEED BLOCK
 if (Features::forSite()->hasAny(['dashboard', 'feed_block'])) {
 	require(JVB_DIR . '/inc/forms/TaxonomySelector.php');
@@ -41,7 +47,6 @@
 require(JVB_DIR . '/inc/managers/SchemaManager.php');
 require(JVB_DIR . '/inc/managers/SEOMetaManager.php');
 require(JVB_DIR . '/inc/managers/DirectoryManager.php');
-require(JVB_DIR . '/inc/managers/EmailManager.php');
 require(JVB_DIR . '/inc/managers/ImageGenerator.php');
 require(JVB_DIR . '/inc/managers/AdminPages.php');
 require(JVB_DIR . '/inc/managers/RoleManager.php');
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 7e7e66d..d6846c4 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -1298,6 +1298,396 @@
         <?php
     }
 
+	private function renderUploadField(string $name, mixed $value, array $field): void
+	{
+		$defaultConfig = [
+			//File Type
+			'subtype' => 'image', // 'image', 'video', 'document', 'any'
+			'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
+			//Upload Behaviour
+			'multiple' => false, // Single or multiple uploads
+			'limit' => 0, // Max number of uploads (0 = unlimited)
+			'mode' => 'direct', // 'direct' or 'selection'
+			//Destination
+			'destination' => 'meta', // 'meta', 'post', 'post_group'
+			//Processing Options
+			'max_size' => null, // Override default size limits
+			'convert' => 'webp', // Image conversion format
+			'quality' => 80, // Conversion quality
+			'create_thumbnails' => true,
+		];
+		$config = array_merge($defaultConfig, $field);
+
+		// Validate destination config
+		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
+			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+			return;
+		}
+
+		// Get accepted types
+		$acceptedTypes = $this->getAllowedTypes($config);
+
+		// Build accept attribute for input
+		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
+		$acceptAttr = implode(',', $acceptExtensions);
+
+		// Determine field attributes
+		$subtype = $config['subtype'] ?? 'image';
+		$multiple = $config['multiple'] ?? false;
+		$limit = $config['limit'] ?? 0;
+		$mode = $config['mode'] ?? 'direct';
+		$destination = $config['destination'];
+
+		// Get existing attachments
+		$attachmentIds = $this->parseAttachmentIds($value);
+
+		// Determine field type for UI
+		$fieldType = $multiple ? 'gallery' : 'single';
+
+		// Build data attributes
+		$dataAttrs = [
+			'data-field' => $name,
+			'data-upload-field' => '',
+			'data-mode' => $mode,
+			'data-type' => $fieldType,
+			'data-subtype' => $subtype,
+			'data-destination' => $destination,
+		];
+		if (!empty($field['content'])) {
+			$dataAttrs['data-content'] = $field['content'];
+		}
+		if ($limit > 0) {
+			$dataAttrs['data-limit'] = $limit;
+		}
+
+		// Build data attributes
+		$conditional = $this->handleConditionalField($field);
+		$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
+
+		if (!empty($field['group'])) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		// Convert data attributes to string
+		$dataAttrString = '';
+		foreach ($dataAttrs as $attr => $val) {
+			$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
+		}
+		?>
+		<div class="field upload <?= esc_attr($name) ?>"
+			<?= $dataAttrString ?>
+			<?= $conditional ?>>
+
+			<div class="file-upload-container">
+				<div class="file-upload-wrapper">
+					<input type="file"
+						   name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   accept="<?= esc_attr($acceptAttr) ?>"
+						   data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
+						<?= $multiple ? 'multiple' : '' ?>
+						<?= !empty($field['required']) ? 'required' : '' ?>>
+
+					<h2><?= esc_html($field['label']) ?></h2>
+
+					<?php if (!empty($field['description'])) : ?>
+						<p><?= esc_html($field['description']) ?></p>
+					<?php endif; ?>
+
+					<p class="file-upload-text">
+						<strong>Click to upload</strong> or drag and drop<br>
+						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
+						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
+					</p>
+
+					<?php if ($destination === 'post_group') {
+						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
+						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
+						?>
+						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
+						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
+					<?php }
+					if (!empty($field['upload_description'])) : ?>
+						<p><?= esc_html($field['upload_description']); ?></p>
+					<?php endif; ?>
+					<div class="file-error"></div>
+				</div>
+			</div>
+
+
+			<?php if ($destination === 'post_group') : ?>
+			<div class="group-display flex col" hidden>
+				<div class="preview-wrap flex col">
+					<div class="preview-actions">
+						<div class="selection-controls">
+							<div class="selected">
+								<div class="field">
+									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
+									<label for="select-all-uploads">
+										Select All
+									</label>
+								</div>
+								<div class="info" hidden>
+
+								</div>
+							</div>
+
+							<div class="selection-actions row btw" hidden>
+								<button type="button" data-action="add-to-group">
+									<?= jvbIcon('add') ?>
+									Group
+								</button>
+								<button type="button" data-action="delete-upload">
+									<?= jvbIcon('delete') ?>
+									Delete
+								</button>
+							</div>
+						</div>
+
+						<button type="button" data-action="upload" class="submit-uploads">
+							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
+						</button>
+					</div>
+					<?php endif; ?>
+
+					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
+					<span class="count">0/0</span>'); ?>
+					<div class="item-grid preview">
+						<?php
+						// Render existing attachments
+						foreach ($attachmentIds as $attachmentId) {
+							echo $this->renderExistingAttachment($attachmentId, $subtype);
+						}
+						?>
+					</div>
+
+					<?php if ($destination === 'post_group') : ?>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+				<div class="sidebar flex col">
+					<div class="header">
+						<h4>New <?= $plural?></h4>
+						<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
+					</div>
+					<div class="item-grid groups">
+						<div class="empty-group">
+							<p>Drag here to create a new <?= $singular ?>!</p>
+						</div>
+					</div>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+			</div>
+		<?php endif; ?>
+
+			<?php if ($destination === 'meta') : ?>
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					   value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>>
+			<?php endif; ?>
+		</div>
+		<?php
+	}
+
+
+	protected function getAllowedTypes(array $config):array
+	{
+		$typeMap = [
+			'image' => [
+				'image/jpeg',
+				'image/png',
+				'image/gif',
+				'image/webp'
+			],
+			'video' => [
+				'video/mp4',
+				'video/webm',
+				'video/ogg',
+				'video/ogv',
+				'video/quicktime'
+			],
+			'document' => [
+				'application/pdf',
+				'application/msword',
+				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+				'application/vnd.ms-excel',
+				'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+				'text/plain',
+				'text/csv'
+			],
+			'any' => [] // Will be merged from all types
+		];
+		// If specific types are defined, use those
+		if (!empty($config['accepted_types'])) {
+			return is_array($config['accepted_types'])
+				? $config['accepted_types']
+				: [$config['accepted_types']];
+		}
+
+		// Otherwise use subtype defaults
+		$subtype = $config['subtype'] ?? 'image';
+
+		if ($subtype === 'any') {
+			return array_merge(
+				$typeMap['image'],
+				$typeMap['video'],
+				$typeMap['document']
+			);
+		}
+
+		return $typeMap[$subtype] ?? $typeMap['image'];
+
+	}
+	/**
+	 * Parse attachment IDs from value
+	 */
+	private function parseAttachmentIds(mixed $value): array
+	{
+		if (empty($value)) return [];
+
+		if (is_array($value)) {
+			return array_filter(array_map('absint', $value));
+		}
+
+		return array_filter(array_map('absint', explode(',', $value)));
+	}
+
+	/**
+	 * Get file extensions for MIME types
+	 */
+	private function getMimeExtensions(array $mimeTypes): array
+	{
+		$extensionMap = [
+			'image/jpeg' => ['.jpg', '.jpeg'],
+			'image/png' => ['.png'],
+			'image/gif' => ['.gif'],
+			'image/webp' => ['.webp'],
+			'video/mp4' => ['.mp4'],
+			'video/webm' => ['.webm'],
+			'video/ogg' => ['.ogg'],
+			'video/ogv' => ['.ogv'],
+			'video/quicktime' => ['.mov'],
+			'application/pdf' => ['.pdf'],
+			'application/msword' => ['.doc'],
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['.docx'],
+			'application/vnd.ms-excel' => ['.xls'],
+			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['.xlsx'],
+			'text/plain' => ['.txt'],
+			'text/csv' => ['.csv'],
+		];
+
+		$extensions = [];
+		foreach ($mimeTypes as $mime) {
+			if (isset($extensionMap[$mime])) {
+				$extensions = array_merge($extensions, $extensionMap[$mime]);
+			}
+		}
+
+		return array_unique($extensions);
+	}
+
+	/**
+	 * Get max file size for subtype
+	 */
+	private function getMaxFileSize(string $subtype): int
+	{
+		$sizes = [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		];
+
+		return $sizes[$subtype] ?? $sizes['image'];
+	}
+	/**
+	 * Get human-readable file size label
+	 */
+	private function getMaxFileSizeLabel(string $subtype): string
+	{
+		$bytes = $this->getMaxFileSize($subtype);
+		$mb = round($bytes / 1048576);
+		return "{$mb}MB";
+	}
+	/**
+	 * Format file size for display
+	 */
+	private function formatFileSize(int $bytes): string
+	{
+		if ($bytes >= 1073741824) {
+			return number_format($bytes / 1073741824, 1) . 'GB';
+		}
+		if ($bytes >= 1048576) {
+			return number_format($bytes / 1048576, 1) . 'MB';
+		}
+		if ($bytes >= 1024) {
+			return number_format($bytes / 1024, 1) . 'KB';
+		}
+		return $bytes . 'B';
+	}
+
+	/**
+	 * Get accepted types label
+	 */
+	private function getAcceptedTypesLabel(string $subtype, array $extensions): string
+	{
+		$labels = [
+			'image' => 'JPG, PNG, GIF, or WEBP',
+			'video' => 'MP4, WEBM, or MOV',
+			'document' => 'PDF, DOC, XLS, or TXT',
+			'any' => 'Images, Videos, or Documents'
+		];
+
+		return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
+			return ltrim($ext, '.');
+		}, array_slice($extensions, 0, 3))));
+	}
+
+	/**
+	 * Render existing attachment
+	 */
+	private function renderExistingAttachment(int $attachmentId, string $subtype): string
+	{
+		$attachment = get_post($attachmentId);
+		if (!$attachment) return '';
+
+		$url = wp_get_attachment_url($attachmentId);
+		$thumbUrl = $subtype === 'image'
+			? wp_get_attachment_image_url($attachmentId, 'medium')
+			: $url;
+
+		ob_start();
+		?>
+		<div class="upload-item existing" data-attachment-id="<?= esc_attr($attachmentId) ?>" data-subtype="<?= esc_attr($subtype) ?>">
+			<div class="preview">
+				<?php if ($subtype === 'image') : ?>
+					<img src="<?= esc_url($thumbUrl) ?>" alt="<?= esc_attr(get_post_meta($attachmentId, '_wp_attachment_image_alt', true)) ?>">
+				<?php elseif ($subtype === 'video') : ?>
+					<video src="<?= esc_url($url) ?>" controls></video>
+				<?php else : ?>
+					<div class="document-preview">
+						<?= jvbIcon('document') ?>
+						<span><?= esc_html(basename($url)) ?></span>
+					</div>
+				<?php endif; ?>
+
+				<div class="overlay">
+					<div class="actions">
+						<button type="button" class="remove" title="Remove">
+							<span class="screen-reader-text">Remove <?= esc_attr($subtype) ?></span>
+							×
+						</button>
+					</div>
+				</div>
+			</div>
+
+			<?php if ($subtype === 'image') {
+				echo jvbImageMeta();
+			} ?>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
     private function renderImageField(string $name, mixed $value, array $field):void
     {
 		$image_url = $title = $alt = $caption = false;
@@ -1327,8 +1717,6 @@
 			 <?=$dataContent?>
 			<?= ' data-type="'.$dataType.'"'?>>
 
-
-
             <div class="file-upload-container">
                 <div class="file-upload-wrapper">
                     <input type="file"
@@ -1401,7 +1789,7 @@
 					</div>
 
 					<?php if ($groupable) : ?>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>&emsp;These will become individual <?= $plural ?>&emsp;<?= jvbIcon('elbow-right-up')?></p>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
 				</div>
 				<div class="sidebar">
 					<div class="header">
@@ -1418,7 +1806,7 @@
 							<p>Drag here to create a new <?= $singular ?>!</p>
 						</div>
 					</div>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>&emsp;Each group will become its own <?= $singular ?>&emsp;<?= jvbIcon('elbow-right-up')?></p>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
 				</div>
 			</div>
 		<?php endif; ?>
diff --git a/inc/meta/MetaSanitizer.php b/inc/meta/MetaSanitizer.php
index 4f3ec52..aeff742 100644
--- a/inc/meta/MetaSanitizer.php
+++ b/inc/meta/MetaSanitizer.php
@@ -137,7 +137,7 @@
 		return $sanitized;
 	}
 
-    protected function sanitizeGallery(array|string $value):string
+    protected function sanitizeUpload(array|string $value):string
     {
         if (empty($value)) {
             return '';
diff --git a/inc/meta/MetaTypeManager.php b/inc/meta/MetaTypeManager.php
index 89fbe37..63348d6 100644
--- a/inc/meta/MetaTypeManager.php
+++ b/inc/meta/MetaTypeManager.php
@@ -55,6 +55,11 @@
             'sanitize' => 'esc_url_raw',
 			'default'	=> '',
         ],
+		'upload'	=> [
+			'type'		=> 'string',
+			'sanitize'	=> 'sanitizeUpload',
+			'default'	=> '',
+		],
         'image' => [
             'type' => 'integer',
             'sanitize' => 'absint',
diff --git a/inc/registry/MakeCalendarType.php b/inc/registry/MakeCalendarType.php
index 61411ef..5025e17 100644
--- a/inc/registry/MakeCalendarType.php
+++ b/inc/registry/MakeCalendarType.php
@@ -64,7 +64,7 @@
 	public function handlePostTypeArchiveLinks(string $url, string $post_type):string
 	{
 		if ($post_type === $this->postType) {
-			$url = get_home_url(2, '/' . $this->slug . '/');
+			$url = get_home_url(null, '/' . $this->slug . '/');
 		}
 		return $url;
 	}
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index c9cf18a..813542b 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -8,6 +8,9 @@
 if (Features::forSite()->has('feed_block')) {
 	require(JVB_DIR . '/inc/rest/routes/FeedRoutes.php');
 }
+if (Features::forSite()->has('magicLink')) {
+	require(JVB_DIR . '/inc/rest/routes/MagicLinkRoutes.php');
+}
 if (Features::forSite()->has('favourites')) {
 	require(JVB_DIR . '/inc/rest/routes/FavouritesRoutes.php');
 }
diff --git a/inc/rest/routes/MagicLinkRoutes.php b/inc/rest/routes/MagicLinkRoutes.php
new file mode 100644
index 0000000..3535d6d
--- /dev/null
+++ b/inc/rest/routes/MagicLinkRoutes.php
@@ -0,0 +1,235 @@
+<?php
+namespace JVBase\rest\routes;
+
+use JVBase\rest\RestRouteManager;
+use JVBase\managers\MagicLinkManager;
+use WP_REST_Request;
+use WP_REST_Response;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Magic Link REST Routes
+ *
+ * Handles API endpoints for passwordless authentication
+ */
+class MagicLinkRoutes extends RestRouteManager
+{
+	protected MagicLinkManager $magic_link;
+
+	public function __construct()
+	{
+		$this->magic_link = new MagicLinkManager();
+		parent::__construct();
+	}
+
+	/**
+	 * Register REST routes
+	 */
+	public function registerRoutes(): void
+	{
+		// Send magic link
+		register_rest_route($this->namespace, '/magic-link', [
+			'methods' => 'POST',
+			'callback' => [$this, 'sendMagicLink'],
+			'permission_callback' => '__return_true', // Public endpoint
+			'args' => [
+				'email' => [
+					'required' => true,
+					'type' => 'string',
+					'format' => 'email',
+					'validate_callback' => function($param) {
+						return is_email($param);
+					}
+				],
+				'type' => [
+					'required' => false,
+					'type' => 'string',
+					'default' => 'login',
+					'enum' => ['login', 'signup', 'referral', 'reset']
+				],
+				'context' => [
+					'required' => false,
+					'type' => 'object',
+					'default' => []
+				]
+			]
+		]);
+
+		// Resend magic link
+		register_rest_route($this->namespace, '/magic-link/resend', [
+			'methods' => 'POST',
+			'callback' => [$this, 'resendMagicLink'],
+			'permission_callback' => '__return_true',
+			'args' => [
+				'email' => [
+					'required' => true,
+					'type' => 'string',
+					'format' => 'email'
+				],
+				'type' => [
+					'required' => true,
+					'type' => 'string'
+				]
+			]
+		]);
+
+		// Check token validity (useful for frontend)
+		register_rest_route($this->namespace, '/magic-link/verify', [
+			'methods' => 'POST',
+			'callback' => [$this, 'verifyToken'],
+			'permission_callback' => '__return_true',
+			'args' => [
+				'token' => [
+					'required' => true,
+					'type' => 'string'
+				],
+				'email' => [
+					'required' => true,
+					'type' => 'string',
+					'format' => 'email'
+				]
+			]
+		]);
+	}
+
+	/**
+	 * Send a magic link via email
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function sendMagicLink(WP_REST_Request $request): WP_REST_Response
+	{
+		$email = sanitize_email($request->get_param('email'));
+		$type = sanitize_text_field($request->get_param('type'));
+		$context = $request->get_param('context') ?? [];
+
+		error_log('SendMagicLink request: '.print_r($email, true));
+		error_log('Type: '.print_r($type, true));
+		error_log('Context: '.print_r($context, true));
+
+		// Validate email
+		if (!is_email($email)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Invalid email address'
+			], 400);
+		}
+
+		// Send the magic link
+		$result = $this->magic_link->sendMagicLink($email, $type, $context);
+		error_log('Result: '.print_r($result, true));
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message(),
+				'code' => $result->get_error_code()
+			], 400);
+		}
+
+		// Return success (never reveal if user exists or not)
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'If an account exists with this email, we\'ve sent a login link.'
+		], 200);
+	}
+
+	/**
+	 * Resend a magic link
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function resendMagicLink(WP_REST_Request $request): WP_REST_Response
+	{
+		// Same as sending, but could add additional logging
+		return $this->sendMagicLink($request);
+	}
+
+	/**
+	 * Verify a token without consuming it
+	 * Useful for frontend validation before redirect
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function verifyToken(WP_REST_Request $request): WP_REST_Response
+	{
+		$token = sanitize_text_field($request->get_param('token'));
+		$email = sanitize_email($request->get_param('email'));
+
+		$cache_key = 'magic_token_' . $token;
+		$token_data = get_transient($cache_key);
+
+		if (!$token_data) {
+			return new WP_REST_Response([
+				'valid' => false,
+				'message' => 'Token expired or invalid'
+			], 400);
+		}
+
+		if ($token_data['email'] !== $email) {
+			return new WP_REST_Response([
+				'valid' => false,
+				'message' => 'Invalid token'
+			], 400);
+		}
+
+		if (time() > $token_data['expires_at']) {
+			return new WP_REST_Response([
+				'valid' => false,
+				'message' => 'Token expired'
+			], 400);
+		}
+
+		return new WP_REST_Response([
+			'valid' => true,
+			'type' => $token_data['type'],
+			'expires_in' => $token_data['expires_at'] - time()
+		], 200);
+	}
+
+	protected function processReferralSignup(array $token_data): void
+	{
+		// Create user account
+		$user_id = wp_create_user(
+			$token_data['email'],
+			wp_generate_password(20, true, true),
+			$token_data['email']
+		);
+
+		if (is_wp_error($user_id)) {
+			wp_die('Failed to create account: ' . $user_id->get_error_message());
+		}
+
+		// Update user info
+		if (!empty($token_data['name'])) {
+			wp_update_user([
+				'ID' => $user_id,
+				'display_name' => $token_data['name'],
+				'first_name' => $token_data['name']
+			]);
+		}
+
+		// Store referral code in user meta (temporary)
+		// ReferralManager::processReferral will pick this up
+		update_user_meta($user_id, BASE . 'pending_referral_code', $token_data['referral_code']);
+
+		// Trigger registration actions (this calls processReferral)
+		do_action('user_register', $user_id);
+
+		// Log the user in
+		wp_set_current_user($user_id);
+		wp_set_auth_cookie($user_id, true);
+		do_action('wp_login', get_user_by('ID', $user_id)->user_login, get_user_by('ID', $user_id));
+
+		// Redirect with referral welcome message
+		wp_safe_redirect(home_url('/dash?referral_welcome=1'));
+		exit;
+	}
+}
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index d7f097f..1589135 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -31,6 +31,49 @@
 			'permission_callback' => [$this, 'checkPermission']
 		]);
 
+		register_rest_route($this->namespace, '/referrals/register', [
+			'methods' => 'POST',
+			'callback' => [$this, 'registerWithReferral'],
+			'permission_callback' => '__return_true',
+			'args' => [
+				'name' => [
+					'required' => true,
+					'type' => 'string',
+					'sanitize_callback' => 'sanitize_text_field'
+				],
+				'email' => [
+					'required' => true,
+					'type' => 'string',
+					'format' => 'email',
+					'validate_callback' => function($param) {
+						return is_email($param);
+					}
+				],
+				'code' => [
+					'required' => true,
+					'type' => 'string',
+					'sanitize_callback' => function($code) {
+						return strtoupper(sanitize_text_field($code));
+					}
+				]
+			]
+		]);
+
+		register_rest_route($this->namespace, '/referrals/check-code', [
+			'methods' => 'POST',
+			'callback' => [$this, 'checkReferralCode'],
+			'permission_callback' => '__return_true',
+			'args' => [
+				'code' => [
+					'required' => true,
+					'type' => 'string',
+					'sanitize_callback' => function($code) {
+						return strtoupper(sanitize_text_field($code));
+					}
+				]
+			]
+		]);
+
 		// Get or create referral code
 		register_rest_route($this->namespace, "/{$this->route}/code", [
 			'methods' => 'GET',
@@ -38,22 +81,6 @@
 			'permission_callback' => [$this, 'checkPermission']
 		]);
 
-		// Update referral code
-		register_rest_route($this->namespace, "/{$this->route}/code", [
-			'methods' => 'POST',
-			'callback' => [$this, 'updateReferralCode'],
-			'permission_callback' => [$this, 'checkPermission'],
-			'args' => [
-				'code' => [
-					'required' => true,
-					'type' => 'string',
-					'validate_callback' => function($param) {
-						return preg_match('/^[A-Z0-9]+$/i', $param);
-					}
-				]
-			]
-		]);
-
 		// Track referral click (public endpoint)
 		register_rest_route($this->namespace, "/{$this->route}/track", [
 			'methods' => 'POST',
@@ -84,13 +111,76 @@
 			]
 		]);
 
-		// Get user stats
-		register_rest_route($this->namespace, "/{$this->route}/stats", [
+		// Send referral invitation
+		register_rest_route($this->namespace, '/'.$this->route.'/invite', [
+			'methods' => 'POST',
+			'callback' => [$this, 'sendInvitation'],
+			'permission_callback' => [$this, 'checkPermission'],
+			'args' => [
+				'email' => [
+					'required' => true,
+					'type' => 'string',
+					'format' => 'email',
+					'validate_callback' => function($param) {
+						return is_email($param);
+					}
+				],
+				'name' => [
+					'required' => true,
+					'type' => 'string',
+					'sanitize_callback' => 'sanitize_text_field'
+				]
+			]
+		]);
+
+		// Send batch invitations
+		register_rest_route($this->namespace, '/'.$this->route.'/invite/batch', [
+			'methods' => 'POST',
+			'callback' => [$this, 'sendBatchInvitations'],
+			'permission_callback' => [$this, 'checkPermission'],
+			'args' => [
+				'invitations' => [
+					'required' => true,
+					'type' => 'array',
+					'validate_callback' => function($param) {
+						return is_array($param) && !empty($param);
+					}
+				]
+			]
+		]);
+
+		// Get invitation stats for current user
+		register_rest_route($this->namespace, '/'.$this->route.'/invite/stats', [
 			'methods' => 'GET',
-			'callback' => [$this, 'getUserStats'],
+			'callback' => [$this, 'getInvitationStats'],
 			'permission_callback' => [$this, 'checkPermission']
 		]);
 
+		// Export referrals for Jane App
+		register_rest_route($this->namespace, '/'.$this->route.'/export', [
+			'methods' => 'POST',
+			'callback' => [$this, 'exportReferrals'],
+			'permission_callback' => function() {
+				return current_user_can('manage_options');
+			},
+			'args' => [
+				'start_date' => [
+					'required' => true,
+					'type' => 'string',
+					'validate_callback' => function($param) {
+						return (bool) strtotime($param);
+					}
+				],
+				'end_date' => [
+					'required' => true,
+					'type' => 'string',
+					'validate_callback' => function($param) {
+						return (bool) strtotime($param);
+					}
+				]
+			]
+		]);
+
 		// Get top referrers (admin only)
 		register_rest_route($this->namespace, "/{$this->route}/leaderboard", [
 			'methods' => 'GET',
@@ -310,4 +400,200 @@
 			'settings' => $settings
 		]);
 	}
+
+	/**
+	 * Send a single referral invitation
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function sendInvitation(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$email = sanitize_email($request->get_param('email'));
+		$name = sanitize_text_field($request->get_param('name'));
+
+		// Send invitation via ReferralManager
+		$referral_manager = JVB()->referrals();
+		$result = $referral_manager->sendReferralInvitation($user_id, $email, $name);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message(),
+				'code' => $result->get_error_code()
+			], 400);
+		}
+
+		return new WP_REST_Response($result, 200);
+	}
+
+	/**
+	 * Send batch referral invitations
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function sendBatchInvitations(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$invitations = $request->get_param('invitations');
+
+		// Validate invitation format
+		foreach ($invitations as $invite) {
+			if (empty($invite['email']) || empty($invite['name'])) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'Each invitation must have email and name'
+				], 400);
+			}
+		}
+
+		// Send batch via ReferralManager
+		$referral_manager = JVB()->referrals();
+		$result = $referral_manager->sendBatchReferralInvitations($user_id, $invitations);
+
+		return new WP_REST_Response($result, 200);
+	}
+
+	/**
+	 * Get invitation stats for current user
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function getInvitationStats(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+
+		$referral_manager = JVB()->referrals();
+		$stats = $referral_manager->getUserInvitationStats($user_id);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'stats' => $stats
+		], 200);
+	}
+
+	/**
+	 * Export referrals for Jane App cross-reference
+	 * Admin only
+	 *
+	 * @param WP_REST_Request $request
+	 * @return WP_REST_Response
+	 */
+	public function exportReferrals(WP_REST_Request $request): WP_REST_Response
+	{
+		$start_date = sanitize_text_field($request->get_param('start_date'));
+		$end_date = sanitize_text_field($request->get_param('end_date'));
+
+		$referral_manager = JVB()->referrals();
+		$csv_content = $referral_manager->exportReferrals($start_date, $end_date);
+
+		// Return CSV for download
+		return new WP_REST_Response([
+			'success' => true,
+			'csv' => $csv_content,
+			'filename' => sprintf('referrals_%s_to_%s.csv', $start_date, $end_date)
+		], 200);
+	}
+
+	public function registerWithReferral(WP_REST_Request $request): WP_REST_Response
+	{
+		$name = sanitize_text_field($request->get_param('name'));
+		$email = sanitize_email($request->get_param('email'));
+		$code = strtoupper(sanitize_text_field($request->get_param('code')));
+
+		// Validate email
+		if (!is_email($email)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Invalid email address'
+			], 400);
+		}
+
+		// Check if user exists
+		if (email_exists($email)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'An account with this email already exists'
+			], 400);
+		}
+
+		// Validate referral code
+		$referral_manager = JVB()->referrals();
+		$referrer = $referral_manager->getUserByReferralCode($code);
+
+		if (!$referrer) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Invalid referral code'
+			], 404);
+		}
+
+		// Get reward text
+		$settings = $referral_manager->getRewardSettings();
+		$reward_amount = $settings['referee_reward_amount'] ?? 20;
+		$reward_type = $settings['referee_reward_type'] ?? 'percentage';
+		$reward_text = $reward_type === 'percentage'
+			? "{$reward_amount}% off your first treatment!"
+			: "\${$reward_amount} off your first treatment!";
+
+		// Send magic link with referral context via MagicLinkManager
+		$magic_link_manager = new \JVBase\managers\MagicLinkManager();
+
+		$result = $magic_link_manager->sendMagicLink(
+			$email,
+			\JVBase\managers\MagicLinkManager::TYPE_REFERRAL,
+			[
+				'name' => $name,
+				'referral_code' => $code,
+				'referrer_id' => $referrer->ID,
+				'referrer_name' => $referrer->display_name,
+				'reward_text' => $reward_text
+			]
+		);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to send registration link. Please try again.'
+			], 500);
+		}
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Check your email! We sent you a link to complete your registration.',
+			'email' => $email
+		], 200);
+	}
+
+	public function checkReferralCode(WP_REST_Request $request): WP_REST_Response
+	{
+		$code = strtoupper(sanitize_text_field($request->get_param('code')));
+
+		if (empty($code)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Code is required'
+			], 400);
+		}
+
+		$referral_manager = JVB()->referrals();
+		$referrer = $referral_manager->getUserByReferralCode($code);
+
+		if (!$referrer) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Invalid referral code'
+			], 404);
+		}
+
+		// Return basic referrer info (no sensitive data)
+		return new WP_REST_Response([
+			'success' => true,
+			'code' => $code,
+			'referrer_name' => $referrer->display_name,
+		], 200);
+	}
 }
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index 269dc2a..da8f52c 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -409,9 +409,12 @@
             $formatted_terms[] = [
                 'id' => $term->term_id,
                 'name' => $term->name,
+				'slug'	=> $term->slug,
                 'parent' => $term->parent,
                 'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
                 'hasChildren' => $has_children,
+				'taxonomy'	=> $term->taxonomy,
+				'count'		=> $term->count,
             ];
         }
 
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 3049d25..e47b5e9 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -5,6 +5,7 @@
 use JVBase\rest\RestRouteManager;
 use JVBase\meta\MetaManager;
 use JVBase\managers\UploadManager;
+use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -29,42 +30,12 @@
      */
     public function registerRoutes():void
     {
-        register_rest_route($this->namespace, '/uploads', [
-            'methods' => 'POST',
-            'callback' => [$this, 'handleUploadRequest'],
-            'permission_callback' => [$this, 'checkPermission'],
-            'args' => [
-                'content' => [
-                    'required' => true,
-                    'type' => 'string'
-                ],
-                'user'   => [
-                    'required'  => true,
-                    'type'  => 'int'
-                ],
-                'post_id'   => [
-                    'required'  => false,
-                    'type'  => 'int'
-                ],
-                'term_id'   => [
-                    'required'  => false,
-                    'type'  => 'int'
-                ],
-                'field_name' => [
-                    'required'  => false,
-                    'type'  => 'string'
-                ],
-                'id' => [
-                    'required'  => false,
-                    'type'  => 'string'
-                ],
-                'mode' => [
-                    'required'  => false,
-                    'type'  => 'string',
-                    'default' => 'direct'
-                ]
-            ]
-        ]);
+		// Main upload endpoint
+		register_rest_route($this->namespace, '/uploads', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleUpload'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
 
 		register_rest_route($this->namespace, '/uploads/groups', [
 			'methods' => 'POST',
@@ -92,55 +63,139 @@
 			'callback' => [$this, 'handleMetadataUpdate'],
 			'permission_callback' => [$this, 'checkPermission'],
 			'args' => [
-				'id' => [
-					'type' => 'string',
-					'required' => false,
-					'description' => 'Original upload operation ID (for updates during processing)'
-				],
-				'attachment_ids' => [
-					'type' => 'array',
-					'required' => false,
-					'description' => 'Direct attachment IDs (for updates after completion)',
-					'items' => ['type' => 'integer']
-				],
-				'upload_ids' => [
-					'type' => 'array',
-					'required' => false,
-					'description' => 'Frontend upload IDs to map to attachments',
-					'items' => ['type' => 'string']
-				],
-				'metadata' => [
-					'type' => 'object',
-					'required' => true,
-					'description' => 'Metadata changes to apply'
-				],
 				'user' => [
 					'type' => 'integer',
 					'required' => true
-				]
+				],
+				'items' => [
+					'type' => 'array',
+					'required' => true,
+					'description' => 'Direct attachment IDs (for updates after completion)',
+				],
 			]
 		]);
     }
 
+	/**
+	 * Build the main $context for UploadManager from the $request params
+	 * @param WP_REST_Request $request
+	 * @return array
+	 */
     protected function buildUploadArgs(WP_REST_Request $request):array
     {
         $data = $request->get_params();
-		error_log('Upload Args: '.print_r($data, true));
-        $args = [
-			'content'        => (array_key_exists('content', $data) && (array_key_exists(jvbGetValidType($data['content']), JVB_CONTENT) || $data['content'] === 'options')) ? jvbGetValidType($data['content']) : false,
-            'user'            => (array_key_exists('user', $data) && $this->userCheck($data['user'])) ? (int)$data['user'] : false,
-            'id'    => (array_key_exists('id', $data) && is_string($data['id'])) ? sanitize_text_field(str_replace('_upload', '', $data['id'])) : false,
-            'post_id'        => (array_key_exists('post_id', $data) && is_numeric($data['post_id'])) ? absint($data['post_id']) : false,
-            'term_id'        => (array_key_exists('term_id', $data) && is_numeric($data['term_id'])) ? absint($data['term_id']) : false,
-            'field_name'    => (array_key_exists('field_name', $data) && is_string($data['field_name'])) ? sanitize_text_field($data['field_name']) : false,
-            'mode'            => (array_key_exists('mode', $data) && in_array($data['mode'], ['direct', 'selection'])) ? sanitize_text_field($data['mode']) : false,
-        ];
+		$args = [];
+		foreach ($data as $key => $value) {
+			switch ($key) {
+				// Post Type/Taxonomy
+				case 'content':
+					$key = str_replace('-', '_', $key);
+					if ($value === 'options' || array_key_exists($value, JVB_CONTENT) || Features::forTaxonomy($key)->has('is_content')) {
+						$args['content'] = $value;
+					}
+					break;
+				case 'destination':
+					if (in_array($value, ['meta', 'post', 'post_group'])) {
+						$args['destination'] = sanitize_text_field($value);
+						if (in_array($value, ['post', 'post_group']) && empty($data['content'])) {
+							throw new Exception("Content type required for destination: {$value}");
+						}
+					}
+					break;
+				// User ID
+				case 'user':
+					if ($this->userCheck($value)) {
+						$args['user'] = (int) $value;
+						if (!array_key_exists('post_id', $data) && !array_key_exists('term_id', $data)) {
+							$args['post_id'] = (int)get_user_meta((int) $value, BASE.'link', true);
+						}
+					}
+					break;
+				// Operation ID
+				case 'id':
+					if (is_string($value)) {
+						$value = sanitize_text_field($value);
+						$args['id'] =  $value;
+						$args['upload'] = $value.'_upload';
+					}
+					break;
+				// Post ID
+				case 'post_id':
+					if (is_numeric($value)) {
+						$args['post_id'] = absint($value);
+					}
+					break;
+				// Term ID
+				case 'term_id':
+					if (is_numeric($value)) {
+						$args['term_id'] = absint($value);
+					}
+					break;
+				// Field Name, as defined for MetaManager.php
+				case 'field_name':
+					if (is_string($value)) {
+						$args['field_name'] = sanitize_text_field($value);
+					}
+					break;
+				// Upload Mode
+				case 'mode':
+					if (in_array($value, ['direct', 'selection'])) {
+						$args['mode'] = sanitize_text_field($value);
+					}
+					break;
+				case 'upload_ids':
+					if (is_string($value)) {
+						// Parse JSON array
+						$decoded = json_decode($value, true);
+						if (is_array($decoded)) {
+							$args['upload_ids'] = $decoded;
+						}
+					} elseif (is_array($value)) {
+						// Already an array (shouldn't happen with FormData JSON, but handle it)
+						$args['upload_ids'] = $value;
+					}
+					break;
+				case 'metadata':
+					if (!empty($value)) {
+						$metadata = is_string($value) ? json_decode($value, true) : $value;
+						if (is_array($metadata)) {
+							foreach ($metadata as $k => $v) {
+								if (in_array($k, ['title', 'caption', 'alt', 'depends_on'])) {
+									$args['metadata'][$k] = sanitize_text_field($v);
+								}
+							}
+						}
+					}
+					break;
+				case 'posts':
+					if (is_string($value)) {
+						$decoded = json_decode($value, true);
+						if (is_array($decoded)) {
+							$args['posts'] = $decoded;
+						}
+					}
+					break;
 
-        if ((!$args['post_id'] && !$args['term_id']) && contentIsJVBUserType($args['content'])) {
-            $args['post_id'] = (int)get_user_meta($args['user'], BASE.'link', true);
-        }
-
-        $args['upload'] = ($args['id']) ? $args['id'].'_upload' : false;
+				case 'group_titles':
+					if (is_string($value)) {
+						$decoded = json_decode($value, true);
+						if (is_array($decoded)) {
+							$args['group_titles'] = array_map('sanitize_text_field', $decoded);
+						}
+					}
+					break;
+				// Other field info
+				case 'field_key':
+				case 'field_type':
+				case 'subtype':
+				case 'item_id':
+				case 'context':
+					if (is_string($value)) {
+						$args[$key] = sanitize_text_field($value);
+					}
+					break;
+			}
+		}
         return $args;
     }
 
@@ -152,111 +207,41 @@
      *
      * @return WP_REST_Response
      */
-	public function handleUploadRequest(WP_REST_Request $request): WP_REST_Response
+	public function handleUpload(WP_REST_Request $request): WP_REST_Response
 	{
 		try {
-			if (!isset($_FILES['files'])) {
-				return $this->createStandardResponse(
-					false,
-					[],
-					__('No files uploaded.', 'jvb')
-				);
-			}
-
-
+			$files = $request->get_file_params();
 			$args = $this->buildUploadArgs($request);
-			error_log('Validating upload args: '.print_r($args, true));
-			// Validation...
-			$validation_errors = [];
-			if (!$args['content'] && !$args['term_id'] && !$args['post_id']) $validation_errors[] = 'content or term or post id required';
-			if (!$args['user']) $validation_errors[] = 'user';
-			if (!$args['id']) $validation_errors[] = 'operation_id';
-			if (!empty($validation_errors)) {
-				error_log('Validation Errors: '.print_r($validation_errors, true));
-				return $this->createStandardResponse(
-					false,
-					['missing_fields' => $validation_errors],
-					'Required fields missing: ' . implode(', ', $validation_errors)
-				);
+
+			if (!$args['content'] || !$args['user']) {
+
+				$this->logError('Missing required data');
+				return new WP_REST_Response([
+					'success'	=> false,
+					'message'	=> 'Missing required data'
+				]);
 			}
 
-			$secured_files = $this->processFileUploads($_FILES['files'], $args);
+			// Step 1: Secure all uploaded files
+			$secured_files = $this->secureFiles($files, $args);
+
 			if (empty($secured_files)) {
-				error_log('Images not processed');
-				return $this->createStandardResponse(
-					false,
-					[],
-					'No valid files could be processed'
-				);
+				$this->logError('No valid files to upload');
+				return new WP_REST_Response([
+					'success'	=> false,
+					'message'	=> 'No valid files to upload'
+				]);
 			}
 
+			// Step 2: Queue for processing via OperationQueue
+			$operation_id = $this->queueProcessing($secured_files, $args);
 
-
-			$queue = JVB()->queue();
-			error_log('Queuing Operation...');
-			$queue->queueOperation(
-				'image_upload',
-				$args['user'],
-				[
-					'secured_files' => $secured_files,
-					'upload_map'	=>  explode(',',$request->get_param('upload_map')),
-					'content' => $args['content'],
-					'post_id' => $args['post_id'],
-					'term_id' => $args['term_id'],
-					'field_name' => $args['field_name'],
-					'mode' => $args['mode'],
-					'operation_id' => $args['id']
-				],
-				[
-					'operation_id' => $args['upload'],
-					'chunk_key'	=> 'secured_files',
-					'chunk_size' => 5,
-					'priority' => 'high',
-					'notification' => true,
-				]
-			);
-
-			error_log('Adding dependent operations...');
-
-
-			if ($args['mode'] !== 'selection') {
-				$queue->queueOperation(
-					'attach_upload_to_content',
-					$args['user'],
-					$args,
-					[
-						'operation_id' => $args['id'],
-						'priority' => 'high',
-						'depends_on' => $args['upload']
-					]
-				);
-			}
-
-			$queue->queueOperation(
-				'temporary_cleanup',
-				$args['user'],
-				[
-					'files' => $secured_files,
-					'content' => $args['content'],
-					'user' => $args['user']
-				],
-				[
-					'priority' => 'low',
-					'chunk_size'	=> 5,
-					'chunk_key'	=> 'files',
-					'depends_on' => $args['upload'],
-				]
-			);
-
-			return $this->createStandardResponse(
-				true,
-				[
-					'files_count' => count($secured_files),
-					'operation_id' => $args['id'],
-					'upload_operation_id' => $args['upload']
-				],
-				'Files secured and queued for processing'
-			);
+			return new WP_REST_Response([
+				'success' => true,
+				'operation_id' => $operation_id,
+				'file_count' => count($secured_files),
+				'message' => 'Files secured and queued for processing'
+			], 200);
 
 		} catch (Exception $e) {
 			// Error handling...
@@ -270,7 +255,7 @@
 				]
 			);
 
-			return $this->createStandardResponse(
+			return $this->sendResponse(
 				false,
 				['error_code' => 'upload_failed'],
 				'Upload processing failed: ' . $e->getMessage()
@@ -278,186 +263,453 @@
 		}
 	}
 
-	protected function processFileUploads(array $files, array $args): array
+	/**
+	 * Secure uploaded files to temporary storage
+	 */
+	protected function secureFiles(array $files, array $args): array
 	{
-		$uploader = new UploadManager($args['content'], $args['user']);
+		$uploader = new UploadManager();
 		$secured_files = [];
+		$upload_map = [];
+		$errors = [];
 
-		// Handle different file array structures
-//		if ($args['mode'] === 'selection') {
-//			// Selection mode: files[group][index]
-//			foreach ($files['tmp_name'] as $group => $images) {
-//				if (is_array($images)) {
-//					foreach ($images as $index => $tmp_name) {
-//						if ($files['error'][$group][$index] === UPLOAD_ERR_OK) {
-//							$file = [
-//								'name' => $files['name'][$group][$index],
-//								'type' => $files['type'][$group][$index],
-//								'tmp_name' => $tmp_name,
-//								'error' => $files['error'][$group][$index],
-//								'size' => $files['size'][$group][$index]
-//							];
-//							$secured_files[$group][] = $uploader->secureUploadedFile($file);
-//						}
-//					}
-//				}
-//			}
-//		} else {
-			// Direct mode: files[index] or files[0][index]
-			$tmp_names = isset($files['tmp_name'][0]) && is_array($files['tmp_name'][0])
-				? $files['tmp_name'][0]
-				: $files['tmp_name'];
+		$context = $args;
+		unset($context['upload_ids']);
 
-			foreach ($tmp_names as $index => $tmp_name) {
-				$file_data = isset($files['tmp_name'][0]) && is_array($files['tmp_name'][0]) ? [
-					'name' => $files['name'][0][$index],
-					'type' => $files['type'][0][$index],
-					'tmp_name' => $tmp_name,
-					'error' => $files['error'][0][$index],
-					'size' => $files['size'][0][$index]
-				] : [
-					'name' => $files['name'][$index],
-					'type' => $files['type'][$index],
-					'tmp_name' => $tmp_name,
-					'error' => $files['error'][$index],
-					'size' => $files['size'][$index]
-				];
+		$config = $this->getFieldConfig($args);
 
-				if ($file_data['error'] === UPLOAD_ERR_OK) {
-					$secured_files[] = $uploader->secureUploadedFile($file_data);
-				}
+		$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'];
+
+		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]
+			] : [
+				'name' => $file_array['name'][$index],
+				'type' => $file_array['type'][$index],
+				'tmp_name' => $tmp_name,
+				'error' => $file_array['error'][$index],
+				'size' => $file_array['size'][$index]
+			];
+
+			if ($file_data['error'] !== UPLOAD_ERR_OK) {
+				$errors[$index] = $this->getUploadErrorMessage($file_data['error']);
+				continue;
 			}
-//		}
 
-		return $secured_files;
+			try {
+				$secured = $uploader->secureUploadedFile($file_data, $context);
+
+				if (empty($secured)) {
+					throw new Exception('Failed to secure file');
+				}
+
+				$frontend_id = $args['upload_ids'][$index] ?? 'upload_' . $index;
+				$upload_map[$index] = $frontend_id;
+
+				$secured_files[] = $secured;
+			} catch (Exception $e) {
+				$errors[$index] = $e->getMessage();
+			}
+		}
+
+		return [
+			'files' => $secured_files,
+			'upload_map' => $upload_map,
+			'errors' => $errors
+		];
 	}
 
-    /**
-     * Process upload operation in the background
-     */
-    /**
-     * @param WP_Error|array $result
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array|WP_Error
-     */
-    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
-    {
-        switch ($operation->type) {
-            case 'image_upload':
-                return $this->processImageUpload($result, $operation, $data);
-			case 'process_upload_groups':
-				return $this->processUploadGroups($result, $operation, $data);
-			case 'update_metadata':
-				return $this->processMetadataUpdate($result, $operation, $data);
-			case 'temporary_cleanup':
-                return $this->processTemporaryCleanup($result, $operation, $data);
-            case 'attach_upload_to_content':
-                return $this->attachUploadToContent($result, $operation, $data);
-            default:
-                return $result;
-        }
-    }
+	protected function getUploadErrorMessage(int $error_code): string
+	{
+		return match($error_code) {
+			UPLOAD_ERR_INI_SIZE => 'File exceeds maximum upload size',
+			UPLOAD_ERR_FORM_SIZE => 'File exceeds form maximum size',
+			UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
+			UPLOAD_ERR_NO_FILE => 'No file was uploaded',
+			UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
+			UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
+			UPLOAD_ERR_EXTENSION => 'Upload stopped by extension',
+			default => 'Unknown upload error'
+		};
+	}
+
 
 	/**
-	 * Process attachment metadata updates
+	 * Queue files for processing
 	 */
-	protected function processAttachmentMetadata(WP_Error|array $result, object $operation, array $data): WP_Error|array
+	protected function queueProcessing(array $secured_data, array $args):string
+	{
+		$operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
+
+		$chunkSize = 5;
+		if ($operation_type === 'video') {
+			$chunkSize = 1;
+		} 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'],
+				],
+				$args
+			),
+			[
+				'operation_id'	=> $args['upload'],
+				'chunk_key'		=> 'secured_files',
+				'chunk_size'	=> $chunkSize
+			]
+		);
+
+		if ($args['mode'] !== 'selection') {
+			JVB()->queue()->queueOperation(
+				'attach_upload_to_content',
+				$args['user'],
+				$args,
+				[
+					'priority'		=> 'high',
+					'operation_id'	=> $args['id'],
+					'depends_on'	=> $args['upload']
+				]
+			);
+		}
+
+		JVB()->queue()->queueOperation(
+			'temporary_cleanup',
+			$args['user'],
+			[
+				'files'		=> $secured_data['files'],
+				'context'	=> $args,
+			],
+			[
+				'priority'		=> 'low',
+				'chunk_size'	=> 5,
+				'chunk_key'		=> 'files',
+				'depends_on'	=> $args['upload']
+			]
+		);
+		return $args['id'];
+	}
+
+	/**
+	 * Determine operation type from file data
+	 */
+	protected function determineOperationType(array $file): string
+	{
+		$file_type = $file['file_type'] ?? 'image';
+
+		return match($file_type) {
+			'video' => 'video_upload',
+			'document' => 'document_upload',
+			default => 'image_upload'
+		};
+	}
+
+	/**
+	 * Step 3: Process operation from queue
+	 * Called by OperationQueue
+	 * @param WP_Error|array $result
+	 * @param object $operation
+	 * @param array $data
+	 *
+	 * @return array|WP_Error
+	 */
+	public function processOperation(array $result, object $operation, array $data): array
+	{
+		// Only handle our operation types
+		$handled_types = [
+			'image_upload',
+			'video_upload',
+			'document_upload',
+			'update_metadata',
+			'temporary_cleanup',
+			'attach_upload_to_content',
+			'process_upload_groups'
+		];
+
+		if (!in_array($operation->type, $handled_types)) {
+			return $result; // Not our operation, pass through
+		}
+
+		try {
+			// Route to appropriate handler
+			$handler_result = match($operation->type) {
+				'image_upload' => $this->processImageUpload($operation, $data),
+				'video_upload' => $this->processVideoUpload($operation, $data),
+				'document_upload' => $this->processDocumentUpload($operation, $data),
+				'update_metadata' => $this->processUploadMeta($result, $operation, $data),
+				'temporary_cleanup' => $this->processTemporaryCleanup($result, $operation, $data),
+				'attach_upload_to_content' => $this->processAttachToContent($operation, $data),
+				'process_upload_groups'	=> $this->processUploadGroups($result, $operation, $data),
+				default => new WP_Error('unknown_type', 'Unknown operation type')
+			};
+
+			// Handle WP_Error
+			if (is_wp_error($handler_result)) {
+				return [
+					'success' => false,
+					'result' => $handler_result->get_error_message()
+				];
+			}
+
+			return $handler_result;
+
+		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadRoutes]:processOperation',
+				$e->getMessage(),
+				[
+					'operation_id' => $operation->id,
+					'operation_type' => $operation->type
+				]
+			);
+
+			return [
+				'success' => false,
+				'result' => $e->getMessage()
+			];
+		}
+	}
+	/**
+	 * Standardize processing result
+	 */
+	protected function standardizeResult(array $result): array
+	{
+		return [
+			'attachment_id' => $result['attachment_id'],
+			'url' => $result['url'],
+			'file' => $result['file'],
+			'upload_id' => $result['upload_id'] ?? null
+		];
+	}
+
+	protected function processAttachToContent(object $operation, array $data): array
 	{
 		try {
-			$metadata_updates = $data['metadata_updates'];
-			$updated_count = 0;
-			$failed_count = 0;
-			$results = [];
+			// Get the results from the upload operation
+			$upload_results = JVB()->queue()->getOperationValue($data['upload'], 'result', true);
 
-			foreach ($metadata_updates as $update) {
-				try {
-					$attachment_id = (int)$update['attachment_id'];
-					$upload_id = $update['upload_id'];
+			if (empty($upload_results)) {
+				throw new Exception('No upload results found for operation: ' . $data['upload']);
+			}
 
-					// Verify attachment exists and user has permission
-					if (!$this->verifyAttachmentAccess($attachment_id, $operation->user_id)) {
-						$failed_count++;
-						$results[] = [
-							'upload_id' => $upload_id,
-							'attachment_id' => $attachment_id,
-							'success' => false,
-							'error' => 'Attachment not found or no permission'
-						];
-						continue;
-					}
-
-					// Apply metadata updates
-					$updated_fields = [];
-
-					// Update alt text
-					if (isset($update['alt'])) {
-						$alt_text = sanitize_text_field($update['alt']);
-						update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
-						$updated_fields[] = 'alt';
-					}
-
-					// Update title
-					if (isset($update['title'])) {
-						$title = sanitize_text_field($update['title']);
-						wp_update_post([
-							'ID' => $attachment_id,
-							'post_title' => $title
-						]);
-						$updated_fields[] = 'title';
-					}
-
-					// Update caption
-					if (isset($update['caption'])) {
-						$caption = sanitize_textarea_field($update['caption']);
-						wp_update_post([
-							'ID' => $attachment_id,
-							'post_excerpt' => $caption
-						]);
-						$updated_fields[] = 'caption';
-					}
-
-					// Update description
-					if (isset($update['description'])) {
-						$description = sanitize_textarea_field($update['description']);
-						wp_update_post([
-							'ID' => $attachment_id,
-							'post_content' => $description
-						]);
-						$updated_fields[] = 'description';
-					}
-
-					$updated_count++;
-					$results[] = [
-						'upload_id' => $upload_id,
-						'attachment_id' => $attachment_id,
-						'success' => true,
-						'updated_fields' => $updated_fields
-					];
-
-				} catch (Exception $e) {
-					$failed_count++;
-					$results[] = [
-						'upload_id' => $update['upload_id'] ?? 'unknown',
-						'attachment_id' => $update['attachment_id'] ?? 0,
-						'success' => false,
-						'error' => $e->getMessage()
-					];
-				}
+			// Now attach to the specified content
+			if (!empty($data['field_name'])) {
+				$this->updateFieldValue($data, $upload_results);
 			}
 
 			return [
 				'success' => true,
-				'data' => $results,
-				'updated_count' => $updated_count,
-				'failed_count' => $failed_count,
-				'message' => "Metadata updated: {$updated_count} succeeded, {$failed_count} failed"
+				'result' => 'Attachments linked to content'
+			];
+
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'result' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Cleanup temporary files after processing
+	 */
+	protected function cleanupTempFiles(array $secured_files, int $user_id): void
+	{
+		$uploader = new UploadManager();
+
+		foreach ($secured_files as $secured) {
+			if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) {
+				@unlink($secured['temp_path']);
+			}
+		}
+
+		// Clean up empty directories
+		$uploader->cleanupEmptyTempDirs($user_id);
+	}
+
+	/**
+	 * Update field value with new attachment IDs
+	 */
+	protected function updateFieldValue(array $data, array $results): void
+	{
+		if ((!array_key_exists('post_id', $data) && !array_key_exists('term_id', $data) && !array_key_exists('user', $data))  && !array_key_exists('field_name', $data)) {
+			return;
+		}
+
+		$attachment_ids = array_column($results, 'attachment_id');
+		if (array_key_exists('post_id', $data)) {
+			$meta = new MetaManager($data['post_id'], 'post');
+		} elseif (array_key_exists('term_id', $data)) {
+			$meta = new MetaManager($data['term_id'], 'term');
+		} else {
+			$link = (int)get_user_meta($data['user'], BASE.'link');
+			$meta = new MetaManager($link, 'post');
+		}
+
+		// Get existing value
+		$existing = $meta->getValue($data['field_name']);
+		$existing_ids = !empty($existing) ? explode(',', $existing) : [];
+
+		// Merge with new IDs
+		$all_ids = array_unique(array_merge($existing_ids, $attachment_ids));
+
+		// Update with comma-separated string
+		$meta->updateValue($data['field_name'], implode(',', $all_ids));
+	}
+
+	/**
+	 * Generic file upload processor - works for images, videos, documents
+	 */
+	protected function processFileUpload(object $operation, array $data, string $file_type): WP_Error|array
+	{
+		try {
+			$uploader = new UploadManager();
+			$processed_results = [];
+			$config = $this->getFieldConfig($data);
+
+			$args = $data;
+			unset($args['secured_files']);
+			unset($args['upload_map']);
+
+			foreach ($data['secured_files'] as $index => $secured_file) {
+				$result = $uploader->processUpload(
+					$secured_file['temp_path'],
+					array_merge(
+						$config,
+						[
+							'file_type' => $file_type,
+							'user_id' => $operation->user_id,
+							'post_id' => (int)($data['post_id'] ?? 0),
+							'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
+						]
+					)
+				);
+
+				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];
+					}
+
+					// Apply frontend metadata if provided
+					if (!empty($data['frontend_metadata'][$index])) {
+						$this->applyMeta(
+							$standardized['attachment_id'],
+							$data['frontend_metadata'][$index]
+						);
+					}
+
+					$processed_results[$standardized['upload_id']] = $standardized;
+				}
+			}
+
+			$this->handleUploadDestination($data, $processed_results);
+
+			// Cleanup temporary files
+			$this->cleanupTempFiles($data['secured_files'], $operation->user_id);
+
+			return [
+				'success' => true,
+				'result' => $processed_results
 			];
 
 		} catch (Exception $e) {
 			JVB()->error()->log(
-				'[UploadRoutes]:processAttachmentMetadata',
+				'[UploadRoutes]:processFileUpload',
+				$e->getMessage(),
+				[
+					'operation_id' => $operation->id,
+					'file_type' => $file_type,
+					'user_id' => $operation->user_id
+				]
+			);
+			return [
+				'success'	=> false,
+				'result'	=> $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Process image uploads
+	 */
+	protected function processImageUpload(object $operation, array $data): WP_Error|array
+	{
+		return $this->processFileUpload($operation, $data, 'image');
+	}
+
+	/**
+	 * Process video uploads
+	 */
+	protected function processVideoUpload(object $operation, array $data): WP_Error|array
+	{
+		return $this->processFileUpload($operation, $data, 'video');
+	}
+
+	/**
+	 * Process document uploads
+	 */
+	protected function processDocumentUpload(object $operation, array $data): WP_Error|array
+	{
+		return $this->processFileUpload($operation, $data, 'document');
+	}
+
+	/**
+	 * Process temporary cleanup operation
+	 * Called by OperationQueue for 'temporary_cleanup' operations
+	 *
+	 * @param array $result
+	 * @param object $operation
+	 * @param array $data
+	 * @return array
+	 */
+	protected function processTemporaryCleanup(array $result, object $operation, array $data): array
+	{
+		try {
+			// Cleanup temporary files if they exist
+			if (!empty($data['secured_files'])) {
+				$this->cleanupTempFiles($data['secured_files'], $operation->user_id);
+			}
+
+			// If specific temp paths provided
+			if (!empty($data['temp_paths']) && is_array($data['temp_paths'])) {
+				foreach ($data['temp_paths'] as $temp_path) {
+					if (file_exists($temp_path)) {
+						@unlink($temp_path);
+					}
+				}
+			}
+
+			// Cleanup empty temp directories for this user
+			if (!empty($operation->user_id)) {
+				$uploader = new UploadManager();
+				$uploader->cleanupEmptyTempDirs($operation->user_id);
+			}
+
+			return [
+				'success' => true,
+				'result' => 'Temporary files cleaned up successfully'
+			];
+
+		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadRoutes]:processTemporaryCleanup',
 				$e->getMessage(),
 				[
 					'operation_id' => $operation->id,
@@ -465,17 +717,275 @@
 				]
 			);
 
-			return new WP_Error(
-				'metadata_processing_failed',
+			return [
+				'success' => false,
+				'result' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Handle metadata update requests
+	 */
+	public function handleMetadataUpdate(WP_REST_Request $request): WP_REST_Response
+	{
+		try {
+			$data = $request->get_params();
+
+			// Validate user permissions
+			if (!$this->userCheck($data['user'])) {
+				return $this->sendResponse(
+					false,
+					['error_code' => 'invalid_user'],
+					'Invalid user specified'
+				);
+			}
+
+			$items = $data['items']??false;
+			if (!$items) {
+				return $this->sendResponse(
+					true,
+					[
+					],
+					'No items to update'
+				);
+			}
+			$pending = [];
+			$attachments = array_filter($items, function ($item) {
+				return is_numeric($item);
+			}, ARRAY_FILTER_USE_KEY);
+			if (count($attachments) !== count($items)) {
+				$pending = array_filter($items, function ($item) {
+					return !is_numeric($item);
+				}, ARRAY_FILTER_USE_KEY);
+			}
+
+
+			if (!empty($attachments)) {
+				// Phase 2B: Direct attachment update (images already processed)
+				return $this->updateMeta($attachments, $data['user']);
+			}
+			elseif (!empty($pending)) {
+				// Phase 2A: Queue metadata update with dependency on upload operation
+				return $this->queueMetaUpdate($pending, $data['user']);
+			}
+
+			return $this->sendResponse(
+				false,
+				['error_code' => 'missing_identifiers'],
+				'Must provide either attachment_ids or operation_id'
+			);
+
+		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadRoutes]:handleMetadataUpdate',
+				$e->getMessage(),
+				[
+					'request_data' => $request->get_params(),
+					'trace' => $e->getTraceAsString()
+				]
+			);
+
+			return $this->sendResponse(
+				false,
+				['error_code' => 'metadata_update_failed'],
+				'Metadata update failed: ' . $e->getMessage()
+			);
+		}
+	}
+	/**
+	 * Update metadata directly on completed attachments
+	 */
+	protected function updateMeta(array $data, int $user): WP_REST_Response
+	{
+		$updated_count = 0;
+		$errors = [];
+
+		foreach ($data as $attachment_id => $info) {
+			try {
+				// Verify attachment exists and user has permission
+				if (!$this->verifyAttachmentAccess($attachment_id, $user)) {
+					$errors[] = "No permission to edit attachment {$attachment_id}";
+					continue;
+				}
+
+				$this->applyMeta($attachment_id, $info);
+				$updated_count++;
+
+			} catch (Exception $e) {
+				$errors[] = "Failed to update attachment {$attachment_id}: " . $e->getMessage();
+			}
+		}
+
+		return $this->sendResponse(
+			$updated_count > 0,
+			[
+				'updated_count' => $updated_count,
+				'errors' => $errors,
+				'attachment_ids' => $data['attachment_ids']
+			],
+			$updated_count > 0 ?
+				"Updated metadata for {$updated_count} attachment(s)" :
+				'No attachments were updated'
+		);
+	}
+	/**
+	 * Queue metadata update with dependency on upload operation
+	 */
+	protected function queueMetaUpdate(array $data, int $user): WP_REST_Response
+	{
+		$queue = JVB()->queue();
+		$depends_on = [];
+		$errors = [];
+		$original = count($data);
+		foreach ($data as $uploadID => $info) {
+			if (!array_key_exists('depends_on', $info)) {
+				unset($data[$uploadID]);
+				$errors[$uploadID] = $info;
+				continue;
+			}
+			if (!in_array($info['depends_on'], $depends_on)) {
+				$depends_on[] = $info['depends_on'];
+			}
+		}
+		$operationID = $queue->queueOperation(
+			'update_metadata',
+			$user,
+			$data,
+			[
+				'depends_on' => $depends_on,
+				'priority' => 'medium',
+			]
+		);
+
+		return $this->sendResponse(
+			true,
+			[
+				'operation_id' => $operationID,
+				'message'		=> "Successfully queued ".count($data)." of {$original} meta updates"
+			],
+			'Metadata update queued - will apply after upload completes'
+		);
+	}
+
+	/**
+	 * Process metadata update operation (called by queue processor)
+	 */
+	public function processUploadMeta(WP_Error|array $result, object $operation, array $data): WP_Error|array
+	{
+		try {
+			if (!is_array($operation->depends_on)) {
+				$operation->depends_on = [$operation->depends_on];
+			}
+			$updated_count = 0;
+			$errors = [];
+			foreach ($operation->depends_on as $dependency) {
+				$operationData = JVB()->queue()->getOperation($dependency);
+				if (!$operationData || $operationData->status !== 'completed') {
+					throw new Exception('Original upload operation not found or not completed');
+				}
+
+				$uploadResults = json_decode($operationData->result, true);
+				if (!$uploadResults || !$uploadResults['success']) {
+					throw new Exception('Original upload operation failed');
+				}
+				$attachmentsToUpdate = array_filter($data, function ($item) use ($dependency) {
+					return $item['depends_on'] === $dependency;
+				});
+
+				$uploadIDToAttachment = $this->buildUploadToAttachmentMapping(
+					array_keys($attachmentsToUpdate),
+					$uploadResults['data']
+				);
+
+				if (empty($uploadIDToAttachment)) {
+					throw new Exception('No valid upload ID to attachment ID mappings found');
+				}
+
+				foreach ($uploadIDToAttachment as $uploadId => $attachmentId) {
+					try {
+						$this->applyMeta($attachmentId, $attachmentsToUpdate[$uploadId]);
+						$updated_count++;
+					} catch (Exception $e) {
+						$errors[] = "Upload {$uploadId} (Attachment {$attachmentId}): " . $e->getMessage();
+					}
+				}
+			}
+
+			return [
+				'success' => $updated_count > 0,
+				'result'	=> [
+					'updated_count' => $updated_count,
+					'mappings' => $uploadIDToAttachment,
+					'errors' => $errors,
+					'message' => "Applied metadata to {$updated_count} attachment(s)"
+				]
+			];
+
+		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadRoutes]:processUploadMeta',
 				$e->getMessage(),
 				[
 					'operation_id' => $operation->id,
-					'error_code' => 'metadata_update_error'
+					'original_operation_id' => $data['original_operation_id'] ?? 'unknown'
 				]
 			);
+
+			return [
+				'success'	=> false,
+				'result'	=> $e->getMessage()
+			];
 		}
 	}
 
+	protected function applyMeta(int $attachment_id, array $metadata): void
+	{
+		// Update alt text
+		if (!empty($metadata['alt'])) {
+			update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt']));
+		}
+		$postUpdates = [];
+		// Update title
+		if (!empty($metadata['title'])) {
+			$postUpdates['post_title'] = $metadata['title'];
+		}
+
+		// Update caption
+		if (!empty($metadata['caption'])) {
+			$postUpdates['post_excerpt'] = sanitize_textarea_field($metadata['caption']);
+		}
+
+		if (!empty($postUpdates)) {
+			$postUpdates['ID'] = $attachment_id;
+			wp_update_post($postUpdates);
+		}
+	}
+
+	/**
+	 * Build mapping from frontend upload IDs to WordPress attachment IDs
+	 */
+	protected function buildUploadToAttachmentMapping(array $upload_ids, array $results): array
+	{
+		$mapping = [];
+
+		foreach ($results as $result) {
+			if (!isset($result['upload_id']) || !isset($result['attachment_id'])) {
+				continue;
+			}
+
+			$upload_id = $result['upload_id'];
+			$attachment_id = $result['attachment_id'];
+
+			// Validate that this upload_id was requested
+			if (in_array($upload_id, $upload_ids)) {
+				$mapping[$upload_id] = $attachment_id;
+			}
+		}
+
+		return $mapping;
+	}
+
 	/**
 	 * Verify user has access to attachment
 	 */
@@ -491,177 +1001,58 @@
 		return ($attachment->post_author == $user_id) || current_user_can('manage_options');
 	}
 
-	protected function processImageUpload(WP_Error|array $result, object $operation, array $data): WP_Error|array
-	{
-		try {
-			$uploader = new UploadManager($data['content'], $operation->user_id);
-			$processed_results = [];
-
-			// Extract frontend metadata if available
-			$frontend_metadata = $this->extractFrontendMetadata($data);
-
-
-			foreach ($data['secured_files'] as $index => $secured_file) {
-				$result = $uploader->processImageFromStorage(
-					$secured_file['temp_path'],
-					$data['content'],
-					(int)($data['post_id'] ?? 0),
-					(int)($data['term_id'] ?? 0),
-					$secured_file['original_name'] ?? ''
-				);
-
-				if (!is_wp_error($result)) {
-					$standardized = $this->standardizeImageResult($result);
-
-					// FIX: Proper mapping using consistent naming
-					if (isset($data['upload_map'][$index])) {
-						$standardized['upload_id'] = $data['upload_map'][$index];
-					} else {
-						error_log("Warning: No upload_map found for index {$index}");
-						$standardized['upload_id'] = 'unknown_' . $index;
-					}
-
-					$file_metadata = $frontend_metadata[$index] ?? null;
-					if ($file_metadata) {
-						$this->applyFrontendMetadata($standardized['attachment_id'], $file_metadata);
-					}
-
-					$processed_results[] = $standardized;
-				}
-			}
-
-			// Update field metadata if specified
-			if ($data['field_name']) {
-				$this->updateFieldMetadata($data, $processed_results);
-			}
-
-			return [
-				'success' => true,
-				'result' => $processed_results
-			];
-
-		} catch (Exception $e) {
-			JVB()->error()->log(
-				'[UploadRoutes]:processImageUpload',
-				$e->getMessage(),
-				[
-					'operation_id' => $operation->id,
-					'user_id' => $operation->user_id,
-					'content' => $data['content'],
-					'upload_map' => $data['upload_map'] ?? 'missing'
-				]
-			);
-
-			return [
-				'success'	=> false,
-				'result'	=> $e->getMessage()
-			];
-		}
-	}
-	protected function extractFrontendMetadata(array $data): array
-	{
-		$metadata = [];
-
-		// Check if we have metadata in the request
-		if (isset($_POST['metadata'])) {
-			foreach ($_POST['metadata'] as $index => $meta_json) {
-				$decoded = json_decode($meta_json, true);
-				if ($decoded) {
-					$metadata[$index] = $decoded;
-				}
-			}
-		}
-
-		return $metadata;
-	}
-
-	protected function applyFrontendMetadata(int $attachment_id, array $metadata): void
-	{
-		// Update alt text
-		if (!empty($metadata['alt'])) {
-			update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt']));
-		}
-
-		// Update title
-		if (!empty($metadata['title'])) {
-			wp_update_post([
-				'ID' => $attachment_id,
-				'post_title' => sanitize_text_field($metadata['title'])
-			]);
-		}
-
-		// Update caption
-		if (!empty($metadata['caption'])) {
-			wp_update_post([
-				'ID' => $attachment_id,
-				'post_excerpt' => sanitize_textarea_field($metadata['caption'])
-			]);
-		}
-	}
-	protected function countProcessedFiles(array $processed_results): int
-	{
-		$count = 0;
-		foreach ($processed_results as $result) {
-			if (is_array($result) && isset($result[0])) {
-				// Grouped results
-				$count += count($result);
-			} else {
-				// Single result
-				$count++;
-			}
-		}
-		return $count;
-	}
-	protected function updateFieldMetadata(array $data, array $processed_results): void
-	{
-		if (!$data['field_name']) return;
-
-		$type = ($data['post_id']) ? 'post' : (($data['term_id']) ? 'term' : false);
-		if (!$type) return;
-
-		$ID = ($type == 'post') ? $data['post_id'] : $data['term_id'];
-		$meta = new MetaManager($ID, $type);
-
-		// Get attachment IDs from processed results
-		$attachment_ids = [];
-		if ($data['mode'] == 'selection') {
-			// Selection mode: grouped results
-			foreach ($processed_results as $group => $files) {
-				foreach ($files as $file_result) {
-					$attachment_ids[] = $file_result['attachment_id'];
-				}
-			}
-		} else {
-			// Direct mode: flat array
-			foreach ($processed_results as $file_result) {
-				$attachment_ids[] = $file_result['attachment_id'];
-			}
-		}
-
-		if (!empty($attachment_ids)) {
-			$value = implode(',', array_map('absint', $attachment_ids));
-			$meta->updateValue($data['field_name'], $value);
-		}
-	}
-
 	/**
-	 * Standardize image processing results for frontend
+	 * Get field configuration for upload processing
 	 */
-	protected function standardizeImageResult(array $result): array
+	protected function getFieldConfig(array $data): array
 	{
-		return [
-			'attachment_id' => $result['attachment_id'],
-			'url' => $result['url'],
-			'file_path' => $result['file'] ?? '',
-			'success' => $result['success'] ?? true,
-			'metadata' => [
-				'alt_text' => get_post_meta($result['attachment_id'], '_wp_attachment_image_alt', true),
-				'title' => get_the_title($result['attachment_id']),
-				'mime_type' => get_post_mime_type($result['attachment_id'])
-			]
+		static $config_cache = [];
+
+		$cache_key = md5(json_encode([
+			$args['content'] ?? '',
+			$args['field_name'] ?? ''
+		]));
+
+		if (isset($config_cache[$cache_key])) {
+			return $config_cache[$cache_key];
+		}
+
+		$config = [
+			'allowed_types' => null,
+			'max_size' => null,
+			'convert' => 'webp',
+			'quality' => 80,
+			'create_thumbnails' => true,
 		];
+
+		// Get field definition from registry if available
+		if (!empty($args['content']) && !empty($args['field_name'])) {
+			$content_type = $args['content'];
+			$field_name = $args['field_name'];
+
+			if (array_key_exists($content_type, JVB_CONTENT)) {
+				$content_fields = JVB_CONTENT[$content_type]['fields'] ?? [];
+				if (array_key_exists($field_name, $content_fields)) {
+					$field_def = $content_fields[$field_name];
+
+					// Extract relevant config from field definition
+					$config = array_merge($config, [
+						'allowed_types' => $field_def['accepted_types'] ?? null,
+						'max_size' => $field_def['max_size'] ?? null,
+						'convert' => $field_def['convert'] ?? 'webp',
+						'quality' => $field_def['quality'] ?? 80,
+						'create_thumbnails' => $field_def['create_thumbnails'] ?? true,
+					]);
+				}
+			}
+		}
+
+		$config_cache[$cache_key] = $config;
+		return $config;
 	}
 
+
+
 	/**
 	 * Get files info for error logging
 	 */
@@ -674,164 +1065,7 @@
 		];
 	}
 
-    /**
-     * @param WP_Error $result
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array|WP_Error
-     */
-    public function processTemporaryCleanup(WP_Error|array $result, object $operation, array $data):WP_Error|array
-    {
-		error_log('Processing Temporary Cleanup...');
-        $JVB = JVB();
-        $logger = $JVB->error();
-
-		$uploader = new UploadManager($data['content'], $data['user']);
-
-        // Check if we have files to clean up
-        if (empty($data['files']) || !is_array($data['files'])) {
-            return ['success' => true, 'result' => 'No files to clean up'];
-        }
-
-        $success_count = 0;
-        $error_count = 0;
-        $skipped_count = 0;
-
-        foreach ($data['files'] as $file_data) {
-            // Skip if we don't have a temp path
-            if (empty($file_data['temp_path'])) {
-                $skipped_count++;
-                continue;
-            }
-
-            $temp_path = $file_data['temp_path'];
-
-            try {
-                // Only remove if the file exists
-                if (file_exists($temp_path)) {
-                    if (@unlink($temp_path)) {
-                        $success_count++;
-                    } else {
-                        $error_count++;
-                        $logger->log(
-                            'temp_cleanup',
-                            'Failed to delete temporary file',
-                            ['file' => $temp_path],
-                            'warning'
-                        );
-                    }
-                } else {
-                    // File already gone, count as success
-                    $skipped_count++;
-                }
-            } catch (Exception $e) {
-                $error_count++;
-                $logger->log(
-                    '[UploadManager]:processTempCleanupOperation',
-                    'Error during temp file cleanup: ' . $e->getMessage(),
-                    ['file' => $temp_path],
-                    'warning'
-                );
-            }
-        }
-
-        // After cleaning individual files, check if directory is empty and can be removed
-        $uploader->cleanupEmptyTempDirs($operation->user_id);
-
-        return [
-            'success' => true,
-			'result' => [
-				'removed' => $success_count,
-				'errors' => $error_count,
-				'skipped' => $skipped_count,
-				'message' => "Temporary files cleanup completed: {$success_count} removed, {$error_count} errors, {$skipped_count} skipped"
-			]
-        ];
-    }
-
-	protected function attachUploadToContent($result, $operation, $args):array|false
-	{
-		error_log('STARTING ATTACHING UPLOAD');
-		error_log('Upload ID: '.print_r($args['upload'], true));
-		$uploadOperation = JVB()->queue()->getOperation($args['upload'], true);
-		error_log('Upload Operation from Attach Image to Content: '.print_r($uploadOperation, true));
-		error_log('Args: '.print_r($args, true));
-
-
-		$field = ($args['field_name']??false) ? $args['field_name'] : 'post_thumbnail';
-		$ID = $args['post_id']??$args['term_id'];
-		$return = [
-			'success'	=> false,
-			'results'	=> []
-		];
-		$images = array_map('absint', array_column(json_decode($uploadOperation->result, true),'attachment_id'));
-		error_log('Images: '.print_r($images, true));
-		error_log('Parsed ID: '.print_r($ID, true));
-		if (array_key_exists('content', $args) && $args['content'] === 'options') {
-			$meta = new MetaManager(null, 'options');
-			if (str_contains($args['field_name'], ':')) {
-				$result = $meta->updateRepeaterRowField($args['field_name'], (count($images) === 1) ? $images[0] : $images);
-			} else {
-				$result = $meta->updateValue($args['field_name'], (count($images) === 1) ? $images[0] : $images);
-			}
-			$return = [
-				'success'	=> $result,
-			];
-		} elseif (str_starts_with($field, 'new_') && !$ID) {
-			//Create post
-			$success = [
-				'created'	=> [],
-				'errors'	=> []
-			];
-			$i = 1;
-			foreach ($images as $img) {
-				$ID = wp_insert_post([
-					'post_type'		=> jvbCheckBase($args['content']),
-					'post_author'	=> $operation->user_id,
-					'post_title'	=> 'New '.JVB_CONTENT[$args['content']]['singular'].' '.$i,
-					'post_content'	=> ' ',
-					'post_excerpt'	=> ' '
-				], true);
-
-				error_log('Created Post: '.print_r($ID, true));
-
-				if ($ID && !is_wp_error($ID)) {
-					$success['created'][] = $ID;
-					set_post_thumbnail($ID, $img);
-				}else {
-					$success['errors'][] = $ID;
-				}
-				$i++;
-			}
-			$return = [
-				'success'	=> true,
-				'result'	=> $success
-			];
-		} else {
-			if ($args['post_id']) {
-				$type = 'post';
-			} elseif ($args['term_id']) {
-				$type = 'term';
-			}
-			error_log('ID: '.print_r($ID, true));
-			error_log('Type: '.print_r($type, true));
-			$meta = new MetaManager($ID, $type);
-
-			$images = implode(',', $images);
-			error_log('Processed Image IDs: '.$images);
-			$success = $meta->updateValue($field, $images);
-
-			$return = [
-				'success'	=> $success,
-				'result'	=> 'Field '.$field.' updated successfully'
-			];
-		}
-
-		return $return;
-	}
-
-	protected function createStandardResponse(bool $success, array $data = [], string $message = '', string $operation_id = ''): WP_REST_Response
+	protected function sendResponse(bool $success, array $data = [], string $message = '', string $operation_id = ''): WP_REST_Response
 	{
 		$response = [
 			'success' => $success,
@@ -847,348 +1081,83 @@
 		return new WP_REST_Response($response);
 	}
 
-	/**
-	 * Handle metadata update requests
-	 */
-	public function handleMetadataUpdate(WP_REST_Request $request): WP_REST_Response
-	{
-		try {
-			$data = $request->get_params();
 
-			// Validate user permissions
-			if (!$this->userCheck($data['user'])) {
-				return $this->createStandardResponse(
-					false,
-					['error_code' => 'invalid_user'],
-					'Invalid user specified'
-				);
-			}
 
-			if (!empty($data['attachment_ids'])) {
-				// Phase 2B: Direct attachment update (images already processed)
-				return $this->updateAttachmentMetadataDirect($data);
-			}
-			elseif (!empty($data['operation_id'])) {
-				// Phase 2A: Queue metadata update with dependency on upload operation
-				return $this->queueMetadataUpdate($data);
-			}
 
-			return $this->createStandardResponse(
-				false,
-				['error_code' => 'missing_identifiers'],
-				'Must provide either attachment_ids or operation_id'
-			);
-
-		} catch (Exception $e) {
-			JVB()->error()->log(
-				'[UploadRoutes]:handleMetadataUpdate',
-				$e->getMessage(),
-				[
-					'request_data' => $request->get_params(),
-					'trace' => $e->getTraceAsString()
-				]
-			);
-
-			return $this->createStandardResponse(
-				false,
-				['error_code' => 'metadata_update_failed'],
-				'Metadata update failed: ' . $e->getMessage()
-			);
-		}
-	}
-	/**
-	 * Update metadata directly on completed attachments
-	 */
-	protected function updateAttachmentMetadataDirect(array $data): WP_REST_Response
-	{
-		$updated_count = 0;
-		$errors = [];
-
-		foreach ($data['attachment_ids'] as $attachment_id) {
-			try {
-				// Verify attachment exists and user has permission
-				if (!$this->canUserEditAttachment($data['user'], $attachment_id)) {
-					$errors[] = "No permission to edit attachment {$attachment_id}";
-					continue;
-				}
-
-				$this->applyMetadataToAttachment($attachment_id, $data['metadata']);
-				$updated_count++;
-
-			} catch (Exception $e) {
-				$errors[] = "Failed to update attachment {$attachment_id}: " . $e->getMessage();
-			}
-		}
-
-		return $this->createStandardResponse(
-			$updated_count > 0,
-			[
-				'updated_count' => $updated_count,
-				'errors' => $errors,
-				'attachment_ids' => $data['attachment_ids']
-			],
-			$updated_count > 0 ?
-				"Updated metadata for {$updated_count} attachment(s)" :
-				'No attachments were updated'
-		);
-	}
-	/**
-	 * Queue metadata update with dependency on upload operation
-	 */
-	protected function queueMetadataUpdate(array $data): WP_REST_Response
-	{
-		$queue = JVB()->queue();
-
-		// Generate unique operation ID for this metadata update
-		$metadata_operation_id = 'meta_' . uniqid();
-
-		$queue->queueOperation(
-			'update_metadata',
-			$data['user'],
-			[
-				'original_operation_id' => $data['operation_id'],
-				'upload_ids' => $data['upload_ids'] ?? [],
-				'metadata' => $data['metadata']
-			],
-			[
-				'operation_id' => $metadata_operation_id,
-				'depends_on' => $data['operation_id'], // Wait for upload completion
-				'priority' => 'medium',
-				'notification' => false // Don't spam notifications for metadata updates
-			]
-		);
-
-		return $this->createStandardResponse(
-			true,
-			[
-				'metadata_operation_id' => $metadata_operation_id,
-				'depends_on' => $data['operation_id'],
-				'upload_ids' => $data['upload_ids'] ?? []
-			],
-			'Metadata update queued - will apply after upload completes'
-		);
-	}
-
-	/**
-	 * Process metadata update operation (called by queue processor)
-	 */
-	public function processMetadataUpdate(WP_Error|array $result, object $operation, array $data): WP_Error|array
-	{
-		try {
-			// Get the completed upload operation to map upload IDs to attachment IDs
-			$uploadOperation = JVB()->queue()->getOperation($data['original_operation_id']);
-
-			if (!$uploadOperation || $uploadOperation->status !== 'completed') {
-				throw new Exception('Original upload operation not found or not completed');
-			}
-
-			$uploadResults = json_decode($uploadOperation->result, true);
-			if (!$uploadResults || !$uploadResults['success']) {
-				throw new Exception('Original upload operation failed');
-			}
-
-			// Build mapping from upload IDs to attachment IDs
-			$uploadIdToAttachment = $this->buildUploadToAttachmentMapping(
-				$data['upload_ids'],
-				$uploadResults['data']
-			);
-
-			if (empty($uploadIdToAttachment)) {
-				throw new Exception('No valid upload ID to attachment ID mappings found');
-			}
-
-			// Apply metadata to each mapped attachment
-			$updated_count = 0;
-			$errors = [];
-
-			foreach ($uploadIdToAttachment as $uploadId => $attachmentId) {
-				try {
-					$this->applyMetadataToAttachment($attachmentId, $data['metadata']);
-					$updated_count++;
-				} catch (Exception $e) {
-					$errors[] = "Upload {$uploadId} (Attachment {$attachmentId}): " . $e->getMessage();
-				}
-			}
-
-			return [
-				'success' => $updated_count > 0,
-				'result'	=> [
-					'updated_count' => $updated_count,
-					'mappings' => $uploadIdToAttachment,
-					'errors' => $errors,
-					'message' => "Applied metadata to {$updated_count} attachment(s)"
-				]
-			];
-
-		} catch (Exception $e) {
-			JVB()->error()->log(
-				'[UploadRoutes]:processMetadataUpdate',
-				$e->getMessage(),
-				[
-					'operation_id' => $operation->id,
-					'original_operation_id' => $data['original_operation_id'] ?? 'unknown'
-				]
-			);
-
-			return [
-				'success'	=> false,
-				'result'	=> $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Build mapping from frontend upload IDs to WordPress attachment IDs
-	 */
-	protected function buildUploadToAttachmentMapping(array $uploadIds, array $uploadResults): array
-	{
-		$mapping = [];
-
-		// Handle both flat array and grouped results
-		if (isset($uploadResults[0]) && is_array($uploadResults[0]) && isset($uploadResults[0]['attachment_id'])) {
-			// Flat array of results
-			foreach ($uploadResults as $index => $result) {
-				if (isset($uploadIds[$index]) && isset($result['attachment_id'])) {
-					$mapping[$uploadIds[$index]] = $result['attachment_id'];
-				}
-			}
-		} else {
-			// Grouped results (selection mode)
-			$resultIndex = 0;
-			foreach ($uploadResults as $group) {
-				if (is_array($group)) {
-					foreach ($group as $result) {
-						if (isset($uploadIds[$resultIndex]) && isset($result['attachment_id'])) {
-							$mapping[$uploadIds[$resultIndex]] = $result['attachment_id'];
-						}
-						$resultIndex++;
-					}
-				}
-			}
-		}
-
-		return $mapping;
-	}
-
-	/**
-	 * Apply metadata to a WordPress attachment
-	 */
-	protected function applyMetadataToAttachment(int $attachment_id, array $metadata): void
-	{
-		$updates = [];
-
-		// Handle title update
-		if (isset($metadata['title']) && !empty($metadata['title'])) {
-			$updates['post_title'] = sanitize_text_field($metadata['title']);
-		}
-
-		// Handle caption update
-		if (isset($metadata['caption'])) {
-			$updates['post_excerpt'] = sanitize_textarea_field($metadata['caption']);
-		}
-
-		// Update post if we have title or caption changes
-		if (!empty($updates)) {
-			$updates['ID'] = $attachment_id;
-			$result = wp_update_post($updates);
-
-			if (is_wp_error($result)) {
-				throw new Exception('Failed to update attachment post: ' . $result->get_error_message());
-			}
-		}
-
-		// Handle alt text update (stored as post meta)
-		if (isset($metadata['alt'])) {
-			$alt_result = update_post_meta(
-				$attachment_id,
-				'_wp_attachment_image_alt',
-				sanitize_text_field($metadata['alt'])
-			);
-
-			if ($alt_result === false) {
-				throw new Exception('Failed to update alt text meta');
-			}
-		}
-
-		// Handle any custom metadata fields
-		if (isset($metadata['custom']) && is_array($metadata['custom'])) {
-			foreach ($metadata['custom'] as $key => $value) {
-				$meta_key = 'jvb_' . sanitize_key($key);
-				update_post_meta($attachment_id, $meta_key, sanitize_text_field($value));
-			}
-		}
-
-		// Clear any attachment caches
-		clean_attachment_cache($attachment_id);
-	}
-
-	/**
-	 * Check if user can edit attachment
-	 */
-	protected function canUserEditAttachment(int $user_id, int $attachment_id): bool
-	{
-		// Basic permission check - attachment must exist
-		$attachment = get_post($attachment_id);
-		if (!$attachment || $attachment->post_type !== 'attachment') {
-			return false;
-		}
-
-		// User must be the author or have edit capabilities
-		if ($attachment->post_author == $user_id) {
-			return true;
-		}
-
-		// Check if user has edit_posts capability
-		$user = get_user_by('id', $user_id);
-		return $user && user_can($user, 'skip_moderation');
-	}
 
 	public function handleGroupingRequest(WP_REST_Request $request): WP_REST_Response
 	{
-		error_log('Handling Group Request from UploadRoutes.php');
 		try {
+			$files = $request->get_file_params();
+			$args = $this->buildUploadArgs($request);
 			$data = $request->get_params();
-			error_log('Processing Data: '.print_r($data, true));
 
-			if (!array_key_exists($data['content'], JVB_CONTENT)) {
-				return $this->createStandardResponse(
+			if (!$args['content'] || !$args['user'] || !$args['posts']) {
+
+				$this->logError('Missing required data');
+				return new WP_REST_Response([
+					'success'	=> false,
+					'message'	=> 'Missing required data'
+				]);
+			}
+
+			// Secure files to temporary storage
+			$secured_files = $this->secureFiles($files, $args);
+
+			if (empty($secured_files['files'])) {
+				return $this->sendResponse(
 					false,
-					['error_code' => 'invalid_content'],
-					'Invalid content type specified'
+					['error_code' => 'no_files'],
+					'No valid files to upload'
 				);
 			}
 
-
-			//Get the operationIDs of the image uploads for the depends on
-			$operationIDs = [];
-			foreach ($data['posts'] as $index => $post) {
-				foreach ($post['images'] as $img) {
-					if (array_key_exists('operationId', $img)) {
-						$operationIDs[] = $img['operationId'];
-					}
-				}
+			// Queue file upload operation
+			$operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
+			$chunkSize = 5;
+			if ($operation_type === 'video') {
+				$chunkSize = 1;
+			} elseif ($operation_type === 'document') {
+				$chunkSize = 10;
 			}
-			$operationIDs = array_unique($operationIDs);
 
-
-			$queue = JVB()->queue();
-			$queue->queueOperation(
-				'process_upload_groups',
-				$data['user'],
-				$data,
+			JVB()->queue()->queueOperation(
+				$operation_type,
+				$args['user'],
+				array_merge(
+					[
+						'secured_files' => $secured_files['files'],
+						'upload_map' => $secured_files['upload_map'],
+					],
+					$args
+				),
 				[
-					'operation_id'	=> $data['id'],
-					'depends_on'	=> $operationIDs
+					'operation_id' => $args['upload'],
+					'chunk_key' => 'secured_files',
+					'chunk_size' => $chunkSize
 				]
 			);
 
-			return $this->createStandardResponse(
+			JVB()->queue()->queueOperation(
+				'process_upload_groups',
+				$args['user'],
+				$args,
+				[
+					'operation_id' => $args['id'],
+					'depends_on' => [$args['upload']],
+					'priority' => 'high'
+				]
+			);
+
+			return $this->sendResponse(
 				true,
 				[
-					'operation_id' => $data['id'],
-					'count' => count($data['posts'] ?? [])
+					'operation_id' => $args['id'],
+					'upload_operation_id' => $args['upload'],
+					'post_count' => count($args['posts']),
+					'file_count' => count($secured_files['files'])
 				],
-				'Operation queued successfully'
+				'Files uploaded and posts queued for creation'
 			);
 
 		} catch (Exception $e) {
@@ -1201,7 +1170,7 @@
 				]
 			);
 
-			return $this->createStandardResponse(
+			return $this->sendResponse(
 				false,
 				['error_code' => 'grouping_failed'],
 				'Grouping operation failed: ' . $e->getMessage()
@@ -1212,86 +1181,99 @@
 	protected function processUploadGroups(WP_Error|array $result, object $operation, array $data): WP_Error|array
 	{
 		try {
-			error_log('Processing Upload Groups - Data: ' . print_r($data, true));
-
 			$queue = JVB()->queue();
 			$all_uploaded_images = [];
-			$used_upload_ids = [];
 
-			// Collect all uploaded images from dependencies
-			foreach(json_decode($operation->dependencies, true) as $operationID) {
-				$o = $queue->getOperation($operationID);
-				$r = json_decode($o->result, true);
+			// Get the upload operation ID from data
+			$dependencies = json_decode($operation->dependencies, true);
+			$uploads = [];
+			foreach($dependencies as $dependency) {
+				$op = $queue->getOperation($dependency);
 
-				error_log('Saved operation'.print_r($r, true));
-				if ($r['success'] && isset($r['data'])) {
-					foreach ($r['data'] as $img) {
-						$uploadKey = $img['upload_id'];
-						$all_uploaded_images[$uploadKey] = [
-							'upload_id' => $img['upload_id'],
-							'attachment_id' => (int)$img['attachment_id'],
-							'operation_id' => $operationID
-						];
-					}
+				$res = json_decode($op->result, true);
+				if ($op->status === 'completed') {
+					$uploads = array_merge($uploads, $res);
 				}
 			}
 
 
+			if (empty($uploads)) {
+				return [
+					'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']
+					];
+				}
+			}
+
 			$content = jvbCheckBase($data['content']);
 			$user = (int)$data['user'];
 			$created_posts = [];
+			$used_upload_ids = [];
 
+			// Create posts from groups
 			foreach ($data['posts'] as $index => $post) {
+				$post_title = !empty($post['fields']['post_title'])
+					? sanitize_text_field($post['fields']['post_title'])
+					: 'New ' . JVB_CONTENT[$content]['singular'] . ' ' . ($index + 1);
+
+				$post_excerpt = !empty($post['fields']['post_excerpt'])
+					? sanitize_textarea_field($post['fields']['post_excerpt'])
+					: '';
+
 				$new_post_id = wp_insert_post([
 					'post_type' => $content,
 					'post_author' => $user,
 					'post_status' => 'draft',
-					'post_title' => array_key_exists('post_title', $post['fields'])
-						? sanitize_text_field($post['fields']['post_title'])
-						: 'New ' . JVB_CONTENT[$data['content']]['singular'],
-					'post_excerpt' => array_key_exists('post_excerpt', $post['fields'])
-						? sanitize_text_field($post['fields']['post_excerpt'])
-						: '',
+					'post_title' => $post_title,
+					'post_excerpt' => $post_excerpt,
 				]);
 
 				if ($new_post_id && !is_wp_error($new_post_id)) {
 					$created_posts[] = $new_post_id;
 
-					// Extract featured image
-					$featured_upload_id = array_key_exists('featured', $post['fields'])
-						? $post['fields']['featured']
-						: $post['images'][0]['upload_id'] ?? null;
-
+					// Get featured image upload_id
+					$featured_upload_id = (int)$post['fields']['featured'] ?? null;
 					$featured_attachment_id = null;
 					$gallery_attachment_ids = [];
 
 					// Process all images for this post
-					foreach ($post['images'] as $img) {
+					foreach ($post['images'] as $key => $img) {
 						$upload_id = $img['upload_id'];
 						$used_upload_ids[] = $upload_id;
 
 						if (isset($all_uploaded_images[$upload_id])) {
-							$attachment_id = $all_uploaded_images[$upload_id]['attachment_id'];
+							$attachment_id = (int)$all_uploaded_images[$upload_id]['attachment_id'];
 
 							if ($upload_id === $featured_upload_id) {
 								$featured_attachment_id = $attachment_id;
 							} else {
 								$gallery_attachment_ids[] = $attachment_id;
 							}
-						} else {
-							error_log("Warning: Could not find attachment for upload_id: {$upload_id}");
 						}
 					}
 
 					// Set featured image
 					if ($featured_attachment_id) {
 						set_post_thumbnail($new_post_id, (int)$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]);
+						array_shift($gallery_attachment_ids);
 					}
 
 					// Set gallery images
 					if (!empty($gallery_attachment_ids)) {
 						$meta = new MetaManager($new_post_id, 'post');
-						$fields = jvbGetFields($data['content'], 'post');
+						$fields = jvbGetFields($content, 'post');
 
 						foreach ($fields as $name => $config) {
 							if ($config['type'] === 'gallery') {
@@ -1303,18 +1285,12 @@
 				}
 			}
 
-			// Cleanup unused uploaded images
-			$unused_images = array_diff_key($all_uploaded_images, array_flip($used_upload_ids));
-			$cleanup_result = $this->cleanupUnusedImages($unused_images);
-
 			return [
 				'success' => true,
-				'result'	=> [
+				'result' => [
 					'created_posts' => $created_posts,
 					'total_posts' => count($created_posts),
 					'used_images' => count($used_upload_ids),
-					'cleaned_up_images' => $cleanup_result['cleaned_count'],
-					'cleanup_errors' => $cleanup_result['errors'],
 					'message' => "Created " . count($created_posts) . " post(s) from uploads"
 				]
 			];
@@ -1325,8 +1301,7 @@
 				$e->getMessage(),
 				[
 					'operation_id' => $operation->id,
-					'user_id' => $operation->user_id,
-					'posts_count' => count($data['posts'] ?? [])
+					'user_id' => $operation->user_id
 				]
 			);
 
@@ -1353,7 +1328,6 @@
 
 					if ($deleted) {
 						$cleaned_count++;
-						error_log("Cleaned up unused image: attachment_id {$attachment_id}, upload_id {$upload_id}");
 					} else {
 						$errors[] = "Failed to delete attachment {$attachment_id} for upload {$upload_id}";
 					}
@@ -1375,9 +1349,6 @@
 
 	function getAttachmentID(array $image, array $storedResults): int|false
 	{
-		error_log('Looking for upload_id: ' . ($image['upload_id'] ?? 'NOT SET'));
-		error_log('Available results: ' . print_r(array_keys($storedResults), true));
-
 		foreach ($storedResults as $operationID => $uploads) {
 			foreach ($uploads as $upload) {
 				// FIX: Handle both case variations
@@ -1385,13 +1356,10 @@
 				$search_upload_id = $image['upload_id'];
 
 				if ($stored_upload_id === $search_upload_id) {
-					error_log("Found match! upload_id: {$search_upload_id} -> attachment_id: {$upload['attachment_id']}");
 					return (int)$upload['attachment_id'];
 				}
 			}
 		}
-
-		error_log("No match found for upload_id: " . ($image['upload_id'] ?? 'MISSING'));
 		return false;
 	}
 
@@ -1523,4 +1491,244 @@
 
 		return $title;
 	}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+	/**
+	 * Determine how to save uploaded files based on configuration
+	 */
+	protected function handleUploadDestination(array $data, array $results): void
+	{
+		// Determine destination from config
+		$destination = $data['destination'] ?? 'meta';
+
+		switch ($destination) {
+			case 'meta':
+				// Save to post/term/user meta
+				$this->saveToMeta($data, $results);
+				break;
+
+			case 'post':
+				// Create individual posts for each file
+				$this->createIndividualPosts($data, $results);
+				break;
+
+			case 'post_group':
+				// Create posts with grouped files
+				$this->createGroupedPosts($data, $results);
+				break;
+
+			default:
+				// No destination specified - files processed but not attached
+				break;
+		}
+	}
+
+	/**
+	 * Infer destination from existing data (backward compatibility)
+	 */
+	protected function inferDestination(array $data): string
+	{
+		// If field_name exists → saving to meta
+		if (!empty($data['field_name'])) {
+			return 'meta';
+		}
+
+		// If post_type exists without field_name → creating posts
+		if (!empty($data['content'])) {
+			return 'post';
+		}
+
+		// No destination
+		return 'none';
+	}
+
+	/**
+	 * Save attachment IDs to meta field
+	 */
+	protected function saveToMeta(array $data, array $results): void
+	{
+		if (empty($data['field_name'])) {
+			return;
+		}
+
+		$attachment_ids = array_column($results, 'attachment_id');
+
+		// Determine meta type
+		if (!empty($data['post_id'])) {
+			$meta = new MetaManager($data['post_id'], 'post');
+		} elseif (!empty($data['term_id'])) {
+			$meta = new MetaManager($data['term_id'], 'term');
+		} elseif (!empty($data['user'])) {
+			$link = (int)get_user_meta($data['user'], BASE.'link', true);
+			$meta = new MetaManager($link, 'post');
+		} else {
+			return;
+		}
+
+		// Get existing value
+		$existing = $meta->getValue($data['field_name']);
+		$existing_ids = !empty($existing) ? explode(',', $existing) : [];
+
+		// Merge with new IDs
+		$all_ids = array_unique(array_merge($existing_ids, $attachment_ids));
+
+		// Update with comma-separated string
+		$meta->updateValue($data['field_name'], implode(',', $all_ids));
+	}
+
+	/**
+	 * Create individual posts from uploads
+	 */
+	protected function createIndividualPosts(array $data, array $results): array
+	{
+		if (empty($data['content'])) {
+			return [];
+		}
+
+		$created_posts = [];
+
+		foreach ($results as $result) {
+			$attachment_id = $result['attachment_id'];
+			$attachment = get_post($attachment_id);
+
+			// Create post
+			$post_data = [
+				'post_type' => jvbCheckBase($data['content']),
+				'post_title' => $attachment->post_title,
+				'post_status' => 'draft',
+				'post_author' => $data['user'] ?? get_current_user_id(),
+			];
+
+			$post_id = wp_insert_post($post_data);
+
+			if (!is_wp_error($post_id)) {
+				// Set as featured image or attach to gallery
+				$this->attachFileToPost($post_id, $attachment_id, $data);
+
+				$created_posts[] = [
+					'post_id' => $post_id,
+					'attachment_id' => $attachment_id,
+				];
+			}
+		}
+
+		return $created_posts;
+	}
+
+	/**
+	 * Create posts with grouped uploads
+	 */
+	protected function createGroupedPosts(array $data, array $results): array
+	{
+		if (empty($data['content'])) {
+			return [];
+		}
+
+		$id_map = [];
+		foreach ($results as $result) {
+			if (isset($result['upload_id'], $result['attachment_id'])) {
+				$id_map[$result['upload_id']] = $result['attachment_id'];
+			}
+		}
+
+		// Groups come from frontend as metadata
+		$groups = $data['groups'] ?? [array_column($results, 'attachment_id')];
+		$created_posts = [];
+
+		foreach ($groups as $group_index => $group_upload_ids) {
+			$group_attachment_ids = array_filter(
+				array_map(fn($uid) => $id_map[$uid] ?? null, $group_upload_ids)
+			);
+
+			if (empty($group_attachment_ids)) continue;
+			// Create post for this group
+			$first_attachment = get_post($group_attachment_ids[0]);
+
+			$post_data = [
+				'post_type' => jvbCheckBase($data['content']),
+				'post_title' => $data['group_titles'][$group_index] ?? $first_attachment->post_title,
+				'post_status' => $data['post_status'] ?? 'draft',
+				'post_author' => $data['user'] ?? get_current_user_id(),
+			];
+
+			$post_id = wp_insert_post($post_data);
+
+			if (!is_wp_error($post_id)) {
+				// Attach all files in group
+				foreach ($group_attachment_ids as $index => $attachment_id) {
+					if ($index === 0) {
+						// First is featured
+						set_post_thumbnail($post_id, $attachment_id);
+					} else {
+						// Others go to gallery
+						$meta = new MetaManager($post_id, 'post');
+						$existing = $meta->getValue('gallery');
+						$existing_ids = !empty($existing) ? explode(',', $existing) : [];
+						$existing_ids[] = $attachment_id;
+						$meta->updateValue('gallery', implode(',', $existing_ids));
+					}
+				}
+
+				$created_posts[] = [
+					'post_id' => $post_id,
+					'attachment_ids' => $group_attachment_ids,
+				];
+			}
+		}
+
+		return $created_posts;
+	}
+
+	/**
+	 * Attach file to post based on file type
+	 */
+	protected function attachFileToPost(int $post_id, int $attachment_id, array $data): void
+	{
+		$attachment = get_post($attachment_id);
+		$mime_type = $attachment->post_mime_type;
+
+		// Determine file type
+		if (str_starts_with($mime_type, 'image/')) {
+			// Set as featured image
+			set_post_thumbnail($post_id, $attachment_id);
+		} elseif (str_starts_with($mime_type, 'video/')) {
+			// Save to video field
+			$meta = new MetaManager($post_id, 'post');
+			$meta->updateValue('video', $attachment_id);
+		} else {
+			// Documents - save to documents field
+			$meta = new MetaManager($post_id, 'post');
+			$existing = $meta->getValue('documents');
+			$existing_ids = !empty($existing) ? explode(',', $existing) : [];
+			$existing_ids[] = $attachment_id;
+			$meta->updateValue('documents', implode(',', $existing_ids));
+		}
+	}
 }
diff --git a/inc/utility/Features.php b/inc/utility/Features.php
index c0ae652..9eaa314 100644
--- a/inc/utility/Features.php
+++ b/inc/utility/Features.php
@@ -92,6 +92,29 @@
 		}
 		return $feature->has($integration);
 	}
+
+	public static function hasAnyIntegration(string $type = 'site', ?string $subType = null):bool
+	{
+		$allowedTypes = ['site', 'content', 'taxonomy', 'user'];
+		if (!in_array($type, $allowedTypes)) {
+			return false;
+		}
+		if (in_array($type, ['content', 'taxonomy', 'user']) && !$subType) {
+			return false;
+		}
+		switch ($type) {
+			case 'site':
+				return (array_key_exists('integrations', JVB_SITE) && !empty(JVB_SITE['integrations']));
+			case 'content':
+				return (array_key_exists($subType, JVB_CONTENT) && array_key_exists('integrations', JVB_CONTENT[$subType]) && !empty(JVB_CONTENT[$subType]['integrations']));
+			case 'taxonomy':
+				return (array_key_exists($subType, JVB_TAXONOMY) && array_key_exists('integrations', JVB_TAXONOMY[$subType]) && !empty(JVB_TAXONOMY[$subType]['integrations']));
+			case 'user':
+				return (array_key_exists($subType, JVB_USER) && array_key_exists('integrations', JVB_USER[$subType]) && !empty(JVB_USER[$subType]['integrations']));
+			default:
+				return false;
+		}
+	}
 	/**
 	 * Create from a specific taxonomy
 	 */
diff --git a/jvb.php b/jvb.php
index 9e57dd1..edc1b76 100644
--- a/jvb.php
+++ b/jvb.php
@@ -96,7 +96,7 @@
 function jvbUserCheck():void
 {
     if (is_admin() && isOurPeople()) {
-        wp_redirect(get_home_url(2, '/dash'));
+        wp_redirect(get_home_url(null, '/dash'));
         exit;
     }
 }
@@ -207,6 +207,19 @@
 		]
 	);
 
+	wp_register_script(
+		'jvb-popup',
+		JVB_URL.'assets/js/min/popup.min.js',
+		[
+			'jvb-a11y'
+		],
+		'1.0.0',
+		[
+			'strategy'	=> 'defer',
+			'in_footer'	=> true
+		]
+	);
+
     //Main js image resizing, and gallery
     //TODO: lots of overlap between modals and this, utilize a11y for trapFocus,
     wp_register_script(
@@ -398,6 +411,7 @@
             'jvb-error',
             'jvb-data-store',
             'jvb-utility',
+			'jvb-popup'
         ],
         '1.0.0',
         [
@@ -474,12 +488,43 @@
 
     //Upload Manager
     wp_register_script(
+        'jvb-handle-selection',
+        JVB_URL.'assets/js/min/handleSelection.min.js',
+        [
+			'jvb-a11y',
+            'jvb-utility',
+        ],
+        '1.0.0',
+        [
+            'strategy'    => 'defer',
+            'in_footer'    => true,
+        ]
+    );
+    wp_register_script(
+        'jvb-drag-handler',
+        JVB_URL.'assets/js/min/dragHandler.min.js',
+        [
+			'jvb-a11y',
+            'jvb-utility',
+        ],
+        '1.0.0',
+        [
+            'strategy'    => 'defer',
+            'in_footer'    => true,
+        ]
+    );
+
+    //Upload Manager
+    wp_register_script(
         'jvb-uploader',
         JVB_URL.'assets/js/min/uploader.min.js',
         [
 			'jvb-cache',
 			'jvb-a11y',
             'jvb-utility',
+			'jvb-handle-selection',
+			'jvb-modal',
+			'jvb-drag-handler',
 //            'jvb-loading',
             'jvb-queue',
             'jvb-notifications'
@@ -545,6 +590,7 @@
         JVB_URL.'assets/js/min/form.min.js',
         [
             'jvb-utility',
+            'jvb-tabs',
             'jvb-selector',
             'jvb-uploader',
 			'sortable-js'
@@ -609,7 +655,8 @@
 			'jvb-a11y',
 			'jvb-utility',
 			'jvb-data-store',
-			'jvb-error'
+			'jvb-error',
+			'jvb-populate-form'
 		],
 		'1.0.0',
 		[
diff --git a/src/video/block.json b/src/video/block.json
index 5e92360..18bbd6d 100644
--- a/src/video/block.json
+++ b/src/video/block.json
@@ -1,7 +1,7 @@
 {
 	"$schema": "https://schemas.wp.org/trunk/block.json",
 	"apiVersion": 3,
-	"name": "jvb/video-cover",
+	"name": "jvb/video",
 	"version": "1.0.0",
 	"title": "Video Cover",
 	"category": "jvb",
@@ -13,9 +13,21 @@
 		"spacing": {
 			"margin": true,
 			"padding": true
+		},
+		"color": {
+			"background": true,
+			"text": true
 		}
 	},
 	"attributes": {
+		"title": {
+			"type": "string",
+			"default": ""
+		},
+		"description": {
+			"type": "rich-text",
+			"default": ""
+		},
 		"posterId": {
 			"type": "number",
 			"default": 0
@@ -63,6 +75,18 @@
 		"fadeEffect": {
 			"type": "boolean",
 			"default": false
+		},
+		"overlayOpacity": {
+			"type": "number",
+			"default": 0
+		},
+		"contentAlignment": {
+			"type": "string",
+			"default": "center"
+		},
+		"minHeight": {
+			"type": "number",
+			"default": 0
 		}
 	},
 	"textdomain": "jvb",
diff --git a/src/video/edit.js b/src/video/edit.js
index e871a44..44b6d35 100644
--- a/src/video/edit.js
+++ b/src/video/edit.js
@@ -4,31 +4,63 @@
 	InspectorControls,
 	MediaUpload,
 	MediaUploadCheck,
-	InnerBlocks
+	InnerBlocks,
+	useInnerBlocksProps
 } from '@wordpress/block-editor';
 import {
 	PanelBody,
 	Button,
 	ToggleControl,
-	BaseControl
+	BaseControl,
+	RangeControl,
+	SelectControl
 } from '@wordpress/components';
 import './editor.scss';
 
 const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'];
 
+const INNER_BLOCKS_TEMPLATE = [
+	['core/heading', {
+		level: 1,
+		placeholder: 'Add heading...',
+		textAlign: 'center'
+	}],
+	['core/paragraph', {
+		placeholder: 'Add description...',
+		align: 'center'
+	}],
+	['core/buttons', {
+		layout: { type: 'flex', justifyContent: 'center' }
+	}]
+];
+
 export default function Edit({ attributes, setAttributes }) {
 	const {
 		posterId,
 		posterUrl,
 		videoSources,
 		mobileSources,
-		fadeEffect
+		fadeEffect,
+		overlayOpacity,
+		contentAlignment,
+		minHeight
 	} = attributes;
 
 	const blockProps = useBlockProps({
-		className: 'video-cover-editor'
+		className: 'video-cover-editor',
+		style: {
+			minHeight: minHeight ? `${minHeight}px` : undefined
+		}
 	});
 
+	const innerBlocksProps = useInnerBlocksProps(
+		{ className: 'video-cover-content' },
+		{
+			template: INNER_BLOCKS_TEMPLATE,
+			templateLock: false
+		}
+	);
+
 	const onSelectPoster = (media) => {
 		setAttributes({
 			posterId: media.id,
@@ -189,13 +221,65 @@
 						onChange={(value) => setAttributes({ fadeEffect: value })}
 					/>
 				</PanelBody>
+
+				<PanelBody title={__('Overlay Settings', 'jvb')} initialOpen={true}>
+					<RangeControl
+						label={__('Overlay Opacity', 'jvb')}
+						help={__('Darken video for better text readability', 'jvb')}
+						value={overlayOpacity}
+						onChange={(value) => setAttributes({ overlayOpacity: value })}
+						min={0}
+						max={100}
+						step={5}
+					/>
+
+					<SelectControl
+						label={__('Content Alignment', 'jvb')}
+						value={contentAlignment}
+						options={[
+							{ label: __('Top Left', 'jvb'), value: 'top-left' },
+							{ label: __('Top Center', 'jvb'), value: 'top-center' },
+							{ label: __('Top Right', 'jvb'), value: 'top-right' },
+							{ label: __('Center Left', 'jvb'), value: 'center-left' },
+							{ label: __('Center', 'jvb'), value: 'center' },
+							{ label: __('Center Right', 'jvb'), value: 'center-right' },
+							{ label: __('Bottom Left', 'jvb'), value: 'bottom-left' },
+							{ label: __('Bottom Center', 'jvb'), value: 'bottom-center' },
+							{ label: __('Bottom Right', 'jvb'), value: 'bottom-right' }
+						]}
+						onChange={(value) => setAttributes({ contentAlignment: value })}
+					/>
+
+					<RangeControl
+						label={__('Minimum Height', 'jvb')}
+						help={__('Minimum height in pixels (leave 0 for auto)', 'jvb')}
+						value={minHeight}
+						onChange={(value) => setAttributes({ minHeight: value })}
+						min={0}
+						max={1000}
+						step={50}
+					/>
+				</PanelBody>
 			</InspectorControls>
 
 			<div {...blockProps}>
-				{posterUrl ? (
+				{posterUrl || videoSources.length > 0 ? (
 					<div className="video-cover-preview">
-						<img src={posterUrl} alt={__('Video poster', 'jvb')} />
-						<div className="video-overlay">
+						{posterUrl && (
+							<>
+								<img src={posterUrl} alt={__('Video poster', 'jvb')} />
+								{overlayOpacity > 0 && (
+									<div
+										className="video-overlay-preview"
+										style={{ opacity: overlayOpacity / 100 }}
+									/>
+								)}
+							</>
+						)}
+						<div className={`video-cover-content-preview align-${contentAlignment}`}>
+							<div {...innerBlocksProps} />
+						</div>
+						<div className="video-info">
 							<p>
 								{videoSources.length} {__('desktop source(s)', 'jvb')}
 								{mobileSources.length > 0 &&
@@ -210,9 +294,6 @@
 					</div>
 				)}
 			</div>
-			<div className="inner-blocks">
-				<InnerBlocks />
-			</div>
 		</>
 	);
 }
diff --git a/src/video/editor.scss b/src/video/editor.scss
index 1600218..9218653 100644
--- a/src/video/editor.scss
+++ b/src/video/editor.scss
@@ -1,4 +1,5 @@
 .video-cover-editor {
+	position: relative;
 	min-height: 200px;
 	background: #f0f0f0;
 	border: 2px dashed #ccc;
@@ -7,6 +8,7 @@
 	.video-cover-preview {
 		position: relative;
 		width: 100%;
+		min-height: 300px;
 
 		img {
 			width: 100%;
@@ -14,7 +16,82 @@
 			display: block;
 		}
 
-		.video-overlay {
+		.video-overlay-preview {
+			position: absolute;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			background: rgba(0, 0, 0, 1);
+			pointer-events: none;
+		}
+
+		.video-cover-content-preview {
+			position: absolute;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			display: flex;
+			z-index: 2;
+			padding: 2rem;
+
+			// Content alignment classes
+			&.align-top-left {
+				align-items: flex-start;
+				justify-content: flex-start;
+			}
+			&.align-top-center {
+				align-items: flex-start;
+				justify-content: center;
+			}
+			&.align-top-right {
+				align-items: flex-start;
+				justify-content: flex-end;
+			}
+			&.align-center-left {
+				align-items: center;
+				justify-content: flex-start;
+			}
+			&.align-center {
+				align-items: center;
+				justify-content: center;
+			}
+			&.align-center-right {
+				align-items: center;
+				justify-content: flex-end;
+			}
+			&.align-bottom-left {
+				align-items: flex-end;
+				justify-content: flex-start;
+			}
+			&.align-bottom-center {
+				align-items: flex-end;
+				justify-content: center;
+			}
+			&.align-bottom-right {
+				align-items: flex-end;
+				justify-content: flex-end;
+			}
+
+			.video-cover-content {
+				width: 100%;
+				max-width: 1200px;
+				color: white;
+				text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+
+				// Style inner blocks for better visibility in editor
+				h1, h2, h3, h4, h5, h6 {
+					color: white;
+				}
+
+				p {
+					color: white;
+				}
+			}
+		}
+
+		.video-info {
 			position: absolute;
 			bottom: 0;
 			left: 0;
@@ -23,6 +100,7 @@
 			color: white;
 			padding: 10px;
 			font-size: 14px;
+			z-index: 3;
 
 			p {
 				margin: 0;
diff --git a/src/video/style.scss b/src/video/style.scss
index c31a76f..223f434 100644
--- a/src/video/style.scss
+++ b/src/video/style.scss
@@ -1,11 +1,113 @@
-.video-cover {
+.video-cover-wrapper {
+	position: relative;
 	width: 100%;
-	height: auto;
-	display: block;
-	object-fit: cover;
+	min-height: 400px;
+	overflow: hidden;
+	display: flex;
 
-	&.fade {
-		animation: fadeIn 1s ease-in;
+	// Video background
+	.video-cover-bg {
+		position: absolute;
+		top: 50%;
+		left: 50%;
+		min-width: 100%;
+		min-height: 100%;
+		width: auto;
+		height: auto;
+		transform: translate(-50%, -50%);
+		object-fit: cover;
+		z-index: 0;
+
+		&.fade {
+			animation: fadeIn 1s ease-in;
+		}
+	}
+
+	// Dark overlay
+	.video-cover-overlay {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 1);
+		z-index: 1;
+	}
+
+	// Content container
+	.video-cover-content {
+		position: relative;
+		z-index: 2;
+		width: 100%;
+		padding: 2rem;
+		color: white;
+
+		// Better text readability
+		h1, h2, h3, h4, h5, h6 {
+			color: white;
+			text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+		}
+
+		p {
+			color: white;
+			text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+		}
+
+		// Button styles
+		.wp-block-button__link {
+			text-shadow: none;
+		}
+	}
+
+	// Alignment classes
+	&.align-top-left {
+		align-items: flex-start;
+		justify-content: flex-start;
+	}
+	&.align-top-center {
+		align-items: flex-start;
+		justify-content: center;
+	}
+	&.align-top-right {
+		align-items: flex-start;
+		justify-content: flex-end;
+	}
+	&.align-center-left {
+		align-items: center;
+		justify-content: flex-start;
+	}
+	&.align-center {
+		align-items: center;
+		justify-content: center;
+	}
+	&.align-center-right {
+		align-items: center;
+		justify-content: flex-end;
+	}
+	&.align-bottom-left {
+		align-items: flex-end;
+		justify-content: flex-start;
+	}
+	&.align-bottom-center {
+		align-items: flex-end;
+		justify-content: center;
+	}
+	&.align-bottom-right {
+		align-items: flex-end;
+		justify-content: flex-end;
+	}
+
+	// Full-width alignment
+	&.alignfull {
+		width: 100vw;
+		max-width: none;
+		margin-left: calc(50% - 50vw);
+		margin-right: calc(50% - 50vw);
+	}
+
+	// Wide alignment
+	&.alignwide {
+		max-width: 1200px;
 	}
 }
 
@@ -18,18 +120,23 @@
 	}
 }
 
+// Responsive adjustments
+@media (max-width: 768px) {
+	.video-cover-wrapper {
+		min-height: 300px;
 
-.video-cover video {
-	pointer-events: none;
-	position: absolute;
-	top: 0;
-	left: 50%;
-	opacity: 0.85;
-	mix-blend-mode: multiply;
-	transform: translate(-40%,-10%);
-	height: 105vh;
-	width: 300vw!important;
-	filter: grayscale(100%) contrast(1);
-	flex: 1 0 100%;
-	max-width: none;
+		.video-cover-content {
+			padding: 1.5rem;
+		}
+	}
+}
+
+@media (max-width: 480px) {
+	.video-cover-wrapper {
+		min-height: 250px;
+
+		.video-cover-content {
+			padding: 1rem;
+		}
+	}
 }
diff --git a/templates/dashboard/sections/favourites.php b/templates/dashboard/sections/favourites.php
index ec3e363..8fca5b7 100644
--- a/templates/dashboard/sections/favourites.php
+++ b/templates/dashboard/sections/favourites.php
@@ -3,7 +3,7 @@
     exit; // Exit if accessed directly
 }
 if (!current_user_can('can_favourite')) {
-    wp_redirect(get_home_url(2, '/dash'));
+    wp_redirect(get_home_url(null, '/dash'));
     exit;
 }
 
diff --git a/templates/dashboard/sections/news.php b/templates/dashboard/sections/news.php
index b59e1fc..f9c06b0 100644
--- a/templates/dashboard/sections/news.php
+++ b/templates/dashboard/sections/news.php
@@ -3,7 +3,7 @@
     exit; // Exit if accessed directly
 }
 if (!isOurPeople()) {
-    wp_redirect(get_home_url(2, '/dash'));
+    wp_redirect(get_home_url(null, '/dash'));
     exit;
 }
 $cache = new JVBase\managers\CacheManager('news', 3600);
diff --git a/templates/dashboard/sections/shop.php b/templates/dashboard/sections/shop.php
index 52205ea..2dccd94 100644
--- a/templates/dashboard/sections/shop.php
+++ b/templates/dashboard/sections/shop.php
@@ -4,7 +4,7 @@
 }
 
 if (!current_user_can('manage_shop')) {
-    wp_redirect(get_home_url(2, '/dash'));
+    wp_redirect(get_home_url(null, '/dash'));
     exit;
 }
 
diff --git a/webpack.jvb.js b/webpack.jvb.js
index 65c76ed..333258d 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -43,6 +43,10 @@
         'hours':      		'./assets/js/dash/CopyHours.js',
 		'favourites':		'./assets/js/concise/FrontendFavourites.js',
 		'votes':		'./assets/js/concise/FrontendVotes.js',
+		'handleSelection':		'./assets/js/concise/HandleSelection.js',
+		'dragHandler':		'./assets/js/concise/DragHandler.js',
+		'referral':		'./assets/js/concise/Referral.js',
+		'popup':		'./assets/js/concise/Popup.js',
     },
     output: {
         filename: '[name].min.js',

--
Gitblit v1.10.0