Jake Vanderwerf
2026-01-11 181938ea32cf2c11d10d56018df22586adff7860
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;
   }
}