From d38d825e3484d822ea3c1f0fb1df37ecf386b18a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 19:54:16 +0000
Subject: [PATCH] =TaxonomyCreator.js debugging

---
 inc/meta/MetaForm.php | 1075 ++++++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 832 insertions(+), 243 deletions(-)

diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 91d9980..aa537c0 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -15,6 +15,7 @@
 class MetaForm
 {
 	protected int $max_file_size = 5242880;
+	protected ?MetaTypeManager $type_manager = null;
 
 	/* ========== MAIN RENDER METHOD ========== */
 	public function return(string $name, mixed $value, array $config, bool $showHidden = false)
@@ -24,7 +25,6 @@
 	public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false): mixed
 	{
 		$out = '';
-
 		if (jvbCheck('hidden', $config) && !$showHidden) {
 			return $out;
 		}
@@ -32,6 +32,9 @@
 		if (!array_key_exists('type', $config)) {
 			return $out;
 		}
+		if (!$value) {
+			$value = $this->getDefaultValue($config['type']);
+		}
 
 		// Handle hidden display type
 		if (array_key_exists('display', $config) && $config['display'] === 'hidden') {
@@ -71,6 +74,18 @@
 		return $out;
 	}
 
+	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 => '',
+		};
+	}
+
 	/* ========== HELPER METHODS ========== */
 
 	/**
@@ -164,6 +179,7 @@
 		if (!empty($field['validation_message'])) {
 			$attrs['data-validation-message'] = $field['validation_message'];
 		}
+
 		$attrs['data-type'] = $field['type'];
 
 		$attrString = '';
@@ -186,6 +202,12 @@
 		$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 ?>
@@ -201,6 +223,7 @@
 					name="<?= esc_attr($data['name']) ?>"
 					value="<?= esc_attr($data['value']) ?>"
 					<?= $inputAttrs ?>
+					<?= $customData?>
 				>
 				<span class="validation-icon success" hidden aria-hidden="true">
                     <?= jvbIcon('check-circle') ?>
@@ -217,44 +240,6 @@
 		<?php
 	}
 
-	protected function renderComplexFieldWrapper(string $name, array $field, callable $renderContent): void
-	{
-		$data = $this->prepareFieldData($name, $field['value'] ?? '', $field);
-		$validationAttrs = $this->buildValidationAttributes($field);
-		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
-
-		// Additional data attributes for complex fields
-		$dataAttrs = '';
-		if (array_key_exists('data', $field) && !empty($field['data'])) {
-			foreach ($field['data'] as $key => $val) {
-				$dataAttrs .= ($val === '') ? ' data-' . $key : ' data-' . $key . '="' . esc_attr($val) . '"';
-			}
-		}
-
-		?>
-		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
-			<?= $conditional ?>
-			 data-field="<?= esc_attr($name) ?>"
-			<?= $validationAttrs ?>
-			<?= $dataAttrs ?>
-			<?= $describedBy ?>>
-
-			<?php if (!empty($field['label']) && (!isset($field['show_label']) || $field['show_label'])) : ?>
-				<h3 class="field-label"><?= esc_html($field['label']) ?></h3>
-			<?php endif; ?>
-
-			<?php $this->renderHintAndDescription($field, $name); ?>
-
-			<div class="field-content">
-				<?php $renderContent($name, $data, $field); ?>
-			</div>
-
-			<span class="validation-message" hidden role="alert"></span>
-		</div>
-		<?php
-	}
-
 	/**
 	 * Render field label with optional character count
 	 */
