From 457c329237f97069063e641b10f384a52d584f21 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 12 May 2026 17:50:11 +0000
Subject: [PATCH] =minor tweaks

---
 assets/js/concise/UploadManager.js | 5065 ++++++++++++++++++++++++-----------------------------------
 1 files changed, 2,051 insertions(+), 3,014 deletions(-)

diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index cf6f6cd..15417be 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -1,102 +1,1470 @@
-/**
- * UploadManager - Refactored for clarity
- *
- * Architecture:
- * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
- * - Maps (uploadElements, fieldElements) = Runtime DOM references
- * - Upload data flows: File → Process → Queue → Server → Clean up stores
- */
 class UploadManager {
 	constructor() {
-		// Load dependencies
-		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
+		this.queue = window.jvbQueue;
 		this.error = window.jvbError;
-		this.fieldStoreReady = false;
-		this.uploadStoreReady = false;
-		this.hasCheckedForUploads = false;
-		const {fields, uploads} = window.jvbStore.register(
+		this.templates = window.jvbTemplates;
+
+		this.subscribers = new Set();
+
+		this.initStores();
+		this.initWorker();
+
+
+		//Maps for DOM references
+		this.fields = new Map();
+		this.uploads = new Map();
+		this.groups = new Map();
+
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.sortables = new Map();
+
+		this.changes = new Map();
+
+		this.previewUrls = new Set();
+		this.initElements();
+		this.initListeners();
+		this.defineTemplates();
+	}
+
+	defineTemplates() {
+		const T = this.templates;
+		const images = this;
+
+		T.define('uploadItem', {
+			refs: {
+				select: '[name="select-item"]',
+				featured: '[name="featured"]',
+				img: 'img',
+				video: 'video',
+				file: 'label > span',
+				details: 'details',
+				alt: '[name="image-alt-text"]',
+				title: '[name="image-title"]',
+				description: '[name="image-caption"]',
+			},
+			manyRefs: {
+				inputs: 'input, select, textarea',
+			},
+			setup({el, refs, manyRefs, data}) {
+				const isNewUpload = Object.hasOwn(data, 'file');
+				let mimeType;
+				let url;
+				let alt;
+				let previewUrl = false;
+				if (isNewUpload) {
+					el.dataset.uploadId = data.uploadId;
+					mimeType = images.getSubtypeFromMime(data.file.type)||'image';
+					url = (mimeType !== 'document') ? images.createPreviewUrl(data.file) : false;
+					previewUrl = url;
+					alt = data.file.name||'';
+				} else {
+					el.dataset.id = data.id;
+					mimeType = images.getSubtypeFromURL(data.medium??data.src);
+					url = data.medium??data.src;
+					alt = data['image-alt-text']??'';
+				}
+
+
+				el.dataset.subtype = mimeType;
+
+				if (refs.featured) {
+					refs.featured.value = data.uploadId;
+				}
+				switch (mimeType) {
+					case 'image':
+						if (refs.img) {
+							refs.img.src = url;
+							refs.img.alt = alt;
+
+							if (previewUrl) refs.img.dataset.previewUrl = previewUrl;
+						}
+						if (refs.video) refs.video.remove();
+						if (refs.file) refs.file.remove();
+						break;
+					case 'video':
+						if (refs.video) {
+							refs.video.src = url;
+							refs.video.alt = alt;
+							if (previewUrl) refs.video.dataset.previewUrl = previewUrl;
+						}
+						if (refs.img) refs.img.remove();
+						if (refs.file) refs.file.remove();
+						break;
+					case 'document':
+						if (refs.preview) {
+							let ext = data.file.name.split('.').pop()?.toLowerCase()??'';
+							let map = {
+								'pdf': 'file-pdf', 'csv': 'file-csv',
+								'doc': 'file-doc', 'docx': 'file-doc',
+								'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+							};
+							let icon = window.getIcon(map[ext]??'file');
+							refs.preview.innerText = data.file.name??data.title;
+							refs.preview.prepend(icon);
+						}
+						if (refs.img) refs.img.remove();
+						if (refs.video) refs.video.remove();
+						break;
+				}
+				if (refs.details) {
+					if (Object.hasOwn(data, 'field') && Object.hasOwn(data.field,'config') && Object.hasOwn(data.field.config, 'showMeta') && !data.field.config.showMeta) {
+						refs.details.remove();
+					} else {
+						if(Object.hasOwn(data, 'id')) {
+							refs.details.dataset.attachmentId = data.id;
+						} else if (Object.hasOwn(data, 'uploadId')) {
+							refs.details.dataset.uploadId = data.uploadId;
+						}
+						refs.details.setAttribute('data-ignore', '');
+
+
+						if (mimeType !== 'image' && refs.alt) {
+							refs.alt.closest('.field')?.remove();
+						} else if (Object.hasOwn(data, 'image-alt-text') && refs.alt) {
+							refs.alt.value = data['image-alt-text'];
+						}
+						if ((Object.hasOwn(data, 'title') || Object.hasOwn(data, 'file')) && refs.title) {
+							refs.title.value = data.title||data.file.name;
+						}
+						if (Object.hasOwn(data, 'image-caption') && refs.description) {
+							refs.description.value = data['image-caption'];
+						}
+					}
+				}
+
+
+				el.draggable = el.dataset.mode !== 'single';
+
+				if (manyRefs.inputs) {
+					for (let input of manyRefs.inputs) {
+						let wrapper = input.closest('[data-field]')??input.closest('.radio-button')??el;
+
+						window.prefixInput(input, `${data.id??data.uploadId}-`, wrapper);
+					}
+				}
+			}
+		});
+
+		T.define('imageGroup', {
+			refs: {
+				selectAll: '[data-select-all]',
+				fields: '.fields',
+				details: 'details',
+				grid: '.item-grid',
+			},
+			setup({el, refs, manyRefs, data}) {
+				el.dataset.groupId = data.groupId;
+				if (refs.selectAll) {
+					let wrapper = refs.selectAll.closest('.field');
+					window.prefixInput(refs.selectAll, `select-all-${data.groupId}`, wrapper,true);
+				}
+				let fields = T.create('groupMetadata', {groupId: data.groupId});
+				if (fields) {
+					refs.fields.append(fields);
+				} else {
+					refs.details.remove();
+				}
+				if (refs.grid) {
+					refs.grid.dataset.groupId = data.groupId;
+				}
+			}
+		});
+
+		T.define('groupMetadata', {
+			manyRefs: {
+				inputs: 'input,textarea,select'
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (manyRefs.inputs) {
+					manyRefs.inputs.forEach(input => {
+						let wrapper = input.closest('[data-field]');
+						input.dataset.groupId = data.groupId;
+						window.prefixInput(input, `${data.groupId}-`, wrapper);
+					});
+				}
+			}
+		});
+
+		T.define('restoreNotification', {
+			refs: {
+				details: '.details',
+				wrap: '.wrap',
+			},
+			setup({el, refs, manyRefs, data}) {
+				if (refs.details) {
+					let source = data.bySource.size > 1 ? ` across ${data.bySource.size} pages` : '';
+					let upload = data.pendingUploads.length > 1 ? 'uploads' : 'upload';
+					refs.details.textContent = `${data.pendingUploads.length} ${upload} can be recovered${source}`;
+				}
+				if (!refs.wrap) {
+					console.warn('No wrap element in template');
+					return;
+				}
+				let i = 1;
+				for (const [src, uploads] of data.bySource) {
+					let data = {
+						index: i,
+						isCurrent: src === window.location.href,
+						src: src,
+						uploads: uploads
+					};
+					refs.wrap.append(T.create('restoreField', data));
+					i++;
+				}
+			}
+		});
+
+		T.define('restoreField', {
+			refs: {
+				h3: 'h3',
+				a: 'h3 a',
+				grid: '.item-grid'
+			},
+			async setup({el, refs, manyRefs, data}) {
+				let fieldId = images.registerField(el, false, false, `recovery_${data.index}`);
+				if (data.isCurrent) {
+					el.open = true;
+
+					refs.a?.remove();
+					if (refs.h3) {
+						refs.h3.textContent = 'From this page:';
+					}
+
+				} else {
+					if (refs.a) {
+						refs.a.href = data.src;
+						refs.a.title = 'Navigate to page and restore';
+						refs.a.textContent = data.src;
+					}
+				}
+
+				let filtered = [... new Set(data.uploads.map(upload => upload.group??'preview'))];
+				for (let groupId of filtered) {
+					let group = (groupId === 'preview') ? true : images.stores.groups.get(groupId);
+					if (!group) continue;
+
+					let element = await images.createGroupElement(groupId, fieldId);
+					let groupGrid = element.querySelector('.item-grid');
+					let groupUploads = data.uploads.filter(upload => upload.group === (groupId === 'preview') ? null : groupId);
+
+					for (const [key,  value] of Object.entries(group.fields??{})) {
+						let field = element.querySelector(`input[name*="${key}"]`);
+						if (field) field.value = value;
+					}
+
+					for (let upload of groupUploads) {
+						let item = await images.createUpload(upload.id, images.formatFile(upload), fieldId);
+						groupGrid.append(item);
+					}
+					refs.grid.append(element);
+				}
+			}
+		});
+	}
+
+	initStores() {
+		const {uploads, groups} = window.jvbStore.register(
 			'uploads',
 			[
 				{
-					storeName: 'fields',
-					keyPath: 'id',
-					indexes: [
-						{ name: 'fieldId', keyPath: 'fieldId' },
-						{ name: 'timestamp', keyPath: 'timestamp' },
-						{ name: 'content', keyPath: 'content' },
-						{ name: 'itemId', keyPath: 'itemId' },
-						{ name: 'status', keyPath: 'status' }
-					],
-					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
-					delayFetch: true
-				},
-				{
 					storeName: 'uploads',
 					keyPath: 'id',
-					storeBlobs: true,
 					indexes: [
-						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'field', keyPath: 'field' },
 						{ name: 'status', keyPath: 'status' },
-						{ name: 'groupId', keyPath: 'groupId' },
-						{ name: 'attachmentId', keyPath: 'attachmentId' }
+						{ name: 'group', keyPath: 'group' },
+						{ name: 'src', keyPath: 'src' },
 					],
-					delayFetch: true
+				},
+				{
+					storeName: 'groups',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'field', keyPath: 'field' },
+						{ name: 'src', keyPath: 'src' }
+					]
 				}
 			]
 		);
-		this.fieldStore = fields;
-		this.uploadStore = uploads;
 
-		window.jvbUploadBlobs = this.uploadStore;
+		this.stores = {
+			uploads: uploads,
+			groups: groups,
+			ready: []
+		};
 
-		// Subscribe to store events
-		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
-		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+		this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads'));
+		this.stores.groups.subscribe(this.handleStores.bind(this, 'groups'));
+		this.queue.subscribe((event, operation) => {
+			if ((event === 'operation-status' || event === 'cancel-operation')
+				&& ['image_upload', 'video_upload', 'document_upload'].includes(operation.type)) {
+				let uploadIds = [];
 
-		// RUNTIME DATA - DOM references and ephemeral state
-		this.uploadElements = new Map();  // uploadId → { element, preview, location }
-		this.fieldElements = new Map();   // fieldId → { element, ui, config }
-		this.groupElements = new Map();   // groupId → { element, grid, fieldId }
+				if (operation.data) {
+					// Handle FormData
+					if (operation.data instanceof FormData) {
+						const dataObj = this.stores.uploads.formDataToObject(operation.data);
+						uploadIds = dataObj['upload_ids'] || [];
+					}
+					// Handle regular object
+					else {
+						uploadIds = operation.data['upload_ids'] || [];
+					}
+				}
 
-		// Selection and UI state
-		this.selected = new Map();
-		this.selectionHandlers = new Map();
-		this.previewUrls = new Set();
-		this.sortableInstances = new Map();
+				// If not in data, check result (for completed operations from backend)
+				if (uploadIds.length === 0 && operation.result && operation.result.upload_ids) {
+					uploadIds = operation.result.upload_ids;
+				}
 
-		// Worker for image processing
-		this.initWorker();
+				// Still no upload_ids? Log warning and bail
+				if (!uploadIds || uploadIds.length === 0) {
+					console.warn('[UploadManager] No upload_ids found for operation:', {
+						id: operation.id,
+						type: operation.type,
+						status: operation.status,
+						hasData: !!operation.data,
+						hasResult: !!operation.result
+					});
+					return;
+				}
 
-		// Notification subscribers
-		this.subscribers = new Set();
+				// Handle cancellation
+				if (event === 'cancel-operation') {
+					return this.handleOperationCancelled(uploadIds);
+				}
 
-		// Selectors
+				// Update upload status based on operation status
+				this.setBulkUpload(uploadIds, 'status', operation.status).then(() => {
+					// Log for debugging
+					console.log(`[UploadManager] Updated ${uploadIds.length} uploads to status: ${operation.status}`);
+				});
+
+				// Handle completion
+				if (operation.status === 'completed') {
+					// For group uploads, mark as processed but keep for reference
+					if (operation.type === 'process_upload_groups') {
+						uploadIds.forEach(uploadId => {
+							this.setBulkUpload([uploadId], 'serverProcessed', true).then(() => {});
+						});
+
+						// Log created posts if available
+						if (operation.result && operation.result.created_posts) {
+							console.log('[UploadManager] Created posts:', operation.result.created_posts);
+						}
+
+						// Remove uploads after a delay to allow UI to update
+						setTimeout(() => {
+							uploadIds.forEach(uploadId => {
+								this.removeUpload(uploadId).then(() => {});
+							});
+						}, 2000);
+					}
+					// For direct uploads, remove immediately
+					else {
+						uploadIds.forEach(uploadId => {
+							this.removeUpload(uploadId).then(() => {});
+						});
+					}
+				}
+
+				// Handle failures
+				if (operation.status === 'failed' || operation.status === 'failed_permanent') {
+					console.error('[UploadManager] Operation failed:', {
+						id: operation.id,
+						type: operation.type,
+						uploadIds: uploadIds,
+						error: operation.error_message
+					});
+				}
+			}
+
+		});
+	}
+
+	storesReady() {
+		return this.stores.ready.length === 2;
+	}
+
+	handleStores(storeName, event) {
+		if (event === 'data-ready') {
+			this.stores.ready.push(storeName);
+			if (this.storesReady()) {
+				this.checkRecovery().then(() => {});
+			}
+		}
+	}
+
+	initWorker() {
+		this.worker = null;
+		this.workerState = {
+			worker: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 3000,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+	}
+
+	initElements() {
 		this.selectors = {
-			field: {
+			fields: {
 				field: '[data-upload-field]',
 				input: 'input[type="file"]',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				progress: '.image-progress'
+				dropZone: '.file-upload-wrapper',
+				preview: '.preview-wrap',
+				grid: '.item-grid.preview',
+				progress: {
+					progress: '.file-upload-container .progress',
+					fill: '.file-upload-container .progress .fill',
+					details: '.file-upload-container .progress .details',
+					icon: '.file-upload-container .progress .icon'
+				},
+				selectAll: '[data-select-all]',
+				actions: '.selection-actions',
+				count: '.selected .info',
+				hidden: 'input[type="hidden"]'
 			},
+			// groups = selectors that affect groups as a whole
 			groups: {
-				container: '.upload-group',
-				grid: '.item-grid.group',
-				header: '.group-header',
+				container: '.group-display',
+				grid: '.item-grid.groups',
+				empty: '.empty-group',
+				header: '.sidebar .header',
+			},
+			// group = selectors that affect individual groups
+			group: {
+				item: '.upload-group',
+				actions: '.selection-actions',
 				selectAll: '[name="select-all-group"]',
-				actions: '.group-actions',
-				count: '.selection-controls .info'
+				count: '.group-header .info',
+				fields: 'details .fields',
+				grid: '.item-grid.group',
+				total: '.group-content .group-count'
 			},
 			items: {
-				item: '[data-upload-id]',
-				checkbox: '[name*="select-item"]',
+				item: '.item.upload',
+				checkbox: '[name="select-item"]',
 				featured: '[name="featured"]',
-				details: 'details'
+				image: 'img',
+				details: 'details',
+				progress: {
+					progress: '.progress',
+					fill: '.fill',
+					details: '.details',
+					icon: '.icon'
+				}
+			}
+		};
+	}
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.dragEnterHandler = this.handleDragEnter.bind(this);
+		this.dragLeaveHandler = this.handleDragLeave.bind(this);
+		this.dragOverHandler = this.handleDragOver.bind(this);
+		this.dropHandler = this.handleDrop.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
+	}
+
+	async setUpload(uploadId, data) {
+		const defaults = {
+			id: uploadId,
+			attachment: null,
+			group: null,
+			field: null,
+			src: window.location.href,
+			blob: null,
+			status: 'local_processing',
+			operationId: null,
+			fields: {}
+		};
+
+		const upload = { ...defaults, ...data };
+
+		Object.preventExtensions(upload);
+		await this.stores.uploads.save(upload);
+
+		if (this.fields.has(upload.field)) {
+			let field = this.fields.get(upload.field);
+			switch (upload.status) {
+				case 'local_processing':
+					this.notify('upload-received', {
+						field: field.element,
+						id: upload.id
+					});
+			}
+		}
+
+
+		return upload;
+	}
+
+	/*********************************************************************
+	 UTILITY
+	*********************************************************************/
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		this.previewUrls.add(url);
+		return url;
+	}
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls.delete(url);
+		}
+	}
+
+	formatFile(upload) {
+		if (!upload.blob) return null;
+		return new File([upload.blob], upload.fields.originalName || 'file', {
+			type: upload.fields.type || upload.blob.type,
+			lastModified: upload.fields.lastModified || Date.now()
+		});
+	}
+	/*********************************************************************
+	 LISTENERS
+	*********************************************************************/
+	handleClick(e) {
+		if (!window.targetCheck(e, this.selectors.fields.field)) return;
+
+		//Open the file input if it's a dropzone
+		let dropZone = window.targetCheck(e, this.selectors.fields.dropZone);
+		if (dropZone && !e.target.matches('input, button, a')){
+			dropZone.querySelector(this.selectors.fields.input)?.click();
+		}
+
+		//Handle action buttons
+		const button = window.targetCheck(e, '[data-action]');
+		if (button) this.handleAction(button);
+	}
+		handleAction(button) {
+			const action = button.dataset.action;
+			const fieldId = this.getFieldIdFromElement(button);
+
+			switch (action) {
+				case 'add-to-group':
+					this.handleAddToGroup(fieldId).then(()=>{});
+					break;
+				case 'delete-group':
+					this.handleDeleteGroup(button);
+					break;
+				case 'delete-upload':
+				case 'remove-from-group':
+					this.handleRemoveItem(button).then(()=>{});
+					break;
+				case 'upload':
+					this.queueUploads('uploads/groups',fieldId).then(()=>{});
+					break;
+				case 'restore':
+					this.handleRestoreSelected().then(()=>{});
+					break;
+				case 'restore-all':
+					this.handleRestoreAll().then(()=>{});
+					break;
+				case 'clear-cache':
+					this.handleClearCache().then(()=>{});
+					break;
+			}
+		}
+	handleChange(e) {
+
+		let fieldId = this.getFieldIdFromElement(e.target);
+		if (!fieldId) {
+			let isMeta = e.target.closest('[data-upload-id], [data-attachment-id]');
+			if (isMeta) {
+				this.queueUploadMeta(e);
+			}
+			return;
+		}
+
+		if (e.target.matches(this.selectors.fields.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0) this.processFiles(fieldId, files).then(()=>{});
+			return;
+		}
+
+		// Skip selection-related inputs
+		if (e.target.matches(this.selectors.items.checkbox) ||
+			e.target.matches(this.selectors.items.featured) ||
+			e.target.matches('[name*="select-"]')) {
+			return;
+		}
+
+		let field = this.fields.get(fieldId);
+		if (field.config.destination === 'post_group') {
+			this.handleGroupMetaChange(e.target);
+		} else {
+			this.queueUploadMeta(e);
+		}
+	}
+	handleGroupMetaChange(input) {
+		// Get the groupId directly from the input's data attribute
+		const groupId = input.dataset.groupId;
+		if (!groupId) return;
+
+		// Capture values immediately
+		const inputName = input.name;
+		if (!inputName) return;
+		const inputValue = input.value;
+
+		// Extract the field name from the input name
+		// Names are like "groupId[post_title]" or "groupId_post_title"
+		const name = inputName
+			.replace(`${groupId}[`, '')
+			.replace(`${groupId}_`, '')
+			.replace(']', '');
+
+		// Schedule the save with captured values
+		window.debouncer.schedule(`group-meta-${groupId}-${name}`, async () => {
+			const group = this.stores.groups.get(groupId);
+			if (!group) return;
+
+			// Initialize fields object if it doesn't exist
+			if (!group.fields) {
+				group.fields = {};
+			}
+
+			group.fields[name] = inputValue;
+			await this.setGroup(groupId, group);
+		}, 300);
+	}
+	handleDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
+	}
+	handleDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
+		}
+	}
+	handleDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
+	}
+	handleDrop(e) {
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (!dropZone) return;
+
+		e.preventDefault();
+		dropZone.classList.remove('dragover');
+		dropZone.classList.add('uploading');
+
+
+		const files = Array.from(e.dataTransfer.files);
+		if (files.length === 0) return;
+
+		const fieldId = this.getFieldIdFromElement(dropZone);
+		if (fieldId) {
+			this.processFiles(fieldId, files).then(()=>{
+				this.updateHandlerItems(fieldId);
+			});
+			this.a11y.announce(`${files.length} file(s) dropped for upload`);
+		}
+	}
+
+	async queueUploads(endpoint, fieldId, dependsOn = null) {
+		let data = new FormData();
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		if (uploads.length === 0) return;
+
+		const [ isUpload, isGroups] =
+			[ endpoint === 'uploads', endpoint === 'uploads/groups'];
+
+		data.append('fieldId', field.id);
+		data.append('content', field.config.content);
+
+		if (isUpload) {
+			data.append('mode', field.config.mode);
+
+			data.append('field_name', field.config.repeaterPath || field.config.name);
+			data.append('fieldId', field.id);
+			data.append('field_type', field.config.type);
+			data.append('subtype', field.config.subtype);
+			data.append('item_id', field.config.itemID);
+			data.append('destination', field.config.destination);
+			if (dependsOn) {
+				data.append('depends_on', dependsOn);
+			}
+		}
+
+		let posts, uploadMap, files;
+		if (isGroups) {
+			({posts, uploadMap, files} = this.collectGroups(fieldId));
+		} else if (isUpload) {
+			({uploadMap, files} = this.collectUploads(fieldId));
+		}
+
+		if (isGroups) {
+			data.append('posts', JSON.stringify(posts));
+		}
+		files.forEach(file => {
+			data.append('files[]', file);
+		});
+		data.append('upload_ids', JSON.stringify(uploadMap));
+
+		let title, popup;
+		if (isUpload) {
+			title = `Uploading ${uploads.length} file${uploads.length>1?'s':''} to server...`;
+			popup = `Uploading ${uploads.length} file${uploads.length>1?'s':''}...`;
+		} else if (isGroups) {
+			title = `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`;
+			popup = `Creating ${posts.length} post${posts.length>1?'s':''}...`;
+		}
+		await this.setBulkUpload(uploads, 'status', 'queued');
+		let operationId = this.sendToQueue(endpoint, data, title, popup);
+
+		if (endpoint === 'uploads/groups') {
+			let details = field.element.closest('details');
+			if (details) {
+				details.open = false;
+			}
+
+
+			this.notify('groups_uploaded', {
+				fieldId: fieldId,
+				posts: posts,
+				content: field.config.content,
+			});
+		}
+		if (operationId) {
+			field.operationId = operationId;
+			await this.setBulkUpload(uploads, 'operationId', operationId);
+			await this.setBulkUpload(uploads, 'status', 'uploading');
+			await this.setBulkGroup(fieldId, 'operationId', operationId);
+			this.fields.set(field.id, field);
+
+
+			this.notify('sent-to-queue', {
+				field: field,
+				operation: operationId,
+			});
+		} else {
+			await this.setBulkUpload(uploads, 'status', 'failed');
+		}
+		return operationId;
+	}
+
+	async sendToQueue(endpoint, data, title = '', popup = '', mergable = false) {
+		if (popup === '') {
+			popup = title;
+		}
+		const operation = {
+			endpoint: endpoint,
+			method: 'POST',
+			data: data,
+			title: title,
+			popup: popup,
+			canMerge: mergable,
+			sendNow: endpoint === 'uploads/groups',
+			headers: {
+				'X-Action-Nonce': window.auth.getNonce('dash')
+			},
+			append: '_upload'
+		}
+
+		try {
+			return await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sentToQueue'
+			});
+			return false;
+		}
+	}
+
+	collectGroups(fieldId) {
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		let groups = this.stores.groups.filterByIndex({field: fieldId});
+
+		let posts = [];
+		let uploadMap = [];
+		let files = [];
+
+		const validGroups = groups.filter(group => {
+			const groupUploads = this.getGroupUploadsInOrder(group);
+			return groupUploads.length > 0 && groupUploads.some(u => this.formatFile(u));
+		});
+
+		for (const group of validGroups) {
+			const groupElement = this.groups.get(group.id)?.element;
+			const fields = this.collectGroupFieldsFromDOM(groupElement, group.id);
+
+			const post = {
+				groupId: group.id,
+				images: [],
+				fields: fields
+			};
+
+			const groupUploads = this.getGroupUploadsInOrder(group);
+
+			for (const upload of groupUploads) {
+				const file = this.formatFile(upload);
+				if (file) {
+					files.push(file);
+					const imageData = {
+						upload_id: upload.id,
+						index: uploadMap.length
+					};
+
+					const uploadEl = this.uploads.get(upload.id);
+					const featuredInput = uploadEl?.element?.querySelector(`input[name="${group.id}_featured"]`);
+					if (featuredInput?.checked) {
+						post.fields.featured = upload.id;
+					}
+
+					post.images.push(imageData);
+					uploadMap.push(upload.id);
+				}
+			}
+
+			if (post.images.length > 0) {
+				posts.push(post);
+			}
+		}
+
+		// Handle remaining uploads not in any group
+		const remaining = uploads.filter(u => !u.group);
+		for (const upload of remaining) {
+			const post = {
+				groupId: window.generateID('group'),
+				images: [],
+				fields: {}
+			};
+
+			const file = this.formatFile(upload);
+			if (file) {
+				files.push(file);
+				const imageData = {
+					upload_id: upload.id,
+					index: uploadMap.length
+				};
+				post.images.push(imageData);
+				uploadMap.push(upload.id);
+			}
+
+			if (post.images.length > 0) {
+				posts.push(post);
+			}
+		}
+
+		return {posts, uploadMap, files};
+	}
+
+	getGroupUploadsInOrder(group) {
+		if (!group.uploads || group.uploads.length === 0) return [];
+
+		return group.uploads
+			.map(uploadId => this.stores.uploads.get(uploadId))
+			.filter(Boolean); // Remove any that don't exist
+	}
+
+	collectGroupFieldsFromDOM(groupElement, groupId) {
+		if (!groupElement) return {};
+
+		const fields = {};
+		const inputs = groupElement.querySelectorAll('input, textarea, select');
+
+		inputs.forEach(input => {
+			// Extract field name from input name like "groupId[post_title]"
+			const name = input.name
+				.replace(`${groupId}[`, '')
+				.replace(`${groupId}_`, '')
+				.replace(']', '');
+
+			// Skip system fields like featured, select-all
+			if (['featured', 'select-all'].some(skip => name.includes(skip))) return;
+
+			if (input.value) {
+				fields[name] = input.value;
+			}
+		});
+
+		return fields;
+	}
+
+	collectUploads(fieldId) {
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		if (uploads.length === 0) return;
+
+		let uploadMap = [];
+		let files = [];
+
+		for (const upload of uploads) {
+			const file = this.formatFile(upload);
+			if (file) {
+				files.push(file);
+				uploadMap.push(upload.id);
+			}
+		}
+		return { uploadMap, files };
+	}
+
+	queueUploadMeta(e) {
+		let attachmentId = e.target.closest('[data-attachment-id]')?.dataset.attachmentId;
+		let isUpload = false;
+		if (!attachmentId) {
+			attachmentId = e.target.closest('[data-upload-id]')?.dataset.uploadId;
+			isUpload = true;
+			if (!attachmentId) return;
+
+
+		}
+
+		if (!this.changes.has(attachmentId)) {
+			let object = {};
+			if (isUpload) {
+				object['uploadId'] = attachmentId;
+			} else {
+				object['attachmentId'] = attachmentId;
+			}
+			this.changes.set(attachmentId, object);
+		}
+
+		let field = e.target.closest('[data-field]');
+		let name = field.dataset.field;
+
+		this.changes.get(attachmentId)[name] = e.target.value;
+
+		this.scheduleSave();
+	}
+	scheduleSave() {
+		window.debouncer.schedule(
+			`upload-meta`,
+			async () => {
+				if (this.changes.size > 0) {
+					let items = {};
+					for (let [id, meta] of this.changes.entries()) {
+						console.log(id, meta);
+						items[id] = meta;
+					}
+					let data = {
+						user: window.auth.getUser(),
+						items: items
+					};
+					await this.sendToQueue('uploads/meta', data, 'Uploading Meta', 'Uploading Meta', true);
+					this.changes.clear();
+				}
+			},
+			2000
+		);
+	}
+
+	/*********************************************************************
+	 FIELD LOGIC
+	*********************************************************************/
+	scanFields(container, autoUpload = true, imageMeta = true) {
+		const fields = container.querySelectorAll(this.selectors.fields.field);
+		fields.forEach(uploader => this.registerField(uploader, autoUpload, imageMeta));
+	}
+
+	registerField(element, autoUpload = true, imageMeta = true, id = null) {
+		const data = {
+			element: element,
+			id: (id) ? id : this.determineFieldId(element),
+			config: this.extractFieldConfig(element, autoUpload, imageMeta),
+			uploads: new Set(),
+			operationId: null,
+			groups: [],
+			ui: window.uiFromSelectors(this.selectors.fields, element),
+			groupUI: window.uiFromSelectors(this.selectors.groups, element)
+		};
+
+
+		this.fields.set(data.id, data);
+
+		element.dataset.uploader = data.id;
+		this.getSelectionHandler(data.id);
+		if (data.config.type !== 'single') {
+			this.initSortable(data.id);
+		}
+		this.maybeLockUploads(data.id);
+
+		return data.id;
+	}
+
+	extractFieldConfig(el, autoUpload, imageMeta) {
+		const config = {
+			autoUpload: autoUpload,
+			showMeta: imageMeta,
+			destination: el.dataset.destination || 'meta',
+			content: this.extractFieldContent(el),
+			mode: el.dataset.mode || 'direct',
+			type: el.dataset.type || 'single',
+			name: el.dataset.field,
+			itemID: this.extractFieldItemId(el) ?? 0,
+			maxFiles: ('max-files' in el.dataset) ? parseInt(el.dataset.maxFiles) : 0,
+			subType: el.dataset.subtype ?? 'image',
+			repeaterPath: null
+		};
+
+		const repeaterRow = el.closest('[data-index]');
+		const repeater = repeaterRow?.closest('[data-field][data-repeater-id]');
+		if (repeater && repeaterRow) {
+			config.repeaterPath = `${repeater.dataset.field}:${repeaterRow.dataset.index}:${config.name}`;
+		}
+
+		return config;
+	}
+
+	extractFieldContent(fieldElement) {
+		return fieldElement.dataset.content ||
+			fieldElement.closest('dialog')?.dataset.content ||
+			fieldElement.closest('form')?.dataset.save || null;
+	}
+	extractFieldItemId(fieldElement) {
+		return fieldElement.dataset.itemId ||
+		fieldElement.closest('dialog')?.dataset.itemId || null;
+	}
+
+	determineFieldId(fieldElement) {
+		let content = this.extractFieldContent(fieldElement);
+		content = (content === null) ? '' : content+'_';
+		let itemID = this.extractFieldItemId(fieldElement);
+		itemID = (itemID === null) ? '' : itemID+'_';
+		const field = fieldElement.dataset.field || '';
+
+		// If inside a repeater row, include repeater name + index for uniqueness
+		const repeaterRow = fieldElement.closest('[data-index]');
+		const repeater = repeaterRow?.closest('[data-field][data-repeater-id]');
+		if (repeater && repeaterRow) {
+			return `${content}${itemID}${repeater.dataset.field}_${repeaterRow.dataset.index}_${field}`;
+		}
+
+		return `${content}${itemID}${field}`;
+	}
+
+	getFieldIdFromElement(el) {
+		const field = el.closest(this.selectors.fields.field);
+		return field?.dataset.uploader || null;
+	}
+
+	updateFieldProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		window.showProgress(field.ui.progress,current, total, message);
+	}
+	/*********************************************************************
+	 IMAGE PROCESSING FILE PROCESSING
+	*********************************************************************/
+	getWorker() {
+		if (!this.workerState.worker && typeof OffscreenCanvas !== 'undefined') {
+			this.workerState.worker = new Worker('worker.js');
+			this.workerState.worker.onmessage = (e) => this.handleWorkerMessage(e);
+			this.workerState.worker.onerror = (e) => this.handleWorkerError(e);
+		}
+		return this.workerState.worker;
+	}
+
+	handleWorkerMessage(e) {
+		const { id, blob } = e.data;
+		const task = this.workerState.tasks.get(id);
+		if (task) {
+			clearTimeout(task.timeoutId);
+			task.resolve(blob);
+			this.workerState.tasks.delete(id);
+		}
+	}
+
+	handleWorkerError(e) {
+		// Reject all pending tasks
+		this.workerState.tasks.forEach(task => {
+			clearTimeout(task.timeoutId);
+			task.reject(e);
+		});
+		this.workerState.tasks.clear();
+		this.restartWorker();
+	}
+
+	restartWorker() {
+		if (this.workerState.worker) {
+			this.workerState.worker.terminate();
+			this.workerState.worker = null;
+		}
+		this.workerState.restart.count++;
+	}
+	async processImages(files, maxWidth = 2200, maxHeight = 2200){
+		const results = [];
+		const queue = [...files];
+		const concurrency = this.workerState.settings.maxConcurrent;
+
+		const processNext = async () => {
+			while (queue.length > 0) {
+				const entry = queue.shift();
+				const blob = await this.processImage(entry.file, maxWidth, maxHeight);
+				results.push({ uploadId: entry.uploadId, blob: blob });
 			}
 		};
 
-		this.statusMapping = {
+		await Promise.all(
+			Array.from({length: Math.min(concurrency, files.length)}, () => processNext())
+		);
+
+		return results;
+	}
+	async processImage(file, maxWidth = 2200, maxHeight = 2200, timeout = 3000){
+		if (typeof OffscreenCanvas=== 'undefined') {
+			return this.resizeImage(file,maxWidth,maxHeight);
+		}
+		try {
+			return await this.withTimeout(
+				this.workerImage(file, maxWidth, maxHeight),
+				timeout
+			);
+		} catch (e) {
+			return this.resizeImage(file, maxWidth, maxHeight);
+		}
+	}
+	withTimeout(promise, ms) {
+		return Promise.race([
+			promise,
+			new Promise((_, reject) =>
+				setTimeout(() => reject(new Error('Timeout')), ms)
+			)
+		]);
+	}
+
+
+	async workerImage(file, maxWidth = 2200, maxHeight = 2200) {
+		const { settings, restart } = this.workerState;
+
+		if (restart.count >= restart.max) {
+			throw new Error('Worker max restarts exceeded');
+		}
+
+		const bitmap = await createImageBitmap(file);
+
+		let { width, height } = bitmap;
+		if (width > maxWidth || height > maxHeight) {
+			const ratio = Math.min(maxWidth / width, maxHeight / height);
+			width = Math.round(width * ratio);
+			height = Math.round(height * ratio);
+		}
+
+		const worker = this.getWorker();
+		const id = crypto.randomUUID();
+
+		return new Promise((resolve, reject) => {
+			const timeoutId = setTimeout(() => {
+				this.workerState.tasks.delete(id);
+				if (settings.restartAfterTimeout) {
+					this.restartWorker();
+				}
+				reject(new Error('Timeout'));
+			}, settings.timeout);
+
+			this.workerState.tasks.set(id, { resolve, reject, timeoutId });
+
+			worker.postMessage(
+				{ id, imageBitmap: bitmap, width, height, type: file.type, quality: 0.9 },
+				[bitmap]
+			);
+		});
+	}
+	resizeImage(file, maxWidth, maxHeight) {
+		return new Promise((resolve) => {
+			const img = new Image();
+			img.onload = () => {
+				URL.revokeObjectURL(img.src);
+				// Calculate new dimensions keeping aspect ratio
+				let { width, height } = img;
+
+				if (width > maxWidth || height > maxHeight) {
+					const ratio = Math.min(maxWidth / width, maxHeight / height);
+					width = Math.round(width * ratio);
+					height = Math.round(height * ratio);
+				}
+
+				// Draw to canvas at new size
+				const canvas = document.createElement('canvas');
+				canvas.width = width;
+				canvas.height = height;
+				canvas.getContext('2d').drawImage(img, 0, 0, width, height);
+
+				// Export as blob for upload
+				canvas.toBlob(resolve, file.type, 0.9);
+			};
+			img.src = URL.createObjectURL(file);
+		});
+	}
+
+	async processFiles(fieldId, files) {
+		let field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (field.groupUI.container) {
+			field.groupUI.container.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processed = 0;
+
+		this.updateFieldProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Create upload records for all files first
+		const uploadEntries = await Promise.all(
+			files.map(async (file) => {
+				const uploadId = window.generateID('upload');
+				const upload = await this.setUpload(uploadId, {
+					id: uploadId,
+					field: fieldId,
+					status: 'local_processing',
+					// blob: null,
+					fields: {
+						originalName: file.name,
+						originalSize: file.size,
+						type: file.type,
+						lastModified: file.lastModified
+					}
+				});
+
+				const element = await this.createUpload(uploadId, file, fieldId);
+				this.uploads.set(uploadId, {
+					element: element,
+					ui: window.uiFromSelectors(this.selectors.items, element)
+				});
+
+				await this.addToGroup(uploadId, null);
+
+				return { uploadId, upload, file };
+			})
+		);
+
+		// Batch process images with concurrency control
+		const imageEntries = uploadEntries.filter(e => e.file.type.startsWith('image/'));
+		const otherEntries = uploadEntries.filter(e => !e.file.type.startsWith('image/'));
+
+		// Process images in batches
+		const processedImages = await this.processImages(
+			imageEntries.map(e => ({ file: e.file, uploadId: e.uploadId }))
+		);
+
+		// Update image uploads with processed blobs
+		for (const { uploadId, blob } of processedImages) {
+			const entry = imageEntries.find(e => e.uploadId === uploadId);
+			if (entry) {
+				entry.upload.blob = blob;
+				entry.upload.fields.size = blob.size;
+				entry.upload.status = 'queued';
+				await this.setUpload(uploadId, entry.upload);
+				processed++;
+				this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+			}
+		}
+
+		// Handle non-image files (no processing needed)
+		for (const { uploadId, upload, file } of otherEntries) {
+			upload.blob = file;
+			upload.status = 'queued';
+			await this.setUpload(uploadId, upload);
+			processed++;
+			this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+		}
+
+		this.maybeLockUploads(fieldId);
+		if (field.config.autoUpload && field.config.destination !== 'post_group') {
+			await this.queueUploads('uploads', fieldId);
+		}
+	}
+	/*************************************************************
+	 RECOVERY
+	*************************************************************/
+	async checkRecovery() {
+		const allGroups = Array.from(this.stores.groups.data.values());
+		for (const group of allGroups) {
+			const hasUploads = this.stores.uploads.filterByIndex({ group: group.id }).length > 0;
+			if (!hasUploads) await this.stores.groups.delete(group.id);
+		}
+	}
+	//TODO: Old method of checkRecovery. All recovery logic has moved to the FormController.js
+	// async checkRecovery() {
+	// 	const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']});
+	// 	const allGroups = Array.from(this.stores.groups.data.values());
+	// 	for (const group of allGroups) {
+	// 		const hasUploads = this.stores.uploads.filterByIndex({group: group.id}).length > 0;
+	// 		if (!hasUploads) {
+	// 			await this.stores.groups.delete(group.id);
+	// 		}
+	// 	}
+	// 	if (pendingUploads.length === 0) return;
+	//
+	// 	// Group by source page
+	// 	const bySource = new Map();
+	// 	pendingUploads.forEach(upload => {
+	// 		const src = upload.src || 'unknown';
+	// 		if (!bySource.has(src)) bySource.set(src, []);
+	// 		bySource.get(src).push(upload);
+	// 	});
+	//
+	// 	let data = {
+	// 		bySource: bySource,
+	// 		pendingUploads: pendingUploads
+	// 	};
+	//
+	// 	document.body.append(this.templates.create('restoreNotification', data));
+	// 	let notification = document.querySelector('dialog.restore-uploads');
+	// 	this.restoreModal = new window.jvbModal(notification);
+	// 	this.restoreSelection = new window.jvbHandleSelection(notification,
+	// 		{
+	// 			wrapper: {
+	// 				wrapper: '.restore-field',
+	// 				id: 'selection'
+	// 			},
+	// 			items: '.item-grid.restore',
+	// 			selectAll: {
+	// 				bulkControls: '.selection-actions',
+	// 				checkbox: '#select-all-restore',
+	// 				count: '.selection-count'
+	// 			}
+	// 	});
+	// 	this.restoreModal.handleOpen();
+	// }
+
+	// async handleRestoreSelected() {
+	// 	if (!this.restoreSelection) return;
+	//
+	// 	let selected = Array.from(this.restoreSelection.selectedItems);
+	// 	if (selected.length === 0) {
+	// 		return;
+	// 	}
+	//
+	// 	await this.restoreSelectedUploads(selected);
+	// }
+	// async handleRestoreAll() {
+	// 	if (!this.restoreModal) return;
+	// 	const allUploads = Array.from(this.restoreModal.modal.querySelectorAll('.item.upload')).map(item => item.dataset.uploadId);
+	//
+	// 	await this.restoreSelectedUploads(allUploads);
+	// }
+	//
+	async restoreSelectedUploads(selectedUploads) {
+		let currentPage = window.location.href;
+
+		let uploads = Array.from(this.stores.uploads.data.values()).filter(
+			upload => selectedUploads.includes(upload.id) && upload.src === currentPage
+		);
+
+		let groups = [... new Set(uploads.map(upload => upload.group))].filter(Boolean);
+
+		let fieldId = uploads[0].field;
+		let field = document.querySelector(`[data-uploader="${fieldId}"]`);
+		if (!field) {
+			if ('crudManager' in window && fieldId.startsWith(window.crudManager.content)) {
+				let [content, itemId, fieldName] = fieldId.split('_');
+				if (parseInt(itemId) > 0) {
+					window.crudManager.openEditModal(itemId);
+					field = document.querySelector(`[data-uploader="${fieldId}"]`);
+				} else {
+					console.log('No field found for '+fieldId);
+					return;
+				}
+			} else {
+				console.log('No field found for '+fieldId);
+				return;
+			}
+
+		}
+		let fieldData = this.fields.get(fieldId);
+		if (fieldData.groupUI.container) {
+			fieldData.groupUI.container.hidden = false;
+		}
+
+		let usedIds = [];
+		for (let gr of groups) {
+			let group = this.stores.groups.get(gr);
+			await this.createGroup(fieldId, gr);
+			let element = this.groups.get(gr);
+
+			let theseUploads = uploads.filter(upload => upload.group === gr);
+			if (group && this.groups.has(gr)) {
+				let fields = group.fields;
+
+				for (const [key, value] of Object.entries(fields)) {
+					let fi = element.element.querySelector(`input[name*="${key}"]`);
+					if (fi) {
+						fi.value = value;
+					}
+				}
+			}else {
+				//Couldn't restore the group for some reason, just add it to the main preview grid instead
+				gr = null;
+			}
+
+			for (let upload of theseUploads) {
+				let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+				this.uploads.set(upload.id, {
+					element: item,
+					ui: window.uiFromSelectors(this.selectors.items, item)
+				});
+				await this.addToGroup(upload.id, gr);
+				usedIds.push(upload.id);
+			}
+
+		}
+
+		let remaining = uploads.filter(upload => !usedIds.includes(upload.id));
+		for (let upload of remaining) {
+			let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+			this.uploads.set(upload.id, {
+				element: item,
+				ui: window.uiFromSelectors(this.selectors.items, item)
+			});
+			await this.addToGroup(upload.id, null);
+		}
+	}
+	//
+	// cleanupRestore() {
+	// 	this.restoreModal.handleClose();
+	// 	this.restoreSelection.destroy();
+	// 	this.restoreSelection = null;
+	// 	this.restoreModal.destroy();
+	// 	this.restoreModal.modal.remove();
+	// 	this.restoreModal = null;
+	// }
+
+	async restoreUploads(uploadIds) {
+		const uploads = uploadIds.map(id => this.stores.uploads.get(id)).filter(Boolean);
+		if (uploads.length === 0) return;
+		await this.restoreSelectedUploads(uploads.map(u => u.id));
+	}
+
+	async clearUploads(uploadIds) {
+		await Promise.all(uploadIds.map(id => this.clearUpload(id)));
+	}
+	/*******************************************************************************
+	 STATUS MANAGEMENT
+	*******************************************************************************/
+	getStatusText(status) {
+		let map = {
 			'received': 'Image Received',
 			'local_processing': 'Processing Image...',
 			'queued': 'Waiting to upload...',
@@ -108,2520 +1476,10 @@
 			'failed_permanent': 'Upload failed permanently'
 		};
 
-		this.init();
-	}
-
-	async init() {
-		this.initializeFields();
-		this.initListeners();
-
-		// Queue integration - handle completion/failure
-		this.queue.subscribe((event, operation) => {
-			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
-				return;
-			}
-
-			const fieldId = operation.data instanceof FormData
-				? operation.data.get('fieldId')
-				: operation.data?.fieldId;
-
-			switch(event) {
-				case 'cancel-operation':
-					if (fieldId) this.handleOperationCancelled(fieldId);
-					break;
-				case 'operation-status':
-					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
-					break;
-				case 'operation-complete':
-					this.handleOperationComplete(operation, fieldId);
-					break;
-				case 'operation-failed':
-				case 'operation-failed-permanent':
-					this.handleOperationFailed(operation, fieldId);
-					break;
-			}
-		});
-
-		window.addEventListener('beforeunload', () => {
-			this.cleanupAllPreviewUrls();
-		});
-	}
-
-	initWorker() {
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: { count: 0, max: 3 },
-			settings: {
-				timeout: 10000,
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-	}
-
-	/*******************************************************************************
-	 * FIELD MANAGEMENT
-	 *******************************************************************************/
-	initializeFields() {
-		const fields = document.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => this.registerUploader(uploader));
-	}
-
-	scanFields(container) {
-		const fields = container.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => this.registerUploader(uploader));
-	}
-
-	registerUploader(uploader) {
-		const fieldId = this.determineFieldId(uploader);
-		const config = this.extractFieldConfig(uploader);
-		const ui = this.buildFieldUI(uploader);
-
-		// Store field data with Sets for runtime
-		const fieldData = {
-			id: fieldId,
-			config: config,
-			uploads: new Set(),
-			groups: [],
-			state: 'ready',
-			timestamp: Date.now()
-		};
-
-		// Save to store (will convert Sets to Arrays automatically)
-		this.fieldStore.save(fieldData);
-
-		// Store DOM references separately
-		this.fieldElements.set(fieldId, { element: uploader, ui, config });
-
-		uploader.dataset.uploader = fieldId;
-		this.addFieldSelectionHandler(fieldId);
-
-		if (config.type !== 'single') {
-			this.initSortable(fieldId);
-		}
-
-		return fieldId;
-	}
-
-	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,
-			itemID: fieldElement.dataset.itemId || 0,
-			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
-			subtype: fieldElement.dataset.subtype || 'image'
-		};
-	}
-
-	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;
-	}
-
-	/*******************************************************************************
-	 * SORTABLE INITIALIZATION
-	 *******************************************************************************/
-	initSortable(fieldId) {
-		if (!window.Sortable) return;
-
-		// Mount MultiDrag plugin once
-		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
-			Sortable.mount(new Sortable.MultiDrag());
-			Sortable._multiDragMounted = true;
-		}
-
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldEl) return;
-
-		// Initialize sortable on all existing grids
-		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
-		grids.forEach(grid => {
-			const groupId = grid.classList.contains('group')
-				? grid.closest('.upload-group')?.dataset.groupId
-				: null;
-			this.createSortableForGrid(grid, fieldId, groupId);
-		});
-
-		// Special handler for empty-group
-		const emptyGroup = fieldEl.element.querySelector('.empty-group');
-		if (emptyGroup && !emptyGroup.sortableInstance) {
-			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
-				animation: 150,
-				draggable: '.item',
-				multiDrag: true,
-				selectedClass: 'selected-for-drag',
-				avoidImplicitDeselect: true,
-				group: { name: fieldId, pull: false, put: true },
-				ghostClass: 'sortable-ghost',
-				chosenClass: 'sortable-chosen',
-				dragClass: 'sortable-drag',
-				onEnd: (evt) => this.handleDrop(evt, fieldId)
-			});
-		}
-	}
-
-	syncSortableSelection(fieldId, selectedItems) {
-		// Update Sortable's selection state to match checkboxes
-		this.sortableInstances.forEach((instance, key) => {
-			if (key.startsWith(fieldId)) {
-				const grid = instance.el;
-				const items = grid.querySelectorAll('.item');
-
-				items.forEach(item => {
-					const uploadId = item.dataset.uploadId;
-					const shouldBeSelected = selectedItems.has(uploadId);
-
-					if (shouldBeSelected) {
-						Sortable.utils.select(item);
-					} else {
-						Sortable.utils.deselect(item);
-					}
-				});
-			}
-		});
-	}
-
-	handleDrop(evt, fieldId) {
-		const dropTarget = evt.to;
-		const sourceTarget = evt.from;
-		const items = evt.items?.length > 0 ? evt.items : [evt.item];
-		const uploadIds = items.map(item => item.dataset.uploadId);
-
-		// Determine drop target type
-		const targetType = this.getDropTargetType(dropTarget);
-
-		switch (targetType) {
-			case 'empty-group':
-				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
-				break;
-
-			case 'preview':
-				this.handleDropToPreview(items, uploadIds, fieldId);
-				break;
-
-			case 'group':
-				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
-				break;
-			default:
-				// Fallback: return to preview
-				this.handleDropToPreview(items, uploadIds, fieldId);
-				break;
-		}
-
-		// Update UI
-		this.updateSortableState(dropTarget);
-		if (sourceTarget !== dropTarget) {
-			this.updateSortableState(sourceTarget);
-		}
-	}
-
-	/**
-	 * Determine what type of drop target this is
-	 */
-	getDropTargetType(target) {
-		if (target.classList.contains('empty-group')) {
-			return 'empty-group';
-		}
-
-		if (target.classList.contains('preview')) {
-			return 'preview';
-		}
-
-		if (target.classList.contains('group')) {
-			return 'group';
-		}
-
-		return 'unknown';
-	}
-
-	/**
-	 * Handle drop to group: add to existing group
-	 */
-	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
-		try {
-			// If same container, it's just a reorder
-			if (dropTarget === sourceTarget) {
-				this.handleReorder({ to: dropTarget, items: items });
-				return;
-			}
-
-			// Moving to different group
-			uploadIds.forEach(uploadId => {
-				this.addToGroup(uploadId, dropTarget, false);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Moved ${items.length} items to group`
-				: 'Moved item to group';
-			this.a11y.announce(message);
-
-			// Clear selection
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		}  catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * Handle drop to preview: remove from groups
-	 */
-	handleDropToPreview(items, uploadIds, fieldId) {
-		try {
-			uploadIds.forEach(uploadId => {
-				this.removeFromGroup(uploadId);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Moved ${items.length} items to preview`
-				: 'Moved item to preview';
-			this.a11y.announce(message);
-
-			// Clear selection
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		} catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * Handle drop errors consistently
-	 */
-	handleDropError(items, fieldId, error, message = 'An error occurred') {
-		console.error('Drop error:', error);
-
-		// Return items to preview as fallback
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (fieldEl?.ui?.preview) {
-			items.forEach(item => fieldEl.ui.preview.appendChild(item));
-		}
-
-		this.a11y.announce(`${message}. Items returned to preview.`);
-	}
-
-	/**
-	 * Handle drop to group: add to existing group
-	 */
-	handleDropToEmptyGroup(items, uploadIds, fieldId) {
-		try {
-			const group = this.createGroup(fieldId);
-			if (!group) {
-				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
-				return;
-			}
-
-			// Move items to new group
-			items.forEach((item, index) => {
-				group.grid.appendChild(item);
-				this.addToGroup(uploadIds[index], group.grid, false);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Created group with ${items.length} items`
-				: 'Created group with item';
-			this.a11y.announce(message);
-
-			// Clear selection after move
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		} catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * Update sortable enabled/disabled state based on item count
-	 */
-	updateSortableState(grid) {
-		const sortable = grid?.sortableInstance;
-		if (!sortable) return;
-
-		// const hasItems = grid.querySelectorAll('.item').length > 0;
-		sortable.option('disabled', false);
-	}
-
-	/**
-	 * Refresh sortable for a field (call after adding/removing items dynamically)
-	 */
-	refreshSortable(fieldId) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldEl) return;
-
-		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
-		grids.forEach(grid => this.updateSortableState(grid));
-	}
-
-	handleReorder(evt) {
-		const grid = evt.to;
-		const fieldWrapper = grid.closest('.field, .upload');
-		if (!fieldWrapper) return;
-
-		const movedItems = evt.items && evt.items.length > 0 ? evt.items : [evt.item];
-
-		// Get current order from DOM
-		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
-			.map(upload => upload.dataset.uploadId)
-			.filter(id => id);
-
-
-		// Update hidden input (for form submission)
-		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
-		if (hiddenInput && items.length > 0) {
-			hiddenInput.value = items.join(',');
-		}
-
-		// Update fieldState with new order
-		const fieldId = this.getFieldIdFromElement(grid);
-		if (fieldId) {
-			const fieldData = this.getFieldData(fieldId);
-
-			// If reordering within a group, update that group's uploads array
-			if (grid.classList.contains('group')) {
-				const groupId = grid.dataset.groupId;
-				const group = fieldData?.groups?.find(g => g.id === groupId);
-				if (group) {
-					group.uploads = items; // Update order
-				}
-			}
-			// If reordering in preview, the order is implicit by DOM position
-			// (we don't store preview order separately)
-
-			this.schedulePersistance(fieldId);
-		}
-
-		this.a11y.announce('Item reordered');
-
-		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
-			detail: {
-				from: evt.from,
-				to: evt.to,
-				oldIndex: evt.oldIndex,
-				newIndex: evt.newIndex,
-				items: items
-			},
-			bubbles: true
-		}));
-	}
-
-	/*******************************************************************************
-	 * FILE DROP HANDLERS
-	 *******************************************************************************/
-
-	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);
-	}
-
-	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;
-		const dropZone = e.target.closest(this.selectors.field.dropZone);
-		if (dropZone) {
-			e.preventDefault();
-			dropZone.classList.add('dragover');
-		}
-	}
-
-	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';
-		}
-	}
-
-	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`);
-		}
-	}
-
-	/*******************************************************************************
-	 * CLICK & CHANGE HANDLERS
-	 *******************************************************************************/
-
-	handleClick(e) {
-		// Trigger file input
-		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 files = Array.from(e.target.files);
-			if (files.length > 0 && fieldId) {
-				this.processFiles(fieldId, files);
-			}
-		}
-
-		// Meta field changes
-		if (fieldId) {
-			const fieldData = this.getFieldData(fieldId);
-			if (fieldData?.config.destination === 'post_group') {
-				this.handleGroupMetaChange(e.target);
-			} else {
-				this.queueUploadMeta(e);
-			}
-		}
-	}
-
-	/*******************************************************************************
-	 * FILE PROCESSING
-	 *******************************************************************************/
-
-	async processFiles(fieldId, files) {
-		const fieldData = this.getFieldData(fieldId);
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldData || !fieldEl) return;
-
-		// Show group display, hide upload zone
-		if (fieldEl.ui.dropZone) {
-			fieldEl.ui.dropZone.hidden = true;
-		}
-		if (fieldEl.ui.groups?.display) {
-			fieldEl.ui.groups.display.hidden = false;
-		}
-
-		const totalFiles = files.length;
-		let processedCount = 0;
-
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-
-		const processPromises = Array.from(files).map(async (file) => {
-			try {
-				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-				// Create initial upload data
-				const uploadData = {
-					id: uploadId,
-					attachmentId: null,
-					fieldId: fieldId,
-					status: 'local_processing',
-					groupId: null,
-					meta: {
-						originalName: file.name,
-						size: file.size,
-						type: file.type
-					}
-				};
-
-				// Save initial data
-				await this.uploadStore.save(uploadData);
-
-				// Process file
-				const preview = this.createPreviewUrl(file);
-				const processedFile = file.type.startsWith('image/')
-					? await this.processImage(file, fieldData.config.subtype)
-					: file;
-
-				// Show progress
-				this.showUploadProgress(uploadId, true);
-				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
-				// Store blob data (this updates the existing uploadData)
-				await this.saveBlobData(uploadId, processedFile || file);
-
-				// Create DOM element
-				const subtype = this.getSubtypeFromMime(file.type);
-				const element = this.createUploadElement({
-					id: uploadId,
-					preview: preview,
-					meta: uploadData.meta,
-					subtype: subtype
-				}, fieldData.config.destination === 'post_group');
-
-				// Add to preview grid
-				if (fieldEl.ui.preview) {
-					fieldEl.ui.preview.appendChild(element);
-
-					// Store runtime element data
-					this.uploadElements.set(uploadId, {
-						element: element,
-						preview: preview,
-						location: fieldEl.ui.preview
-					});
-				}
-
-				// Update status (gets existing data with blobData intact)
-				const storedUpload = this.uploadStore.get(uploadId);
-				if (storedUpload) {
-					storedUpload.status = 'processed';
-					await this.uploadStore.save(storedUpload);
-				}
-
-				// Add to field
-				fieldData.uploads.add(uploadId);
-				await this.saveFieldData(fieldData);
-
-				// Update progress
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				this.updateUploadItemProgress(uploadId, 100, 'processed');
-
-				// Fade out progress
-				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;
-			}
-		});
-
-		await Promise.all(processPromises);
-
-		this.updateFieldState(fieldId);
-		this.refreshSortable(fieldId);
-
-		// Queue for upload if in direct mode
-		if (fieldData.config.destination !== 'post_group') {
-			await this.queueUpload(fieldId);
-			this.maybeLockUploads(fieldId);
-		}
-	}
-
-	/*******************************************************************************
-	 * IMAGE PROCESSING
-	 *******************************************************************************/
-
-	async processImage(file, uploadId) {
-		const timeout = this.worker.settings.timeout;
-
-		return new Promise((resolve, reject) => {
-			let timeoutId;
-			let taskCompleted = false;
-
-			timeoutId = setTimeout(() => {
-				if (!taskCompleted) {
-					taskCompleted = true;
-					this.worker.tasks.delete(uploadId);
-					if (this.worker.settings.restartAfterTimeout) {
-						this.restartCompressionWorker();
-					}
-					reject(new Error(`Processing timeout for ${file.name}`));
-				}
-			}, timeout);
-
-			this.worker.tasks.set(uploadId, { file, timeoutId });
-
-			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) {
-		if (!file.type.startsWith('image/')) {
-			return file;
-		}
-
-		const maxDimension = this.getMaxDimension();
-		const quality = 0.85;
-
-		if (this.shouldUseWorker(file)) {
-			try {
-				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);
-			}
-		}
-
-		return await this.processOnMainThread(file, maxDimension, quality);
-	}
-
-	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;
-				}
-				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;
-
-					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 = this.createPreviewUrl(file);
-				img.src = objectUrl;
-			} catch (error) {
-				cleanup();
-				reject(new Error(`Failed to create object URL: ${error.message}`));
-			}
-		});
-	}
-
-	getOptimalFormat(file) {
-		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
-			return file.type;
-		}
-		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
-	}
-
-	getOptimalQuality(file, requestedQuality) {
-		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
-		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-		return Math.min(requestedQuality, 0.8);
-	}
-
-	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');
-	}
-
-	getMaxDimension() {
-		const screenWidth = window.screen.width;
-		const devicePixelRatio = window.devicePixelRatio || 1;
-		if (screenWidth * devicePixelRatio > 2560) return 2400;
-		if (screenWidth * devicePixelRatio > 1920) return 1920;
-		return 1200;
-	}
-
-	shouldUseWorker(file) {
-		return this.worker.worker &&
-			file.size > 1024 * 1024 &&
-			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;
-			}
-
-			const messageId = `${uploadId}_${Date.now()}`;
-
-			const messageHandler = (e) => {
-				if (e.data.messageId !== messageId) return;
-
-				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}`));
-			};
-
-			this.worker.worker.addEventListener('message', messageHandler);
-			this.worker.worker.addEventListener('error', errorHandler);
-
-			this.worker.worker.postMessage({
-				messageId,
-				file,
-				maxDimension,
-				quality,
-				outputFormat: this.getOptimalFormat(file)
-			});
-		});
-	}
-
-	restartCompressionWorker() {
-		if (this.worker.worker) {
-			this.worker.worker.terminate();
-			this.worker.worker = null;
-		}
-		this.worker.tasks.clear();
-		if (this.worker.restart.count >= this.worker.restart.max) {
-			console.error('Max worker restarts reached, disabling worker');
-			return;
-		}
-		this.worker.restart.count++;
-		this.initCompressionWorker();
-	}
-
-	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 {
-						const bitmap = await createImageBitmap(file);
-						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);
-						const canvas = new OffscreenCanvas(width, height);
-						const ctx = canvas.getContext('2d');
-						ctx.imageSmoothingEnabled = true;
-						ctx.imageSmoothingQuality = 'high';
-						ctx.drawImage(bitmap, 0, 0, width, height);
-						bitmap.close();
-						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(this.createPreviewUrl(blob));
-
-		} catch (error) {
-			console.warn('Failed to initialize compression worker:', error);
-			this.worker.worker = null;
-		}
-	}
-
-	calculateOptimalDimensions(img, maxDimension) {
-		let { width, height } = img;
-		if (width <= maxDimension && height <= maxDimension) {
-			return { width, height };
-		}
-		const scale = Math.min(maxDimension / width, maxDimension / height);
-		return {
-			width: Math.round(width * scale),
-			height: Math.round(height * scale)
-		};
-	}
-
-	supportsWebP() {
-		const canvas = document.createElement('canvas');
-		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
-	}
-
-	createPreviewUrl(file) {
-		const url = URL.createObjectURL(file);
-		if (!this.previewUrls) this.previewUrls = new Set();
-		this.previewUrls.add(url);
-		return url;
-	}
-
-	revokePreviewUrl(url) {
-		if (url?.startsWith('blob:')) {
-			URL.revokeObjectURL(url);
-			this.previewUrls?.delete(url);
-		}
-	}
-
-	/*******************************************************************************
-	 * QUEUE INTEGRATION
-	 *******************************************************************************/
-
-	async submitUploads(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
-			return;
-		}
-
-		let uploadIds = Array.from(fieldData.uploads);
-		if (uploadIds.length === 0) {
-			this.error.log('No uploads to upload', {
-				component: 'UploadManager',
-				action: 'submitGroupedUploads',
-				fieldId: fieldId
-			});
-			return;
-		}
-
-		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 = [];
-
-		// Process each group
-		for (const group of fieldGroups) {
-			const post = {
-				images: [],
-				fields: {}
-			};
-
-			// Add group metadata
-			for (let [name, value] of Object.entries(group.changes)) {
-				post.fields[name] = value;
-			}
-
-			// Get uploads for this group
-			const groupUploadIds = uploadIds.filter(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				return upload?.groupId === group.id;
-			});
-
-			// Add files for this group
-			for (const uploadId of groupUploadIds) {
-				const file = await this.getBlobData(uploadId);
-				if (file) {
-					formData.append('files[]', file);
-
-					const imageData = {
-						upload_id: uploadId,
-						index: uploadMap.length
-					};
-
-					// Check if featured
-					const uploadEl = this.uploadElements.get(uploadId);
-					const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
-					if (radioInput?.checked) {
-						post.fields.featured = uploadId;
-					}
-
-					post.images.push(imageData);
-					uploadMap.push(uploadId);
-				}
-			}
-
-			posts.push(post);
-		}
-
-		// Handle remaining uploads (without groupId) - each becomes its own post
-		const remainingUploadIds = uploadIds.filter(uploadId => {
-			const upload = this.uploadStore.get(uploadId);
-			return !upload?.groupId;
-		});
-
-		for (const uploadId of remainingUploadIds) {
-			const post = {
-				images: [],
-				fields: {}
-			};
-
-			const file = await this.getBlobData(uploadId);
-			if (file) {
-				formData.append('files[]', file);
-
-				const imageData = {
-					upload_id: uploadId,
-					index: uploadMap.length
-				};
-				post.images.push(imageData);
-				uploadMap.push(uploadId);
-			}
-
-			posts.push(post);
-		}
-
-		// Add metadata to FormData
-		formData.append('content', fieldData.config.content);
-		formData.append('user', fieldData.config.itemID);
-		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} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
-			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': window.auth.getNonce('dash')
-			},
-			append: '_upload',
-		};
-
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			// Update upload statuses
-			uploadIds.forEach(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				if (upload) {
-					upload.operationId = operationId;
-					upload.status = 'queued';
-					this.uploadStore.save(upload);
-					this.updateUploadStatus(uploadId, 'queued');
-				}
-			});
-
-			fieldData.operationId = operationId;
-			await this.saveFieldData(fieldData);
-
-			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;
-		}
-	}
-
-	async queueUpload(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
-
-		const uploads = Array.from(fieldData.uploads);
-		const data = this.prepareUploadData(fieldData, uploads);
-
-		this.a11y.announce('Queuing for upload');
-
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
-			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
-			canMerge: false,
-			headers: { 'action_nonce': window.auth.getNonce('dash') },
-			append: '_upload'
-		};
-
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			// Update upload statuses
-			uploads.forEach(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				if (upload) {
-					upload.operationId = operationId;
-					upload.status = 'queued';
-					this.uploadStore.save(upload);
-					this.updateUploadStatus(uploadId, 'queued');
-				}
-			});
-
-			fieldData.operationId = operationId;
-			await this.saveFieldData(fieldData);
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		}
-	}
-
-	async prepareUploadData(fieldData, uploads) {
-		const formData = new FormData();
-		formData.append('content', fieldData.config.content);
-		formData.append('mode', fieldData.config.mode);
-		formData.append('field_name', fieldData.config.name);
-		formData.append('fieldId', fieldData.id);
-		formData.append('field_type', fieldData.config.type);
-		formData.append('subtype', fieldData.config.subtype);
-		formData.append('item_id', fieldData.config.itemID);
-		formData.append('destination', fieldData.config.destination || 'meta');
-
-		let uploadMap = [];
-
-
-		const blobPromises = uploads.map(async (uploadId) => {
-			const upload = this.uploadStore.get(uploadId);
-			if (!upload) return;
-
-			const file = await this.getBlobData(uploadId);
-			if (file) {
-				formData.append('files[]', file);
-				uploadMap.push(upload.id);
-			}
-		});
-
-		await Promise.all(blobPromises);
-
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-		return formData;
-	}
-
-	async queueUploadMeta(e) {
-		const uploadId = this.getUploadIdFromElement(e.target);
-		const upload = this.uploadStore.get(uploadId);
-		if (!upload) return;
-
-		const fieldData = this.getFieldData(upload.fieldId);
-		if (!fieldData) return;
-
-		let data = {};
-		data[e.target.name] = e.target.value;
-
-		upload.meta = { ...upload.meta, ...data };
-		await this.uploadStore.save(upload);
-
-		let queueData = {};
-		queueData[upload.attachmentId ?? upload.id] = upload.meta;
-
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: queueData,
-			title: 'Updating meta',
-			canMerge: true,
-			headers: { 'action_nonce': window.auth.getNonce('dash') }
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
-		}
-	}
-
-	/*******************************************************************************
-	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
-	 *******************************************************************************/
-
-	/**
-	 * Handle successful operation completion - CLEAR STORES
-	 */
-	async handleOperationComplete(operation, fieldId) {
-		const results = operation.result?.data || operation.serverData?.data || [];
-
-		// Update upload statuses with attachment IDs
-		results.forEach(result => {
-			const upload = this.uploadStore.get(result.upload_id);
-			if (upload) {
-				upload.attachmentId = result.attachment_id;
-				upload.status = 'completed';
-				this.uploadStore.save(upload);
-				this.updateUploadStatus(result.upload_id, 'completed');
-			}
-		});
-
-		if (!fieldId) return;
-
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		// Clean up completed uploads from stores
-		const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
-			const upload = this.uploadStore.get(uploadId);
-			return upload?.status === 'completed';
-		});
-
-		for (const uploadId of completedUploads) {
-			await this.clearUpload(uploadId, false);
-			fieldData.uploads.delete(uploadId);
-		}
-
-		// If all uploads complete, clear entire field from stores
-		if (fieldData.uploads.size === 0) {
-			await this.clearFieldFromStores(fieldId);
-			this.a11y.announce('All uploads completed successfully');
-		} else {
-			// Otherwise just update field state
-			await this.saveFieldData(fieldData);
-		}
-
-		this.updateFieldState(fieldId);
-	}
-
-	/**
-	 * Handle operation failure
-	 */
-	handleOperationFailed(operation, fieldId) {
-		const uploadIds = operation.data instanceof FormData
-			? JSON.parse(operation.data.get('upload_ids') || '[]')
-			: operation.data.upload_ids || [];
-
-		uploadIds.forEach(uploadId => {
-			const upload = this.uploadStore.get(uploadId);
-			if (upload) {
-				upload.status = operation.status === 'operation-failed-permanent'
-					? 'failed_permanent'
-					: 'failed';
-				this.uploadStore.save(upload);
-				this.updateUploadStatus(uploadId, upload.status);
-			}
-		});
-
-		if (fieldId) {
-			this.updateFieldState(fieldId);
-		}
-	}
-
-	/**
-	 * Handle operation cancellation
-	 */
-	async handleOperationCancelled(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		const uploadsArray = fieldData.uploads instanceof Set
-			? Array.from(fieldData.uploads)
-			: fieldData.uploads;
-
-		for (const uploadId of uploadsArray) {
-			await this.clearUpload(uploadId, false);
-		}
-
-		await this.clearFieldFromStores(fieldId);
-		this.updateFieldState(fieldId);
-		this.a11y.announce('Upload cancelled');
-	}
-
-	getFieldGroups(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData?.groups) return [];
-
-		return fieldData.groups.map(group => ({
-			id: group.id,
-			uploads: group.uploads || [],
-			changes: group.changes || {}
-		}));
-	}
-
-	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) {
-		const byField = new Map();
-		selectedUploads.forEach(item => {
-			if (!byField.has(item.fieldId)) {
-				byField.set(item.fieldId, []);
-			}
-			byField.get(item.fieldId).push(item.uploadId);
-		});
-
-		for (const [fieldId, uploadIds] of byField.entries()) {
-			const fieldState = this.fieldStore.get(fieldId);
-			if (fieldState) {
-				fieldState.uploads = uploadIds;
-				await this.restoreField(fieldState);
-			}
-		}
-	}
-
-	async restoreField(fieldState) {
-		const { config, context, uploads, groups, id } = 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.fieldElements.has(fieldKey)) {
-			fieldKey = this.registerUploader(fieldElement);
-		}
-
-		const fieldEl = this.fieldElements.get(fieldKey);
-		const fieldData = this.getFieldData(fieldKey);
-
-		if (!fieldEl || !fieldData) {
-			console.error('Failed to register field for restoration');
-			return;
-		}
-
-		// Merge saved state back into field
-		fieldData.state = fieldState.state || 'ready';
-
-		// Rebuild UI references if needed
-		if (!fieldEl.ui) {
-			fieldEl.ui = this.buildFieldUI(fieldElement);
-		}
-
-		if (fieldEl.ui.groups?.display) {
-			fieldEl.ui.groups.display.hidden = false;
-		}
-		if (fieldEl.ui.dropZone) {
-			fieldEl.ui.dropZone.hidden = true;
-		}
-
-		// Restore groups first
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(fieldKey, groups);
-		}
-
-		// Handle both Array and Set for uploads
-		const uploadsArray = uploads instanceof Set
-			? Array.from(uploads)
-			: Array.isArray(uploads)
-				? uploads
-				: [];
-
-		// Restore uploads
-		for (const uploadId of uploadsArray) {
-			// Get upload data from store
-			const uploadData = this.uploadStore.get(uploadId);
-			if (uploadData) {
-				await this.restoreUpload(fieldKey, uploadData);
-			}
-		}
-
-		// Update field state
-		await this.saveFieldData(fieldData);
-		this.updateFieldState(fieldKey);
-		this.maybeLockUploads(fieldKey);
-		this.refreshSortable(fieldKey);
-
-		// Queue for upload if needed
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
-			await this.queueUpload(fieldKey);
-		}
-	}
-
-	async restoreUpload(fieldId, uploadData) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		const fieldData = this.getFieldData(fieldId);
-
-		if (!fieldEl || !fieldData) {
-			console.error('Field not found for upload restoration:', fieldId);
-			return;
-		}
-
-		// Get reconstructed File from blob data
-		const file = await this.getBlobData(uploadData.id);
-
-		if (!file) {
-			console.warn('Blob data not found for upload:', uploadData.id);
-			return;
-		}
-
-		// Create preview URL
-		const previewUrl = this.createPreviewUrl(file);
-
-		// Recreate DOM element
-		const subtype = this.getSubtypeFromMime(file.type);
-		const element = this.createUploadElement({
-			id: uploadData.id,
-			preview: previewUrl,
-			meta: uploadData.meta || {
-				originalName: file.name,
-				size: file.size,
-				type: file.type
-			},
-			subtype: subtype
-		}, fieldData.config.destination === 'post_group');
-
-		// Determine correct location
-		let location;
-		if (uploadData.groupId) {
-			// Check if group exists
-			const groupEl = this.groupElements.get(uploadData.groupId);
-			if (groupEl?.grid) {
-				location = groupEl.grid;
-
-				// Add to group's upload list
-				const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
-				if (group) {
-					if (!group.uploads) group.uploads = [];
-					if (!group.uploads.includes(uploadData.id)) {
-						group.uploads.push(uploadData.id);
-					}
-				}
-			} else {
-				// Group doesn't exist, add to preview
-				location = fieldEl.ui.preview;
-				uploadData.groupId = null;
-			}
-		} else {
-			// No group, add to preview
-			location = fieldEl.ui.preview;
-		}
-
-		// Add element to DOM
-		if (location) {
-			location.appendChild(element);
-		} else if (fieldEl.ui.preview) {
-			fieldEl.ui.preview.appendChild(element);
-			location = fieldEl.ui.preview;
-		}
-
-		// Store runtime element data
-		this.uploadElements.set(uploadData.id, {
-			element: element,
-			preview: previewUrl,
-			location: location
-		});
-
-		// Add to field uploads
-		if (!fieldData.uploads) fieldData.uploads = new Set();
-		fieldData.uploads.add(uploadData.id);
-
-		// Update upload data in store
-		uploadData.status = 'processed';
-		await this.uploadStore.save(uploadData);
-
-		// Update sortable state for the grid
-		if (location) {
-			this.updateSortableState(location);
-		}
-	}
-
-	async restoreGroups(fieldId, groups) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		const fieldData = this.getFieldData(fieldId);
-
-		if (!fieldEl || !fieldData) {
-			console.error('Field not found for group restoration:', fieldId);
-			return;
-		}
-
-		for (const groupData of groups) {
-			const group = this.createGroup(fieldId, groupData.id);
-			if (!group) {
-				console.warn('Failed to create group:', groupData.id);
-				continue;
-			}
-
-			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
-			if (storedGroup) {
-				// Restore metadata
-				if (groupData.changes) {
-					storedGroup.changes = { ...groupData.changes };
-				}
-
-				// Preserve upload order
-				if (groupData.uploads) {
-					storedGroup.uploads = [...groupData.uploads];
-				}
-
-				// Restore form field values
-				if (groupData.changes) {
-					const titleInput = group.element.querySelector('[name*="post_title"]');
-					const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
-
-					if (titleInput && groupData.changes.post_title) {
-						titleInput.value = groupData.changes.post_title;
-					}
-					if (excerptInput && groupData.changes.post_excerpt) {
-						excerptInput.value = groupData.changes.post_excerpt;
-					}
-				}
-			}
-		}
-
-		await this.saveFieldData(fieldData);
-	}
-
-	async openModalForRestore(context) {
-		if (!context) return;
-
-		const { modalType, itemId } = 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
-				if (itemId) {
-					trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
-				}
-				break;
-			case 'bulkEdit':
-				trigger = document.querySelector('[data-action="bulk-edit"]');
-				break;
-		}
-
-		if (trigger) {
-			trigger.click();
-
-			// Wait for modal to open and render
-			await new Promise(resolve => setTimeout(resolve, 300));
-		} else {
-			console.warn('Modal trigger not found for restoration:', context);
-		}
-	}
-
-	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];
-	}
-
-	/*******************************************************************************
-	 * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
-	 *******************************************************************************/
-
-	/**
-	 * Clear individual upload from stores (called after successful upload)
-	 */
-	async clearUpload(uploadId, persist = true) {
-		const uploadEl = this.uploadElements.get(uploadId);
-		if (uploadEl) {
-			this.revokePreviewUrl(uploadEl.preview);
-			if (uploadEl.element) {
-				const previewUrl = uploadEl.element.dataset.previewUrl;
-				this.revokePreviewUrl(previewUrl);
-				delete uploadEl.element.dataset.previewUrl;
-			}
-		}
-
-		// Remove from runtime memory
-		this.uploadElements.delete(uploadId);
-
-		// Remove from store (no separate blob store - it's part of the upload object)
-		await this.uploadStore.delete(uploadId);
-
-		// Update field if needed
-		if (persist) {
-			const upload = this.uploadStore.get(uploadId);
-			if (upload?.fieldId) {
-				await this.schedulePersistance(upload.fieldId);
-			}
-		}
-	}
-
-	/**
-	 * Clear entire field from stores (called when all uploads complete)
-	 */
-	async clearFieldFromStores(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-
-		// Clear all related uploads
-		if (fieldData?.uploads) {
-			const uploadsArray = fieldData.uploads instanceof Set
-				? Array.from(fieldData.uploads)
-				: fieldData.uploads;
-
-			for (const uploadId of uploadsArray) {
-				await this.uploadStore.delete(uploadId);
-			}
-		}
-
-		// Clear field from store
-		await this.fieldStore.delete(fieldId);
-
-		// Keep runtime references (fieldElements, etc) intact for reuse
-	}
-
-	cleanupAllPreviewUrls() {
-		if (this.previewUrls) {
-			this.previewUrls.forEach(url => {
-				try {
-					URL.revokeObjectURL(url);
-				} catch (e) {
-					// Ignore errors during cleanup
-				}
-			});
-			this.previewUrls.clear();
-		}
-	}
-
-	/*******************************************************************************
-	 * UI UPDATE METHODS
-	 *******************************************************************************/
-
-	updateFieldState(fieldId) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldEl || !fieldData) return;
-
-		const container = fieldEl.element;
-		const uploadCount = fieldData.uploads?.size || 0;
-		const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
-		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
-		container.dataset.uploadCount = uploadCount.toString();
-		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
-		if (fieldEl.ui.preview) {
-			fieldEl.ui.preview.setAttribute('aria-label',
-				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
-			);
-		}
-	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldEl?.ui?.progress?.progress) return;
-
-		const progress = fieldEl.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 fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		fieldData.state = status;
-		this.saveFieldData(fieldData);
-	}
-
-	updateUploadStatus(uploadId, status) {
-		const upload = this.uploadStore.get(uploadId);
-		if (!upload) return;
-
-		upload.status = status;
-		this.uploadStore.save(upload);
-		this.updateUploadUI(uploadId);
-	}
-
-	updateUploadUI(uploadId) {
-		const uploadEl = this.uploadElements.get(uploadId);
-		const upload = this.uploadStore.get(uploadId);
-		if (!upload || !uploadEl?.element) return;
-
-		uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
-		uploadEl.element.classList.add(`status-${upload.status}`);
-
-		const progress = uploadEl.element.querySelector('.progress');
-		if (progress) {
-			this.updateUploadItemProgress(uploadId,
-				this.getStatusProgress(upload.status),
-				upload.status
-			);
-		}
-	}
-
-	showUploadProgress(uploadId, show = true) {
-		const uploadEl = this.uploadElements.get(uploadId);
-		if (!uploadEl?.element) return;
-
-		const progressEl = uploadEl.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);
-			}
-		}
-	}
-
-	updateUploadItemProgress(uploadId, percent, status = null) {
-		const uploadEl = this.uploadElements.get(uploadId);
-		if (!uploadEl?.element) return;
-
-		const progressEl = uploadEl.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;
-	}
-
-	maybeLockUploads(fieldId) {
-		const fieldEl = this.fieldElements.get(fieldId);
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldEl?.ui?.dropZone || !fieldData) return;
-
-		const uploadCount = fieldData.uploads?.size || 0;
-
-		// For groupable uploads, set max to 20
-		const maxFiles = fieldData.config.destination === 'post_group'
-			? 20
-			: (fieldData.config?.maxFiles || 999);
-
-		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
-		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
-
-		// Show helpful message for groupable uploads
-		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
-			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
-		}
-	}
-
-	/*******************************************************************************
-	 * GROUP MANAGEMENT
-	 *******************************************************************************/
-	/**
-	 * Create sortable instance for a grid
-	 */
-	createSortableForGrid(grid, fieldId, groupId = null) {
-		if (!grid || grid.sortableInstance) return;
-
-		const sortableInstance = new Sortable(grid, {
-			animation: 150,
-			draggable: '.item',
-			multiDrag: true,
-			selectedClass: 'selected-for-drag',
-			avoidImplicitDeselect: true,
-			group: { name: fieldId, pull: true, put: true },
-			ghostClass: 'sortable-ghost',
-			chosenClass: 'sortable-chosen',
-			dragClass: 'sortable-drag',
-
-			// Centralized drop handler
-			onEnd: (evt) => this.handleDrop(evt, fieldId),
-
-			// Selection sync
-			onSelect: (evt) => {
-				const checkbox = evt.item.querySelector('[name*="select-item"]');
-				if (checkbox && !checkbox.checked) {
-					checkbox.checked = true;
-					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
-				}
-			},
-
-			onDeselect: (evt) => {
-				const checkbox = evt.item.querySelector('[name*="select-item"]');
-				if (checkbox && checkbox.checked) {
-					checkbox.checked = false;
-					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
-				}
-			},
-
-			onAdd: (evt) => this.updateSortableState(evt.to),
-			onRemove: (evt) => this.updateSortableState(evt.from)
-		});
-
-		grid.sortableInstance = sortableInstance;
-
-		const gridId = groupId
-			? `${fieldId}-group-${groupId}`
-			: `${fieldId}-preview`;
-
-		this.sortableInstances.set(gridId, sortableInstance);
-
-		return sortableInstance;
-	}
-	createGroup(fieldId, groupId = null) {
-		const fieldData = this.getFieldData(fieldId);
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldData || !fieldEl) return null;
-
-		if (!groupId) {
-			groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-		}
-
-		const groupElement = this.createGroupElement(groupId, fieldId);
-		if (!groupElement) return null;
-
-		// Store in field UI Map
-		if (!fieldEl.ui.groups) {
-			fieldEl.ui.groups = {
-				groups: new Map(),
-				container: null,
-				empty: null,
-				display: null
-			};
-		}
-
-		fieldEl.ui.groups.groups.set(groupId, groupElement);
-
-		// Insert into DOM
-		if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
-			fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
-		} else if (fieldEl.ui.groups.container) {
-			fieldEl.ui.groups.container.appendChild(groupElement);
-		}
-
-		// Store group element reference
-		const grid = groupElement.querySelector('.item-grid.group');
-		this.groupElements.set(groupId, {
-			element: groupElement,
-			grid: grid,
-			fieldId: fieldId
-		});
-
-		// Add to field groups
-		if (!fieldData.groups) fieldData.groups = [];
-		const existingGroup = fieldData.groups.find(g => g.id === groupId);
-		if (!existingGroup) {
-			fieldData.groups.push({
-				id: groupId,
-				uploads: [],
-				changes: {}
-			});
-			this.saveFieldData(fieldData);
-		}
-
-		// Initialize selection handler
-		this.addGroupSelectionHandler(fieldId, groupId);
-
-		if (grid) {
-			this.createSortableForGrid(grid, fieldId, groupId);
-		}
-
-		return { id: groupId, element: groupElement, grid: grid };
-	}
-	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);
-
-			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]`;
-			}
-
-			const fieldData = this.getFieldData(fieldId);
-			if (fieldData && fieldData.config.content !== '') {
-				let summary = groupElement.querySelector('summary');
-				if (summary) summary.textContent = fieldData.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) {
-		const groupEl = this.groupElements.get(groupId);
-		if (!groupEl) return;
-
-		const fieldData = this.getFieldData(groupEl.fieldId);
-		if (!fieldData) return;
-
-		const group = fieldData.groups?.find(g => g.id === groupId);
-		let keepUploads = true;
-
-		if (confirm && group?.uploads?.length > 0) {
-			keepUploads = !window.confirm('Delete uploads in group?');
-		}
-
-		if (confirm && keepUploads && group?.uploads) {
-			// Move uploads back to preview
-			group.uploads.forEach(uploadId => {
-				this.removeFromGroup(uploadId);
-			});
-		}
-
-		// Remove from field groups
-		if (fieldData.groups) {
-			fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
-			this.saveFieldData(fieldData);
-		}
-
-		// Remove DOM element
-		if (groupEl.element) {
-			groupEl.element.remove();
-			this.a11y.announce('Group removed');
-		}
-
-		// Remove from maps
-		this.groupElements.delete(groupId);
-
-		// Clean up sortable
-		const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
-		const sortable = this.sortableInstances.get(sortableKey);
-		if (sortable?.destroy) {
-			sortable.destroy();
-		}
-		this.sortableInstances.delete(sortableKey);
-
-		this.schedulePersistance(groupEl.fieldId);
-	}
-
-	addToGroup(uploadId, target = null, persist = true) {
-		const upload = this.uploadStore.get(uploadId);
-		const uploadEl = this.uploadElements.get(uploadId);
-		if (!upload || !uploadEl) return;
-
-		const fieldData = this.getFieldData(upload.fieldId);
-		const fieldEl = this.fieldElements.get(upload.fieldId);
-		if (!fieldData || !fieldEl) return;
-
-		// Already in correct location
-		if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
-			return;
-		}
-
-		// Remove from previous group
-		if (upload.groupId) {
-			const group = fieldData.groups?.find(g => g.id === upload.groupId);
-			if (group) {
-				group.uploads = group.uploads.filter(id => id !== uploadId);
-				if (group.uploads.length === 0) {
-					this.deleteGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clear selection checkbox
-		const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
-		if (checkbox) checkbox.checked = false;
-
-		let featured = uploadEl.element.querySelector('[name="featured"]');
-		if (featured) featured.hidden = !target;
-
-		// Moving to preview or to group
-		if (!target || target.classList.contains('preview')) {
-			target = fieldEl.ui.preview;
-			upload.groupId = null;
-		} else {
-			// Moving to group
-			const groupId = target.dataset.groupId;
-			if (featured) featured.name = groupId + '_' + featured.name;
-
-			const group = fieldData.groups?.find(g => g.id === groupId);
-			if (group) {
-				if (!group.uploads) group.uploads = [];
-				group.uploads.push(uploadId);
-				upload.groupId = groupId;
-			}
-		}
-
-		// Update location
-		uploadEl.location = target;
-		target.append(uploadEl.element);
-
-		// Update stores
-		this.uploadStore.save(upload);
-		if (persist) {
-			this.saveFieldData(fieldData);
-		}
-
-		// Update sortable state
-		this.updateSortableState(target);
-		if (uploadEl.location && uploadEl.location !== target) {
-			this.updateSortableState(uploadEl.location);
-		}
-	}
-
-	removeFromGroup(uploadId) {
-		const upload = this.uploadStore.get(uploadId);
-		const uploadEl = this.uploadElements.get(uploadId);
-		if (!upload || !uploadEl) return;
-
-		const fieldData = this.getFieldData(upload.fieldId);
-		const fieldEl = this.fieldElements.get(upload.fieldId);
-		if (!fieldData || !fieldEl) return;
-
-		// Remove from current group
-		if (upload.groupId) {
-			const group = fieldData.groups?.find(g => g.id === upload.groupId);
-			if (group) {
-				group.uploads = group.uploads.filter(id => id !== uploadId);
-				if (group.uploads.length === 0) {
-					this.deleteGroup(upload.groupId, false);
-				}
-			}
-			upload.groupId = null;
-		}
-
-		// Move back to preview
-		if (fieldEl.ui?.preview) {
-			fieldEl.ui.preview.appendChild(uploadEl.element);
-			uploadEl.location = fieldEl.ui.preview;
-		}
-
-		// Hide featured radio
-		const featured = uploadEl.element.querySelector('[name="featured"]');
-		if (featured) {
-			featured.hidden = true;
-			featured.checked = false;
-		}
-
-		this.uploadStore.save(upload);
-		this.updateSortableState(fieldEl.ui.preview);
-	}
-
-	removeUpload(fieldId, uploadId) {
-		const fieldData = this.getFieldData(fieldId);
-		const upload = this.uploadStore.get(uploadId);
-		const uploadEl = this.uploadElements.get(uploadId);
-
-		if (!fieldData || !upload) return;
-
-		// Remove from field
-		fieldData.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = fieldData.groups?.find(g => g.id === upload.groupId);
-			if (group) {
-				group.uploads = group.uploads.filter(id => id !== uploadId);
-				if (group.uploads.length === 0) {
-					this.deleteGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clean up element
-		uploadEl?.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update field state
-		this.saveFieldData(fieldData);
-		this.updateFieldState(fieldId);
-		this.maybeLockUploads(fieldId);
-
-		const handler = this.selectionHandlers.get(fieldId);
-		if (handler) {
-			handler.deselect(uploadId);
-		}
-
-		this.a11y.announce('Upload removed');
-	}
-
-	handleGroupMetaChange(input) {
-		const groupEl = this.getGroupFromElement(input);
-		if (!groupEl) return;
-
-		const fieldData = this.getFieldData(groupEl.fieldId);
-		const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
-		if (!group) return;
-
-		if (!group.changes) group.changes = {};
-
-		let name = input.name;
-		if (name.includes('group')) {
-			name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
-		}
-
-		group.changes[name] = input.value;
-		this.saveFieldData(fieldData);
-		this.schedulePersistance(groupEl.fieldId);
-	}
-
-	/*******************************************************************************
-	 * ACTION HANDLERS
-	 *******************************************************************************/
-
-	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':
-				const fieldEl = this.fieldElements.get(fieldId);
-				if (fieldEl) {
-					fieldEl.element.closest('details').open = false;
-					document.body.classList.add('uploading');
-					this.submitUploads(fieldId);
-				}
-				break;
-			case 'restore':
-				this.handleRestoreUploads().then(() => {});
-				break;
-			case 'restore-all':
-				this.handleRestoreAll().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) {
-			this.createGroup(fieldId);
-		} else {
-			const group = this.createGroup(fieldId);
-			if (!group) return;
-
-			selected.forEach(uploadId => {
-				this.addToGroup(uploadId, group.grid);
-			});
-
-			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;
-		}
-
-		const items = group.querySelectorAll(this.selectors.items.item);
-		items.forEach(item => {
-			const uploadId = item.dataset.uploadId;
-			this.removeFromGroup(uploadId);
-		});
-
-		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
-	 *******************************************************************************/
-
-	addFieldSelectionHandler(fieldId) {
-		if (this.selectionHandlers.has(fieldId)) {
-			return this.selectionHandlers.get(fieldId);
-		}
-
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldEl?.element) return;
-
-		const handler = new window.jvbHandleSelection({
-			container: fieldEl.element,
-			ui: {
-				selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
-				bulkControls: fieldEl.element.querySelector('.selection-actions'),
-				count: fieldEl.element.querySelector('.selection-count')
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]'
-		});
-
-		handler.subscribe((event, data) => {
-			switch(event) {
-				case 'item-selected':
-					// Sync with Sortable
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'item-deselected':
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'range-selected':
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
-			}
-		});
-
-		this.selectionHandlers.set(fieldId, handler);
-		return handler;
-	}
-
-	addGroupSelectionHandler(fieldId, groupId) {
-		const handlerKey = `${fieldId}_${groupId}`;
-		if (this.selectionHandlers.has(handlerKey)) {
-			return this.selectionHandlers.get(handlerKey);
-		}
-
-		const groupEl = this.groupElements.get(groupId);
-		if (!groupEl?.element) return;
-
-		const handler = new window.jvbHandleSelection({
-			container: groupEl.element,
-			ui: {
-				selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
-				bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
-				count: groupEl.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) {
-		// Can add custom logic here if needed
-	}
-
-	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;
-	}
-
-	/*******************************************************************************
-	 * HELPER METHODS
-	 *******************************************************************************/
-
-	/**
-	 * Get field data from store and normalize it
-	 * Always use this instead of directly accessing fieldStore.get()
-	 */
-	getFieldData(fieldId) {
-		const fieldData = this.fieldStore.get(fieldId);
-		if (!fieldData) return null;
-
-		// Only convert uploads back to Set (DataStore returns Arrays)
-		if (Array.isArray(fieldData.uploads)) {
-			fieldData.uploads = new Set(fieldData.uploads);
-		} else if (!fieldData.uploads) {
-			fieldData.uploads = new Set();
-		}
-
-		// Ensure groups is an array
-		if (!Array.isArray(fieldData.groups)) {
-			fieldData.groups = [];
-		}
-
-		return fieldData;
-	}
-
-	/**
-	 * Save field data to store, converting Sets to Arrays
-	 */
-	async saveFieldData(fieldData) {
-		await this.fieldStore.save({
-			...fieldData,
-			timestamp: Date.now()
-		});
-	}
-
-	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',
-				getRuntimeData: (id) => this.fieldElements.get(id),
-				getStoreData: (id) => this.getFieldData(id)
-			},
-			'upload': {
-				selector: this.selectors.items.item,
-				key: 'uploadId',
-				getRuntimeData: (id) => this.uploadElements.get(id),
-				getStoreData: (id) => this.uploadStore.get(id)
-			},
-			'group': {
-				selector: this.selectors.groups.container,
-				key: 'groupId',
-				getRuntimeData: (id) => this.groupElements.get(id),
-				getStoreData: (id) => {
-					// Groups are stored in field.groups array
-					const groupEl = this.groupElements.get(id);
-					if (!groupEl) return null;
-					const fieldData = this.getFieldData(groupEl.fieldId);
-					return fieldData?.groups?.find(g => g.id === id);
-				}
-			}
-		};
-
-		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 combined runtime + store data for convenience
-		const runtime = config.getRuntimeData(id);
-		const store = config.getStoreData(id);
-
-		return { ...runtime, ...store };
-	}
-
-	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
-	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
-	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
-
-	getFieldIdFromElement(el) {
-		const field = this.getFromElement(el, 'field');
-		return field?.id ?? null;
-	}
-	getUploadIdFromElement(el) {
-		const upload = this.getFromElement(el, 'upload');
-		return upload?.id ?? null;
-	}
-	getGroupIdFromElement(el) {
-		const group = this.getFromElement(el, 'group');
-		return group?.id ?? null;
-	}
-
-	getSubtypeFromMime(mimeType) {
-		if (mimeType.startsWith('image/')) return 'image';
-		if (mimeType.startsWith('video/')) return 'video';
-		return 'document';
-	}
-
-	getStatusText(status) {
-		return this.statusMapping[status] || status;
-	}
-
-	getStatusIcon(status) {
-		return window.getIcon(this.queue.icons[status]);
+		return map[status]||status;
 	}
-
 	getStatusProgress(status) {
-		const progress = {
+		let progress = {
 			'local_processing': 28,
 			'queued': 50,
 			'uploading': 66,
@@ -2629,464 +1487,593 @@
 			'processing': 89,
 			'completed': 100
 		};
-		return progress[status] || 0;
+		return progress[status]??0;
 	}
-
-	getModalType(fieldEl) {
-		if (!fieldEl?.element) return null;
-		if (fieldEl._cachedModalType !== undefined) {
-			return fieldEl._cachedModalType;
-		}
-
-		const dialog = fieldEl.element.closest('dialog');
-		if (!dialog) {
-			fieldEl._cachedModalType = null;
-			return null;
-		}
-
-		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;
-
-		fieldEl._cachedModalType = modalType;
-		return modalType;
-	}
-
-	createUploadElement(upload, draggable = false) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) return;
-
-		image.dataset.uploadId = upload.id;
-		image.dataset.subtype = upload.subtype || 'image';
-
-		let [featured, img, video, preview, details] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-
-		if (featured) featured.value = upload.id;
-
-		switch (upload.subtype) {
-			case 'image':
-				if (img) {
-					img.src = upload.preview;
-					img.alt = upload.meta?.originalName || '';
-				}
-				video?.remove();
-				preview?.remove();
-				break;
-			case 'video':
-				if (video) video.src = upload.preview;
-				img?.remove();
-				preview?.remove();
-				break;
-			case 'document':
-				const fileName = 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');
-				if (preview) {
-					preview.innerText = fileName;
-					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
-		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;
-	}
-
 	/*******************************************************************************
-	 * PERSISTENCE
-	 *******************************************************************************/
+	 UPLOAD METHODS
+	*******************************************************************************/
+	async createUpload(uploadId, file, fieldId) {
+		let field = this.fields.get(fieldId);
+		if (!field) return null;
 
-	/**
-	 * Normalize field data loaded from IndexedDB
-	 * Converts Arrays back to Sets, handles missing properties
-	 */
-	normalizeFieldData(fieldData) {
-		if (!fieldData) return null;
-
-		// Convert uploads array back to Set
-		if (Array.isArray(fieldData.uploads)) {
-			fieldData.uploads = new Set(fieldData.uploads);
-		} else if (!fieldData.uploads) {
-			fieldData.uploads = new Set();
-		}
-
-		// Convert groups array, ensure proper structure
-		if (!Array.isArray(fieldData.groups)) {
-			fieldData.groups = [];
-		}
-
-		// Ensure each group has uploads array
-		fieldData.groups = fieldData.groups.map(group => ({
-			...group,
-			uploads: Array.isArray(group.uploads) ? group.uploads : []
-		}));
-
-		return fieldData;
-	}
-
-	schedulePersistance(fieldId) {
-		const key = `persist_${fieldId}`;
-		window.debouncer.schedule(
-			key,
-			() => this.persistFieldState(fieldId),
-			250
-		);
-	}
-
-	async persistFieldState(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		// Save with updated timestamp
-		await this.saveFieldData(fieldData);
-	}
-
-	// In UploadManager, add blob conversion helpers
-	async saveBlobData(uploadId, file) {
-		const arrayBuffer = await file.arrayBuffer();
-
-		const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
-
-		// Store blob data as ArrayBuffer with metadata
-		uploadData.blobData = {
-			buffer: arrayBuffer,
-			name: file.name,
-			type: file.type,
-			size: file.size,
-			lastModified: file.lastModified || Date.now()
+		let data = {
+			uploadId: uploadId,
+			file: file,
+			field: field,
 		};
-
-		await this.uploadStore.save(uploadData);
+		return this.templates.create('uploadItem', data);
 	}
 
-	async getBlobData(uploadId) {
-		const upload = this.uploadStore.get(uploadId);
-		if (!upload?.blobData) return null;
+	getSubtypeFromURL(url) {
+		if (!url || url === '') {
+			return '';
+		}
+		const imgs = ['.webp', '.jpg', '.jpeg', '.png', '.gif', '.svg'];
+		const videos = ['.mp4', '.ogg', '.mov', '.webm', '.avi'];
 
-		// Reconstruct File from ArrayBuffer
-		const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
-		return new File([blob], upload.blobData.name, {
-			type: upload.blobData.type,
-			lastModified: upload.blobData.lastModified
+		const path = url.split('?')[0].toLowerCase();
+
+		if (imgs.some(ext => path.endsWith(ext))) return 'image';
+		if (videos.some(ext => path.endsWith(ext))) return 'video';
+		return 'document';
+	}
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+	/**
+	 * Called by handleAction
+	 * @param button
+	 */
+	async handleRemoveItem(button) {
+		console.log('Handling remove upload');
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		const uploadId = item.dataset.uploadId;
+		const attachmentId = item.dataset.id;
+
+		if (!uploadId && !attachmentId) return;
+		if (!confirm('Remove this item?')) return;
+
+		if (uploadId) {
+			await this.removeUpload(uploadId);
+		} else {
+			const fieldId = this.getFieldIdFromElement(button);
+			item.remove();
+
+			if (fieldId) {
+				this.updateHiddenInput(fieldId);
+				this.maybeLockUploads(fieldId);
+			}
+		}
+
+		this.a11y.announce('Item removed');
+	}
+
+	updateHiddenInput(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field?.ui.hidden) return;
+
+		const remaining = Array.from(field.ui.grid?.querySelectorAll(this.selectors.items.item) || [])
+			.map(el => {
+				if (Object.hasOwn(el.dataset, 'id') && el.dataset.id > 0) {
+					return el.dataset.id;
+				}
+
+				if (Object.hasOwn(el.dataset, 'upload-id') && el.dataset.uploadId > 0) {
+					return el.dataset.uploadId;
+				}
+				//For timeline
+				return el.dataset.itemId;
+			})
+			.filter(Boolean);
+
+		const newValue = remaining.join(',');
+		if (field.ui.hidden.value === newValue) return;
+
+		field.ui.hidden.value = newValue;
+		field.ui.hidden.dispatchEvent(new Event('change', { bubbles: true }));
+	}
+	async setBulkUpload(uploads, key, value) {
+		const promises = Array.from(uploads).map(async (upload) => {
+			if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+			if (!upload) return;
+
+			if (key === 'status') {
+				await this.setUploadStatus(upload, value);
+			}
+			upload[key] = value;
+			return this.stores.uploads.save(upload);
 		});
+		await Promise.all(promises);
+	}
+
+	async setUploadStatus(upload, status) {
+		if (typeof upload === 'string') upload = await this.stores.uploads.get(upload);
+		if (!upload) return;
+		if (upload.progress) {
+			window.showProgress(upload.progress, this.getStatusProgress(status), 100, this.getStatusText(status), this.queue.icons[status]??'');
+		}
+	}
+
+	async removeUpload(uploadId) {
+		let upload = this.stores.uploads.get(uploadId);
+		if (!upload) return;
+		const fieldId = upload.field; // grab before clearing
+
+		if (upload.group) {
+			let group = this.stores.groups.get(upload.group);
+			group.uploads = group.uploads.filter(id => id !== uploadId);
+			if (group.uploads.length === 0) {
+				await this.removeGroup(group.id, false);
+			} else {
+				await this.stores.groups.save(group);
+			}
+		}
+
+		await this.clearUpload(uploadId);
+		this.updateHiddenInput(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		let handler = this.selectionHandlers.get(fieldId);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	async clearUpload(uploadId) {
+		const element = this.uploads.get(uploadId);
+		if (element) {
+			this.revokePreviewUrl(element.preview);
+			if (element.element) {
+				const previewUrl = element.element.dataset.previewUrl;
+				this.revokePreviewUrl(previewUrl);
+				element.element.remove();
+			}
+		}
+		this.uploads.delete(uploadId);
+		await this.stores.uploads.delete(uploadId);
 	}
 
 	/*******************************************************************************
-	 * RECOVERY & RESTORATION
-	 *******************************************************************************/
+	 GROUP METHODS
+	*******************************************************************************/
+	async handleAddToGroup(fieldId) {
+		const selected = this.selected.get(fieldId);
+		if (!selected || selected.size === 0) return;
 
-	handleFieldStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-				this.fieldStoreReady = true;
-				this.checkIfBothStoresReady();
-				break;
-		}
+		let groupId = await this.createGroup(fieldId);
+		if (!groupId) return;
+
+		await Promise.all(
+			Array.from(selected).map(uploadId => this.addToGroup(uploadId, groupId))
+		);
+
+		this.selectionHandlers.get(fieldId)?.clearSelection();
+		this.a11y.announce(`Created group with ${selected.size} items`);
 	}
+	async createGroup(fieldId, groupId = null) {
+		let field = this.fields.get(fieldId);
+		if (!field) return;
 
-	handleUploadStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-				this.uploadStoreReady = true;
-				this.checkIfBothStoresReady();
-				break;
-			case 'item-saved':
-				this.showSaveIndicator(data.key);
-				break;
-		}
-	}
-
-	checkIfBothStoresReady() {
-		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
-			this.hasCheckedForUploads = true;
-			this.checkForStoredUploads();
-		}
-	}
-
-	async checkForStoredUploads() {
-		const allFieldStates = this.fieldStore.getAll();
-
-		const pendingFields = allFieldStates.filter(field => {
-			if (!field.uploads) return false;
-
-			// Handle both Set and Array (from IndexedDB)
-			const uploadsArray = field.uploads instanceof Set
-				? Array.from(field.uploads)
-				: Array.isArray(field.uploads)
-					? field.uploads
-					: [];
-
-			return uploadsArray.some(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				return upload && !upload.operationId &&
-					['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
-			});
-		});
-		if (pendingFields.length === 0) return;
-
-		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;
+		if (!groupId) {
+			groupId = window.generateID('group');
 		}
 
-		// 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.`;
+		const element = this.createGroupElement(groupId, fieldId);
+		if (!element) return null;
+
+		const emptyGroup = field.groupUI.empty;
+		if (emptyGroup?.nextSibling) {
+			field.groupUI.grid.insertBefore(element, emptyGroup.nextSibling);
 		} else {
-			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+			field.groupUI.grid.append(element);
 		}
 
-		const detailsEl = notification.querySelector('.restore-details');
-		if (detailsEl) {
-			detailsEl.textContent = message;
+		// Create Sortable for this group's grid
+		const grid = element.querySelector('.item-grid');
+		if (grid) {
+			grid.dataset.groupId = groupId;
+			this.createSortable(fieldId, grid, groupId);
 		}
 
-		// Build the restoration preview
-		for (const field of pendingFields) {
-			let fieldTemplate = window.getTemplate('restoreField');
-			if (!fieldTemplate) continue;
+		let storedData = this.stores.groups.data.has(groupId)
+			? this.stores.groups.data.get(groupId)
+			: {};
 
-			// Set field name/title
-			const titleEl = fieldTemplate.querySelector('h3');
-			if (titleEl) {
-				titleEl.textContent = field.config.name || 'Unnamed Field';
-			}
+		await this.setGroup(groupId, { ...storedData, id: groupId, field: fieldId });
 
-			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+		return groupId;
+	}
 
-			// Process each upload
-			for (let uploadId of field.uploads) {
-				const upload = this.uploadStore.get(uploadId);
-				let uploadItem = window.getTemplate('uploadItem');
-				if (!uploadItem) continue;
-				//
-				// 	const imgEl = uploadItem.querySelector('img');
-				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
-				//
-				const file = await this.getBlobData(upload.id);
-				if (file) {
+	createGroupElement(groupId, fieldId = null) {
 
-					try {
-						// Create new blob URL from stored data
-						const previewUrl = this.createPreviewUrl(file);
+		let data = {
+			groupId: groupId,
+			fieldId: fieldId,
+		}
+		let element = this.templates.create('imageGroup', data);
 
-						let [
-							featured,
-							img,
-							video,
-							preview,
-							details
-						] = [
-							uploadItem.querySelector('[name="featured"]'),
-							uploadItem.querySelector('img'),
-							uploadItem.querySelector('video'),
-							uploadItem.querySelector('label > span'),
-							uploadItem.querySelector('details')
-						];
+		this.groups.set(groupId, {
+			element: element,
+			ui: window.uiFromSelectors(this.selectors.group, element)
+		});
 
-						uploadItem.dataset.uploadId = upload.id;
+		this.getSelectionHandler(fieldId)?.addWrapper(element);
+		return element;
+	}
 
 
-						uploadItem.dataset.fieldId = field.id;
+	async setGroup(groupId, data) {
+		const defaults = {
+			id: groupId,
+			src: window.location.href,
+			uploads: [],
+			operationId: null,
+			field: null,
+			fields: {}
+		};
+		const group = {...defaults, ...data};
+		Object.preventExtensions(group);
 
-						let subtype = this.getSubtypeFromMime(file.type);
-						uploadItem.dataset.subtype = subtype;
-						switch (subtype) {
-							case 'image':
-								[
-									img.src,
-									img.alt
-								] = [
-									previewUrl,
-									file.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;
-								}
+		await this.stores.groups.save(group);
+	}
 
-								preview.innerText = upload.originalFile.name;
-								preview.prepend(icon);
-								img.remove();
-								video.remove();
-								break;
-						}
+	async setBulkGroup(fieldId, key, value) {
+		let groups = this.stores.groups.filterByIndex({field:fieldId});
+		if (groups.length === 0) {
+			return;
+		}
+		let Promises = groups.map(group => {
+			group[key] = value;
+			this.stores.groups.save(group);
+		});
+		await Promise.all(Promises);
+	}
 
-						// Store URL for cleanup later
-						uploadItem.dataset.previewUrl = previewUrl;
-					} catch (error) {
-						console.warn('Failed to create preview for upload:', upload.id, error);
-					}
-				}
+	async addToGroup(uploadId, groupId = null){
+		const upload = this.stores.uploads.get(uploadId);
+		const element = this.uploads.get(uploadId);
+		if (!upload || !element) return;
+		const field = this.fields.get(upload.field);
+		if (!field) return;
 
-				// Set upload metadata
-				const nameEl = uploadItem.querySelector('summary span');
-				if (nameEl) {
-					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
-				}
+		//Check if it's already in this destination, it's probably a reorder
+		const isInDOM = element.element?.parentElement !== null;
+		if (isInDOM && ((!groupId && upload.group === null) || groupId === upload.group)) {
+			this.handleReorder(upload.field, groupId);
+			return;
+		}
 
-				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);
+		if (upload.group) {
+			const group = this.stores.groups.get(upload.group);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					await this.removeGroup(group.id, false);
+				} else {
+					await this.stores.groups.save(group);
 				}
 			}
-
-			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'),
+		//clear any selection
+		if (element.ui.checkbox) element.ui.checkbox.checked = false;
+		// Remove from field-level selection
+		const fieldHandler = this.selectionHandlers.get(upload.field);
+		if (fieldHandler && fieldHandler.isSelected(uploadId)) {
+			fieldHandler.deselect(uploadId);
+		}
+		if (this.selected.get(upload.field)?.has(uploadId)) {
+			this.selected.get(upload.field).delete(uploadId);
+		}
+		if (element.ui.featured) element.ui.featured.hidden = !groupId;
+
+		if (!groupId) {
+			upload.group = null;
+		} else {
+			if (element.ui.featured) element.ui.featured.name = `${groupId}_featured`;
+			let group = this.stores.groups.get(groupId);
+			if (group) {
+				group.uploads.push(uploadId);
+				upload.group = groupId;
+				await this.stores.groups.save(group);
+			}
+		}
+
+		let target = (groupId) ? this.groups.get(groupId)?.ui.grid : field.ui.grid;
+		if (target) {
+			target.append(element.element);
+			if (groupId) {
+				await this.handleReorder(upload.field, groupId);
+			}
+		}
+		await this.stores.uploads.save(upload);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.group.item);
+		if (!group) return;
+
+		let groupId = group.dataset.groupId;
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		let uploads = this.stores.uploads.filterByIndex({group: groupId});
+
+		Promise.all(
+			uploads.map(upload => this.addToGroup(upload.id, null))
+		).then(() => {
+			this.removeGroup(groupId, false).then(()=>{});
+			this.a11y.announce('Group deleted. Items returned to upload area');
+		});
+	}
+
+	async removeGroup(groupId, confirm = true) {
+		let element = this.groups.get(groupId);
+		let group = this.stores.groups.get(groupId);
+		if (!group) return;
+
+		let keepUploads = true;
+
+		if (confirm && group.uploads.length > 0) {
+			keepUploads = window.confirm('Keep uploads in this group?');
+		}
+
+		await Promise.all(
+			group.uploads.map(uploadId =>
+				keepUploads ? this.addToGroup(uploadId, null) : this.removeUpload(uploadId)
+			)
+		);
+		const field = this.fields.get(group.field);
+		if (field) {
+			const sortableKey = this.getGroupKey(group.field, groupId);
+			const selectionHandler = this.selectionHandlers.get(sortableKey);
+			if (selectionHandler?.destroy) {
+				selectionHandler.destroy();
+			}
+			if (this.selectionHandlers.get(group.field) && element && element.element) {
+				this.selectionHandlers.get(group.field).removeWrapper(element.element)
+			}
+
+			// Existing sortable cleanup
+			if (this.sortables.has(sortableKey)) {
+				const sortable = this.sortables.get(sortableKey);
+				if (sortable?.destroy) {
+					sortable.destroy();
+				}
+
+				this.sortables.delete(sortableKey);
+			}
+
+		}
+
+		if (element?.element) {
+			element.element.remove();
+		}
+		this.groups.delete(groupId);
+
+		await this.stores.groups.delete(groupId);
+
+		this.a11y.announce('Group removed');
+	}
+
+	maybeLockUploads(fieldId) {
+		let field = this.fields.get(fieldId);
+		if (!field || !field.ui.dropZone) return;
+
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		let count = uploads.length;
+		let max = field.config.maxFiles??0;
+
+		field.ui.dropZone.hidden = max > 0 && count >= max;
+	}
+	/*******************************************************************************
+	 OPERATION METHODS
+	*******************************************************************************/
+	async handleOperationCancelled(uploads) {
+		if (uploads.length === 0) return;
+		uploads.forEach(upload => {
+			this.removeUpload(upload);
+		});
+	}
+	/*******************************************************************************
+	 SELECTION HANDLERS
+	*******************************************************************************/
+	getGroupKey(fieldId, groupId = null) {
+		return (groupId) ? `${fieldId}_${groupId}` : `${fieldId}`;
+	}
+
+	getSelectionHandler(fieldId) {
+		let key = this.getGroupKey(fieldId);
+
+		if (!this.selectionHandlers.has(key)) {
+			let field = this.fields.get(fieldId);
+			if (!field) return;
+			if (field.config.destination !== 'post_group') return;
+			let handler = new window.jvbHandleSelection(field.element, {
+				selectAll: {
+					checkbox: this.selectors.fields.selectAll,
+					count: this.selectors.fields.count,
+					bulkControls: this.selectors.fields.actions
+				},
+				item: {
+					item: this.selectors.items.item,
+					checkbox: this.selectors.items.checkbox,
+					idAttribute: 'uploadId',
+				},
+				wrapper: {
+					wrapper: '.preview-wrap, .upload-group',
+					id: 'groupId'
+				},
+			});
+
+			handler.subscribe((event, data) => {
+				this.selected.set(fieldId, data.selectedItems);
+			});
+
+			this.selectionHandlers.set(key, handler);
+		}
+
+		return this.selectionHandlers.get(key);
+	}
+	updateHandlerItems(fieldId) {
+		let handler = this.getSelectionHandler(fieldId);
+		if (!handler) return;
+		handler.collectItems();
+	}
+	/*******************************************************************************
+	 SORTABLE
+	*******************************************************************************/
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
+
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
+		// Create sortable for the main preview grid
+		this.createSortable(fieldId, field.ui.grid, null);
+
+		// Set up empty-group as native drop zone
+		this.initEmptyGroupDropZone(fieldId);
+	}
+
+	createSortable(fieldId, gridElement, groupId) {
+		if (!gridElement) return null;
+
+		const key = this.getGroupKey(fieldId, groupId);
+
+		// Already exists
+		if (this.sortables.has(key)) {
+			return this.sortables.get(key);
+		}
+
+		const sortable = new Sortable(gridElement, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			dragClass: 'dragging',
+			ignore: '.empty-group',
+
+			onStart: (evt) => {
+				// Get the dragged item's ID
+				const draggedItem = evt.item;
+				const uploadId = draggedItem?.dataset.uploadId;
+
+				// Get the selected items Set for this field
+				const selectedItems = this.selected.get(fieldId);
+
+				// If the dragged item isn't selected, select it
+				if (uploadId && (!selectedItems || !selectedItems.has(uploadId))) {
+					const handler = this.selectionHandlers.get(fieldId);
+					if (handler) {
+						handler.select(uploadId);
+					}
+				}
 			},
+			onEnd: (evt) => this.sortableDrop(evt, fieldId),
 		});
 
-		this.restoreModal.handleOpen();
-
+		this.sortables.set(key, sortable);
+		return sortable;
 	}
 
-	async handleRestoreUploads() {
-		let notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) {
-			return;
-		}
+	initEmptyGroupDropZone(fieldId) {
+		const field = this.fields.get(fieldId);
+		const emptyZone = field?.groupUI?.empty;
+		if (!emptyZone) return;
 
-		const selectedUploads = this.getSelectedRestorationUploads(notification);
-		if (selectedUploads.length === 0) {
-			return;
-		}
-		await this.restoreSelectedUploads(selectedUploads);
-
-		this.cleanupRestore();
-	}
-
-	async handleRestoreAll() {
-		let notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) {
-			return;
-		}
-		// Gets ALL uploads from notification without checking selection
-		const allUploads = [];
-		notification.querySelectorAll('.item.upload').forEach(item => {
-			let uploadId = item.dataset.uploadId;
-			let fieldId = item.dataset.fieldId;
-			allUploads.push({ uploadId, fieldId });
+		emptyZone.addEventListener('dragover', (e) => {
+			e.preventDefault();
+			e.stopPropagation();
+			e.dataTransfer.dropEffect = 'move';
+			emptyZone.classList.add('drag-over');
 		});
 
-		await this.restoreSelectedUploads(allUploads);
-		this.cleanupRestore();
+		emptyZone.addEventListener('dragleave', (e) => {
+			if (!emptyZone.contains(e.relatedTarget)) {
+				emptyZone.classList.remove('drag-over');
+			}
+		});
+
+		emptyZone.addEventListener('drop', async (e) => {
+			e.preventDefault();
+			e.stopPropagation();
+			emptyZone.classList.remove('drag-over');
+
+			// Get selected items from our tracking
+			const selectedItems = this.selected.get(fieldId);
+			if (!selectedItems || selectedItems.size === 0) return;
+
+			const groupId = await this.createGroup(fieldId);
+			if (!groupId) return;
+
+			await Promise.all(
+				Array.from(selectedItems).map(uploadId => this.addToGroup(uploadId, groupId))
+			);
+
+			this.selectionHandlers.get(fieldId)?.clearSelection();
+		});
 	}
 
-	showSaveIndicator(key) {
-		// Optional: show user that state is being saved
+	async sortableDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+		const items = evt.items?.length > 0 ? Array.from(evt.items) : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId).filter(Boolean);
+
+		if (uploadIds.length === 0) return;
+
+		const targetGroupId = dropTarget.dataset.groupId || null;
+
+		// Process sequentially to avoid race conditions
+		for (const uploadId of uploadIds) {
+			await this.addToGroup(uploadId, targetGroupId);
+		}
+
+		await this.handleReorder(fieldId, targetGroupId);
+		this.selectionHandlers.get(fieldId)?.clearSelection();
 	}
 
-	cleanupRestore() {
-		this.restoreModal.handleClose();
-		this.restoreSelection.destroy();
-		this.restoreSelection = null;
-		this.restoreModal.destroy();
-		this.restoreModal.modal.remove();
-		this.restoreModal = null;
-	}
+	handleReorder(fieldId, groupId = null) {
+		let target = (groupId)
+			? this.groups.get(groupId)?.ui.grid
+			: this.fields.get(fieldId)?.ui.grid;
 
-	async cleanupStoredUploads() {
-		await this.fieldStore.clear();
-		await this.uploadStore.clear();
-	}
+		if (!target) {
+			console.log('Couldn\'t Reorder items...');
+			return;
+		}
 
+		if (!groupId) {
+			this.updateHiddenInput(fieldId);
+		} else {
+			let items = Array.from(target.children)
+				.filter(el => el.matches(this.selectors.items.item) && !el.classList.contains('ghost'))
+				.map(upload => upload.dataset.uploadId)
+				.filter(id => id);
+
+			let group = this.stores.groups.get(groupId);
+			if (group) {
+				group.uploads = items;
+				this.stores.groups.save(group).then(()=>{});
+			}
+		}
+
+		this.a11y.announce('Items reordered');
+	}
 	/*******************************************************************************
 	 * EVENT SYSTEM
 	 *******************************************************************************/
@@ -3098,49 +2085,99 @@
 
 	notify(event, data = {}) {
 		this.subscribers.forEach(cb => {
-			try {
-				cb(event, data);
-			} catch (error) {
-				console.error('Subscriber error:', error);
-			}
+			try { cb(event, data); } catch (e) { console.error('Subscriber error:', e); }
 		});
 	}
-
-	/*******************************************************************************
-	 * DESTROY & CLEANUP
-	 *******************************************************************************/
-
+	/********************************************************************
+	 CLEANUP
+	********************************************************************/
 	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.subscribers.clear();
+		this.previewUrls.forEach(url => {
+			this.revokePreviewUrl(url);
+		});
+		this.previewUrls.clear();
+	}
 
-		if (this.dragController) {
-			this.dragController.destroy();
+	cleanupAllPreviewUrls() {
+		this.previewUrls.forEach(url => this.revokePreviewUrl(url));
+		this.previewUrls.clear();
+	}
+
+	async handleClearCache() {
+		const currentSrc = window.location.href;
+
+		const uploads = this.stores.uploads.filterByIndex({src: currentSrc});
+		const groups = this.stores.groups.filterByIndex({src:currentSrc});
+
+		await Promise.all([
+			...uploads.map(upload => this.clearUpload(upload.id)),
+			...groups.map(group => {
+				this.groups.get(group.id)?.element?.remove();
+				this.groups.delete(group.id);
+				return this.stores.groups.delete(group.id);
+			})
+		]);
+
+		if (this.restoreModal) {
+			this.cleanupRestore();
 		}
 
-		this.selectionHandlers.forEach(handler => handler.destroy());
-		this.selectionHandlers.clear();
+		this.a11y.announce('Cache cleared for this page');
+	}
 
-		this.cleanupAllPreviewUrls();
+	/**
+	 * Get files from all upload fields in a form
+	 * Returns array of {file, fieldName, uploadId, meta}
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll(this.selectors.fields.field);
+		const allFiles = [];
 
-		this.sortableInstances.forEach(instance => {
-			if (instance?.destroy) instance.destroy();
-		});
-		this.sortableInstances.clear();
+		for (const fieldElement of uploadFields) {
+			const fieldId = this.determineFieldId(fieldElement);
+			const fieldName = fieldElement.dataset.field;
+			const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
 
-		this.uploadElements.clear();
-		this.fieldElements.clear();
-		this.groupElements.clear();
-		this.selected.clear();
-		this.subscribers.clear();
+			for (const upload of uploads) {
+				const file = this.formatFile(upload);
+				if (file) {
+					allFiles.push({
+						file: file,
+						fieldName: fieldName,
+						uploadId: upload.id,
+						meta: upload.fields || {}
+					});
+				}
+			}
+		}
+
+		return allFiles;
+	}
+
+	/**
+	 * Clear all uploads and groups for a specific field from stores
+	 */
+	async clearFieldFromStores(fieldId) {
+		const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
+		const groups = this.stores.groups.filterByIndex({ field: fieldId });
+
+		// Clear all uploads
+		await Promise.all(
+			uploads.map(upload => this.clearUpload(upload.id))
+		);
+
+		// Clear all groups
+		await Promise.all(
+			groups.map(group => {
+				this.groups.get(group.id)?.element?.remove();
+				this.groups.delete(group.id);
+				return this.stores.groups.delete(group.id);
+			})
+		);
 	}
 }
 
-// Initialize when DOM is ready
 document.addEventListener('DOMContentLoaded', async function () {
 	window.auth.subscribe((event) => {
 		if (event === 'auth-loaded') {

--
Gitblit v1.10.0