Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
assets/js/concise/UploadManager.js
@@ -1,62 +1,76 @@
/**
 * 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
      // Load dependencies
      this.queue = window.jvbQueue;
      this.a11y = window.jvbA11y;
      this.error = window.jvbError;
      //Load Datastore
      this.fieldStore = new window.jvbStore({
         name: 'upload_fields',
         storeName: 'fieldStates',
         keyPath: 'id',
         version: 2,
      // RECOVERY STORES - Cleared after successful upload
      this.fieldStore = window.jvbStore.register(
         'upload_fields',
         {
            storeName: 'fieldStates',
            keyPath: 'id',
            version: 2,
            indexes: [
               { name: 'fieldId', keyPath: 'fieldId' },
               { name: 'timestamp', keyPath: 'timestamp' },
               { name: 'content', keyPath: 'content' },
               { name: 'itemId', keyPath: 'itemId' },
               { name: 'status', keyPath: 'status' }
            ],
            TTL: 86400000 * 7, // 1 week
            delayFetch: true
         });
         indexes: [
            { name: 'fieldId', keyPath: 'fieldId' },
            { name: 'timestamp', keyPath: 'timestamp' },
            { name: 'content', keyPath: 'content' },
            { name: 'itemId', keyPath: 'itemId' },
            { name: 'status', keyPath: 'status' }
         ],
      this.uploadStore = window.jvbStore.register(
         'uploads',
         {
            name: 'uploads',
            storeName: 'uploads',
            keyPath: 'id',
            storeBlobs: true,
            indexes: [
               { name: 'fieldId', keyPath: 'fieldId' },
               { name: 'status', keyPath: 'status' },
               { name: 'groupId', keyPath: 'groupId' },
               { name: 'attachmentId', keyPath: 'attachmentId' }
            ],
            delayFetch: true
         });
         stripDOMReferences: true,
         TTL: 86400000*7 // 24 hours -> 1 week
      });
      this.uploadStore = new window.jvbStore({
         name: 'uploads',
         storeName: 'uploads',
         keyPath: 'id',
         storeBlobs: true,
         indexes: [
            { name: 'fieldId', keyPath: 'fieldId' },
            { name: 'status', keyPath: 'status' },
            { name: 'groupId', keyPath: 'groupId' },
            { name: 'attachmentId', keyPath: 'attachmentId' }
         ],
      });
      window.jvbUploadBlobs = this.uploadStore;
      // Subscribe to store events
      this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
      this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
      //Load Worker
      this.initWorker();
      // 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 }
      // Core data structures
      this.fields = new Map();
      this.uploads = new Map();
      this.uploadBlobs = new Map();
      this.groups = new Map();
      // Selection and UI state
      this.selected = new Map();
      this.selectionHandlers = new Map();
      this.previewUrls = new Set();
      //Notification and Subscribers
      this.sortableInstances = new Map();
      // Worker for image processing
      this.initWorker();
      // Notification subscribers
      this.subscribers = new Set();
      // Controllers (will be initialized based on features)
      // Controllers
      this.dragController = null;
      // Selectors
@@ -64,7 +78,6 @@
         field: {
            field: '[data-upload-field]',
            input: 'input[type="file"]',
            hiddenValue: 'input[type="hidden"]',
            dropZone: '.file-upload-container',
            preview: '.item-grid.preview',
            progress: '.image-progress'
@@ -85,7 +98,6 @@
         }
      };
      this.statusMapping = {
         'received': 'Image Received',
         'local_processing': 'Processing Image...',
@@ -98,53 +110,49 @@
         'failed_permanent': 'Upload failed permanently'
      };
      // Sortable configuration
      this.sortableConfig = {
         animation: 150,
         draggable: '.item',
         handle: '.select-item, img',
         ghostClass: 'sortable-ghost',
         chosenClass: 'sortable-chosen',
         dragClass: 'sortable-drag',
         onEnd: (evt) => this.handleReorder(evt)
      };
      this.init();
   }
   async init() {
      // Load existing data
      await this.loadFields();
      await this.loadUploads();
      // Initialize fields
      this.initializeFields();
      // Set up core listeners
      this.initListeners();
      // Queue integration - handle completion/failure
      this.queue.subscribe((event, operation) => {
         if (operation.endpoint !== 'uploads' && operation.endpoint !== 'uploads/meta') {
         if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
            return;
         }
         const fieldId = operation.data instanceof FormData
            ? operation.data.get('fieldId')
            : operation.data.fieldId;
            : operation.data?.fieldId;
         switch(event) {
            case 'cancel-operation':
               if (fieldId) {
                  this.clearField(fieldId);
               }
               if (fieldId) this.handleOperationCancelled(fieldId);
               break;
            case 'operation-status':
               if (fieldId) {
                  this.updateFieldStatus(fieldId, operation.status);
               }
               if (fieldId) this.updateFieldStatus(fieldId, operation.status);
               break;
            case 'operation-complete':
               const results = operation.result?.data || [];
               results.forEach(result => {
                  const upload = this.uploads.get(result.upload_id);
                  if (upload) {
                     upload.attachmentId = result.attachment_id;
                     upload.status = 'completed';
                     this.uploads.set(upload.id, upload);
                  }
               });
               if (fieldId) {
                  this.cleanField(fieldId);
               }
               this.handleOperationComplete(operation, fieldId);
               break;
            case 'operation-failed':
            case 'operation-failed-permanent':
               this.handleOperationFailed(operation, fieldId);
               break;
         }
      });
      window.addEventListener('beforeunload', () => {
@@ -157,12 +165,9 @@
         worker: null,
         timeout: null,
         tasks: new Map(),
         restart: {
            count: 0,
            max: 3,
         },
         restart: { count: 0, max: 3 },
         settings: {
            timeout: 10000, //10 seconds per image
            timeout: 10000,
            batchSize: 1,
            maxConcurrent: 3,
            restartAfterTimeout: true
@@ -170,39 +175,41 @@
      };
   }
   /**
    * Initialize all upload fields on the page
    */
   /*******************************************************************************
    * FIELD MANAGEMENT
    *******************************************************************************/
   initializeFields() {
      const fields = document.querySelectorAll(this.selectors.field.field);
      fields.forEach(uploader => {
         this.registerUploader(uploader);
      });
      fields.forEach(uploader => this.registerUploader(uploader));
   }
   scanFields(container) {
      const fields = container.querySelectorAll(this.selectors.field.field);
      fields.forEach(uploader => {
         this.registerUploader(uploader);
      });
      fields.forEach(uploader => this.registerUploader(uploader));
   }
   registerUploader(uploader) {
      const fieldId = this.determineFieldId(uploader);
      const config = this.extractFieldConfig(uploader);
      const ui = this.buildFieldUI(uploader);
      // Create field data structure
      const field = {
      // Store field data with Sets for runtime
      const fieldData = {
         id: fieldId,
         config: config,
         element: uploader,
         ui: this.buildFieldUI(uploader),
         uploads: new Set(),
         groups: new Set(),
         groups: [],
         state: 'ready',
         timestamp: Date.now()
      };
      this.fields.set(fieldId, field);
      // 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);
@@ -210,28 +217,26 @@
         this.initGroupFeatures();
      }
      if (config.type !== 'single') {
         this.initSortable(fieldId);
      }
      return fieldId;
   }
   /**
    * Extract configuration from field element
    */
   extractFieldConfig(fieldElement) {
      return {
         destination: fieldElement.dataset.destination || 'meta',
         content: fieldElement.dataset.content || null,
         mode: fieldElement.dataset.mode || 'direct',
         type: fieldElement.dataset.type || 'single',
         name: fieldElement.dataset.field,  // Field name for meta
         itemID: fieldElement.dataset.itemId || 0,  // Post/term/user ID
         name: fieldElement.dataset.field,
         itemID: fieldElement.dataset.itemId || 0,
         maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
         subtype: fieldElement.dataset.subtype || 'image'
      };
   }
   /**
    * Build UI element references for a field
    */
   buildFieldUI(fieldElement) {
      let UI = {
         field: fieldElement,
@@ -261,92 +266,129 @@
      return UI;
   }
   /**
    * Set up core event listeners
    */
   initListeners() {
      this.clickHandler = this.handleClick.bind(this);
      this.changeHandler = this.handleChange.bind(this);
   /*******************************************************************************
    * SORTABLE INITIALIZATION
    *******************************************************************************/
      document.addEventListener('click', this.clickHandler);
      document.addEventListener('change', this.changeHandler);
   initSortable(fieldId) {
      if (!window.Sortable) return;
      // External file drops
      this.dragEnterHandler   = this.handleExternalDragEnter.bind(this);
      this.dragLeaveHandler   = this.handleExternalDragLeave.bind(this);
      this.dragOverHandler    = this.handleExternalDragOver.bind(this);
      this.dropHandler     = this.handleExternalDrop.bind(this);
      const fieldEl = this.fieldElements.get(fieldId);
      if (!fieldEl) return;
      document.addEventListener('dragenter', this.dragEnterHandler);
      document.addEventListener('dragleave', this.dragLeaveHandler);
      document.addEventListener('dragover', this.dragOverHandler);
      document.addEventListener('drop', this.dropHandler);
      // Find all sortable grids (preview + group grids, but not restore grids)
      const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
      grids.forEach((grid) => {
         // Skip if already initialized
         if (grid.sortableInstance) return;
         const isGroupGrid = grid.classList.contains('group');
         const gridId = isGroupGrid
            ? `${fieldId}-group-${grid.closest('.upload-group')?.dataset.groupId}`
            : `${fieldId}-preview`;
         const sortableInstance = new Sortable(grid, {
            ...this.sortableConfig,
            group: {
               name: fieldId,
               pull: true,
               put: true
            },
            onAdd: (evt) => {
               // Re-enable when items added
               this.updateSortableState(evt.to);
            },
            onRemove: (evt) => {
               // Disable source if now empty
               this.updateSortableState(evt.from);
            }
         });
         // Store reference on element for easy access
         grid.sortableInstance = sortableInstance;
         this.sortableInstances.set(gridId, sortableInstance);
      });
   }
   /**
    * Initialize group-specific features (drag & drop for rearranging)
    * 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', !hasItems);
   }
   /**
    * 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;
      let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
      let items = Array.from(fieldWrapper.querySelectorAll('.item')).map(upload => upload.dataset.id);
      console.log(items);
      hiddenInput.value = items.join(',');
      if (window.jvbA11y) {
         window.jvbA11y.announce('Item reordered');
      }
      fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
         detail: { from: evt.from, to: evt.to, oldIndex: evt.oldIndex, newIndex: evt.newIndex },
         bubbles: true
      }));
   }
   /*******************************************************************************
    * DRAG & DROP INITIALIZATION
    *******************************************************************************/
   initGroupFeatures() {
      // Initialize drag controller for rearranging items
      this.dragController = new window.jvbDragHandler({
         // What can be dragged
         draggableSelector: this.selectors.items.item,
         // Where items can be dropped
         dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
         // Don't start drag on interactive elements
         ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
         previewElement: 'img, video, .icon',
         // Extract upload ID from element
         getItemId: (element) => {
            return element.dataset.uploadId;
         },
         // Get selected items for multi-drag
         getItemId: (element) => element.dataset.uploadId,
         getSelectedItems: (element) => {
            const fieldId = this.getFieldIdFromElement(element);
            const uploadId = element.dataset.uploadId;
            const selected = this.getCurrentSelection(fieldId);
            if (selected && selected.includes(uploadId)) {
               return selected;
            }
            return [uploadId];
            return (selected && selected.includes(uploadId)) ? selected : [uploadId];
         },
         // Validate drop location
         validateDrop: (itemIds, targetElement) => {
            const targetFieldId = this.getFieldIdFromElement(targetElement);
            const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
            const itemFieldId = this.getFieldIdFromElement(itemElement);
            return targetFieldId === itemFieldId;
         },
         // Handle successful drop
         onDrop: (itemIds, targetElement) => {
            this.handleItemDrop(itemIds, targetElement);
            targetElement.scrollIntoView({behavior:'smooth', block:'center'});
         },
         // Optional callbacks
         onDragStart: (itemIds) => {
         },
         onDragEnd: (itemIds, success) => {
            if (success) {
               // Clear selection after successful move
               const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
               const fieldId = this.getFieldIdFromElement(itemElement);
               const handler = this.selectionHandlers.get(fieldId);
               handler?.clearSelection();
            }
         },
         // Preview options
         previewOptions: {
            multiOffset: { x: -60, y: -80 },
            singleOffset: { x: -50, y: -60 },
@@ -356,22 +398,37 @@
   }
   /*******************************************************************************
    * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop)
    * 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;
      }
      if (!e.dataTransfer.types.includes('Files')) return;
      const dropZone = e.target.closest(this.selectors.field.dropZone);
      if (dropZone) {
         e.preventDefault();
         dropZone.classList.add('dragover');
@@ -380,7 +437,6 @@
   handleExternalDragOver(e) {
      if (!e.dataTransfer.types.includes('Files')) return;
      const dropZone = e.target.closest(this.selectors.field.dropZone);
      if (dropZone) {
         e.preventDefault();
@@ -390,33 +446,25 @@
   handleExternalDrop(e) {
      const dropZone = e.target.closest(this.selectors.field.dropZone);
      if (!dropZone) return;
      e.preventDefault();
      dropZone.classList.remove('dragover');
      const files = Array.from(e.dataTransfer.files);
      if (files.length === 0) return;
      const fieldId = this.getFieldIdFromElement(dropZone);
      if (fieldId) {
         this.processFiles(fieldId, files);
         this.a11y.announce(`${files.length} file(s) dropped for upload`);
      } else {
         console.error('No field ID found for drop zone');
      }
   }
   /*******************************************************************************
    * ITEM DROP HANDLER (for rearranging existing uploads)
    * ITEM DROP HANDLER (for rearranging)
    *******************************************************************************/
   /**
    * Handle items being dropped (called by DragController)
    */
   handleItemDrop(itemIds, targetElement) {
      const isPreviewDrop = targetElement.classList.contains('preview');
      let actualTarget = targetElement;
@@ -425,43 +473,32 @@
      if (targetElement.classList.contains('empty-group')) {
         const fieldId = this.getFieldIdFromElement(targetElement);
         const group = this.createGroup(fieldId);
         if (!group) {
            console.error('Failed to create group');
            return;
         }
         if (!group) return;
         actualTarget = group.grid;
      }
      // Move each item to target
      // Move each item
      itemIds.forEach(uploadId => {
         if (isPreviewDrop) {
            // Moving back to preview (ungrouping)
            this.removeFromGroup(uploadId);
         } else {
            // Moving to a group
            this.addToGroup(uploadId, actualTarget);
         }
      });
      // Persist state
      const fieldId = this.getFieldIdFromElement(targetElement);
      this.schedulePersistance(fieldId);
      // Announce for accessibility
      const message = itemIds.length > 1
         ? `Moved ${itemIds.length} items`
         : 'Moved item';
      const message = itemIds.length > 1 ? `Moved ${itemIds.length} items` : 'Moved item';
      this.a11y.announce(message);
   }
   /*******************************************************************************
    * CLICK HANDLERS
    * CLICK & CHANGE HANDLERS
    *******************************************************************************/
   handleClick(e) {
      // File input triggers
      // 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);
@@ -480,11 +517,10 @@
   handleChange(e) {
      const fieldId = this.getFieldIdFromElement(e.target);
      // File input change
      if (e.target.matches(this.selectors.field.input)) {
         const fieldId = this.getFieldIdFromElement(e.target);
         const files = Array.from(e.target.files);
         if (files.length > 0 && fieldId) {
            this.processFiles(fieldId, files);
         }
@@ -492,7 +528,8 @@
      // Meta field changes
      if (fieldId) {
         if (this.fields.get(fieldId).config.destination === 'post_group') {
         const fieldData = this.getFieldData(fieldId);
         if (fieldData?.config.destination === 'post_group') {
            this.handleGroupMetaChange(e.target);
         } else {
            this.queueUploadMeta(e);
@@ -500,363 +537,39 @@
      }
   }
   /********************************************************************************
    UTILITY
   ********************************************************************************/
   getCurrentSelection(fieldId) {
      let selected = [];
      for (let [key, handler] of this.selectionHandlers) {
         if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
            selected = selected.concat([... handler.selectedItems]);
         }
      }
      return selected;
   }
   getSubtypeFromMime(mimeType) {
      if (mimeType.startsWith('image/')) return 'image';
      if (mimeType.startsWith('video/')) return 'video';
      return 'document';
   }
   getStatusText(status) {
      return this.statusMapping[status] || status;
   }
   getStatusIcon(status) {
      return window.getIcon(this.queue.icons[status]);
   }
   getStatusProgress(status) {
      switch (status) {
         case 'local_processing':
            return 28;
         case 'queued':
            return 50;
         case 'uploading':
            return 66;
         case 'pending':
            return 75;
         case 'processing':
            return 89;
         case 'completed':
            return 100;
         default:
            return 0;
      }
   }
   getModalType(field) {
      // Return cached value if available
      if (field._cachedModalType !== undefined) {
         return field._cachedModalType;
      }
      // Safety check for field.element
      if (!field || !field.element) {
         field._cachedModalType = null;
         return null;
      }
      const dialog = field.element.closest('dialog');
      if (!dialog) {
         field._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;
      // Cache the result
      field._cachedModalType = modalType;
      return modalType;
   }
   /*******************************************************************************
    * GROUP ACTIONS
    *******************************************************************************/
   handleAction(button) {
      const action = button.dataset.action;
      const fieldId = this.getFieldIdFromElement(button);
      switch(action) {
         case 'add-to-group':
            this.handleAddToGroup(button);
            break;
         case 'delete-group':
            this.handleDeleteGroup(button);
            break;
         case 'delete-upload':
         case 'remove-from-group':
            this.handleRemoveItem(button);
            break;
         case 'upload':
            //upload groups
            let field = this.fields.get(fieldId);
            field.element.closest('details').open = false;
            document.body.classList.add('uploading');
            this.submitUploads(fieldId);
            break;
         case 'restore':
            this.handleRestoreUploads().then(()=>{});
            break;
         case 'clear-cache':
            if (!confirm(`Save these uploads for later?`)) {
               this.cleanupStoredUploads();
            }
            this.cleanupRestore();
            break;
      }
   }
   handleAddToGroup(button) {
      const fieldElement = button.closest(this.selectors.field.field);
      const fieldId = fieldElement?.dataset.uploader;
      if (!fieldId) return;
      const selected = this.selected.get(fieldId);
      if (!selected || selected.size === 0) {
         // Create empty group
         this.createGroup(fieldId);
      } else {
         // Create group with selected items
         const group = this.createGroup(fieldId);
         if (!group) return;
         selected.forEach(uploadId => {
            this.addToGroup(uploadId, group.grid);
         });
         // Clear selection
         const handler = this.selectionHandlers.get(fieldId);
         handler?.clearSelection();
         this.a11y.announce(`Created group with ${selected.size} items`);
      }
      this.schedulePersistance(fieldId);
   }
   handleDeleteGroup(button) {
      const group = button.closest(this.selectors.groups.container);
      if (!group) return;
      const groupId = group.dataset.groupId;
      const fieldId = this.getFieldIdFromElement(group);
      if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
         return;
      }
      // Move items back to preview
      const items = group.querySelectorAll(this.selectors.items.item);
      items.forEach(item => {
         const uploadId = item.dataset.uploadId;
         this.removeFromGroup(uploadId);
      });
      // Remove group
      this.deleteGroup(groupId);
      this.a11y.announce('Group deleted, items returned to upload area');
      this.schedulePersistance(fieldId);
   }
   handleRemoveItem(button) {
      const item = button.closest(this.selectors.items.item);
      if (!item) return;
      const uploadId = item.dataset.uploadId;
      const fieldId = this.getFieldIdFromElement(item);
      if (!confirm('Remove this item?')) {
         return;
      }
      this.removeUpload(fieldId, uploadId);
      this.a11y.announce('Item removed');
      this.schedulePersistance(fieldId);
   }
   /*******************************************************************************
    * SELECTION MANAGEMENT
    *******************************************************************************/
   /**
    * Add selection handler for a field
    */
   addFieldSelectionHandler(fieldId) {
      if (this.selectionHandlers.has(fieldId)) {
         return this.selectionHandlers.get(fieldId);
      }
      const field = this.fields.get(fieldId);
      if (!field) return;
      const container = field.ui.field;
      if (!container) return;
      const handler = new window.jvbHandleSelection({
         container: container,
         ui: {
            selectAll: container.querySelector('[name="select-all-uploads"]'),
            bulkControls: container.querySelector('.selection-actions'),
            count: container.querySelector('.selection-count')
         },
         itemSelector: '[data-upload-id]',
         checkboxSelector: '[name*="select-item"]'
      });
      // Subscribe to selection changes
      handler.subscribe((event, data) => {
         switch(event) {
            case 'item-selected':
            case 'item-deselected':
            case 'range-selected':
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'select-all':
               this.handleSelectAll(data.container, data.selected);
               break;
         }
      });
      this.selectionHandlers.set(fieldId, handler);
      return handler;
   }
   /**
    * Add selection handler for a group
    */
   addGroupSelectionHandler(fieldId, groupId) {
      const handlerKey = `${fieldId}_${groupId}`;
      if (this.selectionHandlers.has(handlerKey)) {
         return this.selectionHandlers.get(handlerKey);
      }
      const group = this.groups.get(groupId);
      if (!group) return;
      const handler = new window.jvbHandleSelection({
         container: group.element,
         ui: {
            selectAll: group.element.querySelector(this.selectors.groups.selectAll),
            bulkControls: group.element.querySelector(this.selectors.groups.actions),
            count: group.element.querySelector(this.selectors.groups.count)
         },
         itemSelector: '[data-upload-id]',
         checkboxSelector: '[name*="select-item"]'
      });
      handler.subscribe((event, data) => {
         switch(event) {
            case 'item-selected':
            case 'item-deselected':
            case 'range-selected':
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'select-all':
               this.handleSelectAll(data.container, data.selected);
               break;
         }
      });
      this.selectionHandlers.set(handlerKey, handler);
      return handler;
   }
   handleSelectAll(container, selected) {
   }
   /*******************************************************************************
    * HELPER METHODS
    *******************************************************************************/
   determineFieldId(fieldElement) {
      const content = fieldElement.dataset.content ||
         fieldElement.closest('dialog')?.dataset.content ||
         fieldElement.closest('form')?.dataset.save || '';
      const itemID = fieldElement.dataset.itemId ||
         fieldElement.closest('dialog')?.dataset.itemId || '';
      const field = fieldElement.dataset.field || '';
      return `${content}_${itemID}_${field}`;
   }
   getFromElement(element, type) {
      const map = {
         'field': { selector: this.selectors.field.field, key: 'uploader', store: this.fields },
         'upload': { selector: this.selectors.items.item, key: 'uploadId', store: this.uploads },
         'group': { selector: this.selectors.groups.container, key: 'groupId', store: this.groups }
      };
      const config = map[type];
      if (!config) return null;
      const el = element.closest(config.selector);
      if (!el) return null;
      const id = el.dataset[config.key];
      return config.store.get(id);
   }
   getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
   getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
   getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
   getFieldIdFromElement(el) { return this.getFromElement(el, 'field')?.id ?? null};
   getUploadIdFromElement(el) {return this.getFromElement(el, 'upload')?.id ?? null};
   getGroupIdFromElement(el) {return this.getFromElement(el, 'group')?.id ?? null};
   /*******************************************************************************
    * FILE PROCESSING
    *******************************************************************************/
   async processFiles(fieldId, files) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      // Hide upload container, show group display
      if (field.ui.dropZone) {
         field.ui.dropZone.hidden = true;
   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 (field.ui.groups.display) {
         field.ui.groups.display.hidden = false;
      if (fieldEl.ui.groups?.display) {
         fieldEl.ui.groups.display.hidden = false;
      }
      const totalFiles = files.length;
      let processedCount = 0;
      // Show initial progress
      this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
      // Initialize field uploads set if needed
      if (!field.uploads) {
         field.uploads = new Set();
      }
      // Process files
      const processPromises = Array.from(files).map(async (file, index) => {
      const processPromises = Array.from(files).map(async (file) => {
         try {
            // Create upload ID
            const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            // Create upload data
            // Create upload data (without blob initially)
            const uploadData = {
               id: uploadId,
               attachment_id: null,
               attachmentId: null,
               fieldId: fieldId,
               originalFile: file,
               processedFile: null,
               preview: null,
               status: 'local_processing',
               element: null,
               location: null,
               groupId: null,
               meta: {
                  originalName: file.name,
                  size: file.size,
@@ -864,50 +577,55 @@
               }
            };
            // Create preview URL
            uploadData.preview = this.createPreviewUrl(file);
            // Process file
            const preview = this.createPreviewUrl(file);
            const processedFile = file.type.startsWith('image/')
               ? await this.processImage(file, fieldData.config.subtype)
               : file;
            // Process the file (resize if image)
            if (file.type.startsWith('image/')) {
               uploadData.processedFile = await this.processImage(file, field.subtype);
            } else {
               uploadData.processedFile = file;
            }
            // Store blob data separately in IndexedDB
            await this.uploadStore.saveBlob(uploadId, uploadData.processedFile || file);
            // Store blob data as ArrayBuffer
            await this.saveBlobData(uploadId, processedFile || file);
            // Create DOM element
            const subtype = this.getSubtypeFromMime(file.type);
            uploadData.element = this.createUploadElement({
               ...uploadData,
            const element = this.createUploadElement({
               id: uploadId,
               preview: preview,
               meta: uploadData.meta,
               subtype: subtype
            }, field.config.destination === 'post_group');
            }, fieldData.config.destination === 'post_group');
            // Show progress on the item
            // Show progress
            this.showUploadProgress(uploadId, true);
            this.updateUploadItemProgress(uploadId, 50, 'local_processing');
            // Add to preview grid
            if (field.ui.preview) {
               field.ui.preview.appendChild(uploadData.element);
               uploadData.location = field.ui.preview;
            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
               });
            }
            // Store upload
            this.uploads.set(uploadId, uploadData);
            field.uploads.add(uploadId);
            // Store persistent data
            uploadData.status = 'processed';
            await this.uploadStore.save(uploadData);
            // 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');
            uploadData.status = 'processed';
            // Fade out item progress after a moment
            setTimeout(() => {
               this.showUploadProgress(uploadId, false);
            }, 1000);
            // Fade out progress
            setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
            return uploadId;
@@ -919,192 +637,21 @@
         }
      });
      // Wait for all files to process
      await Promise.all(processPromises);
      this.updateFieldState(fieldId);
      // Cache the state (now without DOM references)
      await this.schedulePersistance(fieldId);
      this.refreshSortable(fieldId);
      // Queue for upload if in direct mode
      if (field.config.destination !== 'post_group') {
      if (fieldData.config.destination !== 'post_group') {
         await this.queueUpload(fieldId);
         // Lock uploads if max reached
         this.maybeLockUploads(fieldId);
      }
   }
   updateFieldState(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field || !field.ui.field) return;
      const container = field.ui.field;
      const uploadCount = field.uploads?.size || 0;
      const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
      // Set data attributes for CSS targeting
      container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
      container.dataset.uploadCount = uploadCount.toString();
      container.dataset.hasGroups = hasGroups ? 'true' : 'false';
      // Update ARIA labels for accessibility
      if (field.ui.preview) {
         field.ui.preview.setAttribute('aria-label',
            `Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
         );
      }
   }
   updateUploadProgress(fieldId, current, total, message) {
      const field = this.fields.get(fieldId);
      if (!field?.ui?.progress?.progress) return;
      const progress = field.ui.progress;
      const percent = total > 0 ? (current / total) * 100 : 0;
      if (progress.fill) {
         progress.fill.style.width = `${percent}%`;
      }
      if (progress.text) {
         progress.text.textContent = message;
      }
      if (progress.count) {
         progress.count.textContent = `${current}/${total}`;
      }
      progress.progress.hidden = (current === total);
   }
   updateFieldStatus(fieldId, status) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      field.state = status;
      // Update UI based on status
   }
   updateUploadStatus(uploadId, status) {
      const upload = this.uploads.get(uploadId);
      if (!upload) return;
      upload.status = status;
      this.updateUploadUI(uploadId);
   }
   updateUploadUI(uploadId) {
      const upload = this.uploads.get(uploadId);
      if (!upload?.element) return;
      // Update status classes
      upload.element.className = upload.element.className.replace(/status-[\w-]+/g, '');
      upload.element.classList.add(`status-${upload.status}`);
      // Update progress if showing
      const progress = upload.element.querySelector('.progress');
      if (progress) {
         this.updateUploadItemProgress(uploadId,
            this.getStatusProgress(upload.status),
            upload.status
         );
      }
   }
   /**
    * Show/hide progress indicator on individual upload items
    */
   showUploadProgress(uploadId, show = true) {
      const upload = this.uploads.get(uploadId);
      if (!upload || !upload.element) return;
      const progressEl = upload.element.querySelector('.progress');
      if (progressEl) {
         if (show) {
            progressEl.style.removeProperty('animation');
            progressEl.hidden = false;
         } else {
            progressEl.style.animation = 'fadeOut var(--transition-base)';
            setTimeout(() => {
               progressEl.hidden = true;
            }, 300);
         }
      }
   }
   /**
    * Update individual upload progress bar
    */
   updateUploadItemProgress(uploadId, percent, status = null) {
      const upload = this.uploads.get(uploadId);
      if (!upload || !upload.element) return;
      const progressEl = upload.element.querySelector('.progress');
      if (!progressEl) return;
      const fill = progressEl.querySelector('.fill');
      const details = progressEl.querySelector('.details');
      const icon = progressEl.querySelector('.icon');
      if (fill) {
         fill.style.width = `${percent}%`;
      }
      if (status && details) {
         details.textContent = this.getStatusText(status);
      }
      if (status && icon) {
         icon.innerHTML = this.getStatusIcon(status).outerHTML;
      }
   }
   checkFieldLimits(fieldId, additionalFiles) {
      const field = this.fields.get(fieldId);
      if (!field) return false;
      const currentCount = field.uploads?.size || 0;
      const totalCount = currentCount + additionalFiles;
      return totalCount <= field.maxFiles;
   }
   validateFile(file, field) {
      // Type validation
      if (!this.settings.allowedTypes.includes(file.type)) {
         this.notify(`Invalid file type: ${file.type}`, 'error');
         return false;
      }
      // Size validation
      if (file.size > this.settings.maxFileSize) {
         this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
         return false;
      }
      return true;
   }
   formatBytes(bytes, decimals = 2) {
      if (bytes === 0) return '0 Bytes';
      const k = 1024;
      const dm = decimals < 0 ? 0 : decimals;
      const sizes = ['Bytes', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
   }
   shouldProcessClientSide(file, subtype) {
      // Only process images client-side
      if (subtype === 'image' && file.type.startsWith('image/')) {
         return true;
      }
      // Videos and documents go straight to server
      return false;
   }
   /*******************************************************************************
    * IMAGE PROCESSING (simplified - keeping existing logic)
    *******************************************************************************/
   async processImage(file, uploadId) {
      const timeout = this.worker.settings.timeout;
@@ -1113,27 +660,19 @@
         let timeoutId;
         let taskCompleted = false;
         // Set timeout
         timeoutId = setTimeout(() => {
            if (!taskCompleted) {
               taskCompleted = true;
               // Remove from active tasks
               this.worker.tasks.delete(uploadId);
               // Maybe restart worker if configured
               if (this.worker.settings.restartAfterTimeout) {
                  this.restartCompressionWorker();
               }
               reject(new Error(`Processing timeout for ${file.name}`));
            }
         }, timeout);
         // Track this task
         this.worker.tasks.set(uploadId, { file, timeoutId });
         // Process image
         this.handleProcess(file, uploadId)
            .then(result => {
               if (!taskCompleted) {
@@ -1155,7 +694,6 @@
   }
   async handleProcess(file, uploadId) {
      // Skip non-images
      if (!file.type.startsWith('image/')) {
         return file;
      }
@@ -1163,14 +701,11 @@
      const maxDimension = this.getMaxDimension();
      const quality = 0.85;
      // Try worker first if available
      if (this.shouldUseWorker(file)) {
         try {
            // Ensure worker is initialized
            if (!this.worker.worker) {
               this.initCompressionWorker();
            }
            if (this.worker.worker) {
               return await this.processWithWorker(file, uploadId, maxDimension, quality);
            }
@@ -1179,13 +714,9 @@
         }
      }
      // Fallback to main thread
      return await this.processOnMainThread(file, maxDimension, quality);
   }
   /**
    * Process image on main thread with better error handling
    */
   async processOnMainThread(file, maxDimension, quality) {
      return new Promise((resolve, reject) => {
         const img = new Image();
@@ -1200,7 +731,6 @@
               URL.revokeObjectURL(objectUrl);
               objectUrl = null;
            }
            // Explicitly clean up canvas
            canvas.width = 1;
            canvas.height = 1;
            ctx.clearRect(0, 0, 1, 1);
@@ -1212,7 +742,6 @@
               canvas.width = width;
               canvas.height = height;
               // Enhanced image smoothing
               ctx.imageSmoothingEnabled = true;
               ctx.imageSmoothingQuality = 'high';
               ctx.drawImage(img, 0, 0, width, height);
@@ -1259,67 +788,41 @@
      });
   }
   /**
    * Get optimal output format
    */
   getOptimalFormat(file) {
      // Keep original format for certain types
      if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
         return file.type;
      }
      // Use WebP if supported, otherwise JPEG
      return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
   }
   /**
    * Get optimal quality setting
    */
   getOptimalQuality(file, requestedQuality) {
      // Higher quality for smaller files
      if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
      if (file.size < 2 * 1024 * 1024) return requestedQuality;
      // Lower quality for very large files
      return Math.min(requestedQuality, 0.8);
   }
   /**
    * Generate processed file name
    */
   getProcessedFileName(originalFile, outputFormat) {
      const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
      const extensions = {
         'image/webp': '.webp',
         'image/jpeg': '.jpg',
         'image/png': '.png',
         'image/gif': '.gif'
      };
      return baseName + (extensions[outputFormat] || '.jpg');
   }
   /**
    * Get maximum dimension based on device capabilities
    */
   getMaxDimension() {
      const screenWidth = window.screen.width;
      const devicePixelRatio = window.devicePixelRatio || 1;
      // Scale based on device capabilities
      if (screenWidth * devicePixelRatio > 2560) return 2400;
      if (screenWidth * devicePixelRatio > 1920) return 1920;
      return 1200;
   }
   /**
    * Determine if we should use Web Worker
    */
   shouldUseWorker(file) {
      // Use worker for large files or when available
      return this.worker.worker &&
         file.size > 1024 * 1024 && // > 1MB
         file.size > 1024 * 1024 &&
         typeof OffscreenCanvas !== 'undefined';
   }
@@ -1330,14 +833,11 @@
            return;
         }
         // Create unique message ID for this task
         const messageId = `${uploadId}_${Date.now()}`;
         // Handler for this specific message
         const messageHandler = (e) => {
            if (e.data.messageId !== messageId) return;
            // Remove handler
            this.worker.worker.removeEventListener('message', messageHandler);
            this.worker.worker.removeEventListener('error', errorHandler);
@@ -1359,11 +859,9 @@
            reject(new Error(`Worker error: ${error.message}`));
         };
         // Add handlers
         this.worker.worker.addEventListener('message', messageHandler);
         this.worker.worker.addEventListener('error', errorHandler);
         // Send message to worker
         this.worker.worker.postMessage({
            messageId,
            file,
@@ -1374,85 +872,45 @@
      });
   }
   /**
    * Restart compression worker
    */
   restartCompressionWorker() {
      // Terminate existing worker
      if (this.worker.worker) {
         this.worker.worker.terminate();
         this.worker.worker = null;
      }
      // Clear active tasks
      this.worker.tasks.clear();
      // Check restart limit
      if (this.worker.restart.count >= this.worker.restart.max) {
         console.error('Max worker restarts reached, disabling worker');
         return;
      }
      this.worker.restart.count++;
      // Reinitialize
      this.initCompressionWorker();
   }
   /**
    * Initialize Web Worker for image compression
    */
   initCompressionWorker() {
      if (this.worker.worker || typeof Worker === 'undefined') return;
      try {
         const workerScript = `
            self.onmessage = async function(e) {
                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
                try {
                    // Create ImageBitmap from file
                    const bitmap = await createImageBitmap(file);
                    // Calculate dimensions
                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
                    const width = Math.round(bitmap.width * scale);
                    const height = Math.round(bitmap.height * scale);
                    // Create OffscreenCanvas
                    const canvas = new OffscreenCanvas(width, height);
                    const ctx = canvas.getContext('2d');
                    // Draw and resize
                    ctx.imageSmoothingEnabled = true;
                    ctx.imageSmoothingQuality = 'high';
                    ctx.drawImage(bitmap, 0, 0, width, height);
                    // Clean up bitmap
                    bitmap.close();
                    // Convert to blob
                    const blob = await canvas.convertToBlob({
                        type: outputFormat,
                        quality: quality
                    });
                    self.postMessage({
                        messageId,
                        success: true,
                        blob: blob,
                        format: outputFormat
                    });
                } catch (error) {
                    self.postMessage({
                        messageId,
                        success: false,
                        error: error.message
                    });
                }
            };
        `;
            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));
@@ -1463,30 +921,18 @@
      }
   }
   /**
    * Calculate optimal dimensions with aspect ratio preservation
    */
   calculateOptimalDimensions(img, maxDimension) {
      let { width, height } = img;
      // Don't upscale
      if (width <= maxDimension && height <= maxDimension) {
         return { width, height };
      }
      // Calculate scale factor
      const scale = Math.min(maxDimension / width, maxDimension / height);
      return {
         width: Math.round(width * scale),
         height: Math.round(height * scale)
      };
   }
   /**
    * Check WebP support
    */
   supportsWebP() {
      const canvas = document.createElement('canvas');
      return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
@@ -1494,7 +940,6 @@
   createPreviewUrl(file) {
      const url = URL.createObjectURL(file);
      // Track for cleanup
      if (!this.previewUrls) this.previewUrls = new Set();
      this.previewUrls.add(url);
      return url;
@@ -1507,131 +952,19 @@
      }
   }
   maybeLockUploads(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field?.ui?.dropZone) return;
      if (field.config.destination === 'post_group') {
         return;
      }
      const uploadCount = field.uploads?.size || 0;
      const maxFiles = field.config?.maxFiles || 999;
      // Hide dropzone if at max files
      field.ui.dropZone.hidden = uploadCount >= maxFiles;
      // Update field state
      field.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
   }
   createUploadElement(upload, draggable = false) {
      let image = window.getTemplate('uploadItem');
      if (!image) {
         console.error('Image template not found');
         return;
      }
      image.dataset.uploadId = upload.id;
      if (upload.originalFile) {
         image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
      }
      image.querySelector('[name="featured"]').value = upload.id;
      let [
         featured,
         img,
         video,
         preview,
         details
      ] = [
         image.querySelector('[name="featured"]'),
         image.querySelector('img'),
         image.querySelector('video'),
         image.querySelector('label > span'),
         image.querySelector('details')
      ];
      [
         featured.value,
         img.src,
         img.alt
      ] = [
         upload.id,
         upload.preview,
         upload.originalFile?.name ?? upload.meta?.originalName ?? '',
      ];
      switch (image.dataset.subtype) {
         case 'image':
            [
               img.src,
               img.alt
            ] = [
               upload.preview,
               upload.originalFile?.name ?? upload.meta?.originalName?? ''
            ];
            video.remove();
            preview.remove();
            break;
         case 'video':
            video.src = upload.preview;
            img.remove();
            preview.remove();
            break;
         case 'document':
            const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
            const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
            const iconMap = {
               'pdf': 'file-pdf',
               'csv': 'file-csv',
               'doc': 'file-doc',
               'docx': 'file-doc',
               'txt': 'file-txt',
               'xls': 'file-xls',
               'xlsx': 'file-xls'
            };
            const icon = window.getIcon(iconMap[extension] || 'file');
            preview.innerText = upload.originalFile.name;
            preview.prepend(icon);
            img.remove();
            video.remove();
            break;
      }
      if (details) {
         let template = window.getTemplate('uploadMeta');
         if (template){
            details.append(template);
         }
      }
      image.draggable = draggable;
      // Update input IDs safely
      image.querySelectorAll('input').forEach(input => {
         let id = input.id;
         if (id) {
            let newId = id + upload.id;
            let label = input.parentNode.querySelector(`label[for="${id}"]`);
            input.id = newId;
            if (label) {
               label.htmlFor = newId;
            }
         }
      });
      return image;
   }
   /*******************************************************************************
    * QUEUE INTEGRATION
    *******************************************************************************/
   async submitUploads(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field?.uploads || field.uploads.size === 0) {
      const fieldData = this.getFieldData(fieldId);
      const fieldEl = this.fieldElements.get(fieldId);
      if (!fieldData?.uploads || fieldData.uploads.size === 0) {
         return;
      }
      let uploads = Array.from(field.uploads);
      if (uploads.length === 0) {
      let uploadIds = Array.from(fieldData.uploads);
      if (uploadIds.length === 0) {
         this.error.log('No uploads to upload', {
            component: 'UploadManager',
            action: 'submitGroupedUploads',
@@ -1656,97 +989,80 @@
      const formData = new FormData();
      let uploadMap = [];
      uploads = uploads.map((upload) => {
         return this.uploads.get(upload);
      });
      fieldGroups.forEach((group, groupIndex) => {
      // 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;
         }
         let groupUploads = uploads.filter((upload) => {
            return upload['groupId'] === group.id;
         // Get uploads for this group
         const groupUploadIds = uploadIds.filter(uploadId => {
            const upload = this.uploadStore.get(uploadId);
            return upload?.groupId === group.id;
         });
         groupUploads.forEach((upload) => {
            if (upload) {
               const fileToUpload = upload.processedFile || upload.originalFile;
               if (fileToUpload) {
                  formData.append('files[]', fileToUpload);
                  const imageData = {
                     upload_id: upload.id,
                     index: uploadMap.length
                  };
                  post.images.push(imageData);
                  uploadMap.push(upload.id);
               }
            }
         });
         // Add images for this group
         // group.uploads.forEach(uploadId => {
         //    const upload = this.uploads.get(uploadId);
         //    if (upload) {
         //       const fileToUpload = upload.processedFile || upload.originalFile;
         //       if (fileToUpload) {
         //          formData.append('files[]', fileToUpload);
         //
         //          const imageData = {
         //             upload_id: upload.id,
         //             index: uploadMap.length
         //          };
         //
         //          // Check if this is the featured image
         //          const radioInput = upload.element?.querySelector('[name="featured"]');
         //          if (radioInput?.checked) {
         //             post.fields.featured = upload.id;
         //          }
         //
         //          post.images.push(imageData);
         //          uploadMap.push(upload.id);
         //       }
         //    }
         // });
         posts.push(post);
      });
      //Each remaining upload (without a groupId) becomes its own post
      let remainingUploads = uploads.filter((upload) => {
         return !Object.hasOwn(upload, 'groupId');
      });
      remainingUploads.forEach((upload) => {
         if (upload) {
            const post = {
               images: [],
               fields: {}
            };
            const fileToUpload = upload.processedFile || upload.originalFile;
            if (fileToUpload) {
               formData.append('files[]', fileToUpload);
         // 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: upload.id,
                  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(upload.id);
               uploadMap.push(uploadId);
            }
            posts.push(post);
         }
         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', field.config.content);
      formData.append('user', field.config.itemID); // Assuming itemID is user ID
      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));
@@ -1754,7 +1070,7 @@
         endpoint: 'uploads/groups',
         method: 'POST',
         data: formData,
         title: `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
         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: {
@@ -1766,15 +1082,20 @@
      try {
         const operationId = await this.queue.addToQueue(operation);
         uploads.forEach(uploadId => {
            let upload = this.uploads.get(uploadId);
         // 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');
            }
         });
         field.operationId = operationId;
         fieldData.operationId = operationId;
         await this.saveFieldData(fieldData);
         this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
         return operationId;
@@ -1785,181 +1106,107 @@
            fieldId: fieldId
         });
         throw error;
      } finally {
         this.schedulePersistance(field.id);
      }
   }
   async queueUpload(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field?.uploads) return;
      const fieldData = this.getFieldData(fieldId);
      if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
      const uploads = Array.from(field.uploads);
      if (uploads.length === 0) {
         return;
      }
      const uploads = Array.from(fieldData.uploads);
      const data = this.prepareUploadData(fieldData, uploads);
      const data = this.prepareUploadData(field, uploads);
      this.a11y.announce('Queuing for upload');
      let img = (uploads.length === 1) ? 'file' : 'files';
      const operation = {
         endpoint: 'uploads',
         method: 'POST',
         data: data,
         title: `Uploading ${uploads.length} ${img} to server...`,
         popup: `Uploading ${uploads.length} ${img}...`,
         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': jvbSettings.dash
         },
         headers: { 'action_nonce': jvbSettings.dash },
         append: '_upload'
      }
      };
      try {
         const operationId = await this.queue.addToQueue(operation);
         // Update upload statuses
         uploads.forEach(uploadId => {
            let upload = this.uploads.get(uploadId);
            if (!upload) {
               return;
            const upload = this.uploadStore.get(uploadId);
            if (upload) {
               upload.operationId = operationId;
               upload.status = 'queued';
               this.uploadStore.save(upload);
               this.updateUploadStatus(uploadId, 'queued');
            }
            upload.operationId = operationId;
            this.updateUploadStatus(uploadId, 'queued');
         });
         field.operationId = operationId;
         fieldData.operationId = operationId;
         await this.saveFieldData(fieldData);
         return operationId;
      } catch (error) {
         throw error;
      } finally {
         this.schedulePersistance(field.id);
      }
   }
   prepareUploadData(field, uploads) {
   async prepareUploadData(fieldData, uploads) {
      const formData = new FormData();
      formData.append('content', field.config.content);
      formData.append('mode', field.config.mode);
      formData.append('field_name', field.config.name);
      formData.append('fieldId', field.id);
      formData.append('field_type', field.config.type);
      formData.append('subtype', field.config.subtype);
      formData.append('item_id', field.config.itemID);      //post, term, or user id
      formData.append('destination', field.config.destination || 'meta'); //meta, post, post_group
      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 fieldGroups = this.getFieldGroups(field.id);
      if (field.config.destination === 'post_group' && fieldGroups.length > 0) {
         // User has created groups
         let groups = [];
         let titles = [];
         let featuredImages = [];
         fieldGroups.forEach(group => {
            let groupUploadIndices = [];
            let featuredIndex = null;
      const blobPromises = uploads.map(async (uploadId) => {
         const upload = this.uploadStore.get(uploadId);
         if (!upload) return;
            group.uploads.forEach(uploadId => {
               let upload = this.uploads.get(uploadId);
               if (upload) {
                  const fileToUpload = upload.processedFile || upload.originalFile;
                  if (fileToUpload) {
                     formData.append('files[]', fileToUpload);
                     const fileIndex = uploadMap.length;
                     uploadMap.push(upload.id);
                     groupUploadIndices.push(upload.id);
                     // Check if this is the featured image
                     const radioInput = upload.element?.querySelector('[name="featured"]');
                     if (radioInput?.checked) {
                        featuredIndex = upload.id;
                     }
                  }
               }
            });
            groups.push(groupUploadIndices);
            titles.push(group.title || '');
            featuredImages.push(featuredIndex);
         });
         formData.append('groups', JSON.stringify(groups));
         formData.append('group_titles', JSON.stringify(titles));
         formData.append('featured_images', JSON.stringify(featuredImages));
      } else {
         // No groups - just append all files
         uploads.forEach(uploadId => {
            let upload = this.uploads.get(uploadId);
            if (upload) {
               const fileToUpload = upload.processedFile || upload.originalFile;
               if (fileToUpload) {
                  formData.append('files[]', fileToUpload);
                  uploadMap.push(upload.id);
               }
            }
         });
      }
      formData.append('upload_ids', JSON.stringify(uploadMap));
      // console.log('Final FormData:');
      // for (let pair of formData.entries()) {
      //    console.log(pair[0], pair[1]);
      // }
      return formData;
   }
   getFieldGroups(fieldId) {
      const groups = [];
      this.groups.forEach((groupData, groupId) => {
         if (groupData.fieldId === fieldId) {
            const field = this.fields.get(fieldId);
            const groupElement = field?.ui?.groups?.groups?.get(groupId);
            groups.push({
               id: groupId,
               uploads: Array.from(groupData.uploads || new Set()),
               changes: groupData.changes || {},
               element: groupElement || null
            });
         const file = await this.getBlobData(uploadId);
         if (file) {
            formData.append('files[]', file);
            uploadMap.push(upload.id);
         }
      });
      return groups;
      await Promise.all(blobPromises);
      formData.append('upload_ids', JSON.stringify(uploadMap));
      return formData;
   }
   async queueUploadMeta(e) {
      const upload = this.getUploadFromElement(e.target);
      const uploadId = this.getUploadIdFromElement(e.target);
      const upload = this.uploadStore.get(uploadId);
      if (!upload) return;
      const field = this.fields.get(upload.fieldId);
      if (!field) return;
      const container = e.target.closest('.upload-meta');
      if (!container) return;
      const fieldData = this.getFieldData(upload.fieldId);
      if (!fieldData) return;
      let data = {};
      data[e.target.name] = e.target.value;
      upload.meta = {
         ...upload.meta,
         ... data
      };
      upload.meta = { ...upload.meta, ...data };
      await this.uploadStore.save(upload);
      let queueData = {};
      //If there is an attachment ID, use that: else, use our generated upload id
      queueData[upload.attachmentId??upload.id] = upload.meta;
      queueData[upload.attachmentId ?? upload.id] = upload.meta;
      const operation = {
         endpoint:   'uploads/meta',
         method:     'POST',
         data:    queueData,
         title:      `Updating meta`,
         canMerge:   true,
         headers: {
            'action_nonce': jvbSettings.dash
         }
         endpoint: 'uploads/meta',
         method: 'POST',
         data: queueData,
         title: 'Updating meta',
         canMerge: true,
         headers: { 'action_nonce': jvbSettings.dash }
      };
      try {
@@ -1972,441 +1219,109 @@
         });
      }
   }
   /*******************************************************************************
    * GROUP MANAGEMENT
    * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
    *******************************************************************************/
   createGroup(fieldKey, groupId = null) {
      const field = this.fields.get(fieldKey);
      if (!field) {
         console.error('Field not found:', fieldKey);
         return null;
      }
   /**
    * Handle successful operation completion - CLEAR STORES
    */
   async handleOperationComplete(operation, fieldId) {
      const results = operation.result?.data || operation.serverData?.data || [];
      if (!groupId) {
         groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      }
      const groupElement = this.createGroupElement(groupId, fieldKey);
      if (!groupElement) {
         console.error('Failed to create group element');
         return null;
      }
      // Store in field UI Map
      if (!field.ui.groups) {
         field.ui.groups = {
            groups: new Map(),
            container: null,
            empty: null,
            display: null
         };
      }
      field.ui.groups.groups.set(groupId, groupElement);
      // Insert into DOM
      if (field.ui.groups.container && field.ui.groups.empty) {
         field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
      } else if (field.ui.groups.container) {
         field.ui.groups.container.appendChild(groupElement);
      }
      // Create group object
      const group = {
         id: groupId,
         fieldId: fieldKey,
         element: groupElement,
         grid: groupElement.querySelector('.item-grid.group'),
         uploads: new Set(),
         changes: {}
      };
      // Store group
      this.groups.set(groupId, group);
      // Initialize selection handler for this group
      this.addGroupSelectionHandler(fieldKey, groupId);
      // Persist state
      this.schedulePersistance(fieldKey);
      return group;
   }
   createGroupElement(groupId, fieldId) {
      let groupElement = window.getTemplate('imageGroup');
      if (!groupElement) return;
      groupElement.dataset.groupId = groupId;
      groupElement.dataset.fieldId = fieldId;
      let fields = window.getTemplate('groupMetadata');
      const fieldsContainer = groupElement.querySelector('.fields');
      if (fieldsContainer && fields) {
         fieldsContainer.append(fields);
         // Set unique IDs and names for form fields
         const titleInput = fieldsContainer.querySelector('[name="post_title"]');
         const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
         if (titleInput) {
            titleInput.id = `${groupId}_title`;
            titleInput.name = `${groupId}[post_title]`;
      // 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 (excerptInput) {
            excerptInput.id = `${groupId}_excerpt`;
            excerptInput.name = `${groupId}[post_excerpt]`;
         }
         let field = this.fields.get(fieldId);
         if (field.config.content !== '') {
            let summary = groupElement.querySelector('summary');
            summary.textContent = field.config.content + ' Fields';
         }
      });
      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 {
         groupElement.querySelector('details').remove();
         // Otherwise just update field state
         await this.saveFieldData(fieldData);
      }
      const gridContainer = groupElement.querySelector('.item-grid.group');
      if (gridContainer) {
         gridContainer.dataset.groupId = groupId;
      }
      return groupElement;
   }
   deleteGroup(groupId, confirm = true) {
      let group = this.groups.get(groupId);
      if (!group) {
         return;
      }
      let keepUploads = true;
      if (confirm && group.uploads && group.uploads.size > 0) {
         keepUploads = !window.confirm('Delete uploads in group?');
      }
      if (confirm && keepUploads) {
         // Move any remaining uploads back to preview
         if (group.uploads && group.uploads.size > 0) {
            Array.from(group.uploads).forEach(uploadId => {
               this.addImageToGroup(uploadId, null, false);
            });
         }
      }
      // Remove from groups Map
      this.groups.delete(groupId);
      // Remove DOM element
      let groupElement = group.element;
      if (groupElement) {
         groupElement.remove();
         this.a11y.announce('Group removed');
      }
      this.schedulePersistance(group.fieldId);
   }
   addToGroup(uploadId, target = null, persist = true) {
      let upload = this.uploads.get(uploadId);
      if(!upload) {
         return;
      }
      let field = this.fields.get(upload.fieldId);
      if (!field) {
         return;
      }
      //Already in the Preview Grid, or already in the group we're moving to
      if ((!target && upload.location === field.ui.preview) || target === upload.location) {
         return;
      }
      // Remove from previous location
      if (upload.location) {
         let groupId = upload.location.dataset.groupId;
         if (groupId) {
            let group = this.groups.get(groupId);
            if (group && group.uploads) {
               group.uploads.delete(uploadId);
               if (group.uploads.size === 0) {
                  this.deleteGroup(groupId);
               }
            }
         }
      }
      const checkbox = upload.element.querySelector('[name*="select-item"]');
      if (checkbox) {
         checkbox.checked = false;
      }
      let featured = upload.element.querySelector('[name="featured"]');
      featured.hidden = !target;
      //If no target, it's going to the preview grid
      if (!target) {
         target = field.ui.preview;
         upload.groupId = null;
      } else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
         // It's a group target
         let groupId = target.dataset.groupId;
         featured.name = groupId+'_'+featured.name;
         let group = this.groups.get(groupId);
         if (!group) {
            group = this.createGroup(upload.fieldId);
            target = group.grid;
            groupId = group.id;
         }
         if (group) {
            group.uploads.add(uploadId);
            upload.groupId = groupId;
         }
      }
      upload.location = target;
      target.append(upload.element);
      if (persist) {
         this.schedulePersistance(field.id);
      }
   }
   removeFromGroup(uploadId) {
      const upload = this.uploads.get(uploadId);
      if (!upload) return;
      const field = this.fields.get(upload.fieldId);
      if (!field) return;
      // Remove from current group if in one
      if (upload.groupId) {
         const group = this.groups.get(upload.groupId);
         if (group?.uploads) {
            group.uploads.delete(uploadId);
            // Delete empty group
            if (group.uploads.size === 0) {
               this.deleteGroup(upload.groupId, false);
            }
         }
         upload.groupId = null;
      }
      // Move back to preview
      if (field.ui?.preview) {
         field.ui.preview.appendChild(upload.element);
         upload.location = field.ui.preview;
      }
      // Hide featured radio
      const featured = upload.element.querySelector('[name="featured"]');
      if (featured) {
         featured.hidden = true;
         featured.checked = false;
      }
   }
   removeUpload(fieldId, uploadId) {
      const field = this.fields.get(fieldId);
      const upload = this.uploads.get(uploadId);
      if (!field || !upload) return;
      // Remove from field
      field.uploads?.delete(uploadId);
      // Remove from group if grouped
      if (upload.groupId) {
         const group = this.groups.get(upload.groupId);
         if (group && group.uploads) {
            group.uploads.delete(uploadId);
            if (group.uploads.size === 0) {
               this.removeGroup(upload.groupId);
            }
         }
      }
      // Clean up element
      upload.element?.remove();
      // Clean up memory
      this.clearUpload(uploadId);
      // Update field state after removal
      this.updateFieldState(fieldId);
      // Update UI
      this.maybeLockUploads(fieldId);
      const handler = this.selectionHandlers.get(field.id);
      if (handler) {
         handler.deselect(uploadId);
      }
      this.a11y.announce('Upload removed');
   }
   /*******************************************************************************
    * STATE MANAGEMENT
    *******************************************************************************/
   schedulePersistance(fieldId) {
      const key = `persist_${fieldId}`;
      window.debouncer.schedule(
         key,
         () => this.persistFieldState(fieldId),
         1000
      );
   }
   /**
    * Handle operation failure
    */
   handleOperationFailed(operation, fieldId) {
      const uploadIds = operation.data instanceof FormData
         ? JSON.parse(operation.data.get('upload_ids') || '[]')
         : operation.data.upload_ids || [];
   async persistFieldState(fieldId) {
      const field = this.fields.get(fieldId);
      if (!field) return;
      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);
         }
      });
      // Convert Sets to Arrays for storage
      const fieldData = {
         ...field,
         id: fieldId, // Use as primary key
         fieldId: fieldId,
         uploads: Array.from(field.uploads || []).map(uploadId => {
            return this.uploads.get(uploadId);;
         }),
         groups: Array.from(this.groups.entries())
            .filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
            .map(([id, data]) => ({
               id: data.id,
               uploads: Array.from(data.uploads),
               changes: data.changes || {}
            })),
         // Context for restoration
         context: {
            url: this.normalizeUrl(window.location.href),
            fullUrl: window.location.href,
            modalType: this.getModalType(field),
            formId: field.formId,
            fieldSelector: `.field.upload[data-field="${field.config.name}"]`
         },
         timestamp: Date.now()
      };
      // Save to store
      await this.fieldStore.save(fieldData);
   }
   normalizeUrl(url) {
      try {
         const urlObj = new URL(url);
         // Return just the origin + pathname (no query string or hash)
         return urlObj.origin + urlObj.pathname;
      } catch (e) {
         return url;
      if (fieldId) {
         this.updateFieldState(fieldId);
      }
   }
   /**
    * Get uploads for a field, optionally cleaned for storage
    * @param {string} fieldId
    * @param {boolean} clean - Remove DOM references for IndexedDB storage
    * @returns {Array}
    * Handle operation cancellation
    */
   getFieldUploads(fieldId, clean = false) {
      const field = this.fields.get(fieldId);
      if (!field || !field.uploads) return [];
   async handleOperationCancelled(fieldId) {
      const fieldData = this.getFieldData(fieldId);  // ✅
      if (!fieldData) return;
      return Array.from(field.uploads)
         .map(uploadId => {
            const upload = this.uploads.get(uploadId);
            if (!upload) return null;
      const uploadsArray = fieldData.uploads instanceof Set
         ? Array.from(fieldData.uploads)
         : fieldData.uploads;
            if (clean) {
               // Return cleaned version without DOM references or blob URLs
               return {
                  id: upload.id,
                  fieldId: upload.fieldId,
                  status: upload.status,
                  // DON'T include preview (blob URL)
                  // DON'T include originalFile or processedFile (in blob storage)
                  attachmentId: upload.attachmentId,
                  operationId: upload.operationId,
                  groupId: upload.groupId || null,
                  changes: upload.changes || {}, // ← ADD: Include changes
                  meta: {
                     originalName: upload.meta?.originalName || upload.originalFile?.name,
                     size: upload.meta?.size || upload.originalFile?.size,
                     type: upload.meta?.type || upload.originalFile?.type,
                     title: upload.meta?.title,
                     alt: upload.meta?.alt,
                     caption: upload.meta?.caption
                  }
               };
            }
            // Return full upload object
            return upload;
         })
         .filter(Boolean);
   }
   async checkForStoredUploads() {
      if (!this.db) return;
      const tx = this.db.transaction(['fieldStates'], 'readonly');
      const fieldStore = tx.objectStore('fieldStates');
      const allFieldStates = await new Promise(resolve => {
         const request = fieldStore.getAll();
         request.onsuccess = () => resolve(request.result);
      });
      //
      // allFieldStates.forEach(field => {
      //    console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
      //    field.uploads.forEach((upload, idx) => {
      //       console.log(`  Upload ${idx}:`, {
      //          id: upload.id,
      //          status: upload.status,
      //          operationId: upload.operationId,
      //          hasOperationId: !!upload.operationId
      //       });
      //    });
      // });
      // Filter for pending uploads (not yet sent to server)
      const pendingFields = allFieldStates.filter(field =>
         field.uploads.some(upload =>
            // If no operationId, it hasn't been sent to server yet
            !upload.operationId &&
            // And it's been processed locally
            (upload.status === 'completed' ||
               upload.status === 'processed' ||
               upload.status === 'local_processing' ||
               upload.status === 'processed-original')
         )
      );
      if (pendingFields.length === 0) return;
      // Show recovery notification
      this.showRecoveryNotification(pendingFields);
   }
   async handleRestoreUploads() {
      let notification = document.querySelector('dialog.restore-uploads');
      if (!notification) {
         return;
      for (const uploadId of uploadsArray) {
         await this.clearUpload(uploadId, false);
      }
      const selectedUploads = this.getSelectedRestorationUploads(notification);
      if (selectedUploads.length === 0) {
         return;
      }
      await this.restoreSelectedUploads(selectedUploads);
      await this.clearFieldFromStores(fieldId);
      this.updateFieldState(fieldId);
      this.a11y.announce('Upload cancelled');
   }
      this.cleanupRestore();
   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) {
@@ -2426,29 +1341,1347 @@
      return selected;
   }
   handleGroupMetaChange(input) {
      let group = this.getGroupFromElement(input);
      if (!group) {
         return;
   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);
         }
      }
      if (!Object.hasOwn(group, 'changes')) {
         group.changes = {};
      }
      let name = input.name;
      if (name.includes('group')) {
         let replace = group.id+'_';
         let replace2 = group.id+'[';
         name = name.replace(replace, '').replace(replace2,'').replace(']', '');
      }
      group.changes[`${name}`] = input.value;
      this.groups.set(group.id, group);
      this.schedulePersistance(group.fieldId);
   }
   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) {
         // Create group using existing method which handles all initialization
         const group = this.createGroup(fieldId, groupData.id);
         if (!group) {
            console.warn('Failed to create group:', groupData.id);
            continue;
         }
         // Find the group in fieldData (createGroup already added it)
         const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
         if (storedGroup) {
            // Restore metadata
            if (groupData.changes) {
               storedGroup.changes = { ...groupData.changes };
            }
            if (groupData.uploads) {
               storedGroup.uploads = [...groupData.uploads];
            }
            // Restore form field values if they exist
            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;
               }
            }
         }
      }
      // Save updated field data
      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];
   }
   /*******************************************************************************
    * RESTORING UPLOADS
    * 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;
      if (fieldData.config.destination === 'post_group') return;
      const uploadCount = fieldData.uploads?.size || 0;
      const maxFiles = fieldData.config?.maxFiles || 999;
      fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
      fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
   }
   /*******************************************************************************
    * GROUP MANAGEMENT
    *******************************************************************************/
   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 = [];
      fieldData.groups.push({
         id: groupId,
         uploads: [],
         changes: {}
      });
      this.saveFieldData(fieldData);
      // Initialize selection handler and sortable
      this.addGroupSelectionHandler(fieldId, groupId);
      // Initialize sortable for this new group grid
      if (grid) {
         const sortableInstance = new Sortable(grid, {
            ...this.sortableConfig,
            group: { name: fieldId, pull: true, put: true },
            disabled: true, // Empty initially
            onAdd: (evt) => this.updateSortableState(evt.to),
            onRemove: (evt) => this.updateSortableState(evt.from)
         });
         grid.sortableInstance = sortableInstance;
         this.sortableInstances.set(`${fieldId}-group-${groupId}`, sortableInstance);
      }
      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 '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':
            case 'item-deselected':
            case 'range-selected':
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'select-all':
               this.handleSelectAll(data.container, data.selected);
               break;
         }
      });
      this.selectionHandlers.set(fieldId, handler);
      return handler;
   }
   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]);
   }
   getStatusProgress(status) {
      const progress = {
         'local_processing': 28,
         'queued': 50,
         'uploading': 66,
         'pending': 75,
         'processing': 89,
         'completed': 100
      };
      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
    *******************************************************************************/
   /**
    * 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),
         1000
      );
   }
   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()
      };
      await this.uploadStore.save(uploadData);
   }
   async getBlobData(uploadId) {
      const upload = this.uploadStore.get(uploadId);
      if (!upload?.blobData) return null;
      // 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
      });
   }
   /*******************************************************************************
    * RECOVERY & RESTORATION
    *******************************************************************************/
   handleFieldStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            // Check for pending uploads to restore
            this.checkForStoredUploads();
            break;
      }
   }
   handleUploadStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            break;
         case 'item-saved':
            this.showSaveIndicator(data.key);
            break;
      }
   }
   async checkForStoredUploads() {
      const allFieldStates = this.fieldStore.getAll();
      console.log('Checking for stored uploads...', {
         fieldStates: allFieldStates.length,
         uploadStoreSize: this.uploadStore.data.size
      });
      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);
         });
      });
      console.log('Found pending fields:', pendingFields.length);
      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) =>