@@ -280,20 +265,36 @@
 	 */
 	protected function renderHintAndDescription(array $field, string $name): void
 	{
-		if (array_key_exists('hint', $field)) {
+		if (!empty($field['hint'])) {
 			$this->renderHint($field['hint']);
 		}
 
-		if (array_key_exists('description', $field)) {
+		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['input_type'] ?? 'text');
+		$this->renderStandardInput($name, $value, $field, $field['subtype'] ?? 'text');
 	}
 
 	public function renderEmailField(string $name, mixed $value, array $field): void
@@ -432,7 +433,7 @@
 						class="decrease"
 						title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
 						aria-label="Decrease <?= esc_attr($field['label']) ?>">
-					<?= jvbIcon('minus') ?>
+					<?= jvbIcon('minus-square') ?>
 				</button>
 
 				<input type="number"
@@ -451,7 +452,7 @@
 						class="increase"
 						title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
 						aria-label="Increase <?= esc_attr($field['label']) ?>">
-					<?= jvbIcon('add') ?>
+					<?= jvbIcon('plus-square') ?>
 				</button>
 			</div>
 
@@ -481,8 +482,7 @@
 				<select
 					id="<?= esc_attr($data['id']) ?>"
 					name="<?= esc_attr($data['name']) ?>"
-					<?= $inputAttrs ?>
-				>
+					<?= $inputAttrs ?>>
 					<?php foreach ($field['options'] as $key => $label) : ?>
 						<option value="<?= esc_attr($key) ?>" <?php selected($value, $key); ?>>
 							<?= esc_html($label) ?>
@@ -524,15 +524,16 @@
 				</legend>
 
 				<?php foreach ($field['options'] as $key => $label) : ?>
-					<label class="radio-option">
-						<input
-							type="radio"
-							name="<?= esc_attr($data['name']) ?>"
-							value="<?= esc_attr($key) ?>"
-							<?php checked($value, $key); ?>
-							<?= !empty($field['required']) ? 'required' : '' ?>
-						>
-						<span><?= esc_html($label) ?></span>
+					<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>
@@ -627,20 +628,32 @@
 
 	/* ========== REPEATER FIELD ========== */
 
-	private function renderRepeaterField(string $name, mixed $value, array $field): void
+	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;
+			$name = $field['group'].'::'.$name;
 		}
-
-		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ($value) {
-			$values = is_array($value) ? $value : [];
-			$rowLabel = $field['row_label'] ?? '';
-			$rowTitle = $field['new_row'] ?? 'New Item';
-			$addLabel = $field['add_label'] ?? 'Add Item';
-
+		$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));
+			}
 			?>
-			<div class="repeater-items" data-label="<?= esc_attr($rowLabel) ?>">
+			<h3><?= esc_html($field['label']); ?></h3>
+
+
+			<div class="repeater-items">
 				<?php
 				if (!empty($values)) {
 					foreach ($values as $index => $row) {
@@ -650,39 +663,45 @@
 				?>
 			</div>
 
-			<template class="<?= uniqid('repeaterTemplate') ?>">
-				<?php $this->renderRepeaterRow($field['fields'], [], '', $name, $rowTitle); ?>
+			<template class="<?=uniqid('repeaterTemplate')?>">
+				<?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
 			</template>
 
-			<button type="button" class="add-repeater-row button secondary">
-				<?= jvbIcon('plus', ['title' => 'Add']) ?>
-				<span><?= esc_html($addLabel) ?></span>
+			<button type="button" class="add-repeater-row">
+				<?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
 			</button>
-			<?php
-		});
+			<?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): void
+	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);
+		$display_number = (is_string($index)) ? $index : ($index + 1);
 		?>
-		<div class="repeater-row" data-index="<?= esc_attr($index) ?>">
-			<details <?= is_string($index) ? 'open' : '' ?>>
+		<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>
+					<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('delete', ['title' => 'Remove']) ?>
+						<?= jvbIcon('trash', ['title'=>'Remove']); ?>
 					</button>
 				</summary>
 				<div class="repeater-row-content">
 					<?php
-					foreach ($fields as $slug => $field) {
-						$field_name = ($base_name === '') ? $slug : sprintf('%s:%s:%s', $base_name, $index, $slug);
-						$field_value = $values[$slug] ?? '';
-						$this->render($field_name, $field_value, $field);
-					}
+					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>
@@ -708,9 +727,11 @@
 	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;
 
@@ -730,14 +751,15 @@
 		$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) ?>"
+		<<?= $fieldset?> class="field group <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 				  data-field="<?= esc_attr($name) ?>"
 			<?= $validationAttrs ?>
 			<?= $describedBy ?>>
-			<legend><?= esc_html($field['label']) ?></legend>
+			<<?=$legend?>><?= esc_html($field['label']) ?></<?=$legend?>>
 
 			<?php $this->renderHintAndDescription($field, $name); ?>
 
@@ -746,7 +768,7 @@
 			</div>
 
 			<span class="validation-message" hidden role="alert"></span>
-		</fieldset>
+		</<?= $fieldset?>>
 		<?php
 	}
 
