From 474109a5df0a06f5343ab184838fe2d80e3872a8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 11 Jan 2026 19:23:20 +0000
Subject: [PATCH] =Fixed timeline CRUD.js issue where this.activeItem was set null when we still needed it

---
 inc/meta/MetaForm.php | 3421 ++++++++++++++++++++++++++--------------------------------
 1 files changed, 1,529 insertions(+), 1,892 deletions(-)

diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index d6846c4..2324496 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -6,7 +6,7 @@
 use DateTime;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
 
 /**
@@ -14,25 +14,31 @@
  */
 class MetaForm
 {
-    protected int $max_file_size = 5242880;
+	protected int $max_file_size = 5242880;
+	protected ?MetaTypeManager $type_manager = null;
 
-    //Rendering fields
-    public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false):mixed
-    {
+	/* ========== MAIN RENDER METHOD ========== */
+	public function return(string $name, mixed $value, array $config, bool $showHidden = false)
+	{
+		return $this->render($name, $value, $config, $showHidden, true);
+	}
+	public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false): mixed
+	{
 		$out = '';
-        if (jvbCheck('hidden', $config) && !$showHidden) {
-            return $out;
-        }
-        // Get conditional attributes if they exist
-        $conditional = array_key_exists('condition', $config) ?
-            $this->handleConditionalField($config) : '';
+		if (jvbCheck('hidden', $config) && !$showHidden) {
+			return $out;
+		}
 
-        if (!array_key_exists('type', $config)) {
-            return $out;
-        }
+		if (!array_key_exists('type', $config)) {
+			return $out;
+		}
+		if (!$value) {
+			$value = $this->getDefaultValue($config['type']);
+		}
 
-		if (array_key_exists('display', $config) && $config['display'] === 'hidden'){
-			$out = '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
+		// Handle hidden display type
+		if (array_key_exists('display', $config) && $config['display'] === 'hidden') {
+			$out = '<input type="hidden" name="' . $name . '" value="' . $value . '" />';
 			if (!$return) {
 				echo $out;
 			}
@@ -40,647 +46,1394 @@
 		}
 
 		ob_start();
-        $type = array_map( 'ucfirst', explode('_', $config['type']));
-        $type = implode('', $type);
-        $method = 'render' . $type . 'Field';
+
+		// Try custom function overrides first
+		$type = array_map('ucfirst', explode('_', $config['type']));
+		$type = implode('', $type);
+		$method = 'render' . $type . 'Field';
 
 		$nameTemp = implode('', array_map('ucfirst', explode('_', $name)));
-		$nameMethod = 'render'.$nameTemp.'Field';
-		if(function_exists($nameMethod)) {
+		$nameMethod = 'render' . $nameTemp . 'Field';
+
+		if (function_exists($nameMethod)) {
 			call_user_func($nameMethod, $value, $config);
 		} elseif (function_exists($method)) {
 			call_user_func($method, $value, $config);
 		} elseif (method_exists($this, $method)) {
-            $this->$method($name, $value, $config);
-        }
-
-
+			$this->$method($name, $value, $config);
+		}
 
 		$out = ob_get_clean();
 
 		do_action('jvbRenderFormField', $name, $config, $value);
 		$out = apply_filters('jvbFilterRenderFormField', $out, $name, $config, $value);
+
 		if (!$return) {
 			echo $out;
 		}
 		return $out;
-    }
+	}
 
-    public function renderTextField(string $name, mixed $value, array $field):void
-    {
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="<?= esc_attr($field['subtype']??'text'); ?>"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-				<?= $describedBy ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-            <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	public function getDefaultValue(string $type):mixed {
+		if (!$this->type_manager) {
+			$this->type_manager = new MetaTypeManager();
+		}
+		return match ($this->type_manager->getMetaType($type)) {
+			'object', 'array' => [],
+			'boolean' => false,
+			'integer' => 0,
+			default => '',
+		};
+	}
 
-    private function renderTelField(string $name, mixed $value, array $field):void
-    	{$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-			$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="tel"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete?>
-				<?= $describedBy ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
-    private function renderEmailField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="email"
-			 <?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete ?>
-				<?= $describedBy ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	/* ========== HELPER METHODS ========== */
 
-    private function renderUrlField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="url"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $describedBy ?>
-				<?= $autocomplete ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	/**
+	 * Prepare common field data
+	 */
+	protected function prepareFieldData(string $name, mixed $value, array $field): array
+	{
+		return [
+			'name' => array_key_exists('group', $field) ? $field['group'] . '::' . $name : $name,
+			'value' => isset($field['value']) ? $field['value'] : $value,
+			'id' => (array_key_exists('base', $field) ? esc_attr($field['base']) : '') . esc_attr($name),
+		];
+	}
 
-    private function renderNumberField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		$description = '<ul class="list-none"><li>Tip: hold Ctrl/Command to increase 5x</li><li>Shift to increase 10x,</li><li>Or Ctrl/Command + Shift to increase 50x</li></ul>';
-		$description .= $field['description']??'';
-        $conditional = $this->handleConditionalField($field);
+	/**
+	 * Build common HTML attributes for inputs
+	 */
+	protected function buildInputAttributes(string $name, array $field): string
+	{
+		$attrs = [];
+
+		// Conditional rendering
+		if (array_key_exists('condition', $field)) {
+			$attrs['conditional'] = $this->handleConditionalField($field);
+		}
+
+		// Accessibility
+		if (!empty($field['description'])) {
+			$attrs['aria-describedby'] = $name . '-help';
+		}
+
+		// Common attributes
+		$common = ['placeholder', 'autocomplete', 'pattern', 'minlength', 'maxlength', 'min', 'max', 'step'];
+		foreach ($common as $attr) {
+			if (array_key_exists($attr, $field)) {
+				$attrs[$attr] = $field[$attr];
+			}
+		}
+
+		// Required
+		if (!empty($field['required'])) {
+			$attrs['required'] = true;
+		}
+
+		// Build attribute string
+		$attrString = '';
+		foreach ($attrs as $key => $val) {
+			if ($key === 'conditional') {
+				$attrString .= ' ' . $val; // Already formatted
+			} elseif ($val === true) {
+				$attrString .= ' ' . $key;
+			} else {
+				$attrString .= ' ' . $key . '="' . esc_attr($val) . '"';
+			}
+		}
+
+		return $attrString;
+	}
+
+	/**
+	 * Build validation data attributes
+	 */
+	protected function buildValidationAttributes(array $field): string
+	{
+		$attrs = [];
+
+		if (!empty($field['pattern'])) {
+			$attrs['data-pattern'] = $field['pattern'];
+		}
+
+		if (!empty($field['validate'])) {
+			$attrs['data-validate'] = $field['validate'];
+		}
+
+		if (isset($field['min'])) {
+			$attrs['data-min'] = $field['min'];
+		}
+
+		if (isset($field['max'])) {
+			$attrs['data-max'] = $field['max'];
+		}
+
+		if (isset($field['minlength'])) {
+			$attrs['data-minlength'] = $field['minlength'];
+		}
+
+		if (isset($field['maxlength'])) {
+			$attrs['data-maxlength'] = $field['maxlength'];
+		}
+
+		if (!empty($field['validation_message'])) {
+			$attrs['data-validation-message'] = $field['validation_message'];
+		}
+
+		$attrs['data-type'] = $field['type'];
+
+		$attrString = '';
+		foreach ($attrs as $key => $val) {
+			$attrString .= ' ' . $key . '="' . esc_attr($val) . '"';
+		}
+
+		return $attrString;
+	}
+
+	/* ========== GENERIC FIELD WRAPPER ========== */
+
+	/**
+	 * Render a standard input field with validation wrapper
+	 */
+	protected function renderStandardInput(string $name, mixed $value, array $field, string $inputType = 'text'): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		$customData = '';
+		if (array_key_exists('data', $field) && !empty($field['data'])) {
+			foreach ($field['data'] as $key => $v) {
+				$customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"';
+			}
+		}
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+				<input
+					type="<?= esc_attr($inputType) ?>"
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					value="<?= esc_attr($data['value']) ?>"
+					<?= $inputAttrs ?>
+					<?= $customData?>
+				>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Render field label with optional character count
+	 */
+	protected function renderLabel(string $name, array $field): void
+	{
+		?>
+		<label for="<?= esc_attr($name) ?>">
+			<?= esc_html($field['label']) ?>
+			<?php if (!empty($field['required'])) : ?>
+				<span class="required" aria-label="required">*</span>
+			<?php endif; ?>
+			<?php if (!empty($field['limit'])) : ?>
+				<span class="char-count" data-limit="<?= esc_attr($field['limit']) ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']) ?>
+                </span>
+			<?php endif; ?>
+		</label>
+		<?php
+	}
+
+	/**
+	 * Render hint and description
+	 */
+	protected function renderHintAndDescription(array $field, string $name): void
+	{
+		if (!empty($field['hint'])) {
+			$this->renderHint($field['hint']);
+		}
+
+		if (!empty($field['description'])) {
+			$this->renderDescription($field['description'], $name);
+		}
+	}
+
+	protected function renderHint(string $hint): void
+	{
+		?>
+		<span class="hint"><?= esc_html($hint) ?></span>
+		<?php
+	}
+
+	protected function renderDescription(string $description, string $name): void
+	{
+		?>
+		<p class="description" id="<?= esc_attr($name) ?>-help">
+			<?= wp_kses_post($description) ?>
+		</p>
+		<?php
+	}
+
+	/* ========== SIMPLE INPUT FIELD TYPES ========== */
+
+	public function renderTextField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, $field['subtype'] ?? 'text');
+	}
+
+	public function renderEmailField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'email'; // Auto-add email validation
+		$this->renderStandardInput($name, $value, $field, 'email');
+	}
+
+	private function renderUrlField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'url'; // Auto-add URL validation
+		$this->renderStandardInput($name, $value, $field, 'url');
+	}
+
+	private function renderTelField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'phone'; // Auto-add phone validation
+		$this->renderStandardInput($name, $value, $field, 'tel');
+	}
+
+	private function renderDateField(string $name, mixed $value, array $field): void
+	{
+		$format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
+
+		// Format the date if we have a value
+		if (!empty($value)) {
+			$date = DateTime::createFromFormat($format, $value);
+			if ($date) {
+				$value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
+			}
+		}
+
+		$this->renderStandardInput($name, $value, $field, 'date');
+	}
+
+	private function renderTimeField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, 'time');
+	}
+
+	private function renderDatetimeField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, 'datetime-local');
+	}
+
+	/* ========== TEXTAREA FIELD ========== */
+
+	public function renderTextareaField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		$rows = isset($field['rows']) ? (int)$field['rows'] : 4;
+		$quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
+
+		if ($quill !== '') {
+			$allowImages = array_key_exists('allowImage', $field);
+			$quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
+		}
+
+		// Handle array values
+		if (is_array($value)) {
+			$value = implode(', ', $value);
+		}
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+                <textarea
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					rows="<?= esc_attr($rows) ?>"
+                    <?= $quill ?>
+					<?= $inputAttrs ?>
+                ><?= esc_textarea($data['value']) ?></textarea>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	/* ========== NUMBER FIELD ========== */
+
+	private function renderNumberField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
 
 		$min = isset($field['min']) ? (float)$field['min'] : 0;
 		$max = isset($field['max']) ? (float)$field['max'] : 100;
 		$step = isset($field['step']) ? (float)$field['step'] : 1;
 