@@ -2497,14 +2730,12 @@
            //    const imgEl = uploadItem.querySelector('img');
            //    const placeholderEl = uploadItem.querySelector('.image-placeholder');
            //
            const blobData = await this.uploadStore.getBlob(upload.id);
            const file = await this.getBlobData(upload.id);
            if (file) {
            if (blobData) {
               try {
                  // Create new blob URL from stored data
                  const blob = new Blob([blobData.data], { type: blobData.type });
                  const previewUrl = this.createPreviewUrl(blob);
                  const previewUrl = this.createPreviewUrl(file);
                  let [
                     featured,
@@ -2525,7 +2756,7 @@
                  uploadItem.dataset.fieldId = field.id;
                  let subtype = this.getSubtypeFromMime(blobData.type);
                  let subtype = this.getSubtypeFromMime(file.type);
                  uploadItem.dataset.subtype = subtype;
                  switch (subtype) {
                     case 'image':
@@ -2534,7 +2765,7 @@
                           img.alt
                        ] = [
                           previewUrl,
                           upload.originalFile?.name ?? upload.meta?.originalName?? ''
                           file.name ?? upload.meta?.originalName ?? ''
                        ];
                        video.remove();
                        preview.remove();
@@ -2629,313 +2860,23 @@
   }
   async restoreSelectedUploads(selectedUploads) {
      // Group by field
      const byField = new Map();
      selectedUploads.forEach(item => {
         if (!byField.has(item.fieldId)) {
            byField.set(item.fieldId, []);
         }
         byField.get(item.fieldId).push(item.uploadId);
      });
      // Get full field states from IndexedDB
      if (!this.db) {
         // this.notifications.add('Cannot restore: Database not available', 'error');
   async handleRestoreUploads() {
      let notification = document.querySelector('dialog.restore-uploads');
      if (!notification) {
         return;
      }
      const tx = this.db.transaction(['fieldStates'], 'readonly');
      const store = tx.objectStore('fieldStates');
      for (const [fieldId, uploadIds] of byField.entries()) {
         const request = store.get(fieldId);
         const fieldState = await new Promise(resolve => {
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => resolve(null);
         });
         if (fieldState) {
            // Filter to only selected uploads
            fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
            await this.restoreField(fieldState);
         }
      }
      // this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
   }
   async restoreField(fieldState) {
      const { config, context, uploads, groups, id } = fieldState;  // ← Use 'id'
      // 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);
      const selectedUploads = this.getSelectedRestorationUploads(notification);
      if (selectedUploads.length === 0) {
         return;
      }
      await this.restoreSelectedUploads(selectedUploads);
      // Register the field if not already registered
      let fieldKey = fieldElement.dataset.uploader;
      if (!fieldKey || !this.fields.has(fieldKey)) {
         fieldKey = this.registerUploader(fieldElement, config);
      }
      const field = this.fields.get(fieldKey);
      if (!field) {
         console.error('Failed to register field for restoration');
         return;
      }
      // Merge saved state back into field
      field.state = fieldState.state || 'ready';
      // Rebuild UI references
      field.ui = this.buildFieldUI(fieldElement);
      if (field.ui.groups?.display) {
         field.ui.groups.display.hidden = false;
      }
      // Restore groups
      if (groups && groups.length > 0) {
         await this.restoreGroups(fieldKey, groups);
      }
      // Restore uploads
      for (const uploadData of uploads) {
         await this.restoreUpload(field, uploadData);
      }
      // Update UI
      this.updateFieldState(fieldKey);
      this.maybeLockUploads(fieldKey);
      // Queue for upload if needed
      if (config.mode === 'direct' && config.destination !== 'post_group') {
         await this.queueUpload(fieldKey);
      }
      this.cleanupRestore();
   }
   async restoreUpload(field, uploadData) {
      // Try to get blob data from IndexedDB
      const blobData = await this.uploadStore.getBlob(uploadData.id);
      if (blobData) {
         const file = blobData.data instanceof File
            ? blobData.data
            : new File(
               [blobData.data],
               blobData.name,
               { type: blobData.type, lastModified: blobData.lastModified }
            );
         uploadData.originalFile = file;
         uploadData.processedFile = file;
         uploadData.preview = this.createPreviewUrl(file);
      } else {
         console.warn('Blob data not found for upload:', uploadData.id);
         return; // Skip this upload if we can't restore the file
      }
      // Add to field
      if (!field.uploads) field.uploads = new Set();
      field.uploads.add(uploadData.id);
      // Recreate DOM element
      const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
      uploadData.element = this.createUploadElement({
         ...uploadData,
         subtype: subtype
      }, field.config.destination === 'post_group');
      // Restore to correct location
      let location;
      if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
         location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
      } else {
         location = field.ui.preview;
      }
      if (location) {
         location.appendChild(uploadData.element);
         uploadData.location = location;
      }
      // Store in memory
      this.uploads.set(uploadData.id, uploadData);
      if (uploadData.groupId) {
         const group = this.groups.get(uploadData.groupId);
         if (group && group.uploads) {
            group.uploads.add(uploadData.id);
         }
      }
   }
   async restoreGroups(fieldKey, groups) {
      for (const groupData of groups) {
         // Use createGroup which properly initializes EVERYTHING including selection handlers
         const group = this.createGroup(fieldKey, groupData.id);
         if (group) {
            // Update the group metadata from saved state
            if (groupData.meta) {
               group.meta = { ...groupData.meta };
            }
            if (groupData.changes) {
               group.changes = { ...groupData.changes };
            }
            // If you saved group titles, restore them
            if (groupData.title) {
               const titleInput = group.element.querySelector('[name*="post_title"]');
               if (titleInput) {
                  titleInput.value = groupData.title;
               }
            }
         }
      }
   }
   async openModalForRestore(context) {
      const { modalType, formId } = context;
      // Find and click the appropriate button to open the modal
      let trigger = null;
      switch(modalType) {
         case 'create':
            trigger = document.querySelector('[data-action="create"]');
            break;
         case 'edit':
            // Need to find the specific edit button
            trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
            break;
         case 'bulkEdit':
            trigger = document.querySelector('[data-action="bulk-edit"]');
            break;
      }
      if (trigger) {
         trigger.click();
         // Wait for modal to open
         await new Promise(resolve => setTimeout(resolve, 300));
      }
   }
   /*******************************************************************************
    INDEXEDDB CACHE FUNCTIONALITY
    *******************************************************************************/
   handleFieldStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            break;
         case 'item-saved':
            console.log(`Field state saved: ${data.key}`);
            break;
      }
   }
   handleUploadStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            this.checkForStoredUploads();
            break;
         case 'item-saved':
            this.showSaveIndicator(data.key);
            break;
      }
   }
   async saveUpload(upload) {
      // Handle blob data separately
      if (upload.file instanceof File || upload.file instanceof Blob) {
         await this.uploadStore.saveBlob(upload.id, upload.file);
         // Don't store the file in the main store
         const { file, originalFile, ...cleanUpload } = upload;
         await this.uploadStore.save(cleanUpload);
      } else {
         await this.uploadStore.save(upload);
      }
   }
   async loadFields() {
      // Load all field states from the store
      const fields = await this.fieldStore.getAll();
      fields.forEach(field => {
         // Reconstruct upload sets
         if (field.uploads && Array.isArray(field.uploads)) {
            field.uploads = new Set(field.uploads.map(u => u.id));
         }
         this.fields.set(field.fieldId, field);
      });
   }
   async loadUploads() {
      const uploads = await this.uploadStore.getAll();
      uploads.forEach(upload => {
         this.uploads.set(upload.id, upload);
      });
   }
   /**************************************************************************
    SUBSCRIBERS
    **************************************************************************/
   /**
    * Event system
    */
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   notify(event, data) {
      this.subscribers.forEach(cb => cb(event, data));
   }
   /*******************************************************************************
    * CLEANUP
    *******************************************************************************/
   destroy() {
      // Remove core listeners
      document.removeEventListener('click', this.clickHandler);
      document.removeEventListener('change', this.changeHandler);
      document.removeEventListener('dragenter', this.dragEnterHandler);
      document.removeEventListener('dragleave', this.dragLeaveHandler);
      document.removeEventListener('dragover', this.dragOverHandler);
      document.removeEventListener('drop', this.dropHandler);
      // Destroy drag controller
      if (this.dragController) {
         this.dragController.destroy();
      }
      // Destroy selection handlers
      this.selectionHandlers.forEach(handler => handler.destroy());
      this.selectionHandlers.clear();
      this.cleanupAllPreviewUrls();
      // Clear data
      this.fields.clear();
      this.uploads.clear();
      this.groups.clear();
      this.selected.clear();
      this.subscribers.clear();
   showSaveIndicator(key) {
      // Optional: show user that state is being saved
   }
   cleanupRestore() {
@@ -2948,65 +2889,60 @@
   }
   async cleanupStoredUploads() {
      this.fieldStore.clear();
      this.uploadStore.clear();
      await this.fieldStore.clear();
      await this.uploadStore.clear();
   }
   /**
    * Clear all uploads for a field and cleanup resources
    */
   async clearField(fieldId) {
      // Clear from stores
      await this.fieldStore.delete(fieldId);
   /*******************************************************************************
    * EVENT SYSTEM
    *******************************************************************************/
      // Clear related uploads
      const field = this.fields.get(fieldId);
      if (field?.uploads) {
         for (const uploadId of field.uploads) {
            await this.uploadStore.delete(uploadId);
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   notify(event, data = {}) {
      this.subscribers.forEach(cb => {
         try {
            cb(event, data);
         } catch (error) {
            console.error('Subscriber error:', error);
         }
      }
      // Clear from memory
      this.fields.delete(fieldId);
      });
   }
   async clearUpload(uploadId, persist = true) {
      const upload = this.uploads.get(uploadId);
      if (!upload) return;
   /*******************************************************************************
    * DESTROY & CLEANUP
    *******************************************************************************/
      // Clean up preview URL using helper
      this.revokePreviewUrl(upload.preview);
   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);
      // Clean up element preview URL
      if (upload.element) {
         const previewUrl = upload.element.dataset.previewUrl;
         this.revokePreviewUrl(previewUrl);
         delete upload.element.dataset.previewUrl;
      if (this.dragController) {
         this.dragController.destroy();
      }
      if (persist) {
         await this.schedulePersistance(upload.fieldId);
      }
      this.selectionHandlers.forEach(handler => handler.destroy());
      this.selectionHandlers.clear();
      // Remove from memory
      this.uploads.delete(uploadId);
      this.cleanupAllPreviewUrls();
      // Remove from IndexedDB
      this.uploadStore.delete(uploadId);
      this.uploadStore.delete(uploadId, 'blobs');
   }
   cleanupAllPreviewUrls() {
      if (this.previewUrls) {
         this.previewUrls.forEach(url => {
            try {
               URL.revokeObjectURL(url);
            } catch (e) {
               // Ignore errors during cleanup
            }
         });
         this.previewUrls.clear();
      }
      this.sortableInstances.forEach(instance => {
         if (instance?.destroy) instance.destroy();
      });
      this.sortableInstances.clear();
      this.uploadElements.clear();
      this.fieldElements.clear();
      this.groupElements.clear();
      this.selected.clear();
      this.subscribers.clear();
   }
}