@@ -776,22 +798,30 @@
 	}
 
 	/* ========== 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
 	{
-		// Merge with defaults
-		$config = array_merge([
-			'subtype' => 'image',
-			'accepted_types' => null,
-			'multiple' => false,
-			'limit' => 0,
-			'mode' => 'direct',
-			'destination' => 'meta',
-			'max_size' => null,
-			'convert' => 'webp',
-			'quality' => 80,
+		$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,
-		], $field);
+		];
+		$config = array_merge($defaultConfig, $field);
 
 		// Validate destination config
 		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
@@ -799,69 +829,241 @@
 			return;
 		}
 
-		if (array_key_exists('group', $field)) {
+		// 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;
 		}
 
-		// Prepare upload configuration
-		$acceptedTypes = $this->getAllowedTypes($config);
-		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
-		$acceptAttr = implode(',', $acceptExtensions);
-		$attachmentIds = $this->parseAttachmentIds($value);
-		$fieldType = $config['multiple'] ? 'gallery' : 'image';
-
-		// Build data attributes for uploader.js
-		$uploadData = [
-			'data-subtype' => $config['subtype'],
-			'data-mode' => $config['mode'],
-			'data-destination' => $config['destination'],
-			'data-multiple' => $config['multiple'] ? 'true' : 'false',
-			'data-limit' => $config['limit'],
-			'data-convert' => $config['convert'],
-			'data-quality' => $config['quality'],
-		];
-
-		if (!empty($config['content'])) {
-			$uploadData['data-content'] = $config['content'];
+		// 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 ?>>
 
-		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use (
-			$config, $acceptAttr, $attachmentIds, $fieldType, $uploadData, $value
-		) {
-			?>
-			<div class="upload-field-wrapper <?= esc_attr($fieldType) ?>"
-			<?php foreach ($uploadData as $attr => $val) : ?>
-				<?= $attr ?>="<?= esc_attr($val) ?>"
-			<?php endforeach; ?>>
-
-			<!-- Preview Area -->
-			<div class="upload-preview-area">
-				<?php $this->renderUploadPreviews($attachmentIds, $config); ?>
-			</div>
-
-			<!-- Upload Area -->
 			<div class="file-upload-container">
 				<div class="file-upload-wrapper">
 					<input type="file"
-						   name="<?= esc_attr($data['name']) ?>_temp"
+						   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) ?>"
-						<?= $config['multiple'] ? 'multiple' : '' ?>>
+						   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->getUploadInstructions($config)) ?>
+						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
+						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
 					</p>
+
+					<?php if ($destination === 'post_group') {
+						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
+						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
+						?>
+						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
+						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
+					<?php }
+					if (!empty($field['upload_description'])) : ?>
+						<p><?= esc_html($field['upload_description']); ?></p>
+					<?php endif; ?>
+					<div class="file-error"></div>
 				</div>
-				<div class="file-error"></div>
+				<?php jvbRenderProgressBar(); ?>
 			</div>
 
-			<!-- Hidden input for storing the IDs -->
-			<input type="hidden"
-				   name="<?= esc_attr($data['name']) ?>"
-				   value="<?= esc_attr($value) ?>"
-				<?= !empty($field['required']) ? 'required' : '' ?>>
+
+			<?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
-		});
+		<?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))));
 	}
 
 	/**
@@ -874,20 +1076,244 @@
 		}
 
 		foreach ($attachmentIds as $id) {
-			if ($config['subtype'] === 'image') {
-				$url = wp_get_attachment_image_url($id, 'thumbnail');
-				if ($url) {
-					echo '<div class="upload-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>';
-				}
+			switch ($config['subtype']) {
+				case 'image':
+					$this->renderImagePreview($id, $config);
+					break;
+				case 'video':
+					$this->renderVideoPreview($id, $config);
+					break;
+				case 'file':
+					$this->renderFilePreview($id, $config);
+					break;
 			}
-			// Add other subtypes (video, document) as needed
 		}
 	}
 
+	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'] : [];
+		$fields = array_merge([
+			'upload_data'	=> [
+				'type'	=> 'group',
+				'wrap'	=> 'details',
+				'label'	=> 'Image Info',
+				'hint'	=> 'These will be automatically generated if left blank.',
+				'fields'	=> [
+					'image-title'.$addID => [
+						'type'	=> 'text',
+						'label'	=> 'Image Title',
+						'value'	=> $title,
+						'data'	=> $dataID
+					],
+					'image-alt-text'.$addID => [
+						'type'	=> 'text',
+						'label'	=> 'Alt Text',
+						'value'	=> $alt,
+						'hint'	=> 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+						'data'	=> $dataID
+					],
+					'image-caption'.$addID => [
+						'type'	=> 'textarea',
+						'value'	=> $caption,
+						'label'	=> 'Image Caption',
+						'data'	=> $dataID
+					]
+				]
+			]
+		], $fields);
+
+		$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
 	 */
@@ -903,7 +1329,7 @@
 
 	/* ========== TAXONOMY/USER SELECTOR FIELDS ========== */
 
