From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.

---
 inc/managers/CRUDManager.php | 1386 +++++++--------------------------------------------------
 1 files changed, 168 insertions(+), 1,218 deletions(-)

diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index e05a0e5..badcd6d 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -1,517 +1,196 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\managers\UserTermsManager;
-use JVBase\meta\MetaForm;
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
-use WP_User;
+use JVBase\base\Site;
+use JVBase\registrar\Registrar;
+use JVBase\ui\CRUDSkeleton;
 
 if (!defined('ABSPATH')) {
-	exit; // Exit if accessed directly
+	exit;
 }
 
+/**
+ * WordPress CRUD Manager
+ * Configures CRUDSkeleton for WordPress post types
+ */
 class CRUD {
-	protected WP_User $user;
-	protected int $user_id;
-	protected array $config;
+	protected CRUDSkeleton $skeleton;
+	protected Cache $cache;
 	protected string $content;
-	protected string $singular;
-	protected string $plural;
-	protected array $filters;
-	protected array $bulkActions;
-	protected MetaManager $meta;
-	protected MetaForm $form;
-	protected array $taxonomies;
-	protected array $statuses;
-	protected array $fields;
-	protected array $sections;
-	protected array $stuck;
+	protected array $taxonomies = [];
+	protected int $user_id;
+	protected Registrar $registrar;
 
+	public function __construct(string $content) {
+		$this->registrar = Registrar::getInstance($content);
 
-	//For Timeline-specific posts
-	protected bool $isTimeline = false;
-	protected array $nonTimelineFields = [];
-	protected array $timelineSharedFields = [];
-	protected array $timelineUniqueFields = [];
-
-	protected bool $userCanPublish = false;
-
-	public function __construct(string $content)
-	{
-		//If we haven't defined this content, bail early
-		if (!array_key_exists($content, JVB_CONTENT)) {
+		if (!$this->registrar) {
 			return;
 		}
-		$this->user = wp_get_current_user();
-		$this->user_id = $this->user->ID;
-		$this->config = JVB_CONTENT[$content];
-		$this->singular = $this->config['singular'];
-		$this->plural = $this->config['plural'];
+
+		$this->user_id = get_current_user_id();
 		$this->content = $content;
-		$this->fields = jvbGetFields($this->content, 'post');
-		$this->maybeSetupTimeline();
-		$this->sections = jvbGetSections($this->content, 'post');
-		$this->stuck = [
-			'post_title',
-			'term_name'
-		];
+		$this->cache = Cache::for('crud')->connect('post')->connect('taxonomy');
 
-		$this->init();
-
-		if ($this->isTimeline) {
-			$this->stuck[] = 'post_thumbnail';
+		if (JVB_TESTING) {
+			$this->cache->flush();
 		}
+
+		// Create and configure skeleton
+		$this->skeleton = new CRUDSkeleton();
+		$this->configure();
+	}
+
+	/**
+	 * Configure CRUDSkeleton from WordPress config
+	 */
+	protected function configure(): void {
+		// Basic info
+		$this->skeleton
+			->content($this->content, $this->registrar->getSingular(), $this->registrar->getPlural())
+			->title(
+				$this->registrar->config('dashboard')->getTitle(),
+				$this->registrar->config('dashboard')->getDescription()?? ''
+			);
+
+		// Initialize meta
+		$this->skeleton->addSearch();
+
+		// Timeline if applicable
+		if ($this->registrar && $this->registrar->hasFeature('is_timeline')) {
+			$this->skeleton->setTimeline();
+		}
+
+		// Fields and sections
+		$this->skeleton->setFields($this->registrar->getFields());
+
+		foreach ($this->registrar->getSections() as $config) {
+			$this->skeleton->addSection($config['id'], $config);
+		}
+
+		// Taxonomies
+		$this->initTaxonomies();
+
+		// Statuses
+		if ($this->registrar->hasFeature('is_calendar')) {
+			$this->skeleton->setCalendar();
+		}
+
+		if ($this->registrar->getType() === 'post') {
+			$this->skeleton->setDefaultStatus();
+		} else {
+			$this->skeleton->setStatuses([]);
+		}
+
+
+		// Views
+		$this->skeleton
+			->addViews()
+			->defaultView('grid');
+		$this->skeleton->addItemActions();
+
+		// Filters
+		$this->skeleton->addDateFilter();
+		$this->skeleton->addCustomDateRange($this->addDateRanges());
+		if (!empty($this->taxonomies)) {
+			$this->skeleton->addTaxonomyFilter($this->taxonomies, 'user');
+		}
+
+		// Capabilities
+		$this->skeleton->addCapabilities(['view', 'edit', 'create', 'delete']);
+
+		$plural = strtolower($this->registrar->getPlural() ?? $this->content . 's');
+		$canPublish = $this->userIsVerified() && user_can($this->user_id, "publish_{$plural}");
+		$this->skeleton->userCanPublish($canPublish);
+
+		// Bulk actions
+		$this->skeleton->addBulkActions(['edit', 'publish', 'draft', 'trash']);
+
+		// Uploader
+		if ($this->registrar->getType() === 'post') {
+			$this->setupUploader();
+		}
+
+
+		// Sticky fields
+		$stuck = ['post_title', 'term_name'];
+		if ($this->skeleton->get('isTimeline')) {
+			$stuck[] = 'post_thumbnail';
+		}
+		$this->skeleton->stickFields($stuck);
+
+		// Hook for create button
 		add_filter('jvbAdditionalActions', [$this, 'createItem']);
 	}
 
-	protected function init():void
-	{
-		$this->initStatuses();
-		$this->initBulkActions();
-		$this->initTaxonomies();
-		$this->initFilters();
-		$this->meta = new MetaManager(null, 'post', $this->content);
-		$this->form = new MetaForm();
-		$plural = strtolower($this->config['plural']??$this->content.'s');
-		$this->userCanPublish = (jvbUserIsVerified()) ?
-					user_can($this->user_id, "publish_{$plural}") : false;
+	protected function userIsVerified():bool {
+		$membership = Site::membership();
 
+		return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation');
 	}
+	/**
+	 * Setup uploader configuration
+	 */
+	protected function setupUploader(): void {
 
-	protected function maybeSetupTimeline():void {
-		$this->isTimeline = Features::forContent($this->content)->has('is_timeline');
+		$isSingleImage = $this->registrar->hasFeature('single_image');
 
-		if (!$this->isTimeline) {
-			return;
-		}
-		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
-				return true;
-			}
-			return false;
-		}));
-		array_unshift($this->timelineSharedFields, 'post_thumbnail');
-		array_unshift($this->timelineSharedFields, 'post_title');
-		array_unshift($this->timelineSharedFields, 'post_status');
-
-		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
-			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
-				return true;
-			}
-			return false;
-		}));
-
-		$all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
-		$this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
-			return !in_array($field, $all);
-		}, ARRAY_FILTER_USE_KEY);
-	}
-
-	protected function initTaxonomies():void
-	{
-		$this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) {
-			return in_array($this->content, $config['for_content']);
-		});
-	}
-
-	protected function initStatuses():void
-	{
-		$this->statuses = (array_key_exists('is_calendar', $this->config)) ?
-			[
-				'all'	=> [
-					'icon'  => 'calendar',
-					'label' => 'Everything',
-				],
-				'future'=> [
-					'label'	=> 'Upcoming',
-					'icon'	=> 'clock-clockwise',
-				],
-				'past'	=> [
-					'label'	=> 'Past',
-					'icon'	=> 'clock-counter-clockwise',
-				],
-				'repeat'=> [
-					'label'	=> 'Recurring',
-					'icon'	=> 'repeat',
-				],
-				'draft'	=> [
-					'icon'	=> 'eye-closed',
-					'label'	=> 'Hidden',
-				],
-				'trash'	=> [
-					'label'	=> 'Scrapped',
-					'icon'	=> 'trash',
-				],
-			] :
-			[
-				'all'	=> [
-					'icon'  => 'infinity',
-					'label' => 'Everything',
-				],
-				'publish'=> [
-					'icon'	=> 'eye',
-					'label'	=> 'Live',
-				],
-				'draft'	=> [
-					'icon'	=> 'eye-closed',
-					'label'	=> 'Hidden',
-				],
-				'trash'	=> [
-					'label'	=> 'Scrapped',
-					'icon'	=> 'trash',
-				],
-			];
-	}
-
-	protected function initBulkActions():void
-	{
-		$this->bulkActions = [
-			'edit'	=> 'Edit',
-			'publish'	=> 'Show',
-			'draft'	=> 'Hide',
-//			'copy'	=> 'Duplicate',
-			'trash'	=> 'Scrap'
-		];
-	}
-
-	protected function initFilters():void
-	{
-		$this->filters = [
-			'status'	=> $this->statuses,
-			'date'		=> [
-				'label'	=> 'Date',
-				'icon'	=> 'calendar'
-			]
+		$config = [
+			'type' => 'upload',
+			'subtype' => 'image',
+			'mode' => $isSingleImage ? 'direct' : 'selection',
+			'create_new' => true,
+			'label' => $this->registrar->getUploadTitle(),
+			'content' => $this->content,
+			'singular' => $this->registrar->getSingular(),
+			'plural' => $this->registrar->getPlural(),
+			'multiple' => true,
+			'destination' => $isSingleImage ? 'post' : 'post_group'
 		];
 
-		foreach ($this->taxonomies as $taxonomy=> $config) {
-			$this->filters['taxonomy'][$taxonomy] = [
-				'label'	=> $config['singular'],
-				'icon'	=> $config['icon']??'folder'
-			];
-		}
-	}
-
-	public function render():void
-	{
-		ob_start();
-		?>
-		<div class="dashboard-page <?= esc_attr($this->content) ?>"<?=($this->isTimeline) ? ' data-timeline' : ''?>>
-			<?php
-			$this->renderHeader();
-			$this->renderContent();
-			$this->renderModals();
-			$this->renderTemplates();
-			?>
-		</div>
-		<?php
-		echo ob_get_clean();
-
-	}
-
-	protected function renderHeader():void
-	{
-		?>
-		<h1>Your <?= $this->config['plural'] ?></h1>
-		<?php
-		if (array_key_exists('page_description', $this->config)) {
-			?>
-			<p class="page-description"><?=$this->config['page_description']?></p>
-			<?php
-		}
-		$this->renderHeaderActions();
-	}
-
-	protected function renderHeaderActions():void
-	{
-		$uploadConfig = [
-			'type'			=> 'upload',
-			'subtype'		=> 'image',
-			'mode'			=> (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection',
-			'create_new'	=> true,
-			'label'			=> (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'],
-			'content'		=> $this->content,
-			'singular'		=> $this->singular,
-			'plural'		=> $this->plural,
-			'multiple'		=> true,
-			'destination'	=> 'post'
-		];
-		if (!array_key_exists('single_image', $this->config) || $this->config['single_image'] === false) {
-			$uploadConfig['destination'] = 'post_group';
-		}
-		$uploadConfig['destination'] = 'post_group';
-		if (!jvbCheck('single_image', $this->config)) {
-			$uploadConfig['label'] = 'Create '.$this->config['plural'];
-			$uploadConfig['upload_text'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
-						<p>You can also select multiple images and click the "Add to Group" button.</p>
-						<p>If a '.$this->singular.' has multiple images, you can select the '.jvbIcon('star').' to set an image as the main one.</p>
-						<p>Images left ungrouped will become individual '.$this->plural.'</p>
-						<p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>';
+		if (!$isSingleImage) {
+			$config['upload_text'] = '<p>Drag images into groups. Each group becomes its own ' . $this->registrar->getSingular() . '.</p>
+				<p>You can also select multiple images and click the "Add to Group" button.</p>
+				<p>If a ' . $this->registrar->getSingular() . ' has multiple images, you can select the ' . jvbDashIcon('star') . ' to set an image as the main one.</p>
+				<p>Images left ungrouped will become individual ' . $this->registrar->getPlural() . '</p>
+				<p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>';
 		} else {
-			$uploadConfig['description'] = 'Each image will become its own '.$this->singular.'.';
+			$config['description'] = 'Each image will become its own ' . $this->registrar->getSingular() . '.';
 		}
-		?>
-		<details open class="uploader">
-			<summary class="row btw"><?= $this->config['upload_title'] ?? 'Bulk Upload '.$this->plural?></summary>
-			<?php
-			$this->meta->render(
-				'form',
-				'new_'.$this->content,
-				$uploadConfig
-			);
-			?>
-		</details>
-		<?php
+
+		$this->skeleton->addUploader($config);
 	}
 
-	protected function renderContent():void
-	{
-		?>
-		<section class="items-list <?=$this->content?> crud" data-content="<?= $this->content ?>">
-			<?php
-			$this->renderFilters();
-			$this->renderBulkControls();
-			?>
-			<div class="<?= $this->content ?> item-grid" role="grid"></div>
-			<div class="scroll-sentinel" aria-hidden="true"></div>
-		</section>
-		<?php
-		$state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->content);
-
-		echo '<template class="emptyState">'.$state.'</template>';
-		?>
-		<?php
-	}
-
-	protected function renderEmptyState():string
-	{
-		ob_start();
-		?>
-		<div class="empty-state">
-			<h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3>
-			<p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p>
-			<p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderFilters():void
-	{
-		?>
-		<div class="all-filters col start" data-ignore>
-			<div class="search row start nowrap">
-				<span class="label">Search:</span>
-				<?= jvbSearch() ?>
-			</div>
-			<div class="controls col start">
-				<?php
-				$this->renderViewFilters();
-				$this->renderStatusFilters();
-				$this->renderOrderFilters();
-				?>
-			</div>
-			<div class="filters row start">
-				<span class="label">Filters:</span>
-				<?php
-					$this->renderTaxonomyFilters();
-					$this->renderDateFilters();
-				?>
-				<button type="button" class="clear-filters row" hidden>
-					<?= jvbIcon('x', ['title'    => 'Clear']); ?>
-					Clear All Filters
-				</button>
-			</div>
-
-			<?= $this->renderColumnSelector(); ?>
-		</div>
-		<?php
-	}
-
-	protected function renderOrderFilters():void
-	{
-		?>
-		<div class="radio-options order row btw w-full">
-			<?php
-				$order = [
-					'orderby' => [
-						'date' => 'Order by date created',
-						'alphabetical' => 'Order alphabetically'
-					],
-					'order' => [
-						'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
-						'sort-descending' => 'In descending order (A-Z, newest to oldest)'
-					]
-				];
-
-				foreach ($order as $o => $option) {
-					?>
-					<div class="row start">
-						<span class="label"><?= ucfirst($o)?>:</span>
-					<?php
-					$i = 0;
-					foreach ($option as $opt => $label) {
-						$icon = $opt === 'date' ? 'calendar' : $opt;
-						?>
-						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
-
-						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($icon)?></label>
-						<?php
-						$i++;
-					}
-					?>
-					</div>
-					<?php
-				}
-			?>
-		</div>
-		<?php
-	}
-	protected function renderStatusFilters():void
-	{
-		if (empty($this->statuses)) {
-			return;
-		}
-		?>
-		<div class="radio-options status row">
-			<span class="label">Status:</span>
-			<?php
-			$i = 1;
-			foreach ($this->statuses as $status => $config) {
-				$checked = ($i == 1) ? ' checked' : '';
-				?>
-				<input type="radio" class="btn" data-filter="status" value="<?=$status?>" name="status" id="<?=$status?>"<?=$checked?>>
-				<label for="<?=$status?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span><?=$config['label']?><span class="count"></span></span>
-				</label>
-				<?php
-				$i++;
-			}
-			?>
-		</div>
-		<?php
-	}
-
-	protected function renderViewFilters():void
-	{
-		?>
-		<div class="radio-options view row">
-			<span class="label">View:</span>
-
-			<?php
-			$views = [
-				'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'],
-				'list' => ['icon' => 'rows', 'label' => 'List View'],
-				'table' => ['icon' => 'table', 'label' => 'Table View']
-			];
-
-			$first = true;
-			foreach ($views as $view => $config):
-				?>
-				<input type="radio"
-					   data-view="<?= esc_attr($view) ?>"
-					   value="<?= esc_attr($view) ?>"
-					   class="btn"
-					   name="view"
-					   id="view-<?= esc_attr($view) ?>"
-					<?= $first ? 'checked' : '' ?>>
-				<label for="view-<?= esc_attr($view) ?>"
-					   title="<?= esc_attr($config['label']) ?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
-				</label>
-				<?php
-				$first = false;
-			endforeach;
-			?>
-		</div>
-		<?php
-	}
 	/**
-	 * Render column selector for table view
+	 * Initialize taxonomies from WordPress config
 	 */
-	protected function renderColumnSelector(): string {
-		ob_start();
-		?>
-		<details class="multi-select" title="Select columns" hidden>
-			<summary class="row start nowrap">
-				<?= jvbIcon('columns') ?>
-				<span class="labels">Toggle Columns</span>
-			</summary>
-			<div class="column-list">
-				<?php foreach ($this->fields as $fieldName => $config):
-					if (array_key_exists('hidden', $config)){
-						continue;
-					}
-					?>
-					<input type="checkbox"
-						   id="show-<?= esc_attr($fieldName) ?>"
-						   class="column-toggle ch"
-						   name="show-<?= esc_attr($fieldName) ?>"
-						   checked>
-					<label for="show-<?= esc_attr($fieldName) ?>">
-						<?= esc_html($config['label']) ?>
-					</label>
-				<?php endforeach; ?>
-			</div>
-		</details>
-		<?php
-		return ob_get_clean();
+	protected function initTaxonomies(): void {
+		$this->taxonomies = ($this->registrar->getType() === 'post') ? $this->registrar->registrar->taxonomies : [];
 	}
-	protected function renderTaxonomyFilters():void
-	{
-		if (empty($this->taxonomies)) {
-			return;
-		}
-		$out = '';
-		foreach ($this->taxonomies as $taxonomy => $config) {
-			$terms = $this->getCommonTerms($taxonomy);
-			if (!empty($terms)) {
-				$out .= sprintf(
-					'<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
-                <select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s">
-                <option value="">by %s</option>',
-					$taxonomy,
-					jvbIcon($config['icon'], ['title'    => $config['plural']]),
-					esc_html($config['plural']),
-					$taxonomy,
-					$taxonomy,
-					$taxonomy,
-					$taxonomy,
-					$config['plural']
-				);
 
-
-				foreach ($terms as $term) {
-					$out .= sprintf(
-						'<option value="%s">%s</option>',
-						esc_attr($term['term_id']),
-						esc_html($term['name'])
-					);
-				}
-				$out .= '</select></div>';
-			}
-		}
-		echo $out;
-	}
 	/**
-	 * Get common terms for taxonomy
-	 * @param string $taxonomy
-	 * @return array
+	 * Add create button to dashboard actions
 	 */
-	protected function getCommonTerms(string $taxonomy):array {
-		$manager = new UserTermsManager();
-		return $manager->getUserTerms($this->user_id, $taxonomy);
+	public function createItem(array $actions): array {
+		$actions[] = [
+			'button' => '<button type="button" class="create-item row" title="Create New ' . $this->registrar->getSingular() . '">'
+				. jvbDashIcon('plus-square')
+				. '<span class="screen-reader-text">Create New ' . $this->registrar->getSingular() . '</span></button>',
+			'content' => '', // Modal is rendered by skeleton
+		];
+
+		return $actions;
 	}
 
-	protected function renderDateFilters():void
+	protected function addDateRanges():array
 	{
-		$postType = jvbCheckBase($this->content);
-		// Get available months
-		global $wpdb;
-		$months = $wpdb->get_results("
+		return $this->cache->remember(
+			'dateRanges',
+			function() {
+				$postType = jvbCheckBase($this->content);
+				// Get available months
+				global $wpdb;
+				$months = $wpdb->get_results("
 			SELECT DISTINCT
 				YEAR(post_date) as year,
 				MONTH(post_date) as month
@@ -520,757 +199,28 @@
 			AND post_author = '{$this->user_id}'
 			ORDER BY post_date DESC
 		");
-
-		// Quick filters
-		$out = '<div class="row nowrap">
-        <label for="filter-date">'.
-			jvbIcon('calendar',['title'=>'Date']).
-			'<span class="screen-reader-text">by Date</span>
-        </label>
-        <select id="filter-date" class="date-filter" data-filter="date">
-            <option value="">by Date</option>
-            <option value="today">Today</option>
-            <option value="week">Past Week</option>
-            <option value="month">Past Month</option>
-            <option value="year">Past Year</option>
-            <option value="custom">Custom Range...</option>
-        </select>
-    </div>';
-
-		$form = '<div class="custom-range row">
-            <label for="date-start" class="col">
-                From
-            </label>
-			<input type="date" id="date-start" class="date-start">
-            <label for="date-end" class="col">
-               To
-            </label>
-            <input type="date" id="date-end" class="date-end">
-        </div>
-        <div class="month-picker">
-            <label>
-                <span>Or select month</span>
-                <select class="month-select">
-                    <option value="">&emsp; . . . &emsp;</option>';
-
-
-		foreach ($months as $date) {
-			$month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year));
-			$value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT);
-			$form .= sprintf(
-				'<option value="%s">%s</option>',
-				esc_attr($value),
-				esc_html($month_name)
-			);
-		}
-
-		$form .= '</select>
-            </label>
-        </div>';
-
-		// Custom date range
-		$out .= jvbNewModal(
-			'date-range',
-			'Filter Results by Date:',
-			$form
-		);
-
-		echo $out;
-	}
-
-	protected function renderBulkControls():void
-	{
-		if (empty($this->bulkActions)) {
-			return;
-		}
-		?>
-		<div class="bulk-controls row nowrap btw">
-			<div class="bulk-select">
-				<input type="checkbox" id="select-all" class="select-all">
-				<label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label>
-			</div>
-			<div class="bulk-actions row nowrap" hidden>
-				<label for="bulk-action-select" class="screen-reader-text">
-					Select what to do with this selection.
-				</label>
-				<select class="bulk-action-select" id="bulk-action-select">
-
-				</select>
-			</div>
-		</div>
-
-		<template class="notTrashOptions">
-			<select class="wrap">
-				<option value="">Bulk Actions...</option>
-				<?php
-				foreach ($this->bulkActions as $control => $label) {
-					$disabled = ($control === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
-					?>
-					<option value="<?=$control?>"<?=$disabled?>><?=$label?></option>
-					<?php
+				$ranges = [];
+				foreach ($months as $date) {
+					$month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year));
+					$value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT);
+					$ranges[$value] = $month_name;
 				}
-				foreach ($this->taxonomies as $taxonomy => $config) {
-				?>
-					<option value="tax-<?=$taxonomy?>">Add to <?= $config['singular'] ?></option>
-				<?php
-				}
-				?>
-			</select>
-
-		</template>
-		<template class="trashOptions">
-			<select class="wrap">
-				<option value="">Bulk Actions...</option>
-				<option value="restore">Restore</option>
-				<option value="delete">Permanently Delete</option>
-			</select>
-		</template>
-		<?php
-	}
-
-	protected function renderModals():void
-	{
-//		$this->renderCreateModal();
-		$this->renderEditModal();
-		$this->renderBulkEditModal();
-	}
-	protected function renderCreateModal():void
-	{
-		echo jvbNewModal(
-			'create',
-			'Creating <span class="count"></span> New '.$this->config['singular'],
-			str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm())
+				return $ranges;
+			}
 		);
 	}
 
-	protected function editForm():string
-	{
-		ob_start();
-		?>
-		<form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
-			<?= jvbFormStatus() ?>
-			<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
-			<input type="hidden" name="content" value="<?=$this->content?>" />
-			<div class="fields">
-				<div class="field-group radio-options row">
-					<span>Status:</span>
-					<?php
-					$this->getApplicableStatuses('edit');
-					?>
-				</div>
-				<?php if (!$this->userCanPublish) { ?>
-                    <p class="description">Your account needs to be verified before you can publish content.</p>
-                <?php }
-
-				if (!empty($this->sections)) {
-					$tabs = [];
-					foreach ($this->sections as $slug => $title) {
-						$tabs[$slug] = [
-							'title'	=> $title,
-							'content' => '',
-							'description' => jvbSectionDescription($slug)??'',
-						];
-						$icon = jvbSectionIcon($slug);
-						if ($icon !== '') {
-							$tabs[$slug]['icon'] = $icon;
-						}
-					}
-				} else {
-					$tabs = false;
-				}
-
-
-				$fields = $this->fields;
-				if (!$this->isTimeline) {
-					$first = ['post_thumbnail', 'post_title', 'price'];
-
-					foreach ($first as $f) {
-						if (array_key_exists($f, $fields)) {
-							if ($tabs) {
-								$tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
-							} else {
-								$this->meta->render('form', $f, $fields[$f]);
-							}
-
-							unset($fields[$f]);
-						}
-					}
-				}
-
-				if ($this->isTimeline) {
-					$temp = array_filter($fields, function ($field) {
-						return in_array($field, $this->timelineUniqueFields);
-					}, ARRAY_FILTER_USE_KEY);
-
-					$config = [
-						'type'		=> 'gallery',
-						'subtype'	=> 'timeline',
-						'data'		=> 'timeline',
-						'label'		=> 'Progression',
-						'fields'	=> $temp
-					];
-					$content = '';
-					foreach ($fields as $slug=> $field) {
-						if (in_array($slug, $this->timelineSharedFields)) {
-							$content .= $this->form->render($slug, null, $field, false, true);
-						}
-					}
-
-
-					$content .= $this->meta->render('form', 'timeline', $config, false,true);
-
-					$tabs['progression']['content'] = $content;
-					$fields = $this->nonTimelineFields;
-				}
-				foreach ($fields as $n => $config) {
-					if ($tabs) {
-						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
-						$tabs[$section]['content'] .= $this->meta->render('form', $n, $config, false, true);
-					} else {
-						$this->meta->render('form', $n, $config);
-					}
-				}
-
-				if ($tabs) {
-					jvbRenderTabs($tabs);
-				}
-				?>
-			</div>
-		</form>
-		<?php
-		return ob_get_clean();
-	}
-
-
-	protected function renderRowFields():void
-	{
-		$fields = $this->fields;
-
-		// Render priority fields first
-		$first = ['post_thumbnail', 'post_title', 'price'];
-		foreach ($first as $f) {
-			if (array_key_exists($f, $fields)) {
-				$this->meta->render('form', $f, $fields[$f]);
-				unset($fields[$f]);
-			}
-		}
-
-		// Render remaining fields
-		foreach ($fields as $name => $config) {
-			if (!array_key_exists('hidden', $config) || !$config['hidden']) {
-				$this->meta->render('form', $name, $config);
-			}
-		}
-	}
-
-	protected function getApplicableStatuses(string $prefix) {
-		foreach ($this->statuses as $status => $config) {
-			if ($status === 'all') {
-				continue;
-			}
-			if (in_array($status, ['future', 'past'])) {
-				if ($status === 'future') {
-					$status = 'publish';
-					$config = [
-						'icon'	=> 'eye',
-						'label'	=> 'Live',
-					];
-				} else {
-					continue;
-				}
-			}
-			$disabled = ($status === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
-			?>
-			<input type ="radio"
-				   name="post_status"
-				   class="btn"
-				   value="<?= esc_attr($status)?>"
-				   id="<?=$prefix?>set-<?= esc_attr($status) ?>"
-				<?= $disabled?>>
-			<label for="<?=$prefix?>set-<?=esc_attr($status)?>">
-				<?= jvbIcon($config['icon'], ['title' => $config['label']]) ?>
-				<span><?= esc_html($config['label'])?></span>
-			</label>
-			<?php
-		}
-	}
-	protected function renderEditModal():void
-	{
-		echo jvbNewModal(
-			'edit',
-			'Edit your '.$this->singular,
-			$this->editForm()
-		);
-	}
-
-	protected function renderBulkEditModal():void
-	{
-		if (empty($this->bulkActions)) return;
-		ob_start();
-		?>
-		<form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->content?>">
-			<?= jvbFormStatus() ?>
-			<div class="selected"></div>
-			<p class="description">You can unselect items by clicking the image here.</p>
-			<p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
-			<div class="fields">
-				<div class="field-group radio-options row">
-					<?php
-					$this->getApplicableStatuses('bulk-');
-                    ?>
-				</div>
-				<?php
-				if (!empty($this->taxonomies)) {
-				?>
-				<div class="taxonomies">
-					<?php
-					foreach ($this->taxonomies as $taxonomy => $config) {
-						$this->meta->render(
-							'form',
-							'bulk-edit-'.$taxonomy,
-							[
-								'type'		=> 'taxonomy',
-								'label'		=> $config['singular'],
-								'taxonomy'	=> $taxonomy,
-								'createNew'	=> jvbUserIsVerified(),
-								'multiple'	=> true,
-								'mode'		=> 'append'
-							]
-						);
-					}
-					?>
-				</div>
-				<?php
-				}
-				$fields = $this->fields;
-				$fields = array_filter($fields, function ($field) {
-					return array_key_exists('bulkEdit', $field);
-				});
-				foreach ($fields as $fieldName => $config) {
-					$this->meta->render('form', $fieldName, $config);
-				}
-				?>
-			</div>
-		</form>
-		<template class="bulkItem">
-			<label>
-				<input type="checkbox">
-				<img>
-			</label>
-		</template>
-		<?php
-		$form = ob_get_clean();
-		echo jvbNewModal(
-			'bulkEdit',
-			'Bulk Edit <span class="selected"></span> '.$this->config['plural'],
-			$form
-		);
-	}
-
-	protected function renderTemplates():void
-	{
-		$this->renderListView();
-		$this->renderGridView();
-		$this->renderTableView();
-		$this->renderTableRow();
-		if ($this->isTimeline) {
-			$temp = array_filter($this->fields, function ($field) {
-				return in_array($field, $this->timelineUniqueFields);
-			}, ARRAY_FILTER_USE_KEY);
-			$form = new MetaForm();
-			echo '<template class="timelineItem">';
-			$form->renderImagePreview(null,['fields' => $temp]);
-			echo '</template>';
-		}
-		echo jvbGetEmptyStateTemplate();
-		echo jvbGetGalleryPreviewTemplate();
-
-
-	}
-
-	protected function renderItemSelect():string
-	{
-		ob_start();
-		?>
-		<div class="item-select">
-			<input type="checkbox" class="select-item">
-			<label class="select-item-label">
-				<span class="screen-reader-text">Select this <?= $this->singular ?></span>
-			</label>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderImage():string
-	{
-		ob_start();
-		?>
-		<img loading="lazy" alt="">
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderItemActions():string
-	{
-		ob_start();
-		?>
-		<div class="item-actions">
-			<button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>">
-				<?=jvbIcon('pencil-simple')?>
-				<span class="screen-reader-text">Edit <?= $this->singular ?></span>
-			</button>
-			<button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>">
-				<?=jvbIcon('trash')?>
-				<span class="screen-reader-text">Scrap <?= $this->singular ?></span>
-			</button>
-<!--			<button type="button" class="action" data-action="toggle-status">-->
-<!--				<span class="screen-reader-text">Toggle --><?php //= $this->singular ?><!-- Visibility</span>-->
-<!--			</button>-->
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderItemFields(bool $form = false):string
-	{
-		ob_start();
-		foreach ($this->fields as $name => $config) {
-			$renderMode = $form ? 'form' : 'render';
-
-			$field = $this->meta->render($renderMode, $name, $config, false, true);
-
-			// Special handling for title in grid view
-			if ($name === 'post_title' && !$form) {
-				$field = str_replace('<p', '<h3', str_replace('</p>', '</h3>', $field));
-			}
-
-			echo $field;
-		}
-		return ob_get_clean();
-	}
-
-	protected function renderGridView():void
-	{
-		?>
-		<template class="gridView">
-			<div class="item <?= $this->content ?>">
-				<input type="checkbox" class="select-item" name="select-item">
-				<label title="Select this <?= $this->singular?>" class="select-item-label">
-					<?= $this->renderImage() ?>
-				</label>
-				<?= $this->renderItemActions(); ?>
-			</div>
-		</template>
-		<?php
-	}
-
-	protected function renderListView():void
-	{
-		?>
-		<template class="listView">
-			<div class="item <?=esc_attr($this->content)?> row nowrap">
-				<?= $this->renderItemSelect()?>
-				<?=$this->renderImage() ?>
-				<div class="col start w-full">
-					<?= $this->renderItemActions()?>
-					<h3 data-field="post_title"></h3>
-					<p data-attr="date"></p>
-					<p data-field="price"></p>
-					<div data-field="post_excerpt"></div>
-				</div>
-			</div>
-		</template>
-		<?php
-	}
-
-	protected function renderTableView():void
-	{
-		if ($this->isTimeline) {
-			$this->renderTimelineTableView();
-			return;
-		}
-		?>
-		<template class="contentTable">
-			<form class="table"
-				  data-save="content"
-				  data-content="<?= esc_attr($this->content) ?>"
-				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
-				<?= jvbFormStatus() ?>
-				<?= $this->renderTableActions() ?>
-
-				<table>
-					<thead>
-					<?= $this->renderTableHeader() ?>
-					</thead>
-					<tbody>
-					<!-- Rows will be inserted here -->
-					</tbody>
-					<tfoot>
-					<?= $this->renderTableHeader() ?>
-					</tfoot>
-				</table>
-			</form>
-		</template>
-		<?php
-	}
 	/**
-	 * Render table row template
+	 * Render the interface
 	 */
-	protected function renderTableRow(): void {
-		if ($this->isTimeline) {
-			$this->renderTimelineTableGroup();
-			return;
-		}
-		?>
-		<template class="tableView">
-			<tr class="item">
-				<td class="select">
-					<?= $this->renderItemSelect() ?>
-				</td>
-				<td class="status" data-field="post_status">
-					<?= $this->renderStatusRadios() ?>
-				</td>
-				<?php
-				$makeDetails = [
-					'group',
-					'repeater',
-					'checkbox',
-					'radio'
-				];
-				foreach ($this->fields as $name => $config):
-					if (array_key_exists('hidden', $config)){
-						continue;
-					}
-					$makeThisDetailed = (in_array($config['type'], $makeDetails));
-					?>
-					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-						<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-						<?php $this->meta->render('form', $name, $config); ?>
-						<?= $makeThisDetailed ? '</details>' : '' ?>
-					</td>
-				<?php endforeach; ?>
-			</tr>
-		</template>
-		<?php
-	}
-
-	protected function renderTimelineTableView():void
-	{
-		?>
-		<template class="contentTable">
-			<form class="table"
-				  data-save="content"
-				  data-content="<?= esc_attr($this->content) ?>"
-				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
-				<?= jvbFormStatus() ?>
-				<?= $this->renderTableActions() ?>
-
-				<table>
-					<thead>
-					<?= $this->renderTimelineTableHeader() ?>
-					</thead>
-					<!-- Rows are inserted as tbody groups -->
-					<tfoot>
-					<?= $this->renderTimelineTableHeader() ?>
-					</tfoot>
-				</table>
-			</form>
-		</template>
-		<?php
-	}
-
-	protected function renderTimelineTableGroup():void
-	{
-		$makeDetails = [
-			'group',
-			'repeater',
-			'checkbox',
-			'radio'
-		];
-		?>
-		<template class="tableView">
-			<tbody class="item">
-				<tr class="shared">
-					<td class="select">
-						<?= $this->renderItemSelect() ?>
-					</td>
-					<td class="show-post_status field" data-field="post_status">
-						<?= $this->renderStatusRadios() ?>
-					</td>
-					<?php
-					foreach ($this->fields as $name => $config) {
-						if(array_key_exists('hidden', $config) || $name === 'post_status') {
-							continue;
-						}
-						if (!in_array($name, $this->timelineSharedFields)) {
-							echo '<td></td>';
-							continue;
-						}
-							$makeThisDetailed = (in_array($config['type'], $makeDetails));
-						?>
-						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-							<?php $this->meta->render('form', $name, $config); ?>
-							<?= $makeThisDetailed ? '</details>' : '' ?>
-						</td>
-						<?php
-					}
-
-					?>
-				</tr>
-				<tr class="timeline-point">
-					<td class="select">
-						<button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbIcon('dots-six') ?></button>
-					</td>
-					<td class="show-post_status field" data-field="post_status">
-						<?= $this->renderStatusRadios() ?>
-					</td>
-					<?php
-					foreach ($this->fields as $name => $config) {
-						if(array_key_exists('hidden', $config) || $name === 'post_status') {
-							continue;
-						}
-						if (!in_array($name, $this->timelineUniqueFields)) {
-							echo '<td></td>';
-							continue;
-						}
-						$makeThisDetailed = (in_array($config['type'], $makeDetails));
-						?>
-						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-							<?php $this->meta->render('form', $name, $config); ?>
-							<?= $makeThisDetailed ? '</details>' : '' ?>
-						</td>
-						<?php
-					}
-					?>
-				</tr>
-			</tbody>
-		</template>
-		<?php
-	}
-	/**
-	 * Render status radio buttons
-	 */
-	protected function renderStatusRadios(): string {
-		ob_start();
-		?>
-		<div class="radio-options status-options row">
-			<?php foreach ($this->statuses as $status => $config):
-				if ($status === 'all') continue;
-
-				// Handle special cases
-				if ($status === 'future') {
-					$status = 'publish';
-					$config = [
-						'icon' => 'eye',
-						'label' => 'Live'
-					];
-				} elseif ($status === 'past') {
-					continue;
-				}
-				?>
-				<input type="radio"
-					   name="post_status"
-					   id="status-<?= esc_attr($status) ?>"
-					   value="<?= esc_attr($status) ?>">
-				<label for="status-<?= esc_attr($status) ?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
-				</label>
-			<?php endforeach; ?>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-	/**
-	 * Render table header
-	 */
-	protected function renderTableHeader(): string {
-		ob_start();
-
-		?>
-		<tr>
-			<th scope="col" class="select-header">
-				<input type="checkbox" id="select-all" name="select-all">
-				<label for="select-all">All</label>
-			</th>
-			<th scope="col" class="status-header">Status</th>
-			<?php foreach ($this->fields as $name => $config):
-				if (array_key_exists('hidden', $config)){
-					continue;
-				}
-				?>
-				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-					<?= esc_html($config['label']) ?>
-				</th>
-			<?php endforeach; ?>
-		</tr>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderTimelineTableHeader(): string {
-		ob_start();
-
-		?>
-		<tr>
-			<th scope="col" class="select-header">
-				<input type="checkbox" id="select-all" name="select-all">
-				<label for="select-all">All</label>
-			</th>
-			<th scope="col" class="show-post_status">
-				Status
-			</th>
-			<?php foreach ($this->fields as $name => $config):
-				if (array_key_exists('hidden', $config) || $name === 'post_status'){
-					continue;
-				}
-				?>
-				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-					<?= esc_html($config['label']) ?>
-				</th>
-			<?php endforeach; ?>
-		</tr>
-		<?php
-		return ob_get_clean();
+	public function render(): void {
+		$this->skeleton->render();
 	}
 
 	/**
-	 * Render table action controls
+	 * Get the skeleton instance for further customization
 	 */
-	protected function renderTableActions(): string {
-		ob_start();
-		?>
-		<div class="table-actions row btw nowrap">
-			<?= jvbRenderToggleTextField(
-				'vertical',
-				'TAB NAV:',
-				'',
-				jvbIcon('caret-double-down'),
-				jvbIcon('caret-double-right')
-			) ?>
-
-			<button type="button" class="add-row" title="Add new row">
-				<?= jvbIcon('plus-square') ?>
-				<span>Add Row</span>
-			</button>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	public function createItem(array $actions):array
-	{
-		ob_start();
-		$this->renderCreateModal();
-		$content = ob_get_clean();
-		$create = [
-			'button'	=> '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
-			'content'	=> $content,
-		];
-		$actions[] = $create;
-		return $actions;
+	public function getSkeleton(): CRUDSkeleton {
+		return $this->skeleton;
 	}
 }

--
Gitblit v1.10.0