-		$data = '';
+		// Handle custom data attributes
+		$customData = '';
 		if (array_key_exists('data', $field) && !empty($field['data'])) {
-			foreach($field['data'] as $key => $v) {
-				if ($v === '') {
-					$data .= ' data-'.$key;
-				} else {
-					$data .= ' data-'.$key.'="'.$v.'"';
-				}
+			foreach ($field['data'] as $key => $v) {
+				$customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"';
 			}
 		}
 
 		if (empty($value)) {
-			$value = $field['default']??0;
+			$value = $field['default'] ?? 0;
 		}
 
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?> row" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="' . $field['autocomplete'] . '"' : '';
 
-			 <div class="quantity"
-				  <?=$data?>>
-
-				 <button type="button"
-						 class="decrease"
-						 title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
-						 aria-label="Decrease <?= esc_attr($field['label']); ?>">
-					 <?= jvbIcon('minus')?>
-				 </button>
-
-				 <input type="number"
-						id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-						name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-						value="<?= esc_attr($value); ?>"
-						min="<?= esc_attr($min); ?>"
-						max="<?= esc_attr($max); ?>"
-						step="<?= esc_attr($step); ?>"
-						class="quantity-input"
-						<?= $describedBy ?>
-						<?= $autocomplete ?>
-					 <?= !empty($field['required']) ? 'required' : ''; ?>>
-
-				 <button type="button"
-						 class="increase"
-						 title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
-						 aria-label="Increase <?= esc_attr($field['label']); ?>">
-					 <?= jvbIcon('add')?>
-				 </button>
-			 </div>
-			 <?php $this->renderDescription($description, $name);  ?>
-        </div>
-        <?php
-    }
-
-    public function renderTextareaField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $rows = isset($field['rows']) ? (int)$field['rows'] : 4;
-        $conditional = $this->handleConditionalField($field);
-        $quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
-        if ($quill !== '') {
-            $allowImages = array_key_exists('allowImage', $field);
-            $quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
-        }
-        // Handle array values
-        if (is_array($value)) {
-            $value = implode(', ', $value);
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name ?? ''); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <textarea
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
-                    <?= $quill ?>
-                    rows="<?= esc_attr($rows); ?>"
-				<?= $placeholder ?>
-				<?= $describedBy ?>
-				<?= !empty($field['required']) ? 'required' : ''; ?>
-            <?= !empty($field['limit']) ? 'data-limit="' . esc_attr($field['limit']) . '"' : ''; ?>
-        ><?= esc_textarea($value); ?></textarea>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-    private function renderSetField(string $name, mixed $value, array $field):void
-    {
-        $this->renderCheckboxField($name, $value, $field);
-    }
-
-    private function renderCheckboxField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $value = !is_array($value) ? explode(',', $value) : $value;
-        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field checkbox-group" <?= $limit ? 'data-limit="' . esc_attr($limit) . '"' : ''; ?> <?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
-            <span class="label"><?= esc_html($field['label']); ?></span>
-            <div class="checkbox-options flex">
-            <?php foreach ($field['options'] as $key => $label) : ?>
-                <input
-                    type="checkbox"
-                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key)?>"
-					<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name)?>[]"
-                    value="<?= esc_attr($key); ?>"
-                    <?= (in_array($key, $value)) ? 'checked' : ''; ?>
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-                >
-                <label class="checkbox-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
-                    <?= esc_html($label); ?>
-                </label>
-            <?php endforeach; ?>
-        </div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-    private function renderRadioField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $value = (array)$value;
-        $conditional = $this->handleConditionalField($field);
-
-        if (!array_key_exists('label', $field)) {
-            error_log('No label for: '.print_r($name, true));
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field radio-group"<?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
-            <label><?= esc_html($field['label']); ?></label>
-
-            <div class="radio-options row">
-                <?php foreach ($field['options'] as $key => $label) : ?>
-                    <input
-                        type="radio"
-                        id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key) ?>"
-                        name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                        value="<?= esc_attr($key); ?>"
-						<?= !empty($field['required']) ? 'required' : ''; ?>
-                        <?= (in_array($key, $value)) ? 'checked' : ''; ?>
-                    >
-                    <label class="radio-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
-                        <?= esc_html($label); ?>
-                    </label>
-                <?php endforeach; ?>
-            </div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-
-        </div>
-        <?php
-    }
-
-    private function renderRepeaterField(string $name, mixed $value, array $field):void
-    {
-		error_log('Rendering Repeater Field!');
-        $values = is_array($value) ? $value : array();
-
-        $conditional = $this->handleConditionalField($field);
-        $row_label = isset($field['row_label']) ? $field['row_label'] : '';
-		$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field repeater <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-			 <?= $describedBy ?>
-            <?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
-            <?=$conditional?>>
-            <?php
-            if (!array_key_exists('label', $field)) {
-                error_log('No label for: '.print_r($name, true));
-            }
-            ?>
-            <h3><?= esc_html($field['label']); ?></h3>
-
-
-            <div class="repeater-items">
-                <?php
-                if (!empty($values)) {
-                    foreach ($values as $index => $row) {
-                        $this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
-                    }
-                }
-                ?>
-            </div>
-
-            <template class="<?=uniqid('repeaterTemplate')?>">
-                <?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
-            </template>
-
-            <button type="button" class="add-repeater-row">
-                <?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
-            </button>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-    private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle = 'New Item'):void
-    {
-        $display_number = (is_string($index)) ? $index : ($index + 1);
-        ?>
-        <div class="repeater-row" data-index="<?= esc_attr($index); ?>">
-            <details <?= (is_string($index)) ? 'open' : ''; ?>>
-                <summary class="repeater-row-header row btw">
-                    <span class="drag-handle"><?= jvbIcon('grab'); ?></span>
-                    <span class="row-number">#<?= esc_html($display_number); ?></span>
-                    <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
-                    <button type="button" class="remove-row" title="Remove">
-                        <?= jvbIcon('delete', ['title'=>'Remove']); ?>
-                    </button>
-                </summary>
-                <div class="repeater-row-content">
-                    <?php
-                    foreach ($fields as $slug => $field) :
-                        if ($base_name === '') {
-                            $field_name = $slug;
-                        } else {
-                            $field_name = sprintf('%s:%s:%s', $base_name, $index, $slug);
-                        }
-                        $field_value = isset($values[$slug]) ? $values[$slug] : '';
-                        $name = $field_name;
-                        $this->render($name, $field_value, $field);
-                    endforeach;
-                    ?>
-                </div>
-            </details>
-        </div>
-        <?php
-    }
-
-    private function getRowTitle(array $fields, array $values, string $rowTitle):string
-    {
-        // Try to find the first text field or textarea value to use as title
-        foreach ($fields as $slug => $field) {
-            if (in_array($field['type'], ['text', 'textarea']) &&
-                isset($values[$slug]) &&
-                !empty($values[$slug])) {
-                return $values[$slug];
-            }
-        }
-        return $rowTitle;
-    }
-
-    private function renderTaxonomyField(string $name, mixed $value, array $field):void
-    {
-		$conditional = $this->handleConditionalField($field);
-		$taxonomy = $field['taxonomy'];
-
-		// Get currently selected terms
-		$selected_terms = ($value === '') ? [] : explode(',', $value);
-
-
-		// Convert selected term IDs to the format expected by single modal
-		$processedSelected = [];
-		if (!empty($selected_terms)) {
-			foreach ($selected_terms as $termId) {
-				if (is_numeric($termId)) {
-					$term = get_term($termId, $taxonomy);
-					if ($term && !is_wp_error($term)) {
-						$processedSelected[$term->term_id] = [
-							'name' => $term->name,
-							'path' => TaxonomySelector::getTermPath($term)
-						];
-					}
-				}
-			}
-		}
-
-		// Create configuration for single modal system
-		$config = [
-			'taxonomy' => $taxonomy,
-			'max' => $field['limit'] ?? 0,
-			'search' => $field['search'] ?? true,
-			'createNew' => $field['createNew'] ?? false,
-			'selected' => $processedSelected,
-			'base' => $field['base'] ?? '',
-		];
-
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
 		?>
-		<div class="field taxonomy <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
-			<div class="field-group-header">
-				<label class="toggle">
-					<?= jvbIcon(str_replace(BASE, '', $taxonomy)) ?>
-					<?= esc_html($field['label']) ?>
-				</label>
-			</div>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?> row"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
 
-			<?php
-			$tax = new TaxonomySelector($name, $taxonomy, $config);
-			$extra = '<input type="hidden"
-				   name="'.esc_attr($name).'"
-				   id="'.esc_attr($name).'"'.$describedBy.'
-				   data-taxonomy="'.esc_attr($taxonomy).'"
-				   value="'.esc_attr(is_array($selected_terms) ? implode(',', $selected_terms) : $selected_terms).'">';
-			echo $tax->render([], $extra);
-			?>
+			<?php $this->renderLabel($name, $field); ?>
 
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
-    }
+			<div class="quantity" <?= $customData ?>>
+				<button type="button"
+						class="decrease"
+						title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
+						aria-label="Decrease <?= esc_attr($field['label']) ?>">
+					<?= jvbIcon('minus-square') ?>
+				</button>
 
-    protected function renderPostSelectorField(string $name, mixed $value, array $field):void
-    {
-		$conditional = $this->handleConditionalField($field);
+				<input type="number"
+					   id="<?= esc_attr($data['id']) ?>"
+					   name="<?= esc_attr($data['name']) ?>"
+					   value="<?= esc_attr($value) ?>"
+					   min="<?= esc_attr($min) ?>"
+					   max="<?= esc_attr($max) ?>"
+					   step="<?= esc_attr($step) ?>"
+					   class="quantity-input"
+					<?= $describedBy ?>
+					<?= $autocomplete ?>
+					<?= !empty($field['required']) ? 'required' : '' ?>>
 
-		// Process selected posts
-		$selected_posts = $value;
-		if (is_string($selected_posts)) {
-			$selected_posts = !empty($selected_posts) ? explode(',', $selected_posts) : [];
-		} elseif (!is_array($selected_posts)) {
-			$selected_posts = [];
-		}
-
-		// Configure the post selector
-		$config = [
-			'multiple' => $field['multiple'] ?? true,
-			'maxSelections' => $field['limit'] ?? 0,
-			'search' => true,
-			'placeholder' => $field['placeholder'] ?? 'Search posts...',
-			'noResults' => 'No posts found',
-			'shop_id' => $field['shop_id'] ?? null,
-			'onClose' => 'updateMetaFormPost'
-		];
-
-		$postSelector = new PostSelector($field['post_type'], $config);
-		$containerId = $name . '-post-selector';
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		?>
-		<div class="field post-selector <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
-			<div class="field-group-header">
-				<label class="toggle">
-					<?= jvbIcon($field['post_type'] . '-selector') ?>
-					<?= esc_html($field['label'] ?? ucfirst($field['post_type'])) ?>
-				</label>
-				<button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>"
-						class="add-item-btn"
-						type="button">
-					<?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
+				<button type="button"
+						class="increase"
+						title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
+						aria-label="Increase <?= esc_attr($field['label']) ?>">
+					<?= jvbIcon('plus-square') ?>
 				</button>
 			</div>
 
-			<?= $postSelector->render($selected_posts, $containerId) ?>
-
-			<!-- Hidden input for form submission -->
-			<input type="hidden"
-				   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name) ?>"
-				   class="post-selector-input"
-				   <?= $describedBy ?>
-				   data-post-type="<?= esc_attr($field['post_type']) ?>"
-				   value="<?= esc_attr(is_array($selected_posts) ? implode(',', $selected_posts) : $selected_posts) ?>">
-
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+			<?php $this->renderHintAndDescription($field, $name); ?>
 		</div>
 		<?php
-    }
+	}
 