-	private function renderTaxonomyField(string $name, mixed $value, array $field): void
+	private function renderTaxonomyField(string $name, string $value, array $field): void
 	{
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'] . '::' . $name;
@@ -912,7 +1338,7 @@
 		$this->renderSelectorField($name, $value, $field, 'taxonomy');
 	}
 
-	private function renderUserField(string $name, mixed $value, array $field): void
+	private function renderUserField(string $name, string $value, array $field): void
 	{
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'] . '::' . $name;
@@ -931,55 +1357,64 @@
 		$validationAttrs = $this->buildValidationAttributes($field);
 		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
 
+		$isSimple = (array_key_exists('mode', $field) && $field['mode']==='simple');
 		// Parse selected values
+		$value = (is_array($value)) ? array_filter(array_map('absint', $value)): $value;
 		$selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value));
 
-		// Create selector instance
+		// 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 = [
-				'multiple' => $field['multiple'] ?? true,
-				'placeholder' => $field['placeholder'] ?? 'Search terms...',
-				'noResults' => 'No terms found',
-				'onClose' => 'updateMetaFormTaxonomy'
+				'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,
 			];
-			$selector = new TaxonomySelector($taxonomy, $selectorConfig);
+			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 = [
-				'multiple' => $field['multiple'] ?? true,
-				'placeholder' => $field['placeholder'] ?? 'Search posts...',
-				'noResults' => 'No posts found',
-				'shop_id' => $field['shop_id'] ?? null,
-				'onClose' => 'updateMetaFormPost'
+				'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($postType, $selectorConfig);
+
+			$selector = new PostSelector($containerId, $postType, $selectorConfig);
 			$icon = $postType;
 		}
 
-		$containerId = $name . '-' . $type . '-selector';
-
 		?>
-		<div class="field <?= esc_attr($type) ?>-selector <?= esc_attr($name) ?>"
+		<div class="field <?= esc_attr($type) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 			 data-field="<?= esc_attr($name) ?>"
 			<?= $validationAttrs ?>
 			<?= $describedBy ?>>
 
-			<div class="field-group-header row btw">
-				<label class="toggle row">
-					<?= jvbIcon($icon) ?>
-					<span><?= esc_html($field['label'] ?? ucfirst($type)) ?></span>
-				</label>
-				<button type="button"
-						class="add-item-btn button secondary"
-						title="Add <?= esc_attr(ucfirst($type)) ?>">
-					<?= jvbIcon('add', ['title' => 'Add ' . ucfirst($type)]) ?>
-				</button>
-			</div>
-
-			<?= $selector->render($selected, $containerId) ?>
+			<?= $selector->render($selected) ?>
 
 			<!-- Hidden input for form submission -->
 			<input type="hidden"
@@ -1006,7 +1441,7 @@
 			return;
 		}
 
-		// Parse stored data
+		// Extract stored values
 		if (is_string($value)) {
 			$value = maybe_unserialize($value);
 		}
@@ -1015,15 +1450,18 @@
 		$address = $stored_data['address'] ?? '';
 		$lat = $stored_data['lat'] ?? '';
 		$lng = $stored_data['lng'] ?? '';
-		$street = $stored_data['street'] ?? '';
 
+		// Generate unique field ID
+		$field_id = esc_attr($name);
+		$map_id = $field_id . '_map';
+
+		// Handle grouped fields
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'] . '::' . $name;
 		}
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
 
-		// Prepare JavaScript configuration
-		$field_id = esc_attr($name);
-		$map_id = $field_id . '_map';
+		// Prepare configuration for JavaScript initialization
 		$js_config = [
 			'fieldId' => $field_id,
 			'initialCoords' => (!empty($lat) && !empty($lng)) ? [
@@ -1032,42 +1470,67 @@
 			] : null
 		];
 
-		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use (
-			$stored_data, $street, $address, $lat, $lng, $map_id, $js_config
-		) {
-			?>
-			<div class="location-field-wrapper"
-				 data-location-field-init="<?= esc_attr(json_encode($js_config)) ?>">
+		// IMPORTANT: Properly escape the JSON for use in HTML attribute
+		$json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8');
+		?>
 
-				<?php if (!empty($street)) : ?>
-					<p class="current-location">
-						<strong>Current location:</strong> <?= esc_html($street) ?>
-					</p>
-				<?php endif; ?>
+		<div class="field location <?= esc_attr($field_id) ?>"
+			 data-field="<?= esc_attr($field_id) ?>"
+			 data-location-field-init="<?= $json_config ?>"<?=$describedBy?>>
 
-				<label for="<?= esc_attr($data['id']) ?>">Address</label>
-				<input type="text"
-					   id="<?= esc_attr($data['id']) ?>"
-					   name="<?= esc_attr($data['name']) ?>[address]"
-					   value="<?= esc_attr($address) ?>"
-					   placeholder="Enter an address"
-					   class="location-search-input"
-					   autocomplete="off"
-					<?= !empty($field['required']) ? 'required' : '' ?>>
-
-				<div id="<?= esc_attr($map_id) ?>" class="location-map" style="height: 300px;"></div>
-
-				<!-- Hidden fields for lat/lng -->
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[lat]" value="<?= esc_attr($lat) ?>" class="location-lat">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[lng]" value="<?= esc_attr($lng) ?>" class="location-lng">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[street]" value="<?= esc_attr($stored_data['street'] ?? '') ?>" class="location-street">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[city]" value="<?= esc_attr($stored_data['city'] ?? '') ?>" class="location-city">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[province]" value="<?= esc_attr($stored_data['province'] ?? '') ?>" class="location-province">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[postal]" value="<?= esc_attr($stored_data['postal'] ?? '') ?>" class="location-postal">
-				<input type="hidden" name="<?= esc_attr($data['name']) ?>[country]" value="<?= esc_attr($stored_data['country'] ?? '') ?>" class="location-country">
-			</div>
 			<?php
-		});
+			if (!empty($stored_data['street'])) {
+				echo '<p><b>Current location:</b> '.esc_html($stored_data['street']).'</p>';
+				echo '<p class="hint"><b>Search below to change:</b></p>';
+			}
+			?>
+			<?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="location-search-wrapper">
+				<div class="autocomplete-wrapper"></div>
+
+				<!-- Map container -->
+				<div class="location-preview">
+					<div id="<?= esc_attr($map_id); ?>"
+						 class="location-map">
+					</div>
+
+					<?php if (!empty($stored_data)):
+						jvbLocationLinks($stored_data);
+					endif; ?>
+				</div>
+
+				<!-- Hidden inputs for data storage -->
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[address]"
+					   value="<?= esc_attr($address); ?>"
+					   data-location-field="address">
+
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lat]"
+					   value="<?= esc_attr($lat); ?>"
+					   data-location-field="lat">
+
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lng]"
+					   value="<?= esc_attr($lng); ?>"
+					   data-location-field="lng">
+
+				<?php
+				// Component fields
+				$components = ['street', 'city', 'province', 'postal_code', 'country'];
+				foreach ($components as $component):
+					?>
+					<input type="hidden"
+						   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[<?= $component; ?>]"
+						   value="<?= esc_attr($stored_data[$component] ?? ''); ?>"
+						   data-location-field="<?= esc_attr($component); ?>">
+				<?php endforeach; ?>
+
+			</div>
+		</div>
+		<?php
 	}
 
 	/* ========== HTML FIELD ========== */
@@ -1109,22 +1572,6 @@
 		);
 	}
 
-	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
-	}
-
 	protected function getAllowedTypes(array $config): array
 	{
 		if (!empty($config['accepted_types'])) {
@@ -1171,5 +1618,147 @@
 
 		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);
+		$validationAttrs = $this->buildValidationAttributes($field);
 
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$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 tag-list <?= esc_attr($name) ?>"
+			 data-field="<?= esc_attr($name) ?>"
+			 data-tag-format="<?= esc_attr($tagFormat) ?>"
+			<?= $describedBy ?>
+			<?= $conditional ?>
+			<?= $validationAttrs ?>>
+
+			<?php if (!empty($field['label'])): ?>
+				<h3><?= esc_html($field['label']) ?></h3>
+			<?php endif; ?>
+
+			<!-- 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;
+
+					// Store required state but don't render it on the input
+					// This prevents form submission validation but allows JS validation
+
+					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;
+
+					$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>
+
+			<!-- 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>
+
+			<!-- 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
+	}
+
+	/**
+	 * Render individual tag item
+	 */
+	protected function renderTagItem(array $fields, array $data, int|string $index, string $base_name, string $format): void
+	{
+		$tag_text = $this->getTagDisplayText($fields, $data, $format);
+		?>
+		<div class="tag-item" data-index="<?= esc_attr($index) ?>">
+			<span class="tag-label"><?= esc_html($tag_text) ?></span>
+
+			<!-- 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="<?= 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
+	}
+
+	/**
+	 * Get tag display text based on format
+	 */
+	protected function getTagDisplayText(array $fields, array $data, string $format): string
+	{
+		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';
+		}
+	}
 }

--
Gitblit v1.10.0