-	protected function renderGroupField(string $name, mixed $value, array $field):void
+	/* ========== SELECT, RADIO, CHECKBOX FIELDS ========== */
+
+	private function renderSelectField(string $name, mixed $value, array $field): void
 	{
-		if (!array_key_exists('fields', $field) || empty($field['fields'])) {
-			return;
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+				<select
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					<?= $inputAttrs ?>>
+					<?php foreach ($field['options'] as $key => $label) : ?>
+						<option value="<?= esc_attr($key) ?>" <?php selected($value, $key); ?>>
+							<?= esc_html($label) ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderRadioField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<fieldset>
+				<legend><?= esc_html($field['label']) ?>
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+				</legend>
+
+				<?php foreach ($field['options'] as $key => $label) : ?>
+					<input
+						type="radio"
+						id="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>"
+						name="<?= esc_attr($data['name']) ?>"
+						value="<?= esc_attr($key) ?>"
+						<?php checked($value, $key); ?>
+						<?= !empty($field['required']) ? 'required' : '' ?>
+					>
+					<label class="radio-option" for="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>">
+						<span><?= $label ?></span>
+					</label>
+				<?php endforeach; ?>
+			</fieldset>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderCheckboxField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		if (!is_array($value)) {
+			$value = !empty($value) ? [$value] : [];
 		}
 
-		// Handle conditional fields
-		$conditional = $this->handleConditionalField($field);
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
 
-		// Ensure value is an array
-		$values = is_array($value) ? $value : [];
-		$original = $name;
+			<fieldset>
+				<legend><?= esc_html($field['label']) ?>
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+				</legend>
+
+				<?php foreach ($field['options'] as $key => $label) : ?>
+					<input
+						type="checkbox"
+						id="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>"
+						name="<?= esc_attr($data['name']) ?>[]"
+						value="<?= esc_attr($key) ?>"
+						<?php checked(in_array($key, $value)); ?>
+					>
+					<label class="checkbox-option" for="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>">
+						<span><?= esc_html($label) ?></span>
+					</label>
+				<?php endforeach; ?>
+			</fieldset>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderTrueFalseField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		?>
+		<div class="field true-false <?= esc_attr($name) ?> row btw"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<label class="toggle-switch row" <?= $describedBy ?>>
+				<input
+					type="checkbox"
+					name="<?= esc_attr($data['name']) ?>"
+					value="1"
+					<?= ($value) ? ' checked' : '' ?>
+					<?= !empty($field['required']) ? 'required' : '' ?>
+				>
+				<div class="slider"></div>
+				<span class="toggle-label">
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+
+					<?= esc_html($field['label']) ?></span>
+			</label>
+			<span class="validation-message" hidden role="alert"></span>
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+
+
+	/* ========== REPEATER FIELD ========== */
+
+	private function renderRepeaterField(string $name, mixed $value, array $field):void
+	{
+		$values = is_array($value) ? $value : array();
+
+		$conditional = $this->handleConditionalField($field);
+		$row_label = isset($field['row_label']) ? $field['row_label'] : '';
+		$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'].'::'.$name;
 		}
 		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		$hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden');
-		if (!$hidden) {
-			?>
-			<fieldset class="field group <?= esc_attr($name) ?>" <?= $conditional ?> data-field="<?=$name?>"<?= $describedBy?>>
-				<legend><?= esc_html($field['label']) ?></legend>
-			<?php
-		}
 		?>
+		<div class="field repeater <?=$name?>"
+			 data-field="<?= esc_attr($name); ?>"
+			<?= $describedBy ?>
+			<?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
+			<?=$conditional?>>
+			<?php
+			if (!array_key_exists('label', $field)) {
+				error_log('No label for: '.print_r($name, true));
+			}
+			?>
+			<h3><?= esc_html($field['label']); ?></h3>
 
-			<div class="group-fields <?=$original?>"<?= ($hidden) ? ' data-field="'.$name.'"' : ''?>"<?= $describedBy ?>>
-		<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-		<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+
+			<div class="repeater-items">
 				<?php
-				foreach ($field['fields'] as $field_name => $config) {
-					// Set the group context for proper field naming
-					$config['group'] = $name;
-
-					// Get the value for this specific field
-					$field_value = $values[$field_name] ?? '';
-
-					// Handle conditional fields within the group
-					if (isset($config['condition'])) {
-						// Convert condition field reference to group context
-						$condition_field = $config['condition']['field'];
-						if (!str_contains($condition_field, '::')) {
-							$config['condition']['field'] = $name . '::' . $condition_field;
-						}
+				if (!empty($values)) {
+					foreach ($values as $index => $row) {
+						$this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
 					}
-
-					$this->render($field_name, $field_value, $config);
 				}
 				?>
 			</div>
+
+			<template class="<?=uniqid('repeaterTemplate')?>">
+				<?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
+			</template>
+
+			<button type="button" class="add-repeater-row">
+				<?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
+			</button>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+		</div>
 		<?php
-		if (!$hidden) {
-			?>
-			</fieldset>
-			<?php
+	}
+
+	private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle = 'New Item'):void
+	{
+		$display_number = (is_string($index)) ? $index : ($index + 1);
+		?>
+		<div class="repeater-row" data-index="<?= esc_attr($index); ?>">
+			<details <?= (is_string($index)) ? 'open' : ''; ?>>
+				<summary class="repeater-row-header row btw">
+					<span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></span>
+					<span class="row-number">#<?= esc_html($display_number); ?></span>
+					<span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
+					<button type="button" class="remove-row" title="Remove">
+						<?= jvbIcon('trash', ['title'=>'Remove']); ?>
+					</button>
+				</summary>
+				<div class="repeater-row-content">
+					<?php
+					foreach ($fields as $slug => $field) :
+						if ($base_name === '') {
+							$field_name = $slug;
+						} else {
+							$field_name = sprintf('%s:%s:%s', $base_name, $index, $slug);
+						}
+						$field_value = isset($values[$slug]) ? $values[$slug] : '';
+						$name = $field_name;
+						$this->render($name, $field_value, $field);
+					endforeach;
+					?>
+				</div>
+			</details>
+		</div>
+		<?php
+	}
+
+	private function getRowTitle(array $fields, array $values, string $rowTitle): string
+	{
+		// Try to find the first text field or textarea value to use as title
+		foreach ($fields as $slug => $field) {
+			if (in_array($field['type'], ['text', 'textarea']) &&
+				isset($values[$slug]) &&
+				!empty($values[$slug])) {
+				return $values[$slug];
+			}
+		}
+		return $rowTitle;
+	}
+
+	/* ========== GROUP FIELD ========== */
+
+	protected function renderGroupField(string $name, mixed $value, array $field): void
+	{
+		if (!array_key_exists('fields', $field) || empty($field['fields'])) {
+			error_log('No fields to render');
+			return;
+		}
+
+
+		$values = is_array($value) ? $value : [];
+		$original = $name;
+
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden');
+
+		if ($hidden) {
+			// Simplified render for hidden groups
+			$this->renderGroupFields($name, $values, $field);
+			return;
+		}
+
+		// Standard fieldset render
+		$conditional = $this->handleConditionalField($field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+		$fieldset = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'details' : 'fieldset';
+		$legend = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'summary' : 'legend';
+		?>
+		<<?= $fieldset?> class="field group <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+				  data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>
+			<?= $describedBy ?>>
+			<<?=$legend?>><?= esc_html($field['label']) ?></<?=$legend?>>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+
+			<div class="group-fields <?= esc_attr($original) ?>">
+				<?php $this->renderGroupFields($name, $values, $field); ?>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+		</<?= $fieldset?>>
+		<?php
+	}
+
+	/**
+	 * Render individual fields within a group
+	 * Reusable for both standard and hidden group modes
+	 */
+	private function renderGroupFields(string $groupName, array $values, array $field): void
+	{
+		foreach ($field['fields'] as $field_name => $config) {
+			// Set the group context for proper field naming
+			$config['group'] = $groupName;
+
+			// Get the value for this specific field
+			$field_value = $values[$field_name] ?? '';
+
+			// Handle conditional fields within the group
+			if (isset($config['condition'])) {
+				$condition_field = $config['condition']['field'];
+				if (!str_contains($condition_field, '::')) {
+					$config['condition']['field'] = $groupName . '::' . $condition_field;
+				}
+			}
+
+			$this->render($field_name, $field_value, $config);
 		}
 	}
+
+	/* ========== UPLOAD FIELD ========== */
+	private function renderGalleryField(string $name, mixed $value, array $field):void
+	{
+		$field['multiple'] = true;
+		$this->renderUploadField($name, $value, $field);
+	}
+	private function renderUploadField(string $name, mixed $value, array $field): void
+	{
+		$defaultConfig = [
+			//File Type
+			'subtype' => 'image', // 'image', 'video', 'document', 'any'
+			'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
+			//Upload Behaviour
+			'multiple' => false, // Single or multiple uploads
+			'limit' => 0, // Max number of uploads (0 = unlimited)
+			'mode' => 'direct', // 'direct' or 'selection'
+			//Destination
+			'destination' => 'meta', // 'meta', 'post', 'post_group'
+			//Processing Options
+			'max_size' => null, // Override default size limits
+			'convert' => 'webp', // Image conversion format
+			'quality' => 90, // Conversion quality
+			'create_thumbnails' => true,
+		];
+		$config = array_merge($defaultConfig, $field);
+
+		// Validate destination config
+		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
+			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+			return;
+		}
+
+		// Get accepted types
+		$acceptedTypes = $this->getAllowedTypes($config);
+
+		// Build accept attribute for input
+		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
+		$acceptAttr = implode(',', $acceptExtensions);
+
+		// Determine field attributes
+		$subtype = $config['subtype'] ?? 'image';
+		$multiple = $config['multiple'] ?? false;
+		$limit = $config['limit'] ?? 0;
+		$mode = $config['mode'] ?? 'direct';
+		$destination = $config['destination'];
+
+		// Get existing attachments
+		$attachmentIds = $this->parseAttachmentIds($value);
+
+		// Determine field type for UI
+		$fieldType = $multiple ? 'gallery' : 'single';
+
+		// Build data attributes
+		$dataAttrs = [
+			'data-field' => $name,
+			'data-upload-field' => '',
+			'data-mode' => $mode,
+			'data-type' => $fieldType,
+			'data-subtype' => $subtype,
+			'data-destination' => $destination,
+		];
+		if (!empty($field['content'])) {
+			$dataAttrs['data-content'] = $field['content'];
+		}
+		if ($limit > 0) {
+			$dataAttrs['data-limit'] = $limit;
+		}
+
+		// Build data attributes
+		$conditional = $this->handleConditionalField($field);
+		$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
+
+		if (!empty($field['group'])) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		// Convert data attributes to string
+		$dataAttrString = '';
+		foreach ($dataAttrs as $attr => $val) {
+			$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
+		}
+		?>
+		<div class="field upload <?= esc_attr($name) ?>"
+			<?= $dataAttrString ?>
+			<?= $conditional ?>>
+
+			<div class="file-upload-container">
+				<div class="file-upload-wrapper">
+					<input type="file"
+						   name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   accept="<?= esc_attr($acceptAttr) ?>"
+						   data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
+						<?= $multiple ? 'multiple' : '' ?>
+						<?= !empty($field['required']) ? 'required' : '' ?>>
+
+					<h2><?= esc_html($field['label']) ?></h2>
+
+					<?php if (!empty($field['description'])) : ?>
+						<p><?= esc_html($field['description']) ?></p>
+					<?php endif; ?>
+
+					<p class="file-upload-text">
+						<strong>Click to upload</strong> or drag and drop<br>
+						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
+						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
+					</p>
+
+					<?php if ($destination === 'post_group') {
+						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
+						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
+						?>
+						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
+						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
+					<?php }
+					if (!empty($field['upload_description'])) : ?>
+						<p><?= esc_html($field['upload_description']); ?></p>
+					<?php endif; ?>
+					<div class="file-error"></div>
+				</div>
+				<?php jvbRenderProgressBar(); ?>
+			</div>
+
+
+			<?php if ($destination === 'post_group') : ?>
+			<div class="group-display flex col" hidden>
+				<div class="preview-wrap flex col">
+					<div class="preview-actions">
+						<div class="selection-controls">
+							<div class="selected">
+								<div class="field">
+									<input type="checkbox" id="select-all-uploads" data-select-all data-selects="item-grid" name="select-all-uploads">
+									<label for="select-all-uploads">
+										Select All
+									</label>
+								</div>
+								<div class="info" hidden>
+
+								</div>
+							</div>
+
+							<div class="selection-actions row btw" hidden>
+								<button type="button" data-action="add-to-group">
+									<?= jvbIcon('plus-square') ?>
+									Group
+								</button>
+								<button type="button" data-action="delete-upload">
+									<?= jvbIcon('trash') ?>
+									Delete
+								</button>
+							</div>
+						</div>
+
+						<button type="button" data-action="upload" class="submit-uploads">
+							<?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
+						</button>
+					</div>
+					<?php endif; ?>
+
+					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
+					<span class="count">0/0</span>'); ?>
+					<div class="item-grid preview">
+						<?php
+						// Render existing attachments
+						foreach ($attachmentIds as $attachmentId) {
+							echo $this->renderExistingAttachment($attachmentId, $subtype);
+						}
+						?>
+					</div>
+
+					<?php if ($destination === 'post_group') : ?>
+					<p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
+				</div>
+				<div class="sidebar flex col">
+					<div class="header">
+						<h4>New <?= $plural?></h4>
+						<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
+					</div>
+					<div class="item-grid groups">
+						<div class="empty-group">
+							<p>Drag here to create a new <?= $singular ?>!</p>
+						</div>
+					</div>
+					<p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
+				</div>
+			</div>
+		<?php endif; ?>
+
+			<?php if ($destination === 'meta') : ?>
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					   value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>>
+			<?php endif; ?>
+		</div>
+		<?php
+	}
+
+	private function renderExistingAttachment(int $attachmentId, string $subtype): string
+	{
+		ob_start();
+
+		switch ($subtype) {
+			case 'image':
+				$this->renderImagePreview($attachmentId);
+				break;
+			case 'video':
+				$this->renderVideoPreview($attachmentId);
+				break;
+			case 'document':
+			case 'file':
+				$this->renderFilePreview($attachmentId);
+				break;
+			default:
+				$this->renderImagePreview($attachmentId);
+				break;
+		}
+
+		return ob_get_clean();
+	}
+
+	/**
+	 * Get max file size for subtype
+	 */
+	private function getMaxFileSize(string $subtype): int
+	{
+		$sizes = [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		];
+
+		return $sizes[$subtype] ?? $sizes['image'];
+	}
+
+	/**
+	 * Format file size for display
+	 */
+	private function formatFileSize(int $bytes): string
+	{
+		if ($bytes >= 1073741824) {
+			return number_format($bytes / 1073741824, 1) . 'GB';
+		}
+		if ($bytes >= 1048576) {
+			return number_format($bytes / 1048576, 1) . 'MB';
+		}
+		if ($bytes >= 1024) {
+			return number_format($bytes / 1024, 1) . 'KB';
+		}
+		return $bytes . 'B';
+	}
+
+	/**
+	 * Get accepted types label
+	 */
+	private function getAcceptedTypesLabel(string $subtype, array $extensions): string
+	{
+		$labels = [
+			'image' => 'JPG, PNG, GIF, or WEBP',
+			'video' => 'MP4, WEBM, or MOV',
+			'document' => 'PDF, DOC, XLS, or TXT',
+			'any' => 'Images, Videos, or Documents'
+		];
+
+		return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
+			return ltrim($ext, '.');
+		}, array_slice($extensions, 0, 3))));
+	}
+
+	/**
+	 * Render upload preview items
+	 */
+	private function renderUploadPreviews(array $attachmentIds, array $config): void
+	{
+		if (empty($attachmentIds)) {
+			return;
+		}
+
+		foreach ($attachmentIds as $id) {
+			switch ($config['subtype']) {
+				case 'image':
+					$this->renderImagePreview($id, $config);
+					break;
+				case 'video':
+					$this->renderVideoPreview($id, $config);
+					break;
+				case 'file':
+					$this->renderFilePreview($id, $config);
+					break;
+			}
+		}
+	}
+
+	public function renderImagePreview(?int $id = null, array $config = []):void
+	{
+		$attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', false) : false;
+		$caption = ($id) ? wp_get_attachment_caption($id) : '';
+		$alt = ($id) ? get_post_meta($id, '_wp_attachment_image_alt',true) : '';
+		$title = ($id) ? get_the_title($id) : '';
+		$addID = ($id) ? '-'.$id : '';
+		$dataID = ($id) ? ['id' => $id] : '';
+		?>
+		<div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>>
+			<div class="preview">
+				<?php jvbRenderProgressBar('',true) ?>
+				<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
+				<label for="select-item<?=$addID?>" aria-label="Select image">
+					<?= ($attachment) ?: '<img>
+                <video></video>
+                <span></span>' ?>
+				</label>
+				<div class="item-actions row btw">
+					<div class="radio-button">
+						<input type="radio" class="featured btn" name="featured" id="featured" hidden>
+						<label for="featured">
+							<?=jvbIcon('star')?>
+							<?=jvbIcon('star', ['style' => 'fill'])?>
+							<span class="screen-reader-text">Set as featured image</span>
+						</label>
+					</div>
+
+					<button type="button" data-action="delete-upload" title="Remove from Group">
+						<?=jvbIcon('trash')?>
+					</button>
+				</div>
+			</div>
+			<details>
+				<summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
+
+				<?php
+				$fields = array_key_exists('fields', $config) ? $config['fields'] : [];
+
+				// Only add image_data if not already provided
+				if (!array_key_exists('image_data', $fields)) {
+					$fields['image_data'] = [
+						'type'   => 'group',
+						'wrap'   => 'details',
+						'label'  => 'Image Info',
+						'hint'   => 'These will be automatically generated if left blank.',
+						'fields' => [
+							'image-title'.$addID => [
+								'type'  => 'text',
+								'label' => 'Image Title',
+								'value' => $title,
+								'data'  => $dataID
+							],
+							'image-alt-text'.$addID => [
+								'type'  => 'text',
+								'label' => 'Alt Text',
+								'value' => $alt,
+								'hint'  => 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+								'data'  => $dataID
+							],
+							'image-caption'.$addID => [
+								'type'  => 'textarea',
+								'value' => $caption,
+								'label' => 'Image Caption',
+								'data'  => $dataID
+							]
+						]
+					];
+				}
+
+				$meta = new MetaManager($id);
+				foreach ($fields as $field => $config) {
+					$meta->render('form', $field, $config);
+				}
+				?>
+			</details>
+		</div>
+		<?php
+	}
+	public function renderVideoPreview(?int $id = null, array $config = []):void
+	{
+		$attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', true) : false;
+		$caption = ($id) ? wp_get_attachment_caption($id) : '';
+		$description = ($id) ? get_the_content($id) : '';
+		$title = ($id) ? get_the_title($id) : '';
+		$addID = ($id) ? '-'.$id : '';
+		$dataID = ($id) ? ['id' => $id] : '';
+		?>
+		<div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>>
+				<div class="preview">
+					<?php jvbRenderProgressBar('',true) ?>
+					<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
+					<label for="select-item<?=$addID?>" aria-label="Select image">
+						<?= ($attachment) ? $attachment : '<img>
+						<video></video>
+						<span></span>'; ?>
+					</label>
+					<div class="item-actions row btw">
+						<div class="radio-button">
+							<input type="radio" class="featured btn" name="featured" id="featured" hidden>
+							<label for="featured">
+								<?=jvbIcon('star')?>
+								<?=jvbIcon('star', ['style' => 'fill'])?>
+								<span class="screen-reader-text">Set as featured image</span>
+							</label>
+						</div>
+
+						<button type="button" data-action="delete-upload" title="Remove from Group">
+							<?=jvbIcon('trash')?>
+						</button>
+					</div>
+				</div>
+				<details>';
+					<summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
+
+		<?php
+		$fields = array_key_exists('fields', $config) ? $config['fields'] : [];
+		$fields = array_merge([
+			'upload_data'	=> [
+				'type'	=> 'group',
+				'wrap'	=> 'details',
+				'label'	=> 'Video Info',
+				'hint'	=> 'These will be automatically generated if left blank.',
+				'fields'	=> [
+					'title' => [
+						'type'	=> 'text',
+						'label'	=> 'Video Title',
+						'value'	=> $title,
+						'data'	=> $dataID
+					],
+					'caption' => [
+						'type'	=> 'textarea',
+						'value'	=> $caption,
+						'label'	=> 'Video Caption',
+						'data'	=> $dataID
+					],
+					'description' => [
+						'type'	=> 'textarea',
+						'value'	=> $description,
+						'label'	=> 'Video Description',
+						'data'	=> $dataID
+					]
+				]
+			]
+		], $fields);
+		$this->render('upload_data', null, $fields);
+		?>
+				</details>
+		</div>
+		<?php
+	}
+	public function renderFilePreview(?int $id = null, array $config = []):void
+	{
+
+		$attachment = ($id) ? wp_get_attachment_image($id, 'thumbnail', true) : false;
+		$caption = ($id) ? wp_get_attachment_caption($id) : '';
+		$description = ($id) ? get_the_content($id) : '';
+		$title = ($id) ? get_the_title($id) : '';
+		$addID = ($id) ? '-'.$id : '';
+		$dataID = ($id) ? ['id' => $id] : '';
+		?>
+		<div class="item upload"<?= ($id) ? ' data-id="'.$id.'"' : '' ?>>
+				<div class="preview">
+					<?php jvbRenderProgressBar('',true) ?>
+					<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
+					<label for="select-item<?=$addID?>" aria-label="Select image">
+						<?= ($attachment) ? $attachment : '<img>
+						<video></video>
+						<span></span>'; ?>
+					</label>
+					<div class="item-actions row btw">
+						<div class="radio-button">
+							<input type="radio" class="featured btn" name="featured" id="featured" hidden>
+							<label for="featured">
+								<?=jvbIcon('star')?>
+								<?=jvbIcon('star', ['style' => 'fill'])?>
+								<span class="screen-reader-text">Set as featured image</span>
+							</label>
+						</div>
+
+						<button type="button" data-action="delete-upload" title="Remove from Group">
+							<?=jvbIcon('trash')?>
+						</button>
+					</div>
+				</div>
+				<details>';
+					<summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
+
+		<?php
+		$fields = array_key_exists('fields', $config) ? $config['fields'] : [];
+		$fields = array_merge([
+			'upload_data'	=> [
+				'type'	=> 'group',
+				'wrap'	=> 'details',
+				'label'	=> 'File Info',
+				'hint'	=> 'These will be automatically generated if left blank.',
+				'fields'	=> [
+					'title' => [
+						'type'	=> 'text',
+						'label'	=> 'File Title',
+						'value'	=> $title,
+						'data'	=> $dataID
+					],
+					'caption' => [
+						'type'	=> 'textarea',
+						'value'	=> $caption,
+						'label'	=> 'File Caption',
+						'data'	=> $dataID
+					],
+					'description' => [
+						'type'	=> 'textarea',
+						'value'	=> $description,
+						'label'	=> 'File Description',
+						'data'	=> $dataID
+					]
+				]
+			]
+		], $fields);
+		$this->render('upload_data', null, $fields);
+		?>
+				</details>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Get upload instruction text based on config
+	 */
+	private function getUploadInstructions(array $config): string
+	{
+		$extensions = $this->getMimeExtensions($this->getAllowedTypes($config));
+		$extList = implode(', ', array_map('strtoupper', $extensions));
+		$maxSize = $config['max_size'] ?? $this->max_file_size;
+		$maxSizeMB = round($maxSize / 1048576, 1);
+
+		return "{$extList} (max. {$maxSizeMB}MB)";
+	}
+
+	/* ========== TAXONOMY/USER SELECTOR FIELDS ========== */
+
+	private function renderTaxonomyField(string $name, string $value, array $field): void
+	{
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$this->renderSelectorField($name, $value, $field, 'taxonomy');
+	}
+
+	private function renderUserField(string $name, string $value, array $field): void
+	{
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$this->renderSelectorField($name, $value, $field, 'post');
+	}
+
+	/**
+	 * Generic selector field renderer
+	 * Handles both taxonomy and post selectors with consistent structure
+	 */
+	public function renderSelectorField(string $name, mixed $value, array $field, string $type): void
+	{
+		$conditional = $this->handleConditionalField($field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		// Parse selected values
+		$value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value;
+		$selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value));
+
+		// Generate unique container ID
+		$containerId = $name . '-' . $type . '-selector';
+
+		// Create selector instance with proper parameters
+		if ($type === 'taxonomy') {
+			$taxonomy = $field['taxonomy'];
+			$icon = JVB_TAXONOMY[$taxonomy]['icon']??'';
+
+			// Map field config to selector config
+			$selectorConfig = [
+				'max'		=> $field['max'] ?? 0,  // 0 = unlimited
+				'search'	=> $field['search'] ?? true,
+				'label'		=> $field['label'] ?? '',
+				'createNew'	=> $field['createNew'] ?? false,
+				'required'	=> $field['required'] ?? false,
+				'base'		=> $field['base'] ?? '',
+				'update'	=> $field['update'] ?? true,
+				'name'		=> $name,
+				'autocomplete'	=> $field['autocomplete'] ?? false,
+			];
+			if ($icon !== '') {
+				$selectorConfig['icon'] = $icon;
+			}
+
+			$selector = new TaxonomySelector($containerId, $taxonomy, $selectorConfig);
+			$icon = $taxonomy;
+		} else {
+			$postType = $field['post_type'];
+
+			// Map field config to selector config
+			$selectorConfig = [
+				'max'		=> $field['max'] ?? 0,
+				'search'	=> $field['search'] ?? true,
+				'label'		=> $field['label'] ?? '',
+				'required'	=> $field['required'] ?? false,
+				'base'		=> $field['base'] ?? '',
+				'update'	=> $field['update'] ?? true,
+				'shop_id'	=> $field['shop_id'] ?? null,
+				'autocomplete'=> $field['autocomplete'] ?? true,
+			];
+
+			$selector = new PostSelector($containerId, $postType, $selectorConfig);
+			$icon = $postType;
+		}
+
+		?>
+		<div class="field selector <?= esc_attr($type) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			 data-type="selector" data-subtype="<?= esc_attr($type)?>"
+			<?= $validationAttrs ?>
+			<?= $describedBy ?>>
+
+			<?= $selector->render($selected) ?>
+
+			<!-- Hidden input for form submission -->
+			<input type="hidden"
+				   class="<?= esc_attr($type) ?>-selector-input"
+				   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>"
+				   data-<?= esc_attr($type) ?>="<?= esc_attr($field[$type === 'taxonomy' ? 'taxonomy' : 'post_type']) ?>"
+				   value="<?= esc_attr(is_array($selected) ? implode(',', $selected) : $value) ?>"
+				<?= !empty($field['required']) ? 'required' : '' ?>>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+
+			<span class="validation-message" hidden role="alert"></span>
+		</div>
+		<?php
+	}
+
+	/* ========== LOCATION FIELD ========== */
+
 	protected function renderLocationField(string $name, mixed $value, array $field): void
 	{
 		$googleMaps = JVB()->connect('maps');
@@ -780,1349 +1533,233 @@
 		</div>
 		<?php
 	}
-    //TODO: This is more or less handled by PostSelector/TaxonomySelector, no?
-    private function renderAssociationField(string $name, mixed $value, array $field):void
-    {
-        // Ensure value is an array
-        if (!is_array($value)) {
-            $value = empty($value) ? [] : [$value];
-        }
 
-        // Get field configuration
-        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
-        $object_types = isset($field['object_types']) ? $field['object_types'] : ['post'];
-        $post_types = isset($field['post_types']) ? $field['post_types'] : ['post'];
-        $taxonomies = isset($field['taxonomies']) ? $field['taxonomies'] : [];
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Create unique ID for this field
-        $field_id = 'association-' . esc_attr($name);
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field association <?=$name?>" data-field="<?= esc_attr($name); ?>" <?= $conditional; ?>>
-            <label><?= esc_html($field['label']); ?></label>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+	/* ========== HTML FIELD ========== */
 
-            <div class="association-container"<?=$describedBy?>>
-                <div class="association-search">
-                    <input type="text"
-                           id="<?= esc_attr($field_id); ?>-search"
-                           class="association-search-input"
-                           placeholder="Search items...">
-
-                    <div class="association-filter">
-                        <?php if (count($object_types) > 1 || count($post_types) > 1 || count($taxonomies) > 0) : ?>
-                            <select class="association-filter-select">
-                                <?php if (in_array('post', $object_types)) : ?>
-                                    <?php foreach ($post_types as $post_type) : ?>
-                                        <?php
-                                        $post_type_obj = get_post_type_object($post_type);
-                                        $label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post_type);
-                                        ?>
-                                        <option value="post:<?= esc_attr($post_type); ?>">
-                                            <?= esc_html($label); ?>
-                                        </option>
-                                    <?php endforeach; ?>
-                                <?php endif; ?>
-
-                                <?php if (in_array('term', $object_types)) : ?>
-                                    <?php foreach ($taxonomies as $taxonomy) : ?>
-                                        <?php
-                                        $tax_obj = get_taxonomy($taxonomy);
-                                        $label = $tax_obj ? $tax_obj->labels->singular_name : ucfirst($taxonomy);
-                                        ?>
-                                        <option value="term:<?= esc_attr($taxonomy); ?>">
-                                            <?= esc_html($label); ?>
-                                        </option>
-                                    <?php endforeach; ?>
-                                <?php endif; ?>
-                            </select>
-                        <?php endif; ?>
-
-                        <button type="button" class="search-button">
-                            <?= jvbIcon('search', ['title' => 'Search']); ?>
-                        </button>
-                    </div>
-                </div>
-
-                <div class="association-results">
-                    <div class="association-available">
-                        <h4>Available Items</h4>
-                        <ul class="available-items"></ul>
-                        <div class="association-loading" hidden>
-                            Loading...
-                        </div>
-                        <div class="association-no-results" hidden>
-                            No items found
-                        </div>
-                        <div class="association-pagination">
-                            <button type="button" class="prev-page" disabled>
-                                <?= jvbIcon('arrow-left', ['title' => 'Previous']); ?>
-                            </button>
-                            <span class="page-info">Page <span class="current-page">1</span></span>
-                            <button type="button" class="next-page" disabled>
-                                <?= jvbIcon('arrow-right', ['title' => 'Next']); ?>
-                            </button>
-                        </div>
-                    </div>
-
-                    <div class="association-actions">
-                        <button type="button" class="add-selected" disabled>
-                            <?= jvbIcon('arrow-right', ['title' => 'Add selected']); ?>
-                        </button>
-                        <button type="button" class="remove-selected" disabled>
-                            <?= jvbIcon('arrow-left', ['title' => 'Remove selected']); ?>
-                        </button>
-                    </div>
-
-                    <div class="association-selected">
-                        <h4>Selected Items
-                            <?php if ($limit) : ?>
-                                <span class="limit-info">(<?= esc_html($limit); ?> max)</span>
-                            <?php endif; ?>
-                        </h4>
-                        <ul class="selected-items row">
-                            <?php
-                            // Display currently selected items
-                            foreach ($value as $item_id) {
-                                // Try to determine the type and get details
-                                $item_type = '';
-                                $item_title = '';
-                                $item_object = '';
-
-                                // Check if it's a post
-                                if (in_array('post', $object_types)) {
-                                    $post = get_post($item_id);
-                                    if ($post && in_array($post->post_type, $post_types)) {
-                                        $item_type = 'post';
-                                        $item_title = $post->post_title;
-                                        $item_object = $post->post_type;
-                                    }
-                                }
-
-                                // Check if it's a term
-                                if (empty($item_type) && in_array('term', $object_types)) {
-                                    foreach ($taxonomies as $taxonomy) {
-                                        $term = get_term($item_id, $taxonomy);
-                                        if (!is_wp_error($term) && $term) {
-                                            $item_type = 'term';
-                                            $item_title = $term->name;
-                                            $item_object = $term->taxonomy;
-                                            break;
-                                        }
-                                    }
-                                }
-
-                                // Only output if we found the item
-                                if (!empty($item_type) && !empty($item_title)) {
-                                    ?>
-                                    <li data-id="<?= esc_attr($item_id); ?>"
-                                        data-type="<?= esc_attr($item_type); ?>"
-                                        data-object="<?= esc_attr($item_object); ?>">
-                                        <span class="item-title"><?= esc_html($item_title); ?></span>
-                                        <span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span>
-                                        <button type="button" class="remove-item row">
-                                            <?= jvbIcon('close', ['title' => 'Remove']); ?>
-                                        </button>
-                                    </li>
-                                    <?php
-                                }
-                            }
-                            ?>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Hidden input to store selected values -->
-            <input type="hidden" name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>" value="<?= esc_attr(implode(',', $value)); ?>">
-        </div>
-
-        <script>
-            (function() {
-                // Initialize association field
-                const container = document.querySelector('[data-field="<?= esc_attr($name); ?>"]');
-                if (!container) return;
-
-                const searchInput = container.querySelector('.association-search-input');
-                const filterSelect = container.querySelector('.association-filter-select');
-                const searchButton = container.querySelector('.search-button');
-                const availableList = container.querySelector('.available-items');
-                const selectedList = container.querySelector('.selected-items');
-                const hiddenInput = container.querySelector('input[name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"]');
-                const addButton = container.querySelector('.add-selected');
-                const removeButton = container.querySelector('.remove-selected');
-                const loadingIndicator = container.querySelector('.association-loading');
-                const noResultsMessage = container.querySelector('.association-no-results');
-                const prevPageButton = container.querySelector('.prev-page');
-                const nextPageButton = container.querySelector('.next-page');
-                const currentPageSpan = container.querySelector('.current-page');
-
-                // Configuration
-                const config = {
-                    limit: <?= $limit ?: 0; ?>,
-                    objectTypes: <?= json_encode($object_types); ?>,
-                    postTypes: <?= json_encode($post_types); ?>,
-                    taxonomies: <?= json_encode($taxonomies); ?>,
-                    perPage: 10,
-                    currentPage: 1
-                };
-
-                // Current state
-                let currentSearch = '';
-                let currentFilter = filterSelect ? filterSelect.value : (config.objectTypes.includes('post') ? 'post:' + config.postTypes[0] : 'term:' + config.taxonomies[0]);
-                let availableItems = [];
-                let selectedItems = [];
-
-                // Parse initial selected items
-                const initialValue = hiddenInput.value;
-                if (initialValue) {
-                    selectedItems = initialValue.split(',').map(id => parseInt(id, 10));
-                }
-
-                // Event Listeners
-                if (searchButton) {
-                    searchButton.addEventListener('click', performSearch);
-                }
-
-                if (searchInput) {
-                    searchInput.addEventListener('keypress', function(e) {
-                        if (e.key === 'Enter') {
-                            e.preventDefault();
-                            performSearch();
-                        }
-                    });
-                }
-
-                if (filterSelect) {
-                    filterSelect.addEventListener('change', function() {
-                        currentFilter = this.value;
-                        config.currentPage = 1;
-                        performSearch();
-                    });
-                }
-
-                if (prevPageButton) {
-                    prevPageButton.addEventListener('click', function() {
-                        if (config.currentPage > 1) {
-                            config.currentPage--;
-                            performSearch();
-                        }
-                    });
-                }
-
-                if (nextPageButton) {
-                    nextPageButton.addEventListener('click', function() {
-                        config.currentPage++;
-                        performSearch();
-                    });
-                }
-
-                // Add items
-                addButton.addEventListener('click', function() {
-                    const selectedAvailableItems = availableList.querySelectorAll('li.selected');
-                    selectedAvailableItems.forEach(item => {
-                        const id = parseInt(item.dataset.id, 10);
-                        // Check limit
-                        if (config.limit && selectedItems.length >= config.limit) {
-                            return;
-                        }
-
-                        // Skip if already selected
-                        if (selectedItems.includes(id)) {
-                            return;
-                        }
-
-                        // Add to selection
-                        selectedItems.push(id);
-
-                        // Clone and modify for selected list
-                        const clone = item.cloneNode(true);
-                        clone.classList.remove('selected');
-
-                        // Replace checkbox with remove button
-                        const checkbox = clone.querySelector('input[type="checkbox"]');
-                        if (checkbox) {
-                            const removeBtn = document.createElement('button');
-                            removeBtn.type = 'button';
-                            removeBtn.className = 'remove-item';
-                            removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>';
-                            removeBtn.addEventListener('click', function() {
-                                removeItem(id, clone);
-                            });
-
-                            checkbox.parentNode.replaceChild(removeBtn, checkbox);
-                        }
-
-                        selectedList.appendChild(clone);
-                    });
-
-                    // Update hidden input
-                    updateHiddenInput();
-
-                    // Update UI state
-                    updateButtonStates();
-                });
-
-                // Remove items
-                removeButton.addEventListener('click', function() {
-                    const selectedSelectedItems = selectedList.querySelectorAll('li.selected');
-                    selectedSelectedItems.forEach(item => {
-                        const id = parseInt(item.dataset.id, 10);
-                        removeItem(id, item);
-                    });
-                });
-
-                // Listen for clicks on items in both lists
-                availableList.addEventListener('click', function(e) {
-                    const item = e.target.closest('li');
-                    if (!item) return;
-
-                    // If clicking checkbox, handle separately
-                    if (e.target.type === 'checkbox') {
-                        updateButtonStates();
-                        return;
-                    }
-
-                    // Toggle selection
-                    if (item.classList.contains('selected')) {
-                        item.classList.remove('selected');
-                        item.querySelector('input[type="checkbox"]').checked = false;
-                    } else {
-                        item.classList.add('selected');
-                        item.querySelector('input[type="checkbox"]').checked = true;
-                    }
-
-                    updateButtonStates();
-                });
-
-                selectedList.addEventListener('click', function(e) {
-                    const item = e.target.closest('li');
-                    if (!item) return;
-
-                    // If clicking remove button, handle it
-                    if (e.target.closest('.remove-item')) {
-                        const id = parseInt(item.dataset.id, 10);
-                        removeItem(id, item);
-                        return;
-                    }
-
-                    // Toggle selection
-                    item.classList.toggle('selected');
-                    updateButtonStates();
-                });
-
-                // Helper Functions
-                function performSearch() {
-                    currentSearch = searchInput.value.trim();
-
-                    // Show loading
-                    loadingIndicator.hidden = false;
-                    noResultsMessage.hidden = true;
-                    availableList.innerHTML = '';
-
-                    // Get filter parts
-                    const [type, object] = currentFilter.split(':');
-
-                    // Prepare data for AJAX
-                    const data = {
-                        action: 'jvb_association_search',
-                        nonce: jvbSettings.nonce,
-                        type: type,
-                        object: object,
-                        search: currentSearch,
-                        page: config.currentPage,
-                        per_page: config.perPage,
-                        selected: selectedItems
-                    };
-
-                    // Make AJAX request to WordPress REST API
-                    fetch(jvbSettings.api + 'terms', {
-                        method: 'POST',
-                        headers: {
-                            'Content-Type': 'application/json',
-                            'X-WP-Nonce': jvbSettings.nonce
-                        },
-                        body: JSON.stringify(data)
-                    })
-                        .then(response => response.json())
-                        .then(response => {
-                            // Hide loading
-                            loadingIndicator.hidden = true;
-
-                            if (response.success && response.items && response.items.length > 0) {
-                                // Update available items
-                                availableItems = response.items;
-
-                                // Render items
-                                renderAvailableItems();
-
-                                // Update pagination
-                                updatePagination(response.total, response.pages);
-                            } else {
-                                // Show no results
-                                noResultsMessage.hidden = false;
-                                prevPageButton.disabled = true;
-                                nextPageButton.disabled = true;
-                                currentPageSpan.textContent = '1';
-                            }
-                        })
-                        .catch(error => {
-                            console.error('Error searching items:', error);
-                            loadingIndicator.hidden = true;
-                            noResultsMessage.hidden = false;
-                        });
-                }
-
-                function renderAvailableItems() {
-                    availableList.innerHTML = '';
-
-                    availableItems.forEach(item => {
-                        const isSelected = selectedItems.includes(item.id);
-
-                        const li = document.createElement('li');
-                        li.dataset.id = item.id;
-                        li.dataset.type = item.type;
-                        li.dataset.object = item.object;
-
-                        // Create checkbox
-                        const checkbox = document.createElement('input');
-                        checkbox.type = 'checkbox';
-                        checkbox.id = `${name}-item-${item.id}`;
-
-                        // Create label for title
-                        const titleSpan = document.createElement('span');
-                        titleSpan.className = 'item-title';
-                        titleSpan.textContent = item.title;
-
-                        // Create label for type
-                        const typeSpan = document.createElement('span');
-                        typeSpan.className = 'item-type';
-                        typeSpan.textContent = item.object_label;
-
-                        // Append elements
-                        li.appendChild(checkbox);
-                        li.appendChild(titleSpan);
-                        li.appendChild(typeSpan);
-
-                        // Disable if already selected
-                        if (isSelected) {
-                            li.classList.add('disabled');
-                            checkbox.disabled = true;
-
-                            // Add note that item is already selected
-                            const note = document.createElement('span');
-                            note.className = 'item-note';
-                            note.textContent = 'Already selected';
-                            li.appendChild(note);
-                        }
-
-                        availableList.appendChild(li);
-                    });
-                }
-
-                function updatePagination(total, pages) {
-                    // Update current page display
-                    currentPageSpan.textContent = config.currentPage;
-
-                    // Update prev/next buttons
-                    prevPageButton.disabled = config.currentPage <= 1;
-                    nextPageButton.disabled = config.currentPage >= pages;
-                }
-
-                function removeItem(id, element) {
-                    // Remove from array
-                    selectedItems = selectedItems.filter(itemId => itemId !== id);
-
-                    // Remove from DOM
-                    if (element) {
-                        element.remove();
-                    }
-
-                    // Update hidden input
-                    updateHiddenInput();
-
-                    // Update buttons
-                    updateButtonStates();
-                }
-
-                function updateHiddenInput() {
-                    hiddenInput.value = selectedItems.join(',');
-                }
-
-                function updateButtonStates() {
-                    // Add button is enabled if at least one available item is selected
-                    // and we haven't reached the limit
-                    const hasSelectedAvailable = availableList.querySelector('li.selected:not(.disabled)') !== null;
-                    addButton.disabled = !hasSelectedAvailable ||
-                        (config.limit > 0 && selectedItems.length >= config.limit);
-
-                    // Remove button is enabled if at least one selected item is selected
-                    const hasSelectedItems = selectedList.querySelector('li.selected') !== null;
-                    removeButton.disabled = !hasSelectedItems;
-                }
-
-                // Initial search
-                performSearch();
-            })();
-        </script>
-        <?php
-    }
-
-    private function renderTrueFalseField(string $name, mixed $value, array $field):void
-    {
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field true-false <?=$name?> row btw" <?=$conditional?> data-field="<?=$name?>">
-            <label class="toggle-switch row"<?=$describedBy?>>
-                <input
-                    type="checkbox"
-                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    value="1"
-                    <?= ($value) ? ' checked':'' ?>
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-                >
-				<div class="slider"></div>
-				<span class="toggle-label"><?= esc_html($field['label']); ?></span>
-            </label>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-	private function renderUploadField(string $name, mixed $value, array $field): void
+	protected function renderHtmlField(string $name, mixed $value, array $field): void
 	{
-		$defaultConfig = [
-			//File Type
-			'subtype' => 'image', // 'image', 'video', 'document', 'any'
-			'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
-			//Upload Behaviour
-			'multiple' => false, // Single or multiple uploads
-			'limit' => 0, // Max number of uploads (0 = unlimited)
-			'mode' => 'direct', // 'direct' or 'selection'
-			//Destination
-			'destination' => 'meta', // 'meta', 'post', 'post_group'
-			//Processing Options
-			'max_size' => null, // Override default size limits
-			'convert' => 'webp', // Image conversion format
-			'quality' => 80, // Conversion quality
-			'create_thumbnails' => true,
-		];
-		$config = array_merge($defaultConfig, $field);
+		$method_name = $field['content'];
+		$content = '';
 
-		// Validate destination config
-		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
-			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+		if (method_exists($this, $method_name)) {
+			$content = $this->$method_name();
+		}
+
+		if ($content === '') {
 			return;
 		}
 
-		// Get accepted types
-		$acceptedTypes = $this->getAllowedTypes($config);
+		echo sprintf(
+			'<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
+			esc_attr($name),
+			$content
+		);
+	}
 
-		// Build accept attribute for input
-		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
-		$acceptAttr = implode(',', $acceptExtensions);
+	/* ========== UTILITY METHODS ========== */
 
-		// Determine field attributes
-		$subtype = $config['subtype'] ?? 'image';
-		$multiple = $config['multiple'] ?? false;
-		$limit = $config['limit'] ?? 0;
-		$mode = $config['mode'] ?? 'direct';
-		$destination = $config['destination'];
+	private function handleConditionalField(array $field):string
+	{
+		if (empty($field['condition'])) {
+			return '';
+		}
 
-		// Get existing attachments
-		$attachmentIds = $this->parseAttachmentIds($value);
+		$condition = $field['condition'];
+		return sprintf(
+			'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
+			esc_attr($field['condition']['field']),
+			esc_attr($field['condition']['value']),
+			esc_attr($field['condition']['operator'] ?? '==')
+		);
+	}
 
-		// Determine field type for UI
-		$fieldType = $multiple ? 'gallery' : 'single';
+	protected function getAllowedTypes(array $config): array
+	{
+		if (!empty($config['accepted_types'])) {
+			return $config['accepted_types'];
+		}
 
-		// Build data attributes
-		$dataAttrs = [
-			'data-field' => $name,
-			'data-upload-field' => '',
-			'data-mode' => $mode,
-			'data-type' => $fieldType,
-			'data-subtype' => $subtype,
-			'data-destination' => $destination,
+		// Default types based on subtype
+		$defaults = [
+			'image' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
+			'video' => ['video/mp4', 'video/webm', 'video/ogg'],
+			'document' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
+			'any' => ['image/*', 'video/*', 'application/pdf']
 		];
-		if (!empty($field['content'])) {
-			$dataAttrs['data-content'] = $field['content'];
+
+		return $defaults[$config['subtype']] ?? $defaults['image'];
+	}
+
+	protected function getMimeExtensions(array $mimeTypes): array
+	{
+		$extensions = [];
+		foreach ($mimeTypes as $mime) {
+			if (str_contains($mime, '*')) {
+				continue; // Skip wildcards
+			}
+			$ext = str_replace(['image/', 'video/', 'application/'], '', $mime);
+			$extensions[] = '.' . $ext;
 		}
-		if ($limit > 0) {
-			$dataAttrs['data-limit'] = $limit;
+		return $extensions;
+	}
+
+	protected function parseAttachmentIds(mixed $value): array
+	{
+		if (empty($value)) {
+			return [];
 		}
 
-		// Build data attributes
+		if (is_array($value)) {
+			return array_filter($value, 'is_numeric');
+		}
+
+		if (is_string($value)) {
+			return array_filter(explode(',', $value), 'is_numeric');
+		}
+
+		return is_numeric($value) ? [$value] : [];
+	}
+	/**
+	 * Render tag list field - inline tag input interface
+	 */
+	protected function renderTagListField(string $name, mixed $value, array $field): void
+	{
+		$values = is_array($value) ? $value : [];
 		$conditional = $this->handleConditionalField($field);
-		$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
+		$validationAttrs = $this->buildValidationAttributes($field);
 
-		if (!empty($field['group'])) {
+		if (array_key_exists('group', $field)) {
 			$name = $field['group'] . '::' . $name;
 		}
 
-		// Convert data attributes to string
-		$dataAttrString = '';
-		foreach ($dataAttrs as $attr => $val) {
-			$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
-		}
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		// Tag display format - defaults to first field value
+		$tagFormat = $field['tag_format'] ?? 'first_field';
 		?>
-		<div class="field upload <?= esc_attr($name) ?>"
-			<?= $dataAttrString ?>
-			<?= $conditional ?>>
+		<div class="field tag-list <?= esc_attr($name) ?>"
+			 data-field="<?= esc_attr($name) ?>"
+			 data-tag-format="<?= esc_attr($tagFormat) ?>"
+			<?= $describedBy ?>
+			<?= $conditional ?>
+			<?= $validationAttrs ?>>
 
-			<div class="file-upload-container">
-				<div class="file-upload-wrapper">
-					<input type="file"
-						   name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
-						   id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
-						   accept="<?= esc_attr($acceptAttr) ?>"
-						   data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
-						<?= $multiple ? 'multiple' : '' ?>
-						<?= !empty($field['required']) ? 'required' : '' ?>>
+			<?php if (!empty($field['label'])): ?>
+				<h3><?= esc_html($field['label']) ?></h3>
+			<?php endif; ?>
 
-					<h2><?= esc_html($field['label']) ?></h2>
+			<!-- Inline input row -->
+			<div class="tag-input-row">
+				<?php foreach ($field['fields'] as $subfield_name => $subfield_config): ?>
+					<?php
+					$subfield_config['label'] = $subfield_config['label'] ?? ucfirst($subfield_name);
+					$input_name = 'new_' . $subfield_name;
 
-					<?php if (!empty($field['description'])) : ?>
-						<p><?= esc_html($field['description']) ?></p>
-					<?php endif; ?>
+					// Store required state but don't render it on the input
+					// This prevents form submission validation but allows JS validation
 
-					<p class="file-upload-text">
-						<strong>Click to upload</strong> or drag and drop<br>
-						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
-						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
-					</p>
+					if (array_key_exists('required', $subfield_config)) {
+						$subfield_config['data']['required'] = true;
+						unset($subfield_config['required']); // Remove required for HTML rendering
+					}
+					$subfield_config['data']['ignore'] = true;
 
-					<?php if ($destination === 'post_group') {
-						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
-						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
-						?>
-						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
-						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
-					<?php }
-					if (!empty($field['upload_description'])) : ?>
-						<p><?= esc_html($field['upload_description']); ?></p>
-					<?php endif; ?>
-					<div class="file-error"></div>
-				</div>
+					$this->render($input_name, '', $subfield_config, false, false);
+					?>
+				<?php endforeach; ?>
+
+				<button type="button" class="button add-tag-item">
+					<?= jvbIcon('plus') ?> <?= $field['add_label'] ?? 'Add' ?>
+				</button>
 			</div>
 
-
-			<?php if ($destination === 'post_group') : ?>
-			<div class="group-display flex col" hidden>
-				<div class="preview-wrap flex col">
-					<div class="preview-actions">
-						<div class="selection-controls">
-							<div class="selected">
-								<div class="field">
-									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
-									<label for="select-all-uploads">
-										Select All
-									</label>
-								</div>
-								<div class="info" hidden>
-
-								</div>
-							</div>
-
-							<div class="selection-actions row btw" hidden>
-								<button type="button" data-action="add-to-group">
-									<?= jvbIcon('add') ?>
-									Group
-								</button>
-								<button type="button" data-action="delete-upload">
-									<?= jvbIcon('delete') ?>
-									Delete
-								</button>
-							</div>
-						</div>
-
-						<button type="button" data-action="upload" class="submit-uploads">
-							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
-						</button>
-					</div>
-					<?php endif; ?>
-
-					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
-					<span class="count">0/0</span>'); ?>
-					<div class="item-grid preview">
-						<?php
-						// Render existing attachments
-						foreach ($attachmentIds as $attachmentId) {
-							echo $this->renderExistingAttachment($attachmentId, $subtype);
-						}
-						?>
-					</div>
-
-					<?php if ($destination === 'post_group') : ?>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-				<div class="sidebar flex col">
-					<div class="header">
-						<h4>New <?= $plural?></h4>
-						<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
-					</div>
-					<div class="item-grid groups">
-						<div class="empty-group">
-							<p>Drag here to create a new <?= $singular ?>!</p>
-						</div>
-					</div>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
+			<!-- Tags display -->
+			<div class="tag-items">
+				<?php foreach ($values as $index => $item_data): ?>
+					<?php $this->renderTagItem($field['fields'], $item_data, $index, $name, $tagFormat); ?>
+				<?php endforeach; ?>
 			</div>
-		<?php endif; ?>
 
-			<?php if ($destination === 'meta') : ?>
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					   value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>>
+			<!-- Template for new tags -->
+			<template class="tag-template">
+				<?php $this->renderTagItem($field['fields'], [], '', $name, $tagFormat); ?>
+			</template>
+
+			<?php if (!empty($field['hint'])): ?>
+				<?php $this->renderHint($field['hint']); ?>
+			<?php endif; ?>
+
+			<?php if (!empty($field['description'])): ?>
+				<?php $this->renderDescription($field['description'], $name); ?>
 			<?php endif; ?>
 		</div>
 		<?php
 	}
 
-
-	protected function getAllowedTypes(array $config):array
-	{
-		$typeMap = [
-			'image' => [
-				'image/jpeg',
-				'image/png',
-				'image/gif',
-				'image/webp'
-			],
-			'video' => [
-				'video/mp4',
-				'video/webm',
-				'video/ogg',
-				'video/ogv',
-				'video/quicktime'
-			],
-			'document' => [
-				'application/pdf',
-				'application/msword',
-				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-				'application/vnd.ms-excel',
-				'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-				'text/plain',
-				'text/csv'
-			],
-			'any' => [] // Will be merged from all types
-		];
-		// If specific types are defined, use those
-		if (!empty($config['accepted_types'])) {
-			return is_array($config['accepted_types'])
-				? $config['accepted_types']
-				: [$config['accepted_types']];
-		}
-
-		// Otherwise use subtype defaults
-		$subtype = $config['subtype'] ?? 'image';
-
-		if ($subtype === 'any') {
-			return array_merge(
-				$typeMap['image'],
-				$typeMap['video'],
-				$typeMap['document']
-			);
-		}
-
-		return $typeMap[$subtype] ?? $typeMap['image'];
-
-	}
 	/**
-	 * Parse attachment IDs from value
+	 * Render individual tag item
 	 */
-	private function parseAttachmentIds(mixed $value): array
+	protected function renderTagItem(array $fields, array $data, int|string $index, string $base_name, string $format): void
 	{
-		if (empty($value)) return [];
-
-		if (is_array($value)) {
-			return array_filter(array_map('absint', $value));
-		}
-
-		return array_filter(array_map('absint', explode(',', $value)));
-	}
-
-	/**
-	 * Get file extensions for MIME types
-	 */
-	private function getMimeExtensions(array $mimeTypes): array
-	{
-		$extensionMap = [
-			'image/jpeg' => ['.jpg', '.jpeg'],
-			'image/png' => ['.png'],
-			'image/gif' => ['.gif'],
-			'image/webp' => ['.webp'],
-			'video/mp4' => ['.mp4'],
-			'video/webm' => ['.webm'],
-			'video/ogg' => ['.ogg'],
-			'video/ogv' => ['.ogv'],
-			'video/quicktime' => ['.mov'],
-			'application/pdf' => ['.pdf'],
-			'application/msword' => ['.doc'],
-			'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['.docx'],
-			'application/vnd.ms-excel' => ['.xls'],
-			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['.xlsx'],
-			'text/plain' => ['.txt'],
-			'text/csv' => ['.csv'],
-		];
-
-		$extensions = [];
-		foreach ($mimeTypes as $mime) {
-			if (isset($extensionMap[$mime])) {
-				$extensions = array_merge($extensions, $extensionMap[$mime]);
-			}
-		}
-
-		return array_unique($extensions);
-	}
-
-	/**
-	 * Get max file size for subtype
-	 */
-	private function getMaxFileSize(string $subtype): int
-	{
-		$sizes = [
-			'image' => 5242880,    // 5MB
-			'video' => 104857600,  // 100MB
-			'document' => 10485760 // 10MB
-		];
-
-		return $sizes[$subtype] ?? $sizes['image'];
-	}
-	/**
-	 * Get human-readable file size label
-	 */
-	private function getMaxFileSizeLabel(string $subtype): string
-	{
-		$bytes = $this->getMaxFileSize($subtype);
-		$mb = round($bytes / 1048576);
-		return "{$mb}MB";
-	}
-	/**
-	 * Format file size for display
-	 */
-	private function formatFileSize(int $bytes): string
-	{
-		if ($bytes >= 1073741824) {
-			return number_format($bytes / 1073741824, 1) . 'GB';
-		}
-		if ($bytes >= 1048576) {
-			return number_format($bytes / 1048576, 1) . 'MB';
-		}
-		if ($bytes >= 1024) {
-			return number_format($bytes / 1024, 1) . 'KB';
-		}
-		return $bytes . 'B';
-	}
-
-	/**
-	 * Get accepted types label
-	 */
-	private function getAcceptedTypesLabel(string $subtype, array $extensions): string
-	{
-		$labels = [
-			'image' => 'JPG, PNG, GIF, or WEBP',
-			'video' => 'MP4, WEBM, or MOV',
-			'document' => 'PDF, DOC, XLS, or TXT',
-			'any' => 'Images, Videos, or Documents'
-		];
-
-		return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
-			return ltrim($ext, '.');
-		}, array_slice($extensions, 0, 3))));
-	}
-
-	/**
-	 * Render existing attachment
-	 */
-	private function renderExistingAttachment(int $attachmentId, string $subtype): string
-	{
-		$attachment = get_post($attachmentId);
-		if (!$attachment) return '';
-
-		$url = wp_get_attachment_url($attachmentId);
-		$thumbUrl = $subtype === 'image'
-			? wp_get_attachment_image_url($attachmentId, 'medium')
-			: $url;
-
-		ob_start();
+		$tag_text = $this->getTagDisplayText($fields, $data, $format);
 		?>
-		<div class="upload-item existing" data-attachment-id="<?= esc_attr($attachmentId) ?>" data-subtype="<?= esc_attr($subtype) ?>">
-			<div class="preview">
-				<?php if ($subtype === 'image') : ?>
-					<img src="<?= esc_url($thumbUrl) ?>" alt="<?= esc_attr(get_post_meta($attachmentId, '_wp_attachment_image_alt', true)) ?>">
-				<?php elseif ($subtype === 'video') : ?>
-					<video src="<?= esc_url($url) ?>" controls></video>
-				<?php else : ?>
-					<div class="document-preview">
-						<?= jvbIcon('document') ?>
-						<span><?= esc_html(basename($url)) ?></span>
-					</div>
-				<?php endif; ?>
+		<div class="tag-item" data-index="<?= esc_attr($index) ?>">
+			<span class="tag-label"><?= esc_html($tag_text) ?></span>
 
-				<div class="overlay">
-					<div class="actions">
-						<button type="button" class="remove" title="Remove">
-							<span class="screen-reader-text">Remove <?= esc_attr($subtype) ?></span>
-							×
-						</button>
-					</div>
-				</div>
-			</div>
-
-			<?php if ($subtype === 'image') {
-				echo jvbImageMeta();
-			} ?>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-    private function renderImageField(string $name, mixed $value, array $field):void
-    {
-		$image_url = $title = $alt = $caption = false;
-		if ($value !== 0 || $value !== '') {
-			$image_url = wp_get_attachment_image_url((int)$value, 'medium') ?: false;
-			$caption = wp_get_attachment_caption((int)$value);
-			$alt = get_post_meta((int)$value, '_wp_attachment_image_alt', true);
-			$title = get_the_title((int)$value);
-		}
-
-        $mode = array_key_exists('mode', $field) ? $field['mode'] : 'direct';
-        $multiple = ($mode === 'selection' || isset($field['multiple']));
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$groupable = (array_key_exists('imageType', $field) && $field['imageType'] === 'groupable');
-		$singular = (array_key_exists('singular', $field)) ? $field['singular'] : 'post';
-		$plural = (array_key_exists('plural', $field)) ? $field['plural'] : 'posts';
-		$dataContent = (array_key_exists('content', $field)) ? ' data-content="'.$field['content'].'"' : '';
-		$dataType = ($groupable) ? 'groupable' : (($multiple) ? 'gallery' : 'single');
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field image <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-			 data-upload-field
-             data-mode="<?= esc_attr($mode); ?>"
-			 <?=$dataContent?>
-			<?= ' data-type="'.$dataType.'"'?>>
-
-            <div class="file-upload-container">
-                <div class="file-upload-wrapper">
-                    <input type="file"
-                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           accept=".jpg,.jpeg,.png,.gif,.webp"
-                           data-max-size="<?= $this->max_file_size; ?>"
-                        <?= $multiple ? 'multiple' : ''; ?>>
-                    <h2><?= esc_html($field['label']); ?></h2>
-                    <?php if (!empty($field['description'])) : ?>
-                        <p><?= esc_html($field['description']); ?></p>
-                    <?php endif; ?>
-                    <p class="file-upload-text">
-                        <strong>Click to upload</strong> or drag and drop<br>
-                        JPG, PNG, GIF, or WEBP (max. 5MB)
-                    </p>
-					<?php if ($groupable) { ?>
-					<p class="hint">You can group images to create separate <?= $plural ?>.</p>
-					<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
-					<?php } ?>
-                    <?php if (!empty($field['upload_description'])) : ?>
-                        <p><?= esc_html($field['upload_description']); ?></p>
-                    <?php endif; ?>
-                </div>
-                <div class="file-error"></div>
-            </div>
-			<?php if ($groupable) : ?>
-			<div class="group-display" hidden>
-				<div class="preview-wrap">
-					<div class="preview-actions">
-						<div class="selection-controls">
-							<div class="selected">
-								<div class="field">
-									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
-									<label for="select-all-uploads">
-										Select All
-									</label>
-								</div>
-								<div class="info" hidden>
-									With <span class="selection-count">0</span> selected
-								</div>
-							</div>
-
-
-							<!-- Selection actions (hidden by default) -->
-							<div class="selection-actions" hidden>
-								<button type="button" class="create-from-selection">
-									<?= jvbIcon('add') ?>
-									Create New <?= $singular ?>
-								</button>
-								<button type="button" class="remove-selection">
-									<?= jvbIcon('delete') ?>
-									Remove
-								</button>
-							</div>
-						</div>
-
-						<button type="button" class="submit-uploads">
-							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
-						</button>
-					</div>
-					<?php endif; ?>
-
-					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
-					<span class="count">0/0</span>'); ?>
-					<div class="item-grid preview">
-						<?php if ($image_url) {
-							echo jvbRenderImageForm((int)$value);
-						} ?>
-					</div>
-
-					<?php if ($groupable) : ?>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-				<div class="sidebar">
-					<div class="header">
-						<h4>New <?= $plural?></h4>
-						<p class="hint">Drag images into groups to create separate <?= $plural ?>.</p>
-						<p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p>
-					</div>
-					<button type="button" class="create-group-from-selection">
-						<?= jvbIcon('add') ?>
-						Create New <?= $singular ?>
-					</button>
-					<div class="item-grid groups">
-						<div class="empty-group">
-							<p>Drag here to create a new <?= $singular ?>!</p>
-						</div>
-					</div>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-			</div>
-		<?php endif; ?>
-
-			<?php if ($mode === 'direct') : ?>
+			<!-- Hidden inputs for data -->
+			<?php foreach ($fields as $field_name => $field_config): ?>
+				<?php
+				$value = $data[$field_name] ?? '';
+				$full_name = is_string($index) ? $field_name : "{$base_name}:{$index}:{$field_name}";
+				?>
 				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					   value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>>
-			<?php endif; ?>
+					   name="<?= esc_attr($full_name) ?>"
+					   value="<?= esc_attr($value) ?>"
+					   data-field="<?= esc_attr($field_name) ?>" />
+			<?php endforeach; ?>
+
+			<button type="button" class="remove-tag" aria-label="Remove">
+				<?= jvbIcon('x') ?>
+			</button>
 		</div>
 		<?php
-    }
+	}
 
-    protected function renderGalleryField(string $name, string|null|false $value, array $field):void
-    {
-        $ids = ($value === '' || is_null($value) || !$value) ? [] : explode(',',$value);
-
-        if (!empty($ids)) {
-            $ids = array_map('absint', $ids);
-        }
-
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		//TODO: This can probably just be a wrapper for renderImageField...
-        ?>
-        <div class="field gallery <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-            <?= $conditional ?>>
-
-            <label><?= esc_html($field['label']); ?></label>
-
-            <!-- Container for existing images -->
-            <div class="gallery-preview">
-                <?php
-                if (!empty($ids)) {
-                    foreach ($ids as $id) {
-                        $url = wp_get_attachment_image_url($id, 'medium');
-                        if ($url) {
-                            echo '<div class="preview-item" data-id="' . esc_attr($id) . '">';
-                            echo '<img src="' . esc_url($url) . '" alt="">';
-                            echo '<button type="button" class="remove-preview">' . jvbIcon('trash', ['title'=>'Remove']) . '</button>';
-                            echo '</div>';
-                        }
-                    }
-                }
-                ?>
-            </div>
-
-            <!-- Hidden file uploader that will be managed by BatchFileUploader -->
-            <div class="file-upload-container">
-                <div class="file-upload-wrapper">
-                    <input type="file"
-                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           accept=".jpg,.jpeg,.png,.gif,.webp"
-                           multiple>
-                    <p class="file-upload-text">
-                        <strong>Click to upload</strong> or drag and drop<br>
-                        JPG, PNG, GIF, or WEBP (max. 5MB)
-                    </p>
-                </div>
-                <div class="file-error"></div>
-            </div>
-
-            <!-- Hidden input for storing the IDs -->
-            <input type="hidden"
-                   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                   value="<?= esc_attr($value); ?>"
-				<?= !empty($field['required']) ? 'required' : ''; ?>>
-
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-
-        <?php
-    }
-    private function renderSelectField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $conditional = $this->handleConditionalField($field);
-        $default = isset($field['default']) ? $field['default'] : '';
-        $value = !empty($value) ? $value : $default;
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" data-field="<?=$name?>" <?=$conditional?>>
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-            <select
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-				<?=$describedBy?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-                <?php foreach ($field['options'] as $key => $label) : ?>
-                    <option value="<?= esc_attr($key); ?>"
-                        <?php selected($value, $key); ?>>
-                        <?= esc_html($label); ?>
-                    </option>
-                <?php endforeach; ?>
-            </select>
-        </div>
-        <?php
-    }
-
-    protected function renderHtmlField(string $name, mixed $value, array $field):void
-    {
-        $method_name = $field['content'];
-        $content = '';
-        if (method_exists($this, $method_name)) {
-            $content = $this->$method_name();
-        }
-
-        echo ($content == '') ? '' : sprintf(
-            '<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
-            esc_attr($name),
-            $content
-        );
-    }
-
-    private function renderDateField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $conditional = $this->handleConditionalField($field);
-        $format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
-
-        // Format the date if we have a value
-        if (!empty($value)) {
-            $date = DateTime::createFromFormat($format, $value);
-            if ($date) {
-                $value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
-            }
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
-            <div class="date-wrapper"<?=$describedBy?>>
-                <input
-                    type="date"
-                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    value="<?= esc_attr($value); ?>"
-                    <?= !empty($field['required']) ? 'required' : ''; ?>
-                    data-format="<?= esc_attr($format); ?>"
-                >
-                <?= jvbIcon('event') ?>
-            </div>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-
-        </div>
-        <?php
-    }
-
-	public function renderTimeField(string $name, mixed $value, array $field):void
+	/**
+	 * Get tag display text based on format
+	 */
+	protected function getTagDisplayText(array $fields, array $data, string $format): string
 	{
-		$conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		// Convert various time formats to HTML time input format (HH:MM)
-		if (!empty($value)) {
-			// If it's already in HH:MM format, use as-is
-			if (preg_match('/^\d{2}:\d{2}$/', $value)) {
-				// Value is already in correct format
-			} else {
-				// Try to parse and convert
-				$timestamp = strtotime($value);
-				if ($timestamp !== false) {
-					$value = date('H:i', $timestamp);
-				} else {
-					$value = '';
+		if (empty($data)) {
+			return 'New Item';
+		}
+
+		switch ($format) {
+			case 'first_field':
+				// Use the first field's value
+				$first_key = array_key_first($fields);
+				return $data[$first_key] ?? 'New Item';
+
+			case 'all_fields':
+				// Show all field values separated by commas
+				$values = array_filter(array_values($data));
+				return implode(', ', $values) ?: 'New Item';
+
+			case 'custom':
+				// Custom format - would need callback
+				return 'New Item';
+
+			default:
+				// Format is a template string like "{name} ({email})"
+				if (strpos($format, '{') !== false) {
+					$text = $format;
+					foreach ($data as $key => $value) {
+						$text = str_replace('{' . $key . '}', $value, $text);
+					}
+					return $text;
 				}
-			}
+				// Use specific field name
+				return $data[$format] ?? 'New Item';
 		}
-
-		if (array_key_exists('group', $field)) {
-			$name = $field['group'].'::'.$name;
-		}
-		?>
-		 <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-			<label for="<?= esc_attr($name); ?>">
-				<?= esc_html($field['label']); ?>
-			</label>
-			<div class="time-wrapper"<?=$describedBy?>>
-				<input
-					type="time"
-					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
-					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
-					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
-				>
-
-			</div>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
-	}
-
-	private function renderDatetimeField(string $name, mixed $value, array $field):void
-	{
-		$conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		// Convert datetime to HTML datetime-local format (YYYY-MM-DDTHH:MM)
-		if (!empty($value)) {
-			$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
-			if (!$date) {
-				// Try alternative formats
-				$formats = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'Y-m-d H:i'];
-				foreach ($formats as $format) {
-					$date = DateTime::createFromFormat($format, $value);
-					if ($date) break;
-				}
-			}
-
-			if ($date) {
-				$value = $date->format('Y-m-d\TH:i'); // HTML datetime-local format
-			} else {
-				$value = '';
-			}
-		}
-
-		if (array_key_exists('group', $field)) {
-			$name = $field['group'].'::'.$name;
-		}
-		?>
-		<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-			<label for="<?= esc_attr($name); ?>">
-				<?= esc_html($field['label']); ?>
-			</label>
-			<div class="datetime-wrapper"<?=$describedBy?>>
-				<input
-					type="datetime-local"
-					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
-					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
-					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
-				>
-				<?= jvbIcon('event') ?>
-			</div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
-	}
-
-
-    public function outputCharacterCountJS():void
-    {
-        ?>
-        <script>
-            document.querySelectorAll('[maxlength]').forEach(field => {
-                const counter = field.closest('.field')?.querySelector('.char-count .current');
-                if (counter) {
-                    const updateCount = () => counter.textContent = field.value.length;
-                    field.addEventListener('input', updateCount);
-                    updateCount();
-                }
-            });
-        </script>
-        <?php
-    }
-
-
-    //Conditional Fields
-    private function handleConditionalField(array $field):string
-    {
-        if (empty($field['condition'])) {
-            return '';
-        }
-
-        $condition = $field['condition'];
-        return sprintf(
-            'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
-            esc_attr($field['condition']['field']),
-            esc_attr($field['condition']['value']),
-            esc_attr($field['condition']['operator'] ?? '==')
-        );
-    }
-
-	protected function renderDescription(string $description, string $name):void
-	{
-		$id = $name.'-help';
-		$out = '<div class="has-tooltip">
-		<span class="tt-toggle">'.jvbIcon('help').'</span>
-		<div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div>
-		</div>';
-		echo $out;
-	}
-
-	protected function renderHint(array|string $hint):void
-	{
-		if (is_array($hint)) {
-			$out = '';
-			foreach($hint as $h) {
-				$out .= '<p class="hint">'.$h.'</p>';
-			}
-		} else {
-			$out = '<p class="hint">'.$hint.'</p>';
-		}
-		echo $out;
 	}
 }

--
Gitblit v1.